diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 246ec2c..67ee398 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -29,12 +29,13 @@ else fi # ── clang-tidy (advisory) ──────────────────────────────────────────────── -# Needs a compile_commands.json. We generate one in build-debug via CMake +# Needs a compile_commands.json. The debug preset (CMakePresets.json) writes +# one into build/, the same tree CI lints against # (CMAKE_EXPORT_COMPILE_COMMANDS is ON in CMakeLists.txt). Match CI: report # warnings but don't block the commit on stylistic findings — .clang-tidy's # WarningsAsErrors='' is authoritative. if command -v clang-tidy >/dev/null 2>&1; then - if [ -f build-debug/compile_commands.json ]; then + if [ -f build/compile_commands.json ]; then cpp_files=() while IFS= read -r f; do case "$f" in @@ -42,11 +43,11 @@ if command -v clang-tidy >/dev/null 2>&1; then esac done <<<"$staged" if [ "${#cpp_files[@]}" -gt 0 ]; then - echo "▶ clang-tidy -p build-debug (${#cpp_files[@]} file(s))" - clang-tidy -p build-debug --quiet "${cpp_files[@]}" || true + echo "▶ clang-tidy -p build (${#cpp_files[@]} file(s))" + clang-tidy -p build --quiet "${cpp_files[@]}" || true fi else - echo "⚠ build-debug/compile_commands.json missing — run scripts/build.sh debug" + echo "⚠ build/compile_commands.json missing — run scripts/build.sh debug" fi else echo "⚠ clang-tidy not installed — skipping (sudo apt install clang-tidy)" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 895b24a..ea87a18 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,9 +50,12 @@ jobs: Acquire::Retries "2"; CONF sudo apt-get update + # catch2: the shared release preset configures with tests ON, and a + # system Catch2 keeps the configure off tests/' FetchContent fallback + # (a network fetch this lane does not otherwise need). sudo apt-get install -y --no-install-recommends \ build-essential cmake ninja-build pkg-config \ - libsodium-dev libsdl2-dev libssl-dev libopus-dev + libsodium-dev libsdl2-dev libssl-dev libopus-dev catch2 # Not apt: noble ships Qt 6.4.2 and this project's floor is 6.7. The # action pulls the official binaries and exports CMAKE_PREFIX_PATH. @@ -65,12 +68,16 @@ jobs: languages: ${{ matrix.language }} queries: security-extended,security-and-quality + # The configure flags live in CMakePresets.json (preset release), shared + # with linux-ci.yml and the local scripts. Two deliberate deltas against + # the old inline configure: the tree is build-release (the preset's + # binaryDir), and tests + translations are configured ON like the ci job + # (the traced build still compiles only Dish; setup-qt above provides + # the Linguist tools the translations flag requires). - name: Build run: | - cmake -S . -B build -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DDISH_BUILD_TESTS=OFF - cmake --build build --parallel --target Dish + cmake --preset release + cmake --build build-release --parallel --target Dish - name: Perform CodeQL analysis # SARIF upload requires GitHub Advanced Security on private repos. diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 9caf354..099eaad 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -72,30 +72,30 @@ jobs: pipx ensurepath echo "$HOME/.local/bin" >> "$GITHUB_PATH" + # The gate itself lives in scripts/check-format.sh so this workflow and + # scripts/ci-local.sh cannot drift on the file set or the invocation. - name: clang-format (check only) if: env.DISH_LINTS == 'true' - run: | - find src tests -type f \( -name '*.cpp' -o -name '*.h' \) \ - -print0 | xargs -0 clang-format --dry-run --Werror + run: ./scripts/check-format.sh + # The configure line lives in CMakePresets.json (preset debug), shared + # with scripts/build.sh and scripts/ci-local.sh. The ccache launcher is + # layered on top here rather than in the preset because a local box need + # not have ccache; CC/CXX come from the matrix env, which CMake respects + # natively. - name: Configure (Debug, tests on) run: | - cmake -S . -B build -G Ninja \ - -DCMAKE_BUILD_TYPE=Debug \ - -DDISH_BUILD_TESTS=ON \ - -DDISH_REQUIRE_TRANSLATIONS=ON \ + cmake --preset debug \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build - run: cmake --build build --parallel + run: cmake --build --preset debug --parallel + # QT_QPA_PLATFORM=offscreen comes from the test preset: no display on + # the runner, and the QML tests construct QGuiApplication. - name: Run tests (Debug) - working-directory: build - # No display on the runner; the QML tests construct QGuiApplication. - env: - QT_QPA_PLATFORM: offscreen - run: ctest --output-on-failure --parallel + run: ctest --preset debug --parallel - name: qmllint (QML static analysis) if: env.DISH_LINTS == 'true' @@ -133,26 +133,22 @@ jobs: -print0 | xargs -0 -n1 -P"$(nproc)" \ clang-tidy -p build --quiet --warnings-as-errors='*' + # The configure line lives in CMakePresets.json (preset release); same + # ccache-on-top note as the Debug configure above. - name: Configure (Release, tests on) run: | - cmake -S . -B build-release -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DDISH_BUILD_TESTS=ON \ - -DDISH_REQUIRE_TRANSLATIONS=ON \ + cmake --preset release \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build release - run: cmake --build build-release --parallel + run: cmake --build --preset release --parallel # -O3 plus LTO is a different compiler: undefined behaviour that Debug # tolerates surfaces here, and until now nothing ever ran the suite # against the configuration that actually ships. - name: Run tests (Release) - working-directory: build-release - env: - QT_QPA_PLATFORM: offscreen - run: ctest --output-on-failure --parallel + run: ctest --preset release --parallel # Ubuntu patches PIE, partial RELRO and stack protection into its gcc # defaults, so these pass by accident on this runner and would silently @@ -275,19 +271,11 @@ jobs: desktop-file-validate packaging/dish.desktop appstreamcli validate --no-net packaging/com.tinkernorth.Dish.metainfo.xml - - name: Configure and build - run: | - cmake -S . -B build-release -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DDISH_BUILD_TESTS=OFF - cmake --build build-release --parallel - + # Configure + build + cpack live in scripts/build-deb.sh (the `package` + # preset), the same path release.yml's deb job and a local + # `scripts/build-deb.sh` run. - name: Build the .deb - run: | - set -euo pipefail - cpack --config build-release/CPackConfig.cmake -G DEB -B build-release/packages - mkdir -p dist && cp build-release/packages/*.deb dist/ + run: ./scripts/build-deb.sh # A lintian error means the package is malformed, not merely untidy. - name: lintian diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47582b1..5c833a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -179,19 +179,11 @@ jobs: desktop-file-validate packaging/dish.desktop appstreamcli validate --no-net packaging/com.tinkernorth.Dish.metainfo.xml - - name: Configure and build - run: | - cmake -S . -B build-release -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DDISH_BUILD_TESTS=OFF - cmake --build build-release --parallel - - - name: Package - run: | - set -euo pipefail - cpack --config build-release/CPackConfig.cmake -G DEB -B build-release/packages - mkdir -p dist && cp build-release/packages/*.deb dist/ + # Configure + build + cpack live in scripts/build-deb.sh (the `package` + # preset), the same path linux-ci.yml's package job and a local + # `scripts/build-deb.sh` run. + - name: Build the .deb + run: ./scripts/build-deb.sh - name: lintian run: lintian --fail-on error --tag-display-limit 0 dist/*.deb @@ -331,19 +323,10 @@ jobs: with: ref: ${{ inputs.tag || github.ref }} - - name: Configure and build - run: | - cmake -S . -B build-release -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DDISH_BUILD_TESTS=OFF - cmake --build build-release --parallel - - - name: Package - run: | - set -euo pipefail - cpack --config build-release/CPackConfig.cmake -G RPM -B build-release/packages - mkdir -p dist && cp build-release/packages/*.rpm dist/ + # Same rails as the .deb: scripts/build-rpm.sh drives the `package` + # preset and swaps the cpack generator. + - name: Build the .rpm + run: ./scripts/build-rpm.sh - name: Assert package contents and scriptlets run: | diff --git a/.gitignore b/.gitignore index 0e72973..d268db7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ out/ cmake-build-*/ _deps/ +# Packaged artifacts (scripts/build-deb.sh, build-rpm.sh, build-appimage.sh) +dist/ + # CMake CMakeFiles/ CMakeCache.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0786886..cb3d832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,26 @@ the repos share a version number. is the honest maximum rather than a shortcut. The two host rumble streams mix per motor by maximum, so neither can cancel the other. +### Changed + +- **Build system: local builds and CI run the same rails.** `CMakePresets.json` + (new) carries the `debug`, `release` and `package` configure lines; + `linux-ci.yml`, `codeql.yml` and `release.yml` call the presets, the shared + `scripts/check-format.sh` gate, and the new `scripts/build-deb.sh` / + `scripts/build-rpm.sh` packaging scripts instead of inline copies (matrix + compilers stay env-injected CC/CXX and the ccache launcher stays + workflow-side, so presets never pin what the matrix varies). Locally: + `scripts/install-deps.sh` (the README apt list plus CI's pinned + clang-format; `--ci-qt` installs the exact Qt 6.9.3 CI builds against), + `scripts/build.sh` rewritten onto the presets (the debug tree is now + `build/`, CI's name, instead of `build-debug/`; the pre-commit hook's + `clang-tidy -p` target follows), and `scripts/ci_local.sh` renamed to + `scripts/ci-local.sh` (a forwarder keeps the old name) with its known gaps + closed: the Debug and Release configures now carry + `DISH_REQUIRE_TRANSLATIONS=ON` like CI, qmllint gains CI's + `-I "$QT_ROOT_DIR/qml"` include, and a `--compiler gcc|clang` flag + reproduces either side of CI's compiler matrix. + ### Fixed - Moonlight motion samples were forwarded as the satellite's raw fixed-point diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..038a1aa --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,74 @@ +{ + "version": 3, + "cmakeMinimumRequired": { "major": 3, "minor": 21, "patch": 0 }, + "configurePresets": [ + { + "name": "debug", + "displayName": "Debug (tests, what linux-ci.yml builds first)", + "description": "The Debug lane linux-ci.yml configures: Ninja, tests on, translations required. No compiler is named: CI's gcc/clang matrix sets CC/CXX in the environment and CMake respects them natively, so a preset naming one would break the other leg. CI adds -DCMAKE_C[XX]_COMPILER_LAUNCHER=ccache on top of the preset; that stays workflow-side because a local box need not have ccache. DISH_REQUIRE_TRANSLATIONS is ON here, unlike CMakeLists' local-friendly OFF default, because local == CI is the point of the preset; note the gate itself also needs lupdate >= 6.9 (see .github/actions/setup-qt), which distro Qt may predate.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "DISH_BUILD_TESTS": "ON", + "DISH_REQUIRE_TRANSLATIONS": "ON" + } + }, + { + "name": "release", + "displayName": "Release (tests on, the shipping optimisation level)", + "description": "The Release lane linux-ci.yml configures and TESTS: -O3 plus LTO is a different compiler, so the suite runs against the configuration that ships, not only against Debug. Same compiler/ccache notes as the debug preset.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "DISH_BUILD_TESTS": "ON", + "DISH_REQUIRE_TRANSLATIONS": "ON" + } + }, + { + "name": "package", + "displayName": "Package (Release, /usr prefix, no tests)", + "description": "What the .deb/.rpm jobs configure (linux-ci.yml package, release.yml deb + rpm, via scripts/build-deb.sh / build-rpm.sh): install rules against /usr, tests off. Its own tree so a local package build never clobbers the release preset's build-release/. Translations are not required here: the package builds against the distro's Qt inside the target container, where the catalogues are compiled but the sync gate (which needs lupdate >= 6.9) is the ci job's business.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-package", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "/usr", + "DISH_BUILD_TESTS": "OFF" + } + } + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "debug" + }, + { + "name": "release", + "configurePreset": "release" + }, + { + "name": "package", + "configurePreset": "package" + } + ], + "testPresets": [ + { + "name": "debug", + "configurePreset": "debug", + "output": { "outputOnFailure": true }, + "environment": { + "QT_QPA_PLATFORM": "offscreen" + } + }, + { + "name": "release", + "configurePreset": "release", + "output": { "outputOnFailure": true }, + "environment": { + "QT_QPA_PLATFORM": "offscreen" + } + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6a75f2..8b3092b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,15 +6,24 @@ captures the conventions that aren't obvious from skimming the code. ## Getting set up ```bash -# 1) Install build deps for your distro (see README "Install build dependencies") -# 2) Generate compile_commands.json + run the test suite +# 1) Install the toolchain CI uses (apt; --ci-qt adds CI's exact Qt 6.9.3) +scripts/install-deps.sh +# 2) Generate compile_commands.json + run the test suite (debug preset -> build/) scripts/build.sh debug test # 3) Point git at the in-tree pre-commit hook scripts/setup-hooks.sh +# 4) Before pushing: every CI gate, in CI's order +scripts/ci-local.sh ``` +`CMakePresets.json` is the single source of configure truth: the `debug`, +`release` and `package` presets are what `linux-ci.yml`, `codeql.yml` and +`release.yml` drive, and the `scripts/` wrappers drive the same ones. The +debug preset writes to `build/` (CI's tree name; this repo used +`build-debug/` before the presets existed). + The pre-commit hook runs `clang-format -i` (autofix, re-stages) and -`clang-tidy -p build-debug` (advisory) on staged C++ files. It skips +`clang-tidy -p build` (advisory) on staged C++ files. It skips gracefully if the tools aren't installed — CI re-runs `clang-format --dry-run --Werror` and `clang-tidy` in strict mode, so anything that slips locally fails the PR. @@ -161,11 +170,15 @@ Security gates (also blocking): - `codeql.yml`: CodeQL `cpp` analysis (security-extended + security-and-quality query packs). -`scripts/ci_local.sh` runs those gates in the same order against your worktree, -so a green run there means a green run in CI. `--no-tidy` skips the slowest step -for a fast loop; `--with-package` adds the CPack/lintian leg. A gate whose tool -is missing FAILS rather than printing a notice — pass `--allow-missing` if you -really want to skip it, and know that you did. +`scripts/ci-local.sh` runs those gates in the same order against your worktree +(through the same presets and `scripts/check-format.sh` the workflow calls), so +a green run there means a green run in CI. `--no-tidy` skips the slowest step +for a fast loop; `--with-package` adds the CPack/lintian leg; +`--with-sanitizers` adds the ASan/UBSan and TSan legs; `--compiler gcc|clang` +sets CC/CXX so you can reproduce either side of CI's compiler matrix (one run +covers one compiler). A gate whose tool is missing FAILS rather than printing +a notice — pass `--allow-missing` if you really want to skip it, and know that +you did. `scripts/ci_local.sh` remains as a forwarder for muscle memory. ## Security @@ -275,8 +288,8 @@ database globally, so a second tree only re-derives the same `src/` entries and costs another full build: ```sh -cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DDISH_BUILD_TESTS=ON -cmake --build build --parallel +cmake --preset debug +cmake --build --preset debug --parallel find src -type f \( -name '*.cpp' -o -name '*.h' \) \ ! -path 'src/UI/*' ! -path 'src/qml/*' -print0 | xargs -0 -n1 -P"$(nproc)" clang-tidy -p build --quiet --warnings-as-errors='*' diff --git a/README.md b/README.md index 21c924a..fd8e5e7 100644 --- a/README.md +++ b/README.md @@ -108,26 +108,29 @@ it in place. What the check sends is spelled out in [`PRIVACY.md`](PRIVACY.md). from the SVG. Without it the build says so and installs only the scalable and 512x512 icons, which is enough for a working menu entry. -On Debian and Ubuntu: - -```sh -sudo apt install build-essential cmake ninja-build pkg-config \ - qt6-base-dev qt6-base-dev-tools qt6-declarative-dev qt6-svg-dev \ - qt6-tools-dev qt6-l10n-tools \ - libsodium-dev libsdl2-dev libssl-dev libopus-dev libdbus-1-dev catch2 \ - librsvg2-bin -``` - -Then: +Local builds and CI run on the same rails: `CMakePresets.json` carries the +configure lines every workflow uses (`debug`, `release`, `package`), and the +scripts under `scripts/` drive those presets. On Debian and Ubuntu: ```sh +scripts/install-deps.sh # the apt list above + CI's pinned clang-format; + # --ci-qt adds the exact Qt 6.9.3 CI builds against scripts/build.sh release ./build-release/dish ``` -`scripts/build.sh debug` builds into `build-debug/` instead, and -`scripts/build.sh test` runs ctest after the build. `CONTRIBUTING.md` has the -long-form CMake invocation and the hook, format and lint setup. +`scripts/build.sh debug` builds the `debug` preset into `build/` (CI's tree +name; earlier versions of this script used `build-debug/`), and +`scripts/build.sh test` runs ctest after a debug build. Before pushing, +`scripts/ci-local.sh` runs every `linux-ci.yml` gate in CI's order. +`CONTRIBUTING.md` has the long-form preset invocation and the hook, format +and lint setup. + +One deliberate difference between distro Qt and CI's Qt: the translation gate +needs `lupdate` 6.9+ to resolve namespaced classes correctly, so on a distro +whose Qt Linguist is older, `scripts/check-translations.sh` can report diffs +CI does not. `scripts/install-deps.sh --ci-qt` installs the same Qt 6.9.3 CI +uses if you hit that. ## How it works @@ -268,8 +271,8 @@ extracting it. ```sh scripts/build.sh test -# or, against an existing build tree -ctest --test-dir build-debug --output-on-failure +# or, against an existing debug-preset tree +ctest --preset debug --parallel ``` One `DishTests` executable links the `dish_core` library. It covers the pure diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh new file mode 100755 index 0000000..2a981ef --- /dev/null +++ b/scripts/build-deb.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2026 Dish contributors. +# +# Build a Debian package (.deb) for Dish. +# +# Output: ./dist/dish__.deb +# Install with: sudo apt install ./dist/dish_*.deb +# +# The exact configure + cpack path the CI packaging jobs run (linux-ci.yml's +# package job and release.yml's deb job both call this script), via the +# `package` preset in CMakePresets.json. One deliberate difference between a +# local run and CI: CI builds inside a debian:trixie container so +# dpkg-shlibdeps computes Depends against Debian's own Qt sonames; a package +# built on another distro proves the CPack wiring and install layout, but its +# Depends line is that distro's, not Debian's. +# +# Prerequisites: scripts/install-deps.sh plus dpkg-dev. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "build-deb.sh: only supported on Linux (got $(uname -s))." >&2 + exit 1 +fi + +if ! command -v dpkg-shlibdeps >/dev/null 2>&1; then + echo "build-deb.sh: dpkg-dev is required (provides dpkg-shlibdeps)." >&2 + echo " Install with: sudo apt install dpkg-dev" >&2 + exit 1 +fi + +DIST_DIR="${DIST_DIR:-dist}" +BUILD_DIR="build-package" + +echo "[*] Configuring (preset package) in ${BUILD_DIR}/" +cmake --preset package + +echo "[*] Building" +cmake --build --preset package --parallel + +echo "[*] Packaging (cpack -G DEB)" +mkdir -p "${DIST_DIR}" +cpack --config "${BUILD_DIR}/CPackConfig.cmake" -G DEB -B "${BUILD_DIR}/packages" +cp -f "${BUILD_DIR}"/packages/*.deb "${DIST_DIR}/" + +echo "" +echo "[OK] Package built:" +ls -lh "${DIST_DIR}"/dish_*.deb +echo "" +echo " Install with: sudo apt install ./${DIST_DIR}/dish_*.deb" +echo " Remove with: sudo apt remove dish" diff --git a/scripts/build-rpm.sh b/scripts/build-rpm.sh new file mode 100755 index 0000000..5abe299 --- /dev/null +++ b/scripts/build-rpm.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2026 Dish contributors. +# +# Build an RPM package for Dish. Same rails as scripts/build-deb.sh (the +# `package` preset plus cpack), with the generator swapped: release.yml's rpm +# job calls this inside a fedora container so rpmbuild's soname scanner +# writes the Requires against Fedora's packages; a local run on another +# distro proves the CPack wiring and layout only. +# +# Output: ./dist/dish-..rpm +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "build-rpm.sh: only supported on Linux (got $(uname -s))." >&2 + exit 1 +fi + +if ! command -v rpmbuild >/dev/null 2>&1; then + echo "build-rpm.sh: rpm-build is required (provides rpmbuild)." >&2 + echo " Install with: sudo dnf install rpm-build (or apt install rpm)" >&2 + exit 1 +fi + +DIST_DIR="${DIST_DIR:-dist}" +BUILD_DIR="build-package" + +echo "[*] Configuring (preset package) in ${BUILD_DIR}/" +cmake --preset package + +echo "[*] Building" +cmake --build --preset package --parallel + +echo "[*] Packaging (cpack -G RPM)" +mkdir -p "${DIST_DIR}" +cpack --config "${BUILD_DIR}/CPackConfig.cmake" -G RPM -B "${BUILD_DIR}/packages" +cp -f "${BUILD_DIR}"/packages/*.rpm "${DIST_DIR}/" + +echo "" +echo "[OK] Package built:" +ls -lh "${DIST_DIR}"/dish-*.rpm diff --git a/scripts/build.sh b/scripts/build.sh index 90d1eab..8857567 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -2,13 +2,20 @@ # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright (C) 2026 Dish contributors. # -# Convenience wrapper around cmake/ninja for local development. +# Build Dish on Linux: scripts/build.sh [debug|release|test|clean] # -# Usage: -# scripts/build.sh # release build into ./build-release -# scripts/build.sh debug # debug build into ./build-debug -# scripts/build.sh test # debug build then run ctest -# scripts/build.sh clean # wipe all build directories +# Thin wrapper over the CMake presets in CMakePresets.json, which are the +# single source of configure truth (the same presets linux-ci.yml, codeql.yml +# and release.yml run). +# +# scripts/build.sh # release preset -> build-release/ +# scripts/build.sh debug # debug preset -> build/ +# scripts/build.sh test # debug build, then ctest (offscreen) +# scripts/build.sh clean # wipe the build directories +# +# Directory note: the debug preset writes to build/ (CI's tree name), not the +# build-debug/ this script used before the presets existed. Ninja is required +# (the presets pin CI's generator); scripts/install-deps.sh installs it. set -euo pipefail @@ -23,15 +30,15 @@ for arg in "$@"; do debug|Debug) config="debug" ;; release|Release) config="release" ;; # Tests are a debug concern: an assertion is worth more than the - # optimizer here. + # optimizer here. (CI also tests Release; ci-local.sh covers that.) test|tests) run_tests=1; config="debug" ;; clean) - rm -rf build build-debug build-release build-tidy + rm -rf build build-release build-package build-debug build-tidy build-appimage build-san-* echo "removed build directories" exit 0 ;; -h|--help) - sed -n '1,15p' "${BASH_SOURCE[0]}" + sed -n '5,19p' "${BASH_SOURCE[0]}" exit 0 ;; *) @@ -41,26 +48,22 @@ for arg in "$@"; do esac done -case "${config}" in - debug) cmake_type="Debug"; build_dir="build-debug" ;; - release) cmake_type="Release"; build_dir="build-release" ;; -esac - -generator="Unix Makefiles" -if command -v ninja >/dev/null 2>&1; then - generator="Ninja" +if ! command -v ninja >/dev/null 2>&1; then + echo "ninja not found; the presets pin CI's Ninja generator. Run scripts/install-deps.sh." >&2 + exit 1 fi -cmake -S . -B "${build_dir}" -G "${generator}" \ - -DCMAKE_BUILD_TYPE="${cmake_type}" \ - -DDISH_BUILD_TESTS=ON - -cmake --build "${build_dir}" --parallel +cmake --preset "${config}" +cmake --build --preset "${config}" --parallel if [[ "${run_tests}" -eq 1 ]]; then - # No display in a bare shell; the QML tests construct QGuiApplication. - (cd "${build_dir}" && QT_QPA_PLATFORM=offscreen ctest --output-on-failure --parallel) + # QT_QPA_PLATFORM=offscreen comes from the test preset: no display in a + # bare shell either, and the QML tests construct QGuiApplication. + ctest --preset "${config}" --parallel fi echo -echo "built ${build_dir}/dish (${cmake_type})" +case "${config}" in + debug) echo "built build/dish (Debug)" ;; + release) echo "built build-release/dish (Release)" ;; +esac diff --git a/scripts/check-format.sh b/scripts/check-format.sh new file mode 100755 index 0000000..212eb1e --- /dev/null +++ b/scripts/check-format.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2026 Dish contributors. +# +# clang-format gate: the exact file set and invocation linux-ci.yml runs. One +# script so the workflow and scripts/ci-local.sh cannot drift on the file set +# or the invocation. Check-only; the pre-commit hook is the autofix path. +# +# CI pins clang-format 22.1.4 (PyPI via pipx). Another version can disagree +# on braced-init lists; treat a surprise verdict with suspicion. +set -euo pipefail +cd "$(dirname "$0")/.." + +find src tests -type f \( -name '*.cpp' -o -name '*.h' \) \ + -print0 | xargs -0 clang-format --dry-run --Werror +echo "clang-format: OK" diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh new file mode 100755 index 0000000..cde265b --- /dev/null +++ b/scripts/ci-local.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2026 Dish contributors. +# +# Runs the gates Linux CI runs, in the same order, against the local tree, so a +# green run here means a green run there. Mirrors .github/workflows/linux-ci.yml +# via the same CMakePresets.json presets the workflow drives. +# +# scripts/ci-local.sh every ci-job gate +# scripts/ci-local.sh --no-tidy skip clang-tidy (fastest loop) +# scripts/ci-local.sh --with-package also build and lint the .deb +# scripts/ci-local.sh --with-sanitizers also run the ASan/UBSan and TSan legs +# scripts/ci-local.sh --allow-missing downgrade a missing tool to a notice +# scripts/ci-local.sh --compiler clang set CC/CXX for this run +# +# Without --allow-missing a gate whose tool is absent FAILS rather than printing +# a notice and continuing: a "green" run that silently skipped four gates is +# worse than no run at all. +# +# One run covers one compiler. CI's ci job is a gcc/clang matrix (the lint +# steps run on gcc; the clang leg exists because -Wconversion, -Wshadow and +# -Wold-style-cast diverge materially); reproduce the other leg with +# --compiler clang. The optional sanitizer and package legs run after the core +# ci-job mirror, in the order CI's parallel jobs are listed. +set -euo pipefail +cd "$(dirname "$0")/.." + +TIDY=1 +PACKAGE=0 +SANITIZE=0 +ALLOW_MISSING=0 +COMPILER="" +while [ "$#" -gt 0 ]; do + case "$1" in + --no-tidy) TIDY=0 ;; + --with-package) PACKAGE=1 ;; + --with-sanitizers) SANITIZE=1 ;; + --allow-missing|--allow-missing-tools) ALLOW_MISSING=1 ;; + --compiler) + shift + COMPILER="${1:-}" + ;; + --compiler=*) COMPILER="${1#--compiler=}" ;; + *) echo "unknown flag: $1" >&2; exit 2 ;; + esac + shift +done + +case "$COMPILER" in + "") ;; + gcc) export CC=gcc CXX=g++ ;; + clang) export CC=clang CXX=clang++ ;; + *) echo "--compiler takes gcc or clang (got '$COMPILER')" >&2; exit 2 ;; +esac + +step() { echo ""; echo "=== $1 ==="; } + +# Returns 0 when the caller should run the gate, 1 when it was skipped by +# permission, and exits when a tool CI gates on is missing. +have() { + local tool="$1" + if command -v "$tool" >/dev/null 2>&1; then return 0; fi + if [ "$ALLOW_MISSING" -eq 1 ]; then + echo "::notice:: $tool is not installed; CI gates this. Skipping (--allow-missing)." + return 1 + fi + echo "$tool is not installed and CI gates it. Install it (scripts/install-deps.sh), or re-run with --allow-missing." >&2 + exit 1 +} + +step "clang-format (check only)" +if have clang-format; then + # CI pins 22.1.4; Ubuntu's 18 disagrees on braced-init lists, which is why the + # pin exists. Treat a surprise from another version with suspicion. + want=22.1.4 + got="$(clang-format --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + if [ "$got" != "$want" ]; then + echo "::notice:: clang-format $got, CI pins $want — disagreements are the version, not the code." + fi + bash scripts/check-format.sh +fi + +step "Action pin lint (40-char SHA required)" +# The same awk _security.yml runs, over the same two directories. +fail=0 +while IFS= read -r -d '' file; do + awk ' + /^[[:space:]]*#/ { next } + { sub(/[[:space:]]+#.*$/, "", $0) } + /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]+[^[:space:]]+/ { + line = $0 + sub(/^[[:space:]]*-?[[:space:]]*uses:[[:space:]]+/, "", line) + sub(/[[:space:]]+$/, "", line) + if (line ~ /^\.\//) { next } + if (line ~ /^docker:\/\/[^@]+@sha256:[0-9a-f]{64}$/) { next } + if (line !~ /@[0-9a-f]{40}$/) { printf "%s: %s\n", FILENAME, line; exit 2 } + if (line ~ /@0{40}$/) { printf "%s: %s (all-zero placeholder)\n", FILENAME, line; exit 2 } + } + ' "$file" || fail=1 +done < <(find .github/workflows .github/actions -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) +[ "$fail" -eq 0 ] || { echo "unpinned action reference" >&2; exit 1; } +echo "action pins: OK" + +step "Configure (Debug, preset debug)" +cmake --preset debug + +step "Build" +cmake --build --preset debug --parallel + +step "Run tests (Debug)" +# QT_QPA_PLATFORM=offscreen comes from the test preset: no display in a bare +# shell either, and the QML tests construct QGuiApplication. +ctest --preset debug --parallel + +step "qmllint (QML static analysis)" +if have qmllint; then + # git's * crosses directory levels; 'src/qml/**/*.qml' would miss the + # top-level Main and AppShell. -I "${QT_ROOT_DIR}/qml" matches CI (setup-qt + # exports QT_ROOT_DIR); without it a distro qmllint may resolve Qt's own + # modules differently than CI's. + qt_qml_args=() + if [ -n "${QT_ROOT_DIR:-}" ] && [ -d "${QT_ROOT_DIR}/qml" ]; then + qt_qml_args=(-I "${QT_ROOT_DIR}/qml") + fi + # shellcheck disable=SC2046 + qmllint -I build "${qt_qml_args[@]+"${qt_qml_args[@]}"}" --unqualified info \ + $(git ls-files 'src/qml/*.qml') +fi + +step "QML literal scanner" +./scripts/qml-lint-literals.sh --mode error + +step "Translation catalogues in sync" +if have lupdate; then ./scripts/check-translations.sh; fi + +if [ "$TIDY" -eq 1 ]; then + step "clang-tidy (src, UI + qml excluded — mirrors CI)" + if have clang-tidy; then + # Same build/ the gates above used: CMakeLists exports the compile database + # globally, so a second tree would only re-derive the same src/ entries. + # --warnings-as-errors keeps the sweep-clean check set gated without + # forking the fleet-canonical .clang-tidy (WarningsAsErrors: ''). + find src -type f \( -name '*.cpp' -o -name '*.h' \) \ + ! -path 'src/UI/*' \ + ! -path 'src/qml/*' \ + -print0 | xargs -0 -n1 -P"$(nproc)" \ + clang-tidy -p build --quiet --warnings-as-errors='*' + fi +fi + +step "Configure + build + test (Release)" +# -O3 plus LTO is a different compiler: undefined behaviour that Debug +# tolerates surfaces here, and the suite must pass against the configuration +# that actually ships. +cmake --preset release +cmake --build --preset release --parallel +ctest --preset release --parallel + +step "Hardening flags reached the binary" +readelf -lW build-release/dish | grep -q 'GNU_RELRO' || { echo "no RELRO" >&2; exit 1; } +readelf -dW build-release/dish | grep -qE 'BIND_NOW|FLAGS.*NOW' || { echo "no full RELRO" >&2; exit 1; } +readelf -lW build-release/dish | grep -qE 'GNU_STACK.*RW ' || { echo "executable stack" >&2; exit 1; } +readelf -hW build-release/dish | grep -q 'Type:.*DYN' || { echo "not PIE" >&2; exit 1; } + +if [ "$SANITIZE" -eq 1 ]; then + for san in address+undefined thread; do + step "Sanitizer: ${san}" + # The sanitizer flag is matrix-injected in CI and deliberately not a + # preset; keep these flags in step with linux-ci.yml's sanitize job. + cmake -S . -B "build-san-${san}" -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DDISH_BUILD_TESTS=ON -DDISH_SANITIZER="${san}" + # DishTests only: the Qt Quick app target adds qmlcachegen output where GCC + # refuses atomic_thread_fence under TSan, and the suite never runs it. + cmake --build "build-san-${san}" --parallel --target DishTests + ( cd "build-san-${san}" && \ + QT_QPA_PLATFORM=offscreen \ + ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 \ + UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ + TSAN_OPTIONS="halt_on_error=1:suppressions=$(pwd)/../tests/tsan.suppressions" \ + ctest --output-on-failure --parallel 1 ) + done +fi + +if [ "$PACKAGE" -eq 1 ]; then + step "Packaging metadata" + if have desktop-file-validate; then desktop-file-validate packaging/dish.desktop; fi + if have appstreamcli; then + appstreamcli validate --no-net packaging/com.tinkernorth.Dish.metainfo.xml + fi + + step "Build and lint the .deb" + # CI does this in a debian:trixie container against Debian's Qt; locally it + # still proves the CPack wiring, the install layout and the lintian tags. + if have dpkg-deb && have cpack; then + rm -rf build-package/packages + bash scripts/build-deb.sh + if have lintian; then + lintian --fail-on error --tag-display-limit 0 build-package/packages/*.deb + fi + dpkg-deb -c build-package/packages/*.deb | grep -E 'udev/rules.d|metainfo|applications|copyright' + fi +fi + +echo "" +echo "All local CI gates passed." diff --git a/scripts/ci_local.sh b/scripts/ci_local.sh index a6056c3..78442f6 100755 --- a/scripts/ci_local.sh +++ b/scripts/ci_local.sh @@ -1,166 +1,6 @@ #!/usr/bin/env bash -# Runs the gates Linux CI runs, in the same order, against the local tree, so a -# green run here means a green run there. Mirrors .github/workflows/linux-ci.yml. -# -# scripts/ci_local.sh every gate -# scripts/ci_local.sh --no-tidy skip clang-tidy (fastest loop) -# scripts/ci_local.sh --with-package also build and lint the .deb -# scripts/ci_local.sh --with-sanitizers also run the ASan/UBSan and TSan legs -# scripts/ci_local.sh --allow-missing downgrade a missing tool to a notice -# -# Without --allow-missing a gate whose tool is absent FAILS rather than printing -# a notice and continuing: a "green" run that silently skipped four gates is -# worse than no run at all. -set -euo pipefail -cd "$(dirname "$0")/.." - -TIDY=1 -PACKAGE=0 -SANITIZE=0 -ALLOW_MISSING=0 -for arg in "$@"; do - case "$arg" in - --no-tidy) TIDY=0 ;; - --with-package) PACKAGE=1 ;; - --with-sanitizers) SANITIZE=1 ;; - --allow-missing|--allow-missing-tools) ALLOW_MISSING=1 ;; - *) echo "unknown flag: $arg" >&2; exit 2 ;; - esac -done - -step() { echo ""; echo "=== $1 ==="; } - -# Returns 0 when the caller should run the gate, 1 when it was skipped by -# permission, and exits when a tool CI gates on is missing. -have() { - local tool="$1" - if command -v "$tool" >/dev/null 2>&1; then return 0; fi - if [ "$ALLOW_MISSING" -eq 1 ]; then - echo "::notice:: $tool is not installed; CI gates this. Skipping (--allow-missing)." - return 1 - fi - echo "$tool is not installed and CI gates it. Install it, or re-run with --allow-missing." >&2 - exit 1 -} - -step "clang-format (check only)" -if have clang-format; then - # CI pins 22.1.4; Ubuntu's 18 disagrees on braced-init lists, which is why the - # pin exists. Treat a surprise from another version with suspicion. - want=22.1.4 - got="$(clang-format --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" - if [ "$got" != "$want" ]; then - echo "::notice:: clang-format $got, CI pins $want — disagreements are the version, not the code." - fi - find src tests -type f \( -name '*.cpp' -o -name '*.h' \) -print0 | - xargs -0 clang-format --dry-run --Werror -fi - -step "Action pin lint (40-char SHA required)" -# The same awk _security.yml runs, over the same two directories. -fail=0 -while IFS= read -r -d '' file; do - awk ' - /^[[:space:]]*#/ { next } - { sub(/[[:space:]]+#.*$/, "", $0) } - /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]+[^[:space:]]+/ { - line = $0 - sub(/^[[:space:]]*-?[[:space:]]*uses:[[:space:]]+/, "", line) - sub(/[[:space:]]+$/, "", line) - if (line ~ /^\.\//) { next } - if (line ~ /^docker:\/\/[^@]+@sha256:[0-9a-f]{64}$/) { next } - if (line !~ /@[0-9a-f]{40}$/) { printf "%s: %s\n", FILENAME, line; exit 2 } - if (line ~ /@0{40}$/) { printf "%s: %s (all-zero placeholder)\n", FILENAME, line; exit 2 } - } - ' "$file" || fail=1 -done < <(find .github/workflows .github/actions -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) -[ "$fail" -eq 0 ] || { echo "unpinned action reference" >&2; exit 1; } - -step "Configure (Debug, tests on)" -cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DDISH_BUILD_TESTS=ON - -step "Build" -cmake --build build --parallel - -step "Run tests (Debug)" -# No display in a bare shell either; the QML tests construct QGuiApplication. -(cd build && QT_QPA_PLATFORM=offscreen ctest --output-on-failure --parallel) - -step "qmllint (QML static analysis)" -if have qmllint; then - # git's * crosses directory levels; 'src/qml/**/*.qml' would miss the - # top-level Main and AppShell. - # shellcheck disable=SC2046 - qmllint -I build --unqualified info $(git ls-files 'src/qml/*.qml') -fi - -step "QML literal scanner" -./scripts/qml-lint-literals.sh --mode error - -step "Translation catalogues in sync" -if have lupdate; then ./scripts/check-translations.sh; fi - -if [ "$TIDY" -eq 1 ]; then - step "clang-tidy (src, UI + qml excluded — mirrors CI)" - if have clang-tidy; then - # Same build/ the gates above used: CMakeLists exports the compile database - # globally, so a second tree would only re-derive the same src/ entries. - find src -type f \( -name '*.cpp' -o -name '*.h' \) ! -path 'src/UI/*' ! -path 'src/qml/*' -print0 | - xargs -0 -n1 -P"$(nproc)" clang-tidy -p build --quiet --warnings-as-errors='*' - fi -fi - -if [ "$SANITIZE" -eq 1 ]; then - for san in address+undefined thread; do - step "Sanitizer: ${san}" - cmake -S . -B "build-san-${san}" -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DDISH_BUILD_TESTS=ON -DDISH_SANITIZER="${san}" - # DishTests only: the Qt Quick app target adds qmlcachegen output where GCC - # refuses atomic_thread_fence under TSan, and the suite never runs it. - cmake --build "build-san-${san}" --parallel --target DishTests - ( cd "build-san-${san}" && \ - QT_QPA_PLATFORM=offscreen \ - ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 \ - UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ - TSAN_OPTIONS="halt_on_error=1:suppressions=$(pwd)/../tests/tsan.suppressions" \ - ctest --output-on-failure --parallel 1 ) - done -fi - -step "Configure + build + test (Release)" -cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release -DDISH_BUILD_TESTS=ON -cmake --build build-release --parallel -(cd build-release && QT_QPA_PLATFORM=offscreen ctest --output-on-failure --parallel) - -step "Hardening flags reached the binary" -readelf -lW build-release/dish | grep -q 'GNU_RELRO' || { echo "no RELRO" >&2; exit 1; } -readelf -dW build-release/dish | grep -qE 'BIND_NOW|FLAGS.*NOW' || { echo "no full RELRO" >&2; exit 1; } -readelf -lW build-release/dish | grep -qE 'GNU_STACK.*RW ' || { echo "executable stack" >&2; exit 1; } -readelf -hW build-release/dish | grep -q 'Type:.*DYN' || { echo "not PIE" >&2; exit 1; } - -if [ "$PACKAGE" -eq 1 ]; then - step "Packaging metadata" - if have desktop-file-validate; then desktop-file-validate packaging/dish.desktop; fi - if have appstreamcli; then - appstreamcli validate --no-net packaging/com.tinkernorth.Dish.metainfo.xml - fi - - step "Build and lint the .deb" - # A package built against a Qt below the 6.7 floor is not what ships; CI does - # this in a debian:trixie container. Locally it still proves the CPack wiring, - # the install layout and the lintian tags. - if have dpkg-deb && have cpack; then - cmake -S . -B build-package -G Ninja \ - -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DDISH_BUILD_TESTS=OFF - cmake --build build-package --parallel - rm -rf build-package/packages - cpack --config build-package/CPackConfig.cmake -G DEB -B build-package/packages - if have lintian; then - lintian --fail-on error --tag-display-limit 0 build-package/packages/*.deb - fi - dpkg-deb -c build-package/packages/*.deb | grep -E 'udev/rules.d|metainfo|applications|copyright' - fi -fi - -echo "" -echo "All local CI gates passed." +# Thin forwarder kept for muscle memory: the CI-parity runner now lives at +# scripts/ci-local.sh (the fleet-wide name; satellite and dish-windows carry +# the same contract). Same flags: --no-tidy --with-package --with-sanitizers +# --allow-missing --compiler gcc|clang. +exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ci-local.sh" "$@" diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh new file mode 100755 index 0000000..d6a3395 --- /dev/null +++ b/scripts/install-deps.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2026 Dish contributors. +# +# Install the Linux build toolchain for Dish: scripts/install-deps.sh [--ci-qt] +# +# Default: the distro packages (apt; the README "Build from source" list plus +# the lint tools CI gates with, clang-format pinned 22.1.4 via pipx like every +# CI lane in the fleet). +# +# --ci-qt additionally installs the exact Qt CI builds against (6.9.3 via +# aqtinstall 3.3.0, mirroring .github/actions/setup-qt) into ~/Qt and prints +# the exports to use it. The tradeoff, so you can choose deliberately: +# +# * Distro Qt (default): integrates with your package manager, but Debian +# 13 ships 6.8 and Ubuntu 24.04 only 6.4 (below the 6.7 floor). The +# translation gate is the sharp edge: lupdate only resolves a class +# defined across a header/source pair back to its namespace from 6.9 on, +# so scripts/check-translations.sh can report spurious diffs under an +# older lupdate that CI's 6.9.3 does not produce. +# * CI Qt (--ci-qt): byte-for-byte what linux-ci.yml uses, so every gate +# agrees with CI; ~1.5 GB under ~/Qt, and you export CMAKE_PREFIX_PATH / +# QT_ROOT_DIR / LD_LIBRARY_PATH yourself (printed at the end). +set -euo pipefail + +CI_QT=0 +for arg in "$@"; do + case "$arg" in + --ci-qt) CI_QT=1 ;; + -h|--help) sed -n '5,24p' "${BASH_SOURCE[0]}"; exit 0 ;; + *) echo "unknown flag: $arg" >&2; exit 2 ;; + esac +done + +step() { echo ""; echo "=== $1 ==="; } + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "install-deps.sh: only supported on Linux (got $(uname -s))." >&2 + exit 1 +fi +if ! command -v apt-get >/dev/null 2>&1; then + echo "This script drives apt (Debian/Ubuntu, what CI runs). On another distro install:" >&2 + echo " gcc/clang, cmake, ninja, pkg-config, Qt 6.7+ (base, declarative, svg, tools/linguist)," >&2 + echo " libsodium, SDL2, OpenSSL, Opus and DBus development headers, catch2, clang-tidy," >&2 + echo " librsvg2 tools, and clang-format 22.1.4 (pipx install clang-format==22.1.4)." >&2 + exit 1 +fi + +step "apt packages (the README list + the lint tools CI gates with)" +sudo apt-get update +sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build pkg-config \ + qt6-base-dev qt6-base-dev-tools qt6-declarative-dev qt6-svg-dev \ + qt6-tools-dev qt6-l10n-tools \ + libsodium-dev libsdl2-dev libssl-dev libopus-dev libdbus-1-dev catch2 \ + librsvg2-bin clang-tidy pipx + +step "clang-format 22.1.4 (pipx; the pin every CI lane uses)" +pipx install clang-format==22.1.4 || pipx upgrade clang-format || true +pipx ensurepath +if ! command -v clang-format >/dev/null 2>&1; then + echo "[NOTE] clang-format installed via pipx; open a new shell (pipx ensurepath) to pick it up." +fi + +if [ "$CI_QT" -eq 1 ]; then + step "Qt 6.9.3 via aqtinstall 3.3.0 (what .github/actions/setup-qt installs)" + pipx install "aqtinstall==3.3.0" || pipx upgrade aqtinstall || true + export PATH="$HOME/.local/bin:$PATH" + aqt install-qt linux desktop 6.9.3 --outputdir "$HOME/Qt" + root="$(find "$HOME/Qt/6.9.3" -mindepth 1 -maxdepth 1 -type d -print -quit)" + echo "" + echo "[OK] Qt 6.9.3 at ${root}. To build against it, export (e.g. in ~/.bashrc):" + echo " export CMAKE_PREFIX_PATH=\"${root}\"" + echo " export QT_ROOT_DIR=\"${root}\"" + echo " export LD_LIBRARY_PATH=\"${root}/lib\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}\"" + echo " export PATH=\"${root}/bin:\$PATH\"" +fi + +echo "" +echo "=== Done ===" +echo "" +echo "Next steps:" +echo " 1. Build: scripts/build.sh" +echo " 2. Test: scripts/build.sh test" +echo " 3. CI parity before pushing: scripts/ci-local.sh" diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh index 6201366..4b855cd 100755 --- a/scripts/setup-hooks.sh +++ b/scripts/setup-hooks.sh @@ -21,5 +21,5 @@ echo " Debian/Ubuntu: sudo apt install clang-format clang-tidy" echo " Fedora: sudo dnf install clang-tools-extra" echo " Arch: sudo pacman -S clang" echo -echo "Note: clang-tidy needs build-debug/compile_commands.json — generate it with:" +echo "Note: clang-tidy needs build/compile_commands.json — generate it with:" echo " scripts/build.sh debug"