diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..d8dd6c078e --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +# The CI image (.github/workflows/cibuild.yml) plus a non-root account, which +# the sniper SDK does not ship. Without one, a rootful-docker container writes +# root-owned files into the bind mount. devcontainer.json passes the host +# username, and updateRemoteUserUID rewrites the UID/GID to match at build time. +FROM registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest + +ARG USERNAME=dev +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN groupadd --gid "${USER_GID}" "${USERNAME}" \ + && useradd --uid "${USER_UID}" --gid "${USER_GID}" -m -s /bin/bash "${USERNAME}" \ + && echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${USERNAME}" \ + && chmod 0440 "/etc/sudoers.d/${USERNAME}" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..8314e54688 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,72 @@ +{ + "name": "NT;RE - Steam Runtime 3 (sniper) SDK", + // The CI image (.github/workflows/cibuild.yml) plus a non-root account; see + // the Dockerfile. + "build": { + "dockerfile": "Dockerfile", + "args": { + // Account name only; file ownership comes from updateRemoteUserUID below. + "USERNAME": "${localEnv:USER:dev}" + } + }, + // SELinux hosts: without this the bind mount reads as Permission denied. + // Not :Z - that relabels the repo and breaks access from outside. + "runArgs": [ + "--security-opt", + "label=disable" + ], + // Must match the USERNAME build arg. updateRemoteUserUID rebuilds the image + // with the local UID/GID so bind-mount files stay owned by the host user. + "remoteUser": "${localEnv:USER:dev}", + "updateRemoteUserUID": true, + "postCreateCommand": "bash tools/ntre-dev-setup.sh --editor none", + "remoteEnv": { + "PATH": "${containerWorkspaceFolder}/.ide/bin:${containerEnv:PATH}" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cmake-tools", + "llvm-vs-code-extensions.vscode-clangd" + ], + // Mirrors what tools/ntre-dev-setup.sh writes for non-container users; + // keep in sync. ${workspaceFolder} is VS Code's, not devcontainer.json's. + "settings": { + "cmake.sourceDirectory": "${workspaceFolder}/src", + "cmake.useCMakePresets": "always", + "cmake.configureOnOpen": false, + // Stops the extension downloading its own clangd. + "clangd.path": "${workspaceFolder}/.ide/bin/clangd", + "clangd.checkUpdates": false, + "clangd.onConfigChanged": "restart", + "clangd.arguments": [ + // Command-line-only option: reads include paths and predefined + // macros from the compiler that actually builds this tree. + "--query-driver=/usr/bin/g++*,/usr/bin/gcc*,/usr/bin/clang*,/usr/lib/llvm-*/bin/clang*", + "--header-insertion=never", + "--background-index", + "--completion-style=detailed", + "--pch-storage=memory" + ], + // clangd provides IntelliSense; stop cpptools double-indexing. + "C_Cpp.intelliSenseEngine": "disabled", + "files.associations": { + "*.h": "cpp", + "*.inc": "cpp" + }, + "[cpp]": { + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" + }, + "files.watcherExclude": { + "**/src/build/**": true, + "**/.cache/**": true, + "**/.ide/**": true + }, + "search.exclude": { + "**/src/build/**": true, + "**/.ide/**": true + } + } + } + } +} diff --git a/.gitattributes b/.gitattributes index 437f140a1c..4effc43a1e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ -buildallprojects text vpc binary -*.sh text +# eol=lf: a shell script checked out with CRLF fails to run at all. +*.sh text eol=lf *.bat text *.txt text *.c text diff --git a/.gitignore b/.gitignore index 2ac93e819a..0de5b1b13c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,16 @@ out /src/CMakeSettings.json +/src/CMakeUserPresets.json + +# Generated by tools/ntre-dev-setup.sh +/.ide/ +/.clangd +/compile_commands.json +/.zed/ + +# Personal devcontainer variants, offered alongside the tracked one +/.devcontainer/local/ # ctest /src/Testing @@ -662,11 +672,6 @@ MigrationBackup/ FodyWeavers.xsd # VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json *.code-workspace # Local History for Visual Studio Code @@ -699,3 +704,10 @@ FodyWeavers.xsd # Selected Background /game/neo/scripts/[Cc]hapter[Bb]ackgrounds.txt + +# Share the ntre-dev-setup.sh task entry point; the rest of .vscode stays local. +# Three patterns because *.vscode/ above excludes the directory itself, and git +# cannot re-include a file whose parent directory is excluded. +!/.vscode/ +/.vscode/* +!/.vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..66edf617dc --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,32 @@ +{ + // The only tracked file under .vscode/ (see the re-include at the end of + // .gitignore); the setup task writes the per-developer settings.json and + // extensions.json. + "version": "2.0.0", + "tasks": [ + { + "label": "ntre-dev-setup: Set up VS Code", + "detail": "Installs the pinned clangd, generates the compile database and writes .vscode/settings.json + extensions.json. Linux or macOS shell (WSL on Windows).", + "type": "shell", + "command": "${workspaceFolder}/tools/ntre-dev-setup.sh", + "args": ["--editor", "vscode"], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "ntre-dev-setup: Refresh compile database", + "detail": "Run after adding, removing or renaming sources, or after pulling changes to a CMakeLists.txt.", + "type": "shell", + "command": "${workspaceFolder}/tools/ntre-dev-setup.sh", + "args": ["--reconfigure"], + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "panel": "shared" + } + } + ] +} diff --git a/README.md b/README.md index 8a201afe15..4ada8c989e 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,31 @@ $ cmake --build --preset PRESET_NAME Available PRESET_NAME values: `windows-debug`, `windows-release`, `linux-debug`, `linux-release`. +## Development using Linux + +Optional tooling for working on the code, rather than for building it: a dev container definition and a setup script that give you a working C++ IntelliSense index. None of it changes how NT;RE is built. The [Qt Creator](#qt-creator-linux) and [CLI](#cli-with-ninja-windows--linux) workflows above are untouched, and every file the script generates is git-ignored. + +### Getting set up + +Either entry point is enough: + +* **Dev container** - open the repo in an editor that supports [dev containers](https://containers.dev/) (VS Code, the `devcontainer` CLI, JetBrains Gateway) and reopen in the container. It uses the same sniper SDK image as the CI runners and the manual container steps above, and runs the setup script for you on create. +* **Run it yourself** - from inside your own sniper container, or on a native toolchain new enough for C++20: + + ```bash + $ ./tools/ntre-dev-setup.sh + ``` + + Add `--editor vscode` to also write `.vscode/settings.json` and `extensions.json`; in VS Code, the *"ntre-dev-setup: Set up VS Code"* task does the same. `--editor zed` writes the equivalent `.zed/settings.json`. Run from an editor's own terminal, the default `--editor auto` picks whichever it detects. `--help` lists the rest. + +### What it sets up + +* A pinned `clangd` under `.ide/`, since distro packages are frequently too old for this tree's C++20. Use `--system-clangd` to keep your own instead, or `source .ide/env.sh` to put the pinned one on `PATH` for editors launched from a shell. +* `compile_commands.json`, symlinked at the repo root where clangd, CLion, Qt Creator and Sublime look for it, alongside a `.clangd` carrying the compile flags that need adjusting for the SteamRT toolchain. +* A `linux-debug-ide` preset in your `src/CMakeUserPresets.json` (per-developer, git-ignored) that generates the database with unity builds off - the header of `tools/ntre-dev-setup.sh` explains why. **Do not build from it** - build with `linux-debug` as documented above. + +Re-run `./tools/ntre-dev-setup.sh --reconfigure` after adding, removing or renaming source files, or use the *"ntre-dev-setup: Refresh compile database"* task in VS Code. + ## Steam mod setup To make it appear in Steam, the install files have to appear under the sourcemods directory or be directed to it. diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json deleted file mode 100644 index 87e569e8df..0000000000 --- a/src/.vscode/tasks.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Build All Projects", - "type": "shell", - "command": "./buildallprojects", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": { - "base": "$gcc", - "fileLocation": ["relative", "${workspaceFolder}"] - }, - "presentation": { - "reveal": "always", - "panel": "shared" - } - } - ] -} - diff --git a/tools/ntre-dev-setup.sh b/tools/ntre-dev-setup.sh new file mode 100755 index 0000000000..6ba85b4a3a --- /dev/null +++ b/tools/ntre-dev-setup.sh @@ -0,0 +1,424 @@ +#!/usr/bin/env bash +# +# Bootstraps C++ IntelliSense: a compile_commands.json symlinked at the repo root +# and a .clangd beside it - the two interfaces every C++ indexer understands. +# +# Why a separate CMake directory: five targets (client, server, tier1, mathlib, +# vgui_controls) build with UNITY_BUILD ON, so the build's own database lists +# only generated unity_*.cxx blobs and ~1300 translation units get no flags at +# all. This configures the same flags with unity off, purely for the database; +# nothing is compiled. +# +# Safe to re-run; it skips what is already in place. See --help. + +set -euo pipefail + +CLANGD_VERSION="22.1.6" +CLANGD_SHA256_LINUX_X86_64="a9c77443af2e447ed467e84771848d3a6ac1c56f84bcfcde717e66318de77cfa" +# clangd older than this mishandles the C++20 this tree is built with. +CLANGD_MIN_MAJOR=17 + +PRESET="linux-debug-ide" +BASE_PRESET="linux-debug" +# Globbed for the GCC 10 and Clang 19 in CONTRIBUTING.md, which install as +# gcc-10, clang-19, /usr/lib/llvm-19/bin/clang++. A driver clangd cannot query +# leaves it guessing the standard library paths. +QUERY_DRIVER='/usr/bin/g++*,/usr/bin/gcc*,/usr/bin/clang*,/usr/lib/llvm-*/bin/clang*' +EDITOR_TARGET="auto" +USE_SYSTEM_CLANGD=0 +RECONFIGURE=0 +FORCE=0 +CLANGD_VERSION_OVERRIDDEN=0 +CLANGD_SHA256_OVERRIDE="" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +IDE_DIR="${REPO_ROOT}/.ide" +SRC_DIR="${REPO_ROOT}/src" +BUILD_DIR="${SRC_DIR}/build/${PRESET}" + +usage() { + cat <<'EOF' +Usage: tools/ntre-dev-setup.sh [options] + + --editor + Write editor-specific config too. "auto" (default) + picks VS Code or Zed when run from that editor's + terminal or task. Every editor works without this; + it just saves pointing your client at + .ide/bin/clangd by hand. + --system-clangd Use clangd from PATH instead of downloading a + pinned one (must be >= major 17). + --clangd-version Override the pinned clangd release. Downloads + unverified unless --clangd-sha256 is also given. + --clangd-sha256 SHA-256 of the linux-x86_64 zip for an overridden + --clangd-version. + --preset IntelliSense preset name (default linux-debug-ide). + --base-preset Preset it inherits build flags from (default linux-debug). + --reconfigure Regenerate the compile database even if it exists. + Do this after adding/removing/renaming sources. + --force Overwrite generated config files (.clangd etc). + -h, --help This text. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --editor) EDITOR_TARGET="$2"; shift 2 ;; + --system-clangd) USE_SYSTEM_CLANGD=1; shift ;; + --clangd-version) CLANGD_VERSION="$2"; CLANGD_VERSION_OVERRIDDEN=1; shift 2 ;; + --clangd-sha256) CLANGD_SHA256_OVERRIDE="$2"; shift 2 ;; + --preset) PRESET="$2"; BUILD_DIR="${SRC_DIR}/build/${PRESET}"; shift 2 ;; + --base-preset) BASE_PRESET="$2"; shift 2 ;; + --reconfigure) RECONFIGURE=1; shift ;; + --force) FORCE=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;; + esac +done + +# The pinned hash only vouches for the pinned version; an override replaces both. +if [[ $CLANGD_VERSION_OVERRIDDEN -eq 1 ]]; then + CLANGD_SHA256_LINUX_X86_64="$CLANGD_SHA256_OVERRIDE" +fi + +say() { printf '\033[1m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[33mwarning:\033[0m %s\n' "$*" >&2; } +die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; } + +# Writes stdin to $1 unless it exists (or --force). Returns 1 when skipped. +write_if_absent() { + local path="$1" + if [[ -e "$path" && $FORCE -eq 0 ]]; then + cat > /dev/null + return 1 + fi + mkdir -p "$(dirname "$path")" + cat > "$path" + return 0 +} + +# ---------------------------------------------------------------- prerequisites +for tool in cmake ninja; do + command -v "$tool" >/dev/null || die "$tool not found in PATH" +done +command -v g++ >/dev/null || warn "g++ not in PATH; the compile database will name a compiler this machine cannot query" +[[ -f "${SRC_DIR}/CMakePresets.json" ]] || die "no src/CMakePresets.json - run this from inside the repo" + +# ------------------------------------------------------------------- 1. clangd +CLANGD_BIN="${IDE_DIR}/bin/clangd" + +clangd_major() { "$1" --version 2>/dev/null | sed -n 's/.*clangd version \([0-9]*\).*/\1/p' | head -1; } + +install_clangd() { + local os arch asset major + os="$(uname -s)"; arch="$(uname -m)" + + case "${os}/${arch}" in + Linux/x86_64) asset="clangd-linux-${CLANGD_VERSION}.zip" ;; + *) + # No pinned release for this platform - notably linux/aarch64, and + # macOS, where this tree does not build anyway. + local sys; sys="$(command -v clangd || true)" + [[ -n "$sys" ]] || die "no clangd release for ${os}/${arch}; install clangd >= ${CLANGD_MIN_MAJOR} and re-run" + major="$(clangd_major "$sys")" + [[ -n "$major" && "$major" -ge $CLANGD_MIN_MAJOR ]] \ + || die "clangd ${major:-?} at ${sys} is too old for this tree's C++20; need >= ${CLANGD_MIN_MAJOR}" + warn "no clangd release for ${os}/${arch}; falling back to ${sys}" + mkdir -p "${IDE_DIR}/bin"; ln -sfn "$sys" "$CLANGD_BIN" + return + ;; + esac + + local dest="${IDE_DIR}/toolchain/clangd_${CLANGD_VERSION}" + if [[ ! -x "${dest}/bin/clangd" ]]; then + command -v curl >/dev/null || die "curl not found; needed to fetch clangd" + command -v unzip >/dev/null || die "unzip not found; needed to unpack clangd" + say "downloading clangd ${CLANGD_VERSION} (${asset})" + local tmp; tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' RETURN + curl -fsSL -o "${tmp}/clangd.zip" \ + "https://github.com/clangd/clangd/releases/download/${CLANGD_VERSION}/${asset}" + if [[ -n "$CLANGD_SHA256_LINUX_X86_64" ]]; then + echo "${CLANGD_SHA256_LINUX_X86_64} ${tmp}/clangd.zip" | sha256sum -c - >/dev/null \ + || die "clangd download failed checksum verification" + else + warn "no checksum for clangd ${CLANGD_VERSION}; skipping verification (pass --clangd-sha256 to pin one)" + fi + mkdir -p "${IDE_DIR}/toolchain" + rm -rf "$dest" + unzip -q "${tmp}/clangd.zip" -d "${IDE_DIR}/toolchain" + fi + mkdir -p "${IDE_DIR}/bin" + ln -sfn "../toolchain/clangd_${CLANGD_VERSION}/bin/clangd" "$CLANGD_BIN" +} + +if [[ $USE_SYSTEM_CLANGD -eq 1 ]]; then + sys="$(command -v clangd || true)" + [[ -n "$sys" ]] || die "--system-clangd given but no clangd in PATH" + major="$(clangd_major "$sys")" + [[ -n "$major" && "$major" -ge $CLANGD_MIN_MAJOR ]] \ + || die "clangd $major is too old for this tree's C++20; need >= ${CLANGD_MIN_MAJOR}" + mkdir -p "${IDE_DIR}/bin"; ln -sfn "$sys" "$CLANGD_BIN" +else + install_clangd +fi +say "clangd: $("$CLANGD_BIN" --version | head -1)" + +# ------------------------------------------- 2. unity-build override for CMake +# Injected via CMAKE_PROJECT__INCLUDE, so no tracked CMakeLists.txt changes +# and upstream merges stay clean. +if write_if_absent "${IDE_DIR}/cmake/ide-overrides.cmake" <<'EOF' +# Loaded via CMAKE_PROJECT_neo_INCLUDE by the IntelliSense preset only; clears +# UNITY_BUILD so the database gets one entry per translation unit (see the +# header of tools/ntre-dev-setup.sh). The real build keeps its unity speedup. + +function(_neo_ide_disable_unity dir) + get_property(targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS) + foreach(target IN LISTS targets) + get_target_property(type ${target} TYPE) + if(NOT type STREQUAL "INTERFACE_LIBRARY" AND NOT type STREQUAL "UTILITY") + set_target_properties(${target} PROPERTIES UNITY_BUILD OFF) + endif() + endforeach() + + get_property(subdirs DIRECTORY "${dir}" PROPERTY SUBDIRECTORIES) + foreach(subdir IN LISTS subdirs) + _neo_ide_disable_unity("${subdir}") + endforeach() +endfunction() + +# Deferred so it runs after every add_subdirectory() has created its targets. +cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" + CALL _neo_ide_disable_unity "${CMAKE_SOURCE_DIR}") +EOF +then say "wrote .ide/cmake/ide-overrides.cmake"; fi + +# ------------------------------------------------------- 3. CMakeUserPresets.json +# Per-developer by design and git-ignored, so writing here never dirties the tree. +# CMake Tools, CLion and `cmake --preset` all read it. +PRESETS_FILE="${SRC_DIR}/CMakeUserPresets.json" +if [[ -f "$PRESETS_FILE" ]] && grep -q "\"${PRESET}\"" "$PRESETS_FILE"; then + say "preset ${PRESET} already present in src/CMakeUserPresets.json" +elif [[ -f "$PRESETS_FILE" ]]; then + command -v python3 >/dev/null || die "src/CMakeUserPresets.json exists without a '${PRESET}' preset; install python3 so it can be merged, or add the preset by hand" + say "adding ${PRESET} to existing src/CMakeUserPresets.json" + PRESET="$PRESET" BASE_PRESET="$BASE_PRESET" python3 - "$PRESETS_FILE" <<'EOF' +import json, os, sys +path = sys.argv[1] +doc = json.load(open(path)) +doc.setdefault("version", 3) +doc.setdefault("configurePresets", []).append({ + "name": os.environ["PRESET"], + "displayName": "Linux Debug (IntelliSense index only)", + "description": "Configure-only, unity off, for the IntelliSense database. Do not build from it.", + "inherits": os.environ["BASE_PRESET"], + "cacheVariables": { + "CMAKE_PROJECT_neo_INCLUDE": "${sourceDir}/../.ide/cmake/ide-overrides.cmake", + "NEO_EXTRA_ASSETS": "OFF", + "NEO_COPY_LIBRARIES": "OFF", + "NEO_USE_CCACHE": "OFF", + }, +}) +json.dump(doc, open(path, "w"), indent=2) +open(path, "a").write("\n") +EOF +else + say "writing src/CMakeUserPresets.json" + cat > "$PRESETS_FILE" </dev/null || die "cmake configure failed; re-run without >/dev/null to see why" +else + say "compile database already present (--reconfigure to regenerate)" +fi + +entries=$(python3 -c "import json,sys; print(len(json.load(open(sys.argv[1]))))" "${BUILD_DIR}/compile_commands.json" 2>/dev/null || echo '?') +unity=$(grep -c 'Unity/unity_' "${BUILD_DIR}/compile_commands.json" 2>/dev/null || true) +say "compile database: ${entries} entries, ${unity:-0} unity blobs" +[[ "${unity:-0}" == "0" ]] || warn "unity entries present - the override in .ide/cmake did not take effect" + +# Root symlink: the location nearly every C++ tool probes by default. +ln -sfn "src/build/${PRESET}/compile_commands.json" "${REPO_ROOT}/compile_commands.json" +say "symlinked compile_commands.json at the repo root" + +# ------------------------------------------------------------------ 6. env.sh +# For shell-launched editors (nvim, helix, emacs): source it and clangd is on PATH. +cat > "${IDE_DIR}/env.sh" <