From 006cec17983bc5408ebbf480a01ac19f98e05fe1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 21:44:50 +0800 Subject: [PATCH 1/9] 0.17.0: the general library for build programs, the resolved toolset in deps-vcpkg and deps-cmake, and a test kit for plugins (mcpp#734) - L2, `plugins-core` (renamed from `surface`, which is kept as an alias until 2027-03-28): `mcpp.plugins.declare`, the new `mcpp.plugins.toolset`, which turns the engine's build information into how a toolset reaches a foreign build system (`instance`, `chain`, `detected`), and the new `mcpp.plugins.fs`, which takes `write_if_changed` and the placement helpers from `mcpp.deps`. `plugins-testing` adds `mcpp.plugins.testing`. - deps-vcpkg builds the ports with the toolset mcpp resolved. A Visual Studio instance is selected with VCPKG_VISUAL_STUDIO_PATH and the standard triplet; any other toolset is named in a derived triplet `-mcpp-` whose text and chain-loaded toolchain hold no path, with the derived triplet as the host triplet and a kept PATH on the MSVC ABI. An MSBuild port under `chain` is refused by name. The C runtime follows the program's contract, and a project triplet that contradicts it is refused naming both statements. - deps-cmake: the Visual Studio generator pointed at the instance, or Ninja with mcpp's ninja and the named tools; CMAKE_MSVC_RUNTIME_LIBRARY follows the program; one build directory per toolset statement. - Compatibility units under `compat/` (`toolset = detected`, the 0.16.0 names in `mcpp::deps`), and a CI check that fails once a unit's date has passed. - `[package] mcpp = ">=2026.9.28.3"`. - Tests: `tests/plugin-logic` (14 cases on stated contexts, every host), the C runtime leg on the Visual Studio row, and a Windows job with Visual Studio masked and the managed toolset: a CMake port, a CMake subproject, a make port (icu) and the MSBuild refusal (libusb). --- .github/scripts/check-compat-retirement.sh | 51 +++ .github/scripts/check-deps-and-qt.sh | 156 +++++++- .github/workflows/ci.yml | 174 ++++++++- README.md | 37 +- deps/archive.cppm | 5 +- deps/cmake.cppm | 151 ++++++-- deps/compat/names.cppm | 60 +++ deps/deps.cppm | 107 +----- deps/vcpkg.cppm | 407 ++++++++++++++++----- docs/deps.md | 109 +++++- docs/plugin-development.md | 49 ++- docs/rules-qt.md | 2 +- mcpp.toml | 102 ++++-- src/compat/detected_toolset.cppm | 47 +++ src/fs.cppm | 102 ++++++ src/plugins.cppm | 2 +- src/testing.cppm | 340 +++++++++++++++++ src/toolset.cppm | 254 +++++++++++++ tests/plugin-logic/build.mcpp | 278 ++++++++++++++ tests/plugin-logic/mcpp.toml | 24 ++ tests/plugin-logic/src/main.cpp | 1 + tests/vcpkg-make-port/build.mcpp | 10 + tests/vcpkg-make-port/mcpp.toml | 22 ++ tests/vcpkg-make-port/src/main.cpp | 3 + tests/vcpkg-make-port/vcpkg.json | 6 + tests/vcpkg-msbuild-port/build.mcpp | 10 + tests/vcpkg-msbuild-port/mcpp.toml | 23 ++ tests/vcpkg-msbuild-port/src/main.cpp | 2 + tests/vcpkg-msbuild-port/vcpkg.json | 6 + 29 files changed, 2244 insertions(+), 296 deletions(-) create mode 100755 .github/scripts/check-compat-retirement.sh create mode 100644 deps/compat/names.cppm create mode 100644 src/compat/detected_toolset.cppm create mode 100644 src/fs.cppm create mode 100644 src/testing.cppm create mode 100644 src/toolset.cppm create mode 100644 tests/plugin-logic/build.mcpp create mode 100644 tests/plugin-logic/mcpp.toml create mode 100644 tests/plugin-logic/src/main.cpp create mode 100644 tests/vcpkg-make-port/build.mcpp create mode 100644 tests/vcpkg-make-port/mcpp.toml create mode 100644 tests/vcpkg-make-port/src/main.cpp create mode 100644 tests/vcpkg-make-port/vcpkg.json create mode 100644 tests/vcpkg-msbuild-port/build.mcpp create mode 100644 tests/vcpkg-msbuild-port/mcpp.toml create mode 100644 tests/vcpkg-msbuild-port/src/main.cpp create mode 100644 tests/vcpkg-msbuild-port/vcpkg.json diff --git a/.github/scripts/check-compat-retirement.sh b/.github/scripts/check-compat-retirement.sh new file mode 100755 index 0000000..13eca91 --- /dev/null +++ b/.github/scripts/check-compat-retirement.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Compatibility units retire themselves (mcpp#734, design §3.7). +# +# A compatibility unit keeps a behaviour for six months after the release that +# replaced it. Its header states the date: +# +# // COMPATIBILITY UNIT (# COMPATIBILITY UNIT in mcpp.toml) +# // retires: 2027-03-28 +# +# This check lists every unit and fails when one is past its date, so that a +# deferral cannot outlive its reason unnoticed. Every file under a `compat/` +# directory must carry the header; a file there without one is reported, which +# keeps the list from being empty because the marker was misspelt. +# +# bash .github/scripts/check-compat-retirement.sh +# COMPAT_TODAY=2027-03-29 bash .github/scripts/check-compat-retirement.sh # the failure, on demand +set -euo pipefail +cd "$(dirname "$0")/../.." + +today="${COMPAT_TODAY:-$(date -u +%Y-%m-%d)}" +fail=0 +units=0 + +# Every file under a compat/ directory, outside build outputs and fixtures. +while IFS= read -r f; do + grep -qE '^(//|#) COMPATIBILITY UNIT$' "$f" || { echo "FAIL: $f is under compat/ and has no COMPATIBILITY UNIT header"; fail=1; } +done < <(find . -path ./tests -prune -o -path '*/target' -prune -o -path '*/compat/*' -type f -print | sort) + +# Every header, wherever it is: its retirement date. +while IFS=$'\t' read -r file date; do + units=$((units + 1)) + if [ -z "$date" ]; then + echo "FAIL: $file: a COMPATIBILITY UNIT without a 'retires:' date" + fail=1 + elif [[ "$today" > "$date" ]]; then + echo "FAIL: $file: retired on $date (today is $today); remove the unit and what it keeps" + fail=1 + else + echo "ok: $file retires on $date" + fi +done < <(find . -path ./tests -prune -o -path '*/target' -prune -o -path ./.git -prune -o \ + \( -name '*.cppm' -o -name 'mcpp.toml' \) -type f -print | sort | + xargs awk ' + /^(\/\/|#) COMPATIBILITY UNIT$/ { if (pending) print FILENAME "\t"; pending = 1; next } + pending && /retires:/ { match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/); + print FILENAME "\t" substr($0, RSTART, RLENGTH); pending = 0 } + ENDFILE { if (pending) print FILENAME "\t"; pending = 0 }') + +[ "$units" -gt 0 ] || { echo "FAIL: no COMPATIBILITY UNIT header found"; fail=1; } +echo "$units compatibility unit(s)" +exit "$fail" diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index 8320faf..1a87fa9 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -70,6 +70,22 @@ vcpkg_consumer() { fail "share/fmt/copyright was not deployed beside the program" echo "ok: a file of the prefix is deployed beside the program" + # THE MECHANISM (0.17.0). On the Visual Studio row the instance mcpp + # resolved is selected and the standard triplet is used, so the ABI hash is + # the one vcpkg computes by itself; on the other rows a derived triplet + # names the resolved tools. + if is_windows; then + grep -rqs 'VCPKG_VISUAL_STUDIO_PATH=' target --include=build.ninja || + fail "the installation does not select the Visual Studio instance mcpp resolved" + [ -d target/vcpkg_installed/x64-windows/x64-windows ] || fail "the instance mechanism did not use x64-windows" + echo "ok: the installation selects the Visual Studio instance and keeps the standard triplet" + else + ls -d target/vcpkg_installed/*-mcpp-*/*-mcpp-* > /dev/null 2>&1 || + fail "no derived -mcpp- prefix under target/vcpkg_installed" + grep -rqs 'MCPP_VCPKG_CXX=' target --include=build.ninja || fail "the installation does not hand vcpkg the resolved compiler" + echo "ok: the installation names the resolved compiler in a derived triplet" + fi + if is_windows; then # The pack collects it from the runtime search directory. ls target/vcpkg_installed/x64-windows/x64-windows/bin/fmt.dll > /dev/null || fail "x64-windows built no fmt.dll" @@ -84,22 +100,25 @@ vcpkg_consumer() { } # LINUX UNDER A libc++ TOOLCHAIN. The host compiler vcpkg and CMake find uses -# libstdc++, whose `std::` symbols a libc++ program cannot link. The default -# triplet is then the generated `x64-linux-libcxx`, whose ports build with -# mcpp's clang; fmt's interface returns `std::string`, so the link itself is the -# criterion. The prefix is then shown to survive the default toolchain's own -# installation, which vcpkg would remove if the two triplets shared one. +# libstdc++, whose `std::` symbols a libc++ program cannot link. The ports +# build with mcpp's clang through a derived triplet `-linux-mcpp-` +# (0.17.0; `-linux-libcxx` before); fmt's interface returns +# `std::string`, so the link itself is the criterion. The prefix is then shown +# to survive the default toolchain's own installation, which vcpkg would remove +# if the two triplets shared one. vcpkg_libcxx() { - local llvm="${MCPP_LLVM:-llvm@22.1.8}" gen=x64-linux-libcxx - [ "$(uname -m)" = aarch64 ] && gen=arm64-linux-libcxx + local llvm="${MCPP_LLVM:-llvm@22.1.8}" gen cd "$ROOT/tests/vcpkg-consumer" rm -rf target vcpkg_installed mkdir -p target/ci "$MCPP" build --toolchain "$llvm" 2>&1 | tee target/ci/libcxx-build.log "$MCPP" run --toolchain "$llvm" | tee target/ci/libcxx-run.log grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/libcxx-run.log || fail "the libc++ program did not print through fmt" - local lib; lib=$(find target/vcpkg_installed -path "*/$gen/$gen/lib/libfmt.a" | head -1) - [ -n "$lib" ] || fail "no $gen prefix with libfmt.a" + local lib; lib=$(find target/vcpkg_installed -path "*-mcpp-*/lib/libfmt.a" | head -1) + [ -n "$lib" ] || fail "no derived -linux-mcpp- prefix with libfmt.a" + gen=$(basename "$(dirname "$(dirname "$lib")")") + find target -path '*deps-vcpkg/triplets/*' -name "$gen.cmake" -exec grep -l 'clang' {} + | grep -q . || + fail "the derived triplet $gen does not record the clang toolset" grep -q 'std::__1::' <(nm -C "$lib") || fail "$lib is not built against libc++" echo "ok: under $llvm the ports build with mcpp's clang and the program links them" @@ -107,6 +126,8 @@ vcpkg_libcxx() { "$MCPP" run | tee target/ci/default-run.log grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/default-run.log || fail "the default toolchain's program did not print through fmt" [ -f "$lib" ] || fail "the default toolchain's installation removed the $gen prefix" + [ "$(ls -d target/vcpkg_installed/*-mcpp-* | wc -l)" -ge 2 ] || + fail "the default toolchain did not derive a triplet of its own" local stamp; stamp=$(find target -path '*deps-vcpkg*' -name "$gen.stamp" | head -1) [ -n "$stamp" ] || fail "no $gen installation stamp" touch -r "$stamp" target/ci/before-switch-back @@ -340,6 +361,116 @@ qt_import_only() { echo "ok: a package that enables rules-qt only for its module runs no build program and reports nothing" } +# THE TEST KIT (0.17.0). The deps members' decisions -- which mechanism, which +# triplet, which C runtime -- tested against stated build contexts on every +# host, with nothing installed. The build program is the test; the verdicts +# are counted against the cases the fixture declares. +plugin_logic() { + cd "$ROOT/tests/plugin-logic" + rm -rf target + mkdir -p target/ci + "$MCPP" build > target/ci/build.log 2>&1 || { cat target/ci/build.log; fail "a plugin-logic case failed"; } + local results declared passed + results=$(find target -path '*plugins-testing/results.txt' | head -1) + [ -n "$results" ] || fail "the kit wrote no results.txt" + declared=$(grep -cE '^ \{ "' build.mcpp) + passed=$(grep -c '^PASS ' "$results") + cat "$results" + [ "$declared" -gt 0 ] && [ "$passed" -eq "$declared" ] || + fail "$passed of $declared declared cases passed" + echo "ok: $passed of $declared plugin-logic cases passed" +} + +# WINDOWS, THE PROGRAM'S C RUNTIME (0.17.0). Run on the Visual Studio row. A +# self-contained program links the C runtime statically, so its ports must be +# built against the same one: the default triplet becomes x64-windows-static, +# and a project triplet that says otherwise is refused naming both statements +# (a static library of the other runtime fails the link with /failifmismatch; +# a DLL of it puts a second C++ runtime into the process without a word). +vcpkg_crt() { + is_windows || { echo "skip: the C runtime linkage is an MSVC-ABI question"; return 0; } + local d="$ROOT/tests/.ci-vcpkg-crt" + rm -rf "$d" + cp -r "$ROOT/tests/vcpkg-consumer" "$d" + trap 'rm -rf "$ROOT/tests/.ci-vcpkg-crt"' RETURN + cd "$d" + rm -rf target vcpkg_installed + printf '\n[build]\ncxx_runtime = "self-contained"\n' >> mcpp.toml + mkdir -p target/ci + "$MCPP" build 2>&1 | tee target/ci/static-build.log + "$MCPP" run | tee target/ci/static-run.log + grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/static-run.log || fail "the self-contained program did not run" + [ -d target/vcpkg_installed/x64-windows-static/x64-windows-static ] || + fail "a self-contained program's ports were not installed with x64-windows-static" + ! find target -path '*/bin/*' -iname 'fmt.dll' | grep -q . || fail "a self-contained program received fmt.dll" + echo "ok: a self-contained program's ports link the C runtime statically, and the link agrees" + + sed -i 's|^ o.libraries = { "fmt" };| o.libraries = { "fmt" };\n o.triplet = "x64-windows";|' build.mcpp + grep -q 'o.triplet = "x64-windows";' build.mcpp || fail "the fixture edit did not apply" + if "$MCPP" build > target/ci/contradiction.log 2>&1; then + cat target/ci/contradiction.log; fail "a triplet contradicting the program's C runtime was accepted" + fi + grep -q "the triplet 'x64-windows' links the C runtime dynamic" target/ci/contradiction.log || + { cat target/ci/contradiction.log; fail "the refusal does not name the triplet's linkage"; } + echo "ok: a triplet that contradicts the program's C runtime is refused, naming both statements" +} + +# WINDOWS WITHOUT VISUAL STUDIO (0.17.0). Run on the row whose Visual Studio is +# masked, with the managed toolset `$MSVC_MANAGED` (xim:msvc@). The +# toolset is named in a derived triplet: a CMake port (fmt) builds, the program +# links it, and a CMake subproject builds with Ninja and the toolset's cl.exe. +vcpkg_managed() { + : "${MSVC_MANAGED:?MSVC_MANAGED names the managed toolset, e.g. xim:msvc@14.44.35207}" + cd "$ROOT/tests/vcpkg-consumer" + rm -rf target vcpkg_installed + mkdir -p target/ci + "$MCPP" build --toolchain "$MSVC_MANAGED" 2>&1 | tee target/ci/managed-build.log + "$MCPP" run --toolchain "$MSVC_MANAGED" | tee target/ci/managed-run.log + grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/managed-run.log || fail "the program did not print through fmt" + ls -d target/vcpkg_installed/x64-windows-mcpp-*/x64-windows-mcpp-*/bin/fmt.dll > /dev/null 2>&1 || + fail "no derived x64-windows-mcpp- prefix with fmt.dll" + grep -rqs -- '--host-triplet=x64-windows-mcpp-' target --include=build.ninja || fail "the host triplet is not the derived one" + echo "ok: without Visual Studio, a CMake port builds with the managed toolset named in a derived triplet" + + cd "$ROOT/tests/cmake-consumer" + rm -rf target + mkdir -p target/ci + "$MCPP" build --toolchain "$MSVC_MANAGED" 2>&1 | tee target/ci/managed-build.log + "$MCPP" run --toolchain "$MSVC_MANAGED" | grep -q '^cmake-consumer: greet says 42$' || fail "the cmake-consumer did not run" + grep -rqs 'CMAKE_GENERATOR:INTERNAL=Ninja' target --include=CMakeCache.txt || fail "the subproject was not configured with Ninja" + grep -rqsi 'CMAKE_CXX_COMPILER:[A-Z]*=.*xim-x-msvc.*cl.exe' target --include=CMakeCache.txt || + fail "the subproject was not configured with the managed toolset's cl.exe" + echo "ok: without Visual Studio, a CMake subproject builds with Ninja and the managed toolset" +} + +# The same row: a port that needs MSBuild is refused by name. +vcpkg_msbuild_refused() { + : "${MSVC_MANAGED:?MSVC_MANAGED names the managed toolset}" + cd "$ROOT/tests/vcpkg-msbuild-port" + rm -rf target + mkdir -p target/ci + if "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1; then + cat target/ci/build.log; fail "an MSBuild port built without Visual Studio" + fi + grep -q "builds with MSBuild, which needs a Visual Studio instance" target/ci/build.log || + { tail -60 target/ci/build.log; fail "the MSBuild port failed without the plugin's reason"; } + echo "ok: an MSBuild port is refused by name without Visual Studio" +} + +# The same row: a port that configures with make under msys (icu) finds +# link.exe on the PATH vcpkg keeps. +vcpkg_make_port() { + : "${MSVC_MANAGED:?MSVC_MANAGED names the managed toolset}" + cd "$ROOT/tests/vcpkg-make-port" + rm -rf target + mkdir -p target/ci + "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1 || + { tail -80 target/ci/build.log; fail "the make-based port did not build with the managed toolset"; } + "$MCPP" run --toolchain "$MSVC_MANAGED" | tee target/ci/run.log + grep -qE '^vcpkg-make-port: icu [0-9]' target/ci/run.log || fail "the program did not read icu's version" + echo "ok: a make-based port builds with the managed toolset first on the kept PATH" +} + case "${1:-}" in vcpkg-consumer) vcpkg_consumer ;; vcpkg-libcxx) vcpkg_libcxx ;; @@ -350,5 +481,10 @@ case "${1:-}" in qt-widgets-consumer) qt_widgets_consumer ;; qt-sdk-consumer) qt_sdk_consumer ;; qt-import-only) qt_import_only ;; - *) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only"; exit 2 ;; + plugin-logic) plugin_logic ;; + vcpkg-crt) vcpkg_crt ;; + vcpkg-managed) vcpkg_managed ;; + vcpkg-msbuild-refused) vcpkg_msbuild_refused ;; + vcpkg-make-port) vcpkg_make_port ;; + *) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer|qt-sdk-consumer|qt-import-only|plugin-logic|vcpkg-crt|vcpkg-managed|vcpkg-msbuild-refused|vcpkg-make-port"; exit 2 ;; esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4fa593..354e94a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1441,7 +1441,7 @@ jobs: # incomplete while the extractor was what broke. # MEMBERS, NOT EVERY FEATURE. A member is a feature named for its # family -- `rules-*`, `tools-*`, `dist-*`, the README's naming table - # -- and a consumer names it. `surface` is not one: it carries the + # -- and a consumer names it. `plugins-core` is not one: it carries the # build-program half of the generated surface, every member implies # it, and a consumer never writes it. Keeping it out keeps this check # meaning "the fixture names every member a consumer can activate"; @@ -1639,6 +1639,21 @@ jobs: done [ "$fail" -eq 0 ] + # ── the general library and the test kit (0.17.0, mcpp#734) ─────────── + # + # The deps members' decisions -- which mechanism reaches the foreign + # system, which triplet, which C runtime -- tested against stated build + # contexts (a Visual Studio row, a managed MSVC row, the Linux GCC and + # libc++ rows) with nothing installed. The verdicts are counted against + # the cases the fixture declares. + - name: the deps members' decisions hold against stated build contexts (mcpp.plugins.testing) + run: bash .github/scripts/check-deps-and-qt.sh plugin-logic + + # A compatibility unit keeps a behaviour for six months after the release + # that replaced it, and this step fails once its date has passed. + - name: compatibility units retire themselves + run: bash .github/scripts/check-compat-retirement.sh + # ── deps-vcpkg, deps-cmake and rules-qt (0.13.0) ────────────────────── # # vcpkg keeps everything it reuses in one per-user directory -- its @@ -1845,7 +1860,7 @@ jobs: # `sources` once per feature and reported the fixture as # incomplete while the extractor was what broke. # MEMBERS, NOT EVERY FEATURE: a feature named for its family - # (`rules-*`, `tools-*`, `dist-*`), which a consumer names; `surface`, + # (`rules-*`, `tools-*`, `dist-*`), which a consumer names; `plugins-core`, # which every member implies and no consumer writes, is not one. By # name and not by "nothing implies it" since 0.11.0, when # `dist-apk-kotlin` and `dist-apk-maven` came to imply `dist-apk`. The @@ -2331,6 +2346,11 @@ jobs: exit 1; } echo "ok: $(grep -m1 'entries declared by dependencies' prov.log)" + # The test kit on this host: the same stated contexts, so a decision that + # depends on the host the build program runs on is caught here. + - name: the deps members' decisions hold against stated build contexts (mcpp.plugins.testing) + run: bash .github/scripts/check-deps-and-qt.sh plugin-logic + # ── deps-vcpkg, deps-cmake and rules-qt (0.13.0) ────────────────────── # # The same criteria as the Linux job, from the same script. On Windows the @@ -2357,6 +2377,13 @@ jobs: - name: deps-cmake builds a CMake subproject as an action and links it run: bash .github/scripts/check-deps-and-qt.sh cmake-consumer + # The C runtime linkage follows the program's contract (0.17.0): a + # self-contained program's ports are x64-windows-static, and a project + # triplet that contradicts it is refused. Windows only; the function + # skips elsewhere. + - name: deps-vcpkg follows the program's C runtime, and refuses a contradicting triplet + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-crt + - name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK the project declares run: bash .github/scripts/check-deps-and-qt.sh qt-consumer @@ -2371,3 +2398,146 @@ jobs: - name: a package that enables rules-qt only for its module builds without a report run: bash .github/scripts/check-deps-and-qt.sh qt-import-only + + # ── deps WITHOUT VISUAL STUDIO (0.17.0, mcpp#734) ─────────────────────────── + # + # The managed toolset `xim:msvc@14.44.35207` on a runner whose Visual Studio + # is masked: no instance exists, so deps-vcpkg and deps-cmake name the + # toolset (the `chain` mechanism). The legs are the ones the design measured + # before implementing it (design §2.2, M3a to M3c and M2c): a CMake port + # (fmt) and a CMake subproject build; a make-based port (icu) finds link.exe + # on the PATH vcpkg keeps; an MSBuild port (libusb) is refused by name. + deps-managed-msvc: + name: deps without Visual Studio (windows x86_64, managed msvc) + runs-on: windows-2022 + timeout-minutes: 150 + defaults: + run: + shell: bash + env: + MSVC_MANAGED: xim:msvc@14.44.35207 + steps: + - uses: actions/checkout@v4 + + - name: Pin MCPP_HOME under this runner's home + run: | + set -e + # The value has to be in the HOST's path syntax, not Git Bash's. + # `$HOME` there is `/c/Users/runneradmin`, and mcpp hands this + # variable to xlings, which is a native Windows program: it answered + # `The filename, directory name, or volume label syntax is + # incorrect.` and exited 1, from inside a provisioning step whose own + # message named a package rather than a path. + H="$HOME/.mcpp" + command -v cygpath >/dev/null && H=$(cygpath -m "$H") + echo "MCPP_HOME=$H" >> "$GITHUB_ENV" + + - name: Cache the mcpp sandbox + uses: actions/cache@v4 + with: + path: ~/.mcpp + key: mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}-${{ hashFiles('tests/vcpkg-consumer/mcpp.toml') }} + restore-keys: | + mcpp-sandbox-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}- + + - name: Fetch the released mcpp + run: | + set -e + curl -L -fsS --retry 3 --retry-all-errors -o mcpp.pkg \ + "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-windows-x86_64.zip" + case "windows-x86_64.zip" in + *.zip) unzip -q mcpp.pkg ;; + *) tar -xzf mcpp.pkg ;; + esac + dir="mcpp-${MCPP_VERSION}-windows-x86_64" + MCPP="$PWD/$dir/bin/mcpp" + "$MCPP" --version + # The bundled xlings, named explicitly because MCPP_HOME is pinned + # away from the tarball. See the Linux job for the measurement. + # + # The suffix is the host's: the Windows distribution ships + # `registry/bin/xlings.exe`, and a variable pointing at a file that + # does not exist produces `error: xlings binary not found` naming + # three remedies, none of which is the one that applies. + # Two host differences, and the second one is the interesting one. + # The suffix: the Windows distribution ships `xlings.exe`. The PATH + # SHAPE: this job runs under Git Bash, whose `$PWD` is a POSIX path + # (`/d/a/...`), while `mcpp` is a native Windows program and reads + # this variable as a Windows path. Both failures report the same + # `error: xlings binary not found` and offer the same three remedies, + # none of which is "the path you gave is in the other syntax". + XL="$PWD/$dir/registry/bin/xlings" + [ -f "$XL" ] || XL="$XL.exe" + test -f "$XL" || { echo "no vendored xlings at $XL"; exit 1; } + command -v cygpath >/dev/null && XL=$(cygpath -m "$XL") + export MCPP_VENDORED_XLINGS="$XL" + "$MCPP" self config --mirror GLOBAL + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + + # THE ENGINE THE STEPS BELOW RUN: the release fetched above, or mcpp built + # from `MCPP_SOURCE_REF` (see the workflow's `env`). A cold build of mcpp + # is the longest step of the job, which is what its timeout is sized for. + - name: the mcpp under review, built from MCPP_SOURCE_REF when it is set + timeout-minutes: 45 + run: bash .github/scripts/mcpp-under-review.sh + + # THE CRITERION FOR THE STEP ABOVE, IN A LATER STEP, because what that step + # writes to `GITHUB_ENV` governs only the steps after it. + - name: the steps below run the mcpp under review + if: env.MCPP_SOURCE_REF != '' + timeout-minutes: 5 + run: | + set -eu + [ "$MCPP" = "$MCPP_UNDER_REVIEW" ] || { + echo "::error::the steps run $MCPP, and the mcpp built from $MCPP_SOURCE_REF is $MCPP_UNDER_REVIEW"; exit 1; } + version=$("$MCPP" --version | head -1) + [ "$version" = "$MCPP_UNDER_REVIEW_VERSION" ] || { + echo "::error::$MCPP prints '$version', and the build printed '$MCPP_UNDER_REVIEW_VERSION'"; exit 1; } + # A reading, not a criterion: the two engines' versions, which may be + # equal when the reference has not moved the version yet, so the + # criterion above is the path and not the version. + echo "READING released: $("$MCPP_RELEASED" --version | head -1); under review: $version" + echo "ok: the steps run $version, built from $MCPP_SOURCE_REF" + + - name: Install the managed MSVC toolset + run: | + set -e + "$MCPP" toolchain install msvc 14.44.35207 2>&1 | tail -5 + "$MCPP" toolchain list 2>&1 | grep -q 14.44.35207 || { echo "::error::msvc 14.44.35207 is not listed"; exit 1; } + + # The instance is made unreachable the way the measurement did it: vswhere + # and every instance's VC directory are renamed, and the step fails if a + # toolset is still found. + - name: Mask Visual Studio + shell: pwsh + run: | + $vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" } + Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" -ErrorAction SilentlyContinue | ForEach-Object { + try { Rename-Item -LiteralPath $_.Path -NewName "VC.masked" -ErrorAction Stop } + catch { Write-Host "could not rename $($_.Path): $_" } + } + $left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" -Directory -ErrorAction SilentlyContinue + if ($left) { Write-Host "FAIL: VC tools still present after masking"; exit 1 } + Write-Host "ok: no Visual Studio toolset is reachable" + + - name: Cache vcpkg's per-user directory + uses: actions/cache@v4 + with: + path: ~/AppData/Local/vcpkg + key: vcpkg-user-managed-${{ runner.os }}-${{ hashFiles('tests/vcpkg-*/vcpkg.json') }} + restore-keys: vcpkg-user-managed-${{ runner.os }}- + + - name: the test kit on this row + run: bash .github/scripts/check-deps-and-qt.sh plugin-logic + + - name: a CMake port and a CMake subproject build with the managed toolset named + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-managed + + - name: an MSBuild port is refused by name + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-msbuild-refused + + - name: a make-based port finds the toolset on the PATH vcpkg keeps + timeout-minutes: 90 + run: bash .github/scripts/check-deps-and-qt.sh vcpkg-make-port diff --git a/README.md b/README.md index 624a8aa..ef8e47c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ module name the member declares, and configures it there. ```toml [build-dependencies.mcpp] -plugins = { version = "0.16.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.17.0", features = ["rules-spirv"], host-module = true } ``` ```cpp @@ -40,12 +40,17 @@ linked into the artifact (mcpp docs/05 §2.6.1). | deps | `mcpp.deps.` | where a library comes from | | identity | `mcpp.plugins` | the lib root, compiled before every member; it states the collection's version | -The `mcpp.` prefix is reserved for this package. The full account of the -families, and of how the engine routes a file to a rule, is in -[docs/engine-and-rules.md](docs/engine-and-rules.md). +The second segments `core`, `plugins`, `deps`, `rules`, `dist` and `tools` of +`mcpp.` belong to this package and the engine; a third-party plugin names its +modules `mcpp..*` ([docs/plugin-development.md](docs/plugin-development.md#3-layers-names-and-the-engine-floor)). +The full account of the families, and of how the engine routes a file to a +rule, is in [docs/engine-and-rules.md](docs/engine-and-rules.md). ## Members +From 0.17.0 the package as a whole needs mcpp 2026.9.28.3 (`[package] mcpp`); +the column states the release each member's behaviour first needed. + | feature | module | mcpp floor | what it does | doc | |---|---|---|---|---| | `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.6 | Compiles Ascend C (`*.asc`) with BiSheng in mixed mode, so the object joins the ordinary link. | [rules](docs/rules.md#rules-ascendc) | @@ -64,13 +69,23 @@ families, and of how the engine routes a file to a rule, is in | `dist-apple` | `mcpp.dist.apple` | 2026.9.14.2 | Lays out a macOS or iOS application bundle, signs it and writes a disk image. | [dist-apple](docs/dist-apple.md) | | `dist-web` | `mcpp.dist.web` | 2026.9.13.1 | Copies a `wasm32-emscripten` program and its files into a web directory with an `index.html`. | [dist](docs/dist.md#dist-web) | | `dist-apk` | `mcpp.dist.apk` | 2026.9.14.2 | Packs the native closure into a signed APK or App Bundle, with Java, Kotlin and Maven libraries. | [dist-apk](docs/dist-apk.md) | -| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.9.26.2 | Installs a `vcpkg.json` manifest as an action and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) | -| `deps-cmake` | `mcpp.deps.cmake` | 2026.9.26.2 | Builds and installs a CMake subproject as an action and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) | +| `deps-vcpkg` | `mcpp.deps.vcpkg` | 2026.9.28.3 | Installs a `vcpkg.json` manifest as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-vcpkg) | +| `deps-cmake` | `mcpp.deps.cmake` | 2026.9.28.3 | Builds and installs a CMake subproject as an action with the toolset mcpp resolved, and maps the prefix into the build. | [deps](docs/deps.md#deps-cmake) | | `deps-archive` | `mcpp.deps.archive` | 2026.9.26.2 | Extracts a zip archive the project keeps and places its tree beside the program. | [deps](docs/deps.md#deps-archive) | Some features add a sub-capability to a member: `dist-apk-kotlin` and -`dist-apk-maven` (Kotlin sources, a Maven graph), and `surface` and `deps`, -which the members imply. A feature states a mechanism and the tools that +`dist-apk-maven` (Kotlin sources, a Maven graph), and `deps`, which the deps +members imply. + +**The general library (0.17.0).** `plugins-core` gives a build program +`mcpp.plugins.declare`, `mcpp.plugins.toolset` (the resolved toolchain, +translated for a foreign build system) and `mcpp.plugins.fs` (deterministic file +generation and placement). Every member implies it; a build program that uses +only these modules, and a third-party plugin built on them, name it on the +dependency edge. `plugins-testing` adds `mcpp.plugins.testing`, which runs a +plugin function against a stated build context and compares what it declared. +Both need mcpp 2026.9.28.3. `surface`, the name of `plugins-core` before +0.17.0, is kept until 2027-03-28. A feature states a mechanism and the tools that mechanism runs; the libraries and SDKs a program links are the project's declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). @@ -80,6 +95,12 @@ declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). mcpp.toml the package: one feature per member src/plugins.cppm export module mcpp.plugins; the version, mcpp::plugins::surface (the declarations a consumer names) and mcpp::plugins::xml +src/declare.cppm, the general library for build programs (`plugins-core`): +src/toolset.cppm, mcpp.plugins.declare, mcpp.plugins.toolset, mcpp.plugins.fs +src/fs.cppm +src/testing.cppm mcpp.plugins.testing (`plugins-testing`) +src/compat/, deps/compat/ + compatibility units, each retired on the date its header states rules/.cppm export module mcpp.rules.; tools/.cppm export module mcpp.tools.; dist/.cppm export module mcpp.dist.; diff --git a/deps/archive.cppm b/deps/archive.cppm index 29faab7..74eef89 100644 --- a/deps/archive.cppm +++ b/deps/archive.cppm @@ -32,6 +32,7 @@ import std; import mcpp; import mcpp.plugins; import mcpp.deps; +import mcpp.plugins.fs; export namespace mcpp::deps::archive { @@ -51,7 +52,7 @@ struct options { struct result { // One entry per file of the archive: the extracted copy and its path // beside the program. - std::vector files; + std::vector files; bool ok = false; explicit operator bool() const { return ok; } }; @@ -191,7 +192,7 @@ inline result unpack(const options& opt) { const std::string arc = mcpp::deps::generic(archive); const std::string dst = mcpp::deps::generic(into); const fs::path script = fs::path(mcpp::out_dir()) / "deps-archive" / (name + ".cmake"); - mcpp::deps::write_if_changed(script, + mcpp::plugins::fs::write_if_changed(script, "# Written by mcpp.deps.archive: extract " + archive.filename().string() + ".\n" "file(REMOVE_RECURSE " + bracket(dst) + ")\n" "file(MAKE_DIRECTORY " + bracket(dst) + ")\n" diff --git a/deps/cmake.cppm b/deps/cmake.cppm index b996c35..3808561 100644 --- a/deps/cmake.cppm +++ b/deps/cmake.cppm @@ -19,13 +19,17 @@ // include directory, libraries by full path, the shared libraries deployed // beside the program. // -// THE COMPILER IS CMAKE'S OWN CHOICE, EXCEPT WHERE ITS C++ LIBRARY DIFFERS. A -// subproject is configured the way its authors build it -- on Windows, CMake's -// default generator and the Visual Studio toolset it finds. On Linux under a -// libc++ toolchain the compilers are mcpp's own clang (`-DCMAKE_C_COMPILER`, -// `-DCMAKE_CXX_COMPILER`), because the host compiler CMake finds uses libstdc++ -// and the two do not link (`mcpp::deps::program_compilers`). A project that -// passes either compiler or a toolchain file through `cache_args` decides. +// THE COMPILER IS THE PROGRAM'S (0.17.0). What the subproject builds is linked +// into the program, so it is built with the toolset mcpp resolved +// (`mcpp.plugins.toolset`). A toolset from a Visual Studio instance keeps +// CMake's Visual Studio generator, pointed at that instance +// (`CMAKE_GENERATOR_INSTANCE`) and toolset version (`-T version=`); any other +// toolset is named by path, with the Ninja generator and mcpp's own ninja, and +// runs with the environment the engine runs it with. On the MSVC ABI the C +// runtime follows the program's (`CMAKE_MSVC_RUNTIME_LIBRARY`). A project that +// passes either compiler or a toolchain file through `cache_args` decides, +// and `options::toolset` set to `detected` keeps 0.16.0's behaviour -- CMake's +// default generator and the toolset it finds -- until 2027-03-28. // // `xim:cmake` is declared by this feature; `options::cmake` names another. @@ -35,6 +39,8 @@ import std; import mcpp; import mcpp.plugins; import mcpp.deps; +import mcpp.plugins.fs; +import mcpp.plugins.toolset; export namespace mcpp::deps::cmake { @@ -69,14 +75,32 @@ struct options { std::string cmake; // Files of the prefix placed beside the program, as `mcpp.deps.vcpkg` // takes them (`{"bin/tool.cfg", "."}`). - std::vector deploy; + std::vector deploy; + // Which toolset builds the subproject (0.17.0). The default `resolved` is + // the toolset mcpp builds the program with: a Visual Studio instance keeps + // CMake's Visual Studio generator, pointed at that instance and toolset + // version; any other toolset is named, with the Ninja generator and + // mcpp's own ninja. `detected` lets CMake find its own toolset, as 0.16.0 + // did, until 2027-03-28. Compilers or a toolchain file in `cache_args` + // decide instead. See docs/deps.md. + mcpp::plugins::toolset::choice toolset; + // `default` keeps the generator the mechanism implies; `ninja` uses the + // Ninja generator with mcpp's ninja under an instance as well. + enum class generator_kind { default_, ninja }; + generator_kind generator = generator_kind::default_; + // The C runtime linkage on the MSVC ABI, "static" or "dynamic" + // (`CMAKE_MSVC_RUNTIME_LIBRARY`). Empty follows the program's C++ runtime + // contract (`mcpp::msvc_crt_linkage()`). + std::string crt_linkage; }; // The prefix, by name (SPEC-007 R1.3). struct prefix { std::string root, include, lib, bin; // The copies `options::deploy` produced, for a project's own layout. - std::vector deployed; + std::vector deployed; + // How the toolset reached CMake: "instance", "chain" or "detected". + std::string mechanism; explicit operator bool() const { return !root.empty(); } }; @@ -96,6 +120,7 @@ inline std::string cmake_exe(const options& opt) { inline prefix use(const options& opt) { namespace fs = std::filesystem; + namespace ts = mcpp::plugins::toolset; constexpr std::string_view who = "mcpp.deps.cmake"; mcpp::fact("mcpp.plugins", std::string(mcpp::plugins::version).c_str()); @@ -108,16 +133,55 @@ inline prefix use(const options& opt) { "`git submodule update --init`.\n", who, opt.source); return {}; } + + // THE TOOLSET. Compilers or a toolchain file among the cache arguments + // are the project's decision, and so is a generator it names. + auto names = [&](std::initializer_list keys) { + return std::ranges::any_of(opt.cache_args, [&](const std::string& x) { + return std::ranges::any_of(keys, [&](std::string_view k) { return x.starts_with(k); }); + }); + }; + const bool chosen = std::ranges::any_of(opt.cache_args, [](const std::string& x) { + return x.contains("CMAKE_C_COMPILER") || x.contains("CMAKE_CXX_COMPILER") + || x.contains("CMAKE_TOOLCHAIN_FILE"); + }); + const bool generatorChosen = names({"-G", "-DCMAKE_GENERATOR"}); + const bool wantNinja = opt.generator == options::generator_kind::ninja; + auto tools = wantNinja ? ts::resolve_named(opt.toolset) : ts::resolve(opt.toolset); + if (!tools) { + std::cerr << std::format("{}: {}\n", who, tools.error()); + return {}; + } + const std::string crt = opt.crt_linkage.empty() ? tools->crt : opt.crt_linkage; + if (!crt.empty() && crt != "static" && crt != "dynamic") { + std::cerr << std::format("{}: options::crt_linkage is '{}'; it is \"static\" or \"dynamic\".\n", + who, crt); + return {}; + } + const std::string ninja = mcpp::ninja_program(); + const bool named = !chosen && tools->how == ts::mechanism::chain && !tools->cxx.empty(); + const bool useNinja = named && !generatorChosen && !ninja.empty(); + const bool instance = !chosen && !generatorChosen && tools->how == ts::mechanism::instance; + + // One build directory per toolset statement: CMake refuses a cache made + // with another generator or instance. What 0.16.0 configured stays in + // `build/`. + std::string key; + if (useNinja) key = "ninja;" + tools->identity + ";" + tools->cxx; + else if (named) key = "named;" + tools->identity + ";" + tools->cxx; + else if (instance) key = "instance;" + tools->instance_dir + ";" + tools->toolset_version; const std::string name = opt.name.empty() ? source.filename().string() : opt.name; const fs::path base = fs::path(mcpp::out_dir()) / "deps-cmake" / name; - const fs::path build = base / "build"; + const fs::path build = base / (key.empty() ? std::string("build") : "build-" + mcpp::deps::short_name(key)); const fs::path root = base / "install"; prefix p; - p.root = mcpp::deps::generic(root); - p.include = mcpp::deps::generic(root / opt.dirs.include); - p.lib = mcpp::deps::generic(root / opt.dirs.lib); - p.bin = mcpp::deps::generic(root / opt.dirs.bin); + p.root = mcpp::deps::generic(root); + p.include = mcpp::deps::generic(root / opt.dirs.include); + p.lib = mcpp::deps::generic(root / opt.dirs.lib); + p.bin = mcpp::deps::generic(root / opt.dirs.bin); + p.mechanism = std::string(ts::name(named ? ts::mechanism::chain + : instance ? ts::mechanism::instance : ts::mechanism::detected)); const std::string cmake = cmake_exe(opt); if (cmake.empty()) { @@ -146,16 +210,38 @@ inline prefix use(const options& opt) { } configure.push_back("-DCMAKE_PREFIX_PATH=" + joined); } - const bool chosen = std::ranges::any_of(opt.cache_args, [](const std::string& x) { - return x.contains("CMAKE_C_COMPILER") || x.contains("CMAKE_CXX_COMPILER") - || x.contains("CMAKE_TOOLCHAIN_FILE"); - }); - if (const auto cc = mcpp::deps::program_compilers(); cc && !chosen) { - configure.push_back("-DCMAKE_C_COMPILER=" + cc.c); - configure.push_back("-DCMAKE_CXX_COMPILER=" + cc.cxx); + if (useNinja) { + configure.push_back("-G"); + configure.push_back("Ninja"); + configure.push_back("-DCMAKE_MAKE_PROGRAM=" + ts::forward(ninja)); + } + if (named) { + configure.push_back("-DCMAKE_C_COMPILER=" + tools->cc); + configure.push_back("-DCMAKE_CXX_COMPILER=" + tools->cxx); + if (tools->msvc_abi) { + if (!tools->rc.empty()) configure.push_back("-DCMAKE_RC_COMPILER=" + tools->rc); + if (!tools->mt.empty()) configure.push_back("-DCMAKE_MT=" + tools->mt); + if (!tools->ld.empty()) configure.push_back("-DCMAKE_LINKER=" + tools->ld); + } + } + if (instance) { + // CMake's documented selection of a Visual Studio instance and of + // a toolset version within it. + configure.push_back("-DCMAKE_GENERATOR_INSTANCE=" + tools->instance_dir); + if (!tools->toolset_version.empty() && tools->toolset_version != tools->instance_default) { + configure.push_back("-T"); + configure.push_back("version=" + tools->toolset_version); + } + } + // The C runtime follows the program's (CMake 3.15+, policy CMP0091). + if (tools->msvc_abi && !crt.empty() && !names({"-DCMAKE_MSVC_RUNTIME_LIBRARY"})) { + configure.push_back("-DCMAKE_POLICY_DEFAULT_CMP0091=NEW"); + configure.push_back(std::string("-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded$<$:Debug>") + + (crt == "dynamic" ? "DLL" : "")); } for (auto const& x : opt.cache_args) configure.push_back(x); std::string text = "# Written by mcpp.deps.cmake: configure, build and install " + name + ".\n" + "# toolset: " + ts::describe(*tools) + "\n" "execute_process(COMMAND ${CMAKE_COMMAND}"; for (auto const& x : configure) text += "\n " + bracket(x); text += "\n RESULT_VARIABLE rc)\n" @@ -163,13 +249,32 @@ inline prefix use(const options& opt) { "execute_process(COMMAND ${CMAKE_COMMAND} --build " + bracket(mcpp::deps::generic(build)) + " --config " + bracket(opt.config) + " --target install --parallel\n RESULT_VARIABLE rc)\n" "if(NOT rc EQUAL 0)\n message(FATAL_ERROR \"build and install exited ${rc}\")\nendif()\n"; - mcpp::deps::write_if_changed(script, text); + mcpp::plugins::fs::write_if_changed(script, text); const std::string scriptS = mcpp::deps::generic(script); mcpp::action a; a.id = id.c_str(); a.role = mcpp::roles::prepare; a.description = desc.c_str(); a.arg(cmake.c_str()).arg("-P").arg(scriptS.c_str()); + // Named tools run with the environment the engine runs them with, and + // their directories first on PATH. + if (named) { + for (auto const& [k, v] : tools->env) a.env(k.c_str(), v.c_str()); + if (!tools->path_dirs.empty()) { + const char sep = ts::path_separator(); + std::string path; + for (auto const& d : tools->path_dirs) { + if (!path.empty()) path += sep; + std::string n = d; + if (ts::host_is_windows()) + for (std::size_t i = 0; i < n.size(); ++i) if (n[i] == '/') n[i] = '\\'; + path += n; + } + const char* cur = std::getenv("PATH"); + if (cur && *cur) { path += sep; path += cur; } + a.env("PATH", path.c_str()); + } + } a.input(cmake.c_str()); a.input(scriptS.c_str()); for (auto const& f : mcpp::deps::files_under(source)) a.input(f.c_str()); @@ -177,7 +282,7 @@ inline prefix use(const options& opt) { a.output(stamp.c_str()); a.output_dir(p.root.c_str()); a.submit(); - p.deployed = mcpp::deps::deploy_after("deps-cmake-" + name, stamp, fs::path(p.root), opt.deploy); + p.deployed = mcpp::plugins::fs::deploy_after("deps-cmake-" + name, stamp, fs::path(p.root), opt.deploy); } mcpp::include_dir(p.include.c_str()); diff --git a/deps/compat/names.cppm b/deps/compat/names.cppm new file mode 100644 index 0000000..b9042a9 --- /dev/null +++ b/deps/compat/names.cppm @@ -0,0 +1,60 @@ +// COMPATIBILITY UNIT +// keeps: the 0.16.0 names in `mcpp::deps`: `write_if_changed`, +// `deploy_entry`, `deployed_file`, `deploy_after`, +// `compilers` and `program_compilers` +// since: 0.17.0 (2026-09-28) +// retires: 2027-03-28 +// replacement: `mcpp::plugins::fs` (module `mcpp.plugins.fs`) for the first +// four; `mcpp::plugins::toolset::resolve` for the last two +// note: "mcpp.plugins: mcpp::deps::program_compilers is kept until +// 2027-03-28; ..." (printed by `program_compilers`; the other +// names are aliases, which print nothing) +// +// `mcpp.deps` re-exports this module, so a build program that imported +// `mcpp.deps` for these names keeps compiling. Retirement removes this file, +// its entry in `[features.deps]` and the re-export in `deps/deps.cppm`. +// `.github/scripts/check-compat-retirement.sh` fails once the date has passed. + +export module mcpp.deps.compat; + +import std; +import mcpp; +import mcpp.plugins.fs; +import mcpp.plugins.toolset; + +export namespace mcpp::deps { + +using deploy_entry = mcpp::plugins::fs::deploy_entry; +using deployed_file = mcpp::plugins::fs::deployed_file; +using mcpp::plugins::fs::write_if_changed; +using mcpp::plugins::fs::deploy_after; + +struct compilers { + std::string c, cxx; + explicit operator bool() const { return !cxx.empty(); } +}; + +// 0.16.0's answer: mcpp's clang on the Linux libc++ row, nothing elsewhere. +inline compilers program_compilers() { + static bool noted = false; + if (!noted) { + noted = true; + mcpp::report({ + .severity = "note", + .message = "mcpp.plugins: mcpp::deps::program_compilers is kept until 2027-03-28", + .impact = "none until then; the name is removed afterwards", + .hint = "use mcpp::plugins::toolset::resolve({.cc = mcpp::plugins::toolset::compiler::row}), " + "which names the row's tools on every row", + }); + } + if (std::string_view(mcpp::target_os()) != "linux" + || std::string_view(mcpp::cxx_stdlib()) != "libc++" + || std::string_view(mcpp::compiler()) != "clang" + || std::string_view(mcpp::host()) != std::string_view(mcpp::target())) return {}; + auto r = mcpp::plugins::toolset::resolve({mcpp::plugins::toolset::source::resolved, + mcpp::plugins::toolset::compiler::row}); + if (!r) return {}; + return { r->cc, r->cxx }; +} + +} // namespace mcpp::deps diff --git a/deps/deps.cppm b/deps/deps.cppm index 6621f82..3c68c87 100644 --- a/deps/deps.cppm +++ b/deps/deps.cppm @@ -34,6 +34,9 @@ export module mcpp.deps; import std; import mcpp; import mcpp.plugins; +// The 0.16.0 names of the helpers that moved to `mcpp.plugins.fs` and +// `mcpp.plugins.toolset` (a compatibility unit, until 2027-03-28). +export import mcpp.deps.compat; export namespace mcpp::deps { @@ -204,108 +207,4 @@ inline std::string short_name(std::string_view text) { return std::format("{:08x}", std::uint32_t(h ^ (h >> 32))); } -// ── The compiler a subproject builds with ────────────────────────────────── -// -// A prefix's C++ libraries are linked into the program, so their C++ standard -// library must be the program's. On Windows every compiler of the MSVC ABI -// uses Microsoft's, and on macOS every compiler uses libc++; on Linux two -// incompatible libraries exist, the host compiler that vcpkg and CMake choose -// uses libstdc++, and mcpp's clang uses libc++ (`std::__1::`). So when the -// program's library is libc++ on Linux, a subproject builds with mcpp's own -// clang, whose configuration file names libc++ and the C library mcpp links -// against; otherwise the installer's own choice already agrees and nothing is -// stated. -struct compilers { - std::string c, cxx; - explicit operator bool() const { return !cxx.empty(); } -}; - -inline compilers program_compilers() { - namespace fs = std::filesystem; - if (std::string_view(mcpp::target_os()) != "linux" - || std::string_view(mcpp::cxx_stdlib()) != "libc++" - || std::string_view(mcpp::compiler()) != "clang" - || std::string_view(mcpp::host()) != std::string_view(mcpp::target())) return {}; - const fs::path bin = fs::path(mcpp::toolchain_dir()) / "bin"; - std::error_code ec; - if (!fs::is_regular_file(bin / "clang++", ec) || !fs::is_regular_file(bin / "clang", ec)) return {}; - return { generic(bin / "clang"), generic(bin / "clang++") }; -} - -// Writes `content` to `file` unless the file already holds it, so a file the -// build program generates keeps its time stamp from one plan to the next and -// the action that reads it does not run again. -inline void write_if_changed(const std::filesystem::path& file, const std::string& content) { - namespace fs = std::filesystem; - std::error_code ec; - if (fs::is_regular_file(file, ec)) { - std::ifstream in(file, std::ios::binary); - const std::string old{ std::istreambuf_iterator(in), std::istreambuf_iterator() }; - if (old == content) return; - } - fs::create_directories(file.parent_path(), ec); - std::ofstream(file, std::ios::binary | std::ios::trunc) << content; -} - -// ── Files beside the program ─────────────────────────────────────────────── -// -// Data a program reads at run time from beside itself -- a dictionary, a -// configuration, an embedded interpreter -- is DEPLOYED (`mcpp::deploy`): the -// engine places it beside the program after the build, `mcpp run` finds it -// there, and `mcpp pack` carries it. A deployed file must be one the build -// graph knows how to produce, and a file that a `prepare` action installs is -// not: its name is unknown to the graph until the action has run. So the file -// is first copied by an action that NAMES it as an output (SPEC-007 R3.2), and -// the copy is deployed. Everything here is declared before anything exists, so -// `mcpp emit build-database` plans it on a machine that never built. - -// One file of a prefix to place beside the program: `file` relative to the -// prefix root (`share/opencc/t2s.json`), `to` the directory beside the program -// it is placed in (`BaseConfig/opencc`; empty or `.` is the program's own -// directory). -struct deploy_entry { - std::string file; - std::string to; -}; - -// A file this package deploys: `path`, the copy the build produces (a node of -// the build graph, which a project's own actions may take as an input), and -// `to`, its path beside the program. -struct deployed_file { - std::string path; - std::string to; -}; - -// Copies each entry's file out of `root` once the action that writes `stamp` -// has run, and deploys the copy. `who` names the member in action ids. -inline std::vector deploy_after(std::string_view who, const std::string& stamp, - const std::filesystem::path& root, - std::span entries) { - namespace fs = std::filesystem; - std::vector out; - const fs::path base = fs::path(mcpp::out_dir()) / "deps-deploy" / std::string(who); - for (auto const& e : entries) { - const fs::path rel = fs::path(e.file).lexically_normal(); - const std::string src = generic(root / rel); - const std::string copy = generic(base / rel); - // Index loops over a string: GCC 16 fails to inline a string iterator - // inside a module's interface. - std::string key = rel.generic_string(); - for (std::size_t i = 0; i < key.size(); ++i) if (key[i] == '/') key[i] = '.'; - const std::string id = std::format("{}:deploy:{}", who, key); - const std::string desc = std::format("DEPLOY {}", rel.generic_string()); - mcpp::action a; - a.id = id.c_str(); - a.role = mcpp::roles::artifact; - a.description = desc.c_str(); - a.arg("${mcpp.self}").arg("stage").arg("--output").arg(copy.c_str()).arg(src.c_str()) - .input(stamp.c_str()).output(copy.c_str()).submit(); - const std::string dir = e.to.empty() ? std::string(".") : e.to; - mcpp::deploy(copy.c_str(), dir.c_str()); - const std::string to = generic(fs::path(dir) / rel.filename()); - out.push_back({copy, to}); - } - return out; -} - } // namespace mcpp::deps diff --git a/deps/vcpkg.cppm b/deps/vcpkg.cppm index 68bbf5c..2f60241 100644 --- a/deps/vcpkg.cppm +++ b/deps/vcpkg.cppm @@ -42,15 +42,18 @@ import std; import mcpp; import mcpp.plugins; import mcpp.deps; +import mcpp.plugins.fs; +import mcpp.plugins.toolset; export namespace mcpp::deps::vcpkg { struct options { - // The vcpkg triplet. Empty derives it from the target: `x64-windows`, + // The vcpkg triplet. Empty derives it from the target: `x64-windows` + // (`x64-windows-static` for a statically linked C runtime), // `arm64-windows`, `x64-mingw-dynamic`, `x64-linux`, `arm64-linux`, - // `x64-osx`, `arm64-osx`; on Linux under a libc++ toolchain, the generated - // `x64-linux-libcxx` or `arm64-linux-libcxx`, whose ports build with mcpp's - // clang. A custom triplet is named here and found through the manifest's + // `x64-osx`, `arm64-osx`. Under the `chain` mechanism the installation uses + // a triplet derived from this one, which names the resolved tools. A + // custom triplet is named here and found through the manifest's // `overlay-triplets` like any other. std::string triplet; // Library names in link order: `fmt` denotes `lib/fmt.lib` on Windows and @@ -77,8 +80,23 @@ struct options { std::string vcpkg_root; // Files of the prefix the program reads at run time, placed beside it // (`{"share/opencc/t2s.json", "BaseConfig/opencc"}`): `mcpp run` finds them - // and `mcpp pack` carries them. See `mcpp::deps::deploy_after`. - std::vector deploy; + // and `mcpp pack` carries them. See `mcpp::plugins::fs::deploy_after`. + std::vector deploy; + // Which toolset builds the ports (0.17.0). The default `resolved` is the + // toolset mcpp builds the program with: a Visual Studio instance is + // selected with `VCPKG_VISUAL_STUDIO_PATH` (every port kind builds, and + // with the instance vcpkg would choose anyway nothing is rebuilt); any + // other toolset is named in a derived triplet `-mcpp-` + // (CMake and make ports build; an MSBuild port needs Visual Studio and is + // refused by name). `detected` lets vcpkg find its own toolset, as 0.16.0 + // did, until 2027-03-28. See docs/deps.md. + mcpp::plugins::toolset::choice toolset; + // The C runtime linkage of the ports on the MSVC ABI, "static" or + // "dynamic". Empty follows the program's C++ runtime contract + // (`mcpp::msvc_crt_linkage()`): a `self-contained` program links it + // statically, so the default triplet becomes `-windows-static`. A + // triplet named in `triplet` whose linkage contradicts it is refused. + std::string crt_linkage; }; // The prefix, by name: the installation fills it during the build, and the @@ -91,18 +109,26 @@ struct prefix { std::string share; // root/share std::string triplet; // The copies `options::deploy` produced, for a project's own layout. - std::vector deployed; + std::vector deployed; + // How the toolset reached vcpkg: "instance", "chain" or "detected". + std::string mechanism; explicit operator bool() const { return !root.empty(); } }; // ─── The triplet ─────────────────────────────────────────────────────────── -inline std::string default_triplet() { +// The target's standard triplet. `crt` is the C runtime linkage on the MSVC +// ABI: "static" selects `-windows-static`, whose C runtime and libraries +// are both static. +inline std::string default_triplet(std::string_view crt = {}) { const std::string os = mcpp::target_os(), arch = mcpp::target_arch(), env = mcpp::target_env(); const std::string a = arch == "x86_64" ? "x64" : arch == "aarch64" ? "arm64" : (arch == "i686" || arch == "x86") ? "x86" : arch; - if (os == "windows") return env == "gnu" ? a + "-mingw-dynamic" : a + "-windows"; + if (os == "windows") { + if (env == "gnu") return a + "-mingw-dynamic"; + return crt == "static" ? a + "-windows-static" : a + "-windows"; + } if (os == "macos") return a + "-osx"; if (os == "linux") return a + "-linux"; return {}; @@ -129,48 +155,144 @@ inline std::vector manifest_overlays(const std::filesyste return out; } -// Whether the triplet links libraries as shared objects. Read from the -// triplet file itself -- `set(VCPKG_LIBRARY_LINKAGE dynamic)` outside any -// `if()`, which is where a per-port exception lives -- and otherwise from -// vcpkg's convention: dynamic on Windows, static elsewhere, a `-dynamic` -// suffix for the community triplets that say so in their name. -inline bool shared_linkage(const std::string& triplet, - std::span search) { +// The file that defines `triplet`, searched in vcpkg's order: overlays first. +inline std::filesystem::path triplet_file(const std::string& triplet, + std::span search) { std::error_code ec; for (auto const& dir : search) { const auto file = dir / (triplet + ".cmake"); - if (!std::filesystem::is_regular_file(file, ec)) continue; - mcpp::rerun_if_changed(mcpp::deps::generic(file).c_str()); - std::ifstream in(file); - std::string line; - int depth = 0; - while (std::getline(in, line)) { - // By index: under GCC 16 a range-for over a non-const std::string in - // a module unit fails with "inlining failed in call to always_inline - // ... function body not available" (measured on this file). - std::string s(line.size(), ' '); - for (std::size_t i = 0; i < line.size(); ++i) - s[i] = char(std::tolower(static_cast(line[i]))); - const auto first = s.find_first_not_of(" \t"); - if (first == std::string::npos || s[first] == '#') continue; - s = s.substr(first); - if (s.starts_with("if(") || s.starts_with("if (")) ++depth; - else if (s.starts_with("endif(") || s.starts_with("endif (")) { if (depth) --depth; } - else if (depth == 0 && s.starts_with("set(vcpkg_library_linkage")) { - return s.find("dynamic") != std::string::npos; - } + if (std::filesystem::is_regular_file(file, ec)) return file; + } + return {}; +} + +inline std::string read_text(const std::filesystem::path& file) { + std::ifstream in(file, std::ios::binary); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +// The value a triplet file gives `variable` outside any `if()` -- where a +// per-port exception lives -- the last such `set()` winning, as in CMake. +// Empty when the file does not set it there. +inline std::string top_level_setting(const std::string& text, std::string_view variable) { + std::string want = "set("; + for (std::size_t i = 0; i < variable.size(); ++i) + want += char(std::tolower(static_cast(variable[i]))); + std::string value; + int depth = 0; + std::size_t pos = 0; + while (pos < text.size()) { + const auto nl = text.find('\n', pos); + const std::string line = text.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos); + pos = nl == std::string::npos ? text.size() : nl + 1; + // By index: under GCC 16 a range-for over a non-const std::string in + // a module unit fails with "inlining failed in call to always_inline + // ... function body not available" (measured on this file). + std::string s(line.size(), ' '); + for (std::size_t i = 0; i < line.size(); ++i) + s[i] = char(std::tolower(static_cast(line[i]))); + const auto first = s.find_first_not_of(" \t"); + if (first == std::string::npos || s[first] == '#') continue; + s = s.substr(first); + if (s.starts_with("if(") || s.starts_with("if (")) ++depth; + else if (s.starts_with("endif(") || s.starts_with("endif (")) { if (depth) --depth; } + else if (depth == 0 && s.starts_with(want)) { + std::string rest = s.substr(want.size()); + const auto close = rest.find(')'); + rest = rest.substr(0, close); + std::string v; + for (std::size_t i = 0; i < rest.size(); ++i) + if (rest[i] != ' ' && rest[i] != '\t' && rest[i] != '"') v += rest[i]; + value = v; } - break; + } + return value; +} + +// Whether the triplet links libraries as shared objects: its own +// `VCPKG_LIBRARY_LINKAGE`, and otherwise vcpkg's convention: dynamic on +// Windows, static elsewhere, a `-dynamic` suffix for the community triplets +// that say so in their name. +inline bool shared_linkage(const std::string& triplet, + std::span search) { + if (const auto file = triplet_file(triplet, search); !file.empty()) { + mcpp::rerun_if_changed(mcpp::deps::generic(file).c_str()); + const auto v = top_level_setting(read_text(file), "VCPKG_LIBRARY_LINKAGE"); + if (!v.empty()) return v == "dynamic"; } if (triplet.ends_with("-dynamic")) return true; if (triplet.ends_with("-static") || triplet.ends_with("-static-md")) return false; return mcpp::deps::is_windows(); } +// The vcpkg toolchain a chain-loaded toolchain file includes after naming the +// tools, so that ports keep vcpkg's standard flags for the target system. +inline std::string_view vcpkg_system_toolchain() { + const std::string_view os = mcpp::target_os(); + if (os == "windows") return std::string_view(mcpp::target_env()) == "gnu" ? "mingw" : "windows"; + if (os == "macos") return "osx"; + return "linux"; +} + +// The chain-loaded toolchain file. It holds no path: every tool is read from +// the installation's environment (`MCPP_VCPKG_*`, passed through untracked), +// so vcpkg's ABI hash sees the toolset's identity and its compilers and never +// where they are. A Windows path goes through `file(TO_CMAKE_PATH)`, since a +// backslash in a CMake string is an escape. +inline std::string chain_toolchain_text() { + std::string t = + "# Written by mcpp.deps.vcpkg: the tools mcpp resolved, read from the\n" + "# environment of the installation, then vcpkg's own toolchain for the\n" + "# target system.\n"; + for (auto const& [var, env] : { std::pair + {"CMAKE_C_COMPILER", "MCPP_VCPKG_CC"}, + {"CMAKE_CXX_COMPILER", "MCPP_VCPKG_CXX"}, + {"CMAKE_RC_COMPILER", "MCPP_VCPKG_RC"}, + {"CMAKE_MT", "MCPP_VCPKG_MT"} }) { + t += std::format("if(DEFINED ENV{{{0}}} AND NOT \"$ENV{{{0}}}\" STREQUAL \"\")\n" + " file(TO_CMAKE_PATH \"$ENV{{{0}}}\" z_mcpp_tool)\n" + " set({1} \"${{z_mcpp_tool}}\")\n" + "endif()\n", env, var); + } + t += "if(CMAKE_HOST_APPLE AND NOT CMAKE_OSX_SYSROOT)\n" + " # A compiler other than Apple's has no default SDK.\n" + " execute_process(COMMAND xcrun --show-sdk-path OUTPUT_VARIABLE z_mcpp_sdk\n" + " OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)\n" + " if(z_mcpp_sdk)\n set(CMAKE_OSX_SYSROOT \"${z_mcpp_sdk}\")\n endif()\n" + "endif()\n" + "file(TO_CMAKE_PATH \"$ENV{MCPP_VCPKG_ROOT}\" z_mcpp_vcpkg_root)\n"; + t += std::format("include(\"${{z_mcpp_vcpkg_root}}/scripts/toolchains/{}.cmake\")\n", + vcpkg_system_toolchain()); + return t; +} + +// What an MSBuild port meets under `chain`: vcpkg runs MSBuild with +// `/p:PlatformToolset=external` and fails without naming the cause. The +// MSBuild helpers read `VCPKG_PLATFORM_TOOLSET` to build that argument; the +// watch stops the port there and says why. +inline std::string msbuild_refusal_text(std::string_view identity) { + return std::format( + "# mcpp.deps.vcpkg: a port that builds with MSBuild needs a Visual Studio\n" + "# instance, and this toolset comes from none.\n" + "if(PORT AND NOT DEFINED Z_MCPP_MSBUILD_WATCH)\n" + " set(Z_MCPP_MSBUILD_WATCH 1)\n" + " function(z_mcpp_msbuild_watch variable access value current_file)\n" + " if(access STREQUAL \"READ_ACCESS\" AND current_file MATCHES \"msbuild\")\n" + " message(FATAL_ERROR \"mcpp.deps.vcpkg: the port '${{PORT}}' builds with MSBuild, which " + "needs a Visual Studio instance; the toolset mcpp resolved ({}) comes from none. Build with " + "the toolchain msvc@system on a machine with Visual Studio, or set the deps-vcpkg option " + "toolset.toolset to detected for this project.\")\n" + " endif()\n" + " endfunction()\n" + " variable_watch(VCPKG_PLATFORM_TOOLSET z_mcpp_msbuild_watch)\n" + "endif()\n", identity); +} + // ─── The member ──────────────────────────────────────────────────────────── inline prefix use(const options& opt = {}) { namespace fs = std::filesystem; + namespace ts = mcpp::plugins::toolset; constexpr std::string_view who = "mcpp.deps.vcpkg"; mcpp::fact("mcpp.plugins", std::string(mcpp::plugins::version).c_str()); @@ -190,75 +312,175 @@ inline prefix use(const options& opt = {}) { return {}; } - // THE C++ LIBRARY. Where the host compiler's C++ library is not the - // program's -- libc++ on Linux -- the default triplet is a generated one, - // `-libcxx`, whose ports build with mcpp's own clang - // (`mcpp::deps::program_compilers`). A triplet the project names is used - // as it stands. - const mcpp::deps::compilers cc = opt.triplet.empty() ? mcpp::deps::program_compilers() - : mcpp::deps::compilers{}; - const std::string triplet = !opt.triplet.empty() ? opt.triplet - : cc ? default_triplet() + "-libcxx" : default_triplet(); - if (triplet.empty() || triplet == "-libcxx") { - std::cerr << std::format("{}: no default vcpkg triplet for the target '{}'; name one with " - "options::triplet.\n", who, std::string(mcpp::target())); + // THE TOOLSET, and how it reaches vcpkg (mcpp.plugins.toolset). + const auto tools = ts::resolve(opt.toolset); + if (!tools) { + std::cerr << std::format("{}: {}\n", who, tools.error()); return {}; } - const fs::path installRoot = opt.install_root.empty() - ? manifestRoot / "vcpkg_installed" : mcpp::deps::absolute_from_root(opt.install_root); - const fs::path tripletRoot = installRoot / triplet; - const fs::path root = tripletRoot / triplet; - - prefix p; - p.root = mcpp::deps::generic(root); - p.include = mcpp::deps::generic(root / "include"); - p.lib = mcpp::deps::generic(root / "lib"); - p.bin = mcpp::deps::generic(root / "bin"); - p.share = mcpp::deps::generic(root / "share"); - p.triplet = triplet; + // THE C RUNTIME. On the MSVC ABI a port's C runtime must be the program's: + // a static library built against the other one fails the link + // (`/failifmismatch`), and a DLL built against it puts a second C++ + // runtime into the process without a word. + const std::string crt = opt.crt_linkage.empty() ? tools->crt : opt.crt_linkage; + if (!crt.empty() && crt != "static" && crt != "dynamic") { + std::cerr << std::format("{}: options::crt_linkage is '{}'; it is \"static\" or \"dynamic\".\n", + who, crt); + return {}; + } // ── the tool ── const std::string vcpkgRoot = opt.vcpkg_root.empty() ? std::string(mcpp::xpkg_dir("xim", "vcpkg")) : mcpp::deps::generic(mcpp::deps::absolute_from_root(opt.vcpkg_root)); const fs::path exe = vcpkgRoot.empty() ? fs::path() - : fs::path(vcpkgRoot) / (std::string(mcpp::host()).find("windows") != std::string::npos - ? "vcpkg.exe" : "vcpkg"); + : fs::path(vcpkgRoot) / (ts::host_is_windows() ? "vcpkg.exe" : "vcpkg"); // ── the overlays: the manifest's own, then the project's extras ── std::vector overlayTriplets = manifest_overlays(manifestRoot, "overlay-triplets"); for (auto const& d : opt.overlay_triplets) overlayTriplets.push_back(mcpp::deps::absolute_from_root(d)); const std::vector overlayPorts = manifest_overlays(manifestRoot, "overlay-ports"); - - // The generated triplet and the toolchain file it chains: vcpkg's own - // Linux toolchain, with the compilers set first. - fs::path generated; - if (cc && !vcpkgRoot.empty()) { - generated = fs::path(mcpp::out_dir()) / "deps-vcpkg" / "triplets"; - const std::string arch = triplet.substr(0, triplet.find('-')); - const fs::path chain = generated / (triplet + ".toolchain.cmake"); - mcpp::deps::write_if_changed(generated / (triplet + ".cmake"), std::format( - "# Written by mcpp.deps.vcpkg: the ports build with the program's compiler,\n" - "# so their C++ standard library is the program's.\n" - "set(VCPKG_TARGET_ARCHITECTURE {})\n" - "set(VCPKG_CRT_LINKAGE dynamic)\n" - "set(VCPKG_LIBRARY_LINKAGE static)\n" - "set(VCPKG_CMAKE_SYSTEM_NAME Linux)\n" - "set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE \"{}\")\n", - arch, mcpp::deps::generic(chain))); - mcpp::deps::write_if_changed(chain, std::format( - "set(CMAKE_C_COMPILER \"{}\")\n" - "set(CMAKE_CXX_COMPILER \"{}\")\n" - "include(\"{}\")\n", - cc.c, cc.cxx, mcpp::deps::generic(fs::path(vcpkgRoot) / "scripts" / "toolchains" / "linux.cmake"))); - } - std::vector tripletSearch = overlayTriplets; - if (!generated.empty()) tripletSearch.insert(tripletSearch.begin(), generated); if (!vcpkgRoot.empty()) { tripletSearch.push_back(fs::path(vcpkgRoot) / "triplets"); tripletSearch.push_back(fs::path(vcpkgRoot) / "triplets" / "community"); } + + // ── the base triplet: the project's, or the target's standard one ── + const std::string base = !opt.triplet.empty() ? opt.triplet : default_triplet(tools->msvc_abi ? crt : ""); + if (base.empty()) { + std::cerr << std::format("{}: no default vcpkg triplet for the target '{}'; name one with " + "options::triplet.\n", who, std::string(mcpp::target())); + return {}; + } + const fs::path baseFile = triplet_file(base, tripletSearch); + const std::string baseText = baseFile.empty() ? std::string() : read_text(baseFile); + if (!baseFile.empty()) mcpp::rerun_if_changed(mcpp::deps::generic(baseFile).c_str()); + + // Two explicit statements that cannot both hold are an error: the + // project's triplet and the program's C runtime. + if (tools->msvc_abi && !crt.empty() && !opt.triplet.empty() && !baseFile.empty()) { + std::string tripletCrt = top_level_setting(baseText, "VCPKG_CRT_LINKAGE"); + if (tripletCrt.empty()) tripletCrt = "dynamic"; + if (tripletCrt != crt) { + std::cerr << std::format( + "{}: the triplet '{}' links the C runtime {} (VCPKG_CRT_LINKAGE), and the program " + "links it {} ({}).\n" + " A static library built against the other C runtime fails the link, and a DLL " + "built against it\n" + " puts a second C++ runtime into the process.\n" + " hint: name a triplet whose VCPKG_CRT_LINKAGE is {} ({}), or set " + "options::crt_linkage to state the linkage the ports use.\n", + who, base, tripletCrt, crt, + opt.crt_linkage.empty() ? std::format("the program's C++ runtime contract, {}", + std::string(mcpp::cxx_runtime())) + : std::string("options::crt_linkage"), + crt, crt == "static" ? default_triplet("static") : default_triplet("dynamic")); + return {}; + } + } + + // ── the triplet vcpkg installs with ── + // + // `detected`, and `instance` with the instance's own toolset version, use + // the base triplet as it stands. `chain`, and `instance` with another + // version, use a DERIVED triplet `-mcpp-`: the base inlined + // (vcpkg hashes a triplet file's content, not a file it includes), then + // the resolved toolset. Its name changes exactly when its content does. + // A project triplet that chain-loads its own toolchain has decided the + // tools, and is used as it stands. + const bool projectChains = !top_level_setting(baseText, "VCPKG_CHAINLOAD_TOOLCHAIN_FILE").empty(); + const bool chain = tools->how == ts::mechanism::chain && !projectChains; + const bool pinVersion = tools->how == ts::mechanism::instance && !tools->toolset_version.empty() + && !tools->instance_default.empty() + && tools->toolset_version != tools->instance_default; + std::string triplet = base; + fs::path generated; + std::vector> actionEnv; + std::vector untracked; + if (chain || pinVersion) { + generated = fs::path(mcpp::out_dir()) / "deps-vcpkg" / "triplets"; + std::string text = std::format( + "# Written by mcpp.deps.vcpkg: the triplet '{}', then the toolset mcpp resolved.\n" + "# toolset: {}\n", base, ts::describe(*tools)); + text += baseText; + if (!text.ends_with('\n')) text += '\n'; + text += "\n# ── mcpp.deps.vcpkg ──\n"; + if (tools->msvc_abi && !crt.empty()) text += std::format("set(VCPKG_CRT_LINKAGE {})\n", crt); + if (pinVersion) + text += std::format("set(VCPKG_PLATFORM_TOOLSET_VERSION {})\n", tools->toolset_version); + if (chain) { + const std::string chainFile = std::format("mcpp-chain-{}.cmake", vcpkg_system_toolchain()); + mcpp::plugins::fs::write_if_changed(generated / chainFile, chain_toolchain_text()); + text += std::format("set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE \"${{CMAKE_CURRENT_LIST_DIR}}/{}\")\n", chainFile); + auto put = [&](std::string name, const std::string& value) { + if (value.empty()) return; + actionEnv.emplace_back(name, value); + untracked.push_back(std::move(name)); + }; + const bool win = ts::host_is_windows(); + auto native = [&](std::string p) { + if (win) for (std::size_t i = 0; i < p.size(); ++i) if (p[i] == '/') p[i] = '\\'; + return p; + }; + put("MCPP_VCPKG_CC", native(tools->cc)); + put("MCPP_VCPKG_CXX", native(tools->cxx)); + put("MCPP_VCPKG_RC", native(tools->rc)); + put("MCPP_VCPKG_MT", native(tools->mt)); + put("MCPP_VCPKG_ROOT", native(vcpkgRoot)); + for (auto const& [k, v] : tools->env) put(k, v); + std::string list; + for (auto const& u : untracked) list += " " + u; + text += std::format("set(VCPKG_ENV_PASSTHROUGH_UNTRACKED ${{VCPKG_ENV_PASSTHROUGH_UNTRACKED}}{})\n", list); + // PATH: make-based ports find `link.exe` there, and vcpkg's clean + // environment keeps the caller's PATH only when told to. The tools' + // directories go first, then the system's own. + if (tools->msvc_abi && win) { + std::string path; + for (auto const& d : tools->path_dirs) { if (!path.empty()) path += ';'; path += native(d); } + const char* sr = std::getenv("SystemRoot"); + const std::string root = sr && *sr ? sr : "C:\\Windows"; + for (auto const& d : { root + "\\system32", root, root + "\\System32\\Wbem", + root + "\\System32\\WindowsPowerShell\\v1.0" }) + path += ";" + d; + actionEnv.emplace_back("PATH", path); + const char* keep = std::getenv("VCPKG_KEEP_ENV_VARS"); + actionEnv.emplace_back("VCPKG_KEEP_ENV_VARS", + keep && *keep ? std::string(keep) + ";PATH" : std::string("PATH")); + text += msbuild_refusal_text(tools->identity); + } + } + triplet = std::format("{}-mcpp-{}", base, mcpp::deps::short_name(text)); + mcpp::plugins::fs::write_if_changed(generated / (triplet + ".cmake"), text); + } + if (tools->how == ts::mechanism::instance) { + // The instance mcpp resolved, selected the way vcpkg documents. When + // it is the instance vcpkg would choose anyway, the ABI hash is + // unchanged and nothing is rebuilt. + std::string dir = tools->instance_dir; + for (std::size_t i = 0; i < dir.size(); ++i) if (dir[i] == '/') dir[i] = '\\'; + actionEnv.emplace_back("VCPKG_VISUAL_STUDIO_PATH", dir); + } + if (!generated.empty()) tripletSearch.insert(tripletSearch.begin(), generated); + + const fs::path installRoot = opt.install_root.empty() + ? manifestRoot / "vcpkg_installed" : mcpp::deps::absolute_from_root(opt.install_root); + const fs::path tripletRoot = installRoot / triplet; + const fs::path root = tripletRoot / triplet; + + prefix p; + p.root = mcpp::deps::generic(root); + p.include = mcpp::deps::generic(root / "include"); + p.lib = mcpp::deps::generic(root / "lib"); + p.bin = mcpp::deps::generic(root / "bin"); + p.share = mcpp::deps::generic(root / "share"); + p.triplet = triplet; + // A project triplet that chain-loads its own toolchain is reported as + // `detected`: this member names no tool for it. + p.mechanism = std::string(ts::name(chain ? ts::mechanism::chain + : tools->how == ts::mechanism::instance ? ts::mechanism::instance + : ts::mechanism::detected)); + const bool shared = shared_linkage(triplet, tripletSearch); // ── the installation, as an edge ── @@ -289,6 +511,7 @@ inline prefix use(const options& opt = {}) { const fs::path user = mcpp::deps::vcpkg_user_dir(); const fs::path work = user / "mcpp" / mcpp::deps::short_name(mcpp::deps::generic(tripletRoot)); mcpp::rerun_if_env_changed("VCPKG_DOWNLOADS"); + mcpp::rerun_if_env_changed("VCPKG_KEEP_ENV_VARS"); const char* downloads = std::getenv("VCPKG_DOWNLOADS"); mcpp::action a; a.id = id.c_str(); @@ -305,6 +528,11 @@ inline prefix use(const options& opt = {}) { .arg(("--x-packages-root=" + mcpp::deps::generic(work / "pk")).c_str()) .arg("--clean-buildtrees-after-build") .arg("--clean-packages-after-build"); + // The host triplet is the derived one on the MSVC ABI: vcpkg would + // otherwise build its host ports with a standard triplet and look + // for Visual Studio, which the managed toolset does not come from. + if (chain && tools->msvc_abi && std::string_view(mcpp::host()) == std::string_view(mcpp::target())) + a.arg(("--host-triplet=" + triplet).c_str()); // Downloads are shared by every project on the machine; a user who has // moved them already (`VCPKG_DOWNLOADS`) keeps that. if (!downloads || !*downloads) @@ -318,6 +546,7 @@ inline prefix use(const options& opt = {}) { for (auto const& f : mcpp::deps::files_under(generated)) a.input(f.c_str()); } for (auto const& x : opt.install_args) a.arg(x.c_str()); + for (auto const& [k, v] : actionEnv) a.env(k.c_str(), v.c_str()); a.input(exeS.c_str()); a.input(mcpp::deps::generic(manifestFile).c_str()); if (fs::is_regular_file(configFile, ec)) a.input(mcpp::deps::generic(configFile).c_str()); @@ -334,7 +563,7 @@ inline prefix use(const options& opt = {}) { a.output(stamp.c_str()); a.output_dir(p.root.c_str()); a.submit(); - p.deployed = mcpp::deps::deploy_after("deps-vcpkg-" + triplet, stamp, root, opt.deploy); + p.deployed = mcpp::plugins::fs::deploy_after("deps-vcpkg-" + triplet, stamp, root, opt.deploy); } // ── the prefix, into the build ── diff --git a/docs/deps.md b/docs/deps.md index fb2a2d3..ea6be19 100644 --- a/docs/deps.md +++ b/docs/deps.md @@ -4,13 +4,13 @@ The `deps-*` members answer where a library comes from: a vcpkg manifest, a CMak ## `deps-vcpkg` -Module `mcpp.deps.vcpkg`; engine floor: 2026.9.26.2 (mcpp#702). +Module `mcpp.deps.vcpkg`; engine floor: 2026.9.28.3 (0.17.0, mcpp#734; 2026.9.26.2 before). **Needs and behaviour.** `xim:vcpkg` (the tool and the scripts released with it), which this feature declares on the host axis. From 0.13.0. Installs a `vcpkg.json` manifest as a `prepare` action and maps `//` into the build by name, its shared-library directory a runtime search directory; `mcpp emit build-database` installs nothing. See [the section below](#deps-vcpkg-the-libraries-a-vcpkg-manifest-names) ## `deps-cmake` -Module `mcpp.deps.cmake`; engine floor: 2026.9.26.2 (mcpp#702). +Module `mcpp.deps.cmake`; engine floor: 2026.9.28.3 (0.17.0, mcpp#734; 2026.9.26.2 before). **Needs and behaviour.** `xim:cmake`, which this feature declares on the host axis. From 0.13.0. Configures, builds and installs a CMake subproject as one `prepare` action whose inputs are the subproject's files, and maps the prefix as `deps-vcpkg` does @@ -24,7 +24,7 @@ Module `mcpp.deps.archive`; engine floor: 2026.9.26.2. ```toml [build-dependencies.mcpp] -plugins = { version = "0.16.0", features = ["deps-vcpkg"], host-module = true } +plugins = { version = "0.17.0", features = ["deps-vcpkg"], host-module = true } ``` ```cpp @@ -67,7 +67,9 @@ states every path it can, and the build is where the absence fails. | option | meaning | |---|---| -| `triplet` | empty derives it from the target: `x64-windows`, `arm64-windows`, `x64-mingw-dynamic`, `x64-linux`, `arm64-linux`, `x64-osx`, `arm64-osx`; on Linux under a libc++ toolchain the generated `x64-linux-libcxx` or `arm64-linux-libcxx` (0.15.0); a custom triplet is found through the manifest's `overlay-triplets` | +| `triplet` | the base triplet; empty derives it from the target: `x64-windows` (`x64-windows-static` when the program links the C runtime statically), `arm64-windows`, `x64-mingw-dynamic`, `x64-linux`, `arm64-linux`, `x64-osx`, `arm64-osx`. Under the `chain` mechanism the installation uses a triplet derived from it (below). A custom triplet is found through the manifest's `overlay-triplets` | +| `toolset` | which toolset builds the ports: `{.toolset = resolved or detected, .cc = abi_native or row}`; the default is `resolved` with `abi_native` (0.17.0, see [the toolset](#the-toolset-instance-chain-detected)) | +| `crt_linkage` | the ports' C runtime linkage on the MSVC ABI, `"static"` or `"dynamic"`; empty follows the program's C++ runtime contract (0.17.0) | | `libraries` | the link, in order; a name that matches no installed file fails the link, naming its path | | `manifest_root` | the directory holding `vcpkg.json` | | `install_root` | empty is vcpkg's default, `/vcpkg_installed`. Each triplet is its own vcpkg installation, `/`, because vcpkg's manifest mode removes from an installation the packages of every other triplet (0.15.0; 0.14.0's prefix `/` is no longer read) | @@ -93,17 +95,75 @@ workspace members installing one root run one after the other; without it the second fails, "failed to take lock" (0.15.1). vcpkg fetches its own CMake, Ninja and 7-Zip, and on Windows a portable git; on Linux and macOS its documented host prerequisites (git, curl, zip, unzip, tar, a C compiler) -are the host's. Ports are compiled with vcpkg's default toolchain for the -triplet: the Visual Studio toolset on Windows, the host compiler elsewhere. The -program links them, so its C++ standard library must be theirs. On Windows every -compiler of the MSVC ABI, mcpp's clang included, uses Microsoft's, and on macOS -every compiler uses libc++, so a port links as it stands. Linux has two -libraries that do not link with each other: the host compiler uses libstdc++, -and mcpp's clang uses libc++ (`std::__1::`). Under a libc++ toolchain the -default triplet is therefore a generated one, `-linux-libcxx`, whose -ports build with mcpp's clang through vcpkg's chain-loaded toolchain file; the -clang's own configuration names libc++ and the C library mcpp links against. -Under a gcc toolchain the default triplet is vcpkg's own (0.15.0). +are the host's. + +### The toolset: `instance`, `chain`, `detected` + +The ports are linked into the program, so they are built with the toolset mcpp +builds the program with (0.17.0). The member reads the toolset from the engine +(mcpp 2026.9.28.3, `mcpp::abi_tool`, `mcpp::tool_env`, `mcpp::toolset_identity`, +`mcpp::msvc_instance_dir`) through `mcpp.plugins.toolset`, and the way it reaches +vcpkg follows where the toolset came from: + +| mechanism | when | what vcpkg receives | port kinds that build | +|---|---|---|---| +| `instance` | an MSVC toolset from a Visual Studio instance (`msvc@system`, the default on a machine with Visual Studio) | `VCPKG_VISUAL_STUDIO_PATH` naming that instance; the standard triplet, or a derived one with `VCPKG_PLATFORM_TOOLSET_VERSION` when the resolved toolset is not the instance's default | CMake, make and MSBuild | +| `chain` | any other toolset: a managed MSVC toolset (`xim:msvc@`), and the toolsets of the Linux and macOS rows | a derived triplet `-mcpp-` that chain-loads a toolchain naming the tools, and the tools' environment | CMake and make; an MSBuild port is refused by name | +| `detected` | `options.toolset.toolset = detected` | nothing: vcpkg finds its own toolset, as in 0.16.0 | as vcpkg's own detection allows | + +**The derived triplet.** The base triplet's text is copied into it, not +included, because vcpkg hashes a triplet file's content and not the files it +includes. The member appends the chain-loaded toolchain +(`${CMAKE_CURRENT_LIST_DIR}/mcpp-chain-.cmake`), the environment +variables that pass through untracked (`MCPP_VCPKG_CC`, `MCPP_VCPKG_CXX`, +`MCPP_VCPKG_RC`, `MCPP_VCPKG_MT`, `MCPP_VCPKG_ROOT`, and on the MSVC ABI +`INCLUDE` and `LIB`), a comment with the toolset's identity, and on the MSVC ABI +`VCPKG_CRT_LINKAGE`. The toolchain file reads each tool from the environment +through `file(TO_CMAKE_PATH)` and then includes vcpkg's own +`scripts/toolchains/.cmake`, so ports keep vcpkg's standard flags. +Neither file holds a path, so the triplet's name, and vcpkg's ABI hash, depend +on the toolset's identity and its compilers and not on where they are +installed: the same toolset on another machine restores the same binary +packages. On the MSVC ABI the derived triplet is also the host triplet, and the +installation runs with the toolset's directories first on a `PATH` that vcpkg +keeps (`VCPKG_KEEP_ENV_VARS=PATH`), because a make-based port (icu) finds +`link.exe` there. + +**An MSBuild port under `chain`.** vcpkg would run MSBuild with +`/p:PlatformToolset=external` and fail without naming the cause. The derived +triplet stops such a port with a message saying that it needs a Visual Studio +instance, and that the toolchain `msvc@system` or `toolset = detected` builds +it. + +**The C runtime (MSVC ABI).** The ports' C runtime is the program's: the +default triplet is `x64-windows-static` for a program that links the C runtime +statically (`cxx_runtime = "self-contained"` or `linkage = "static"`), and a +triplet named in `triplet` whose `VCPKG_CRT_LINKAGE` contradicts the program's +is refused naming both statements. A static library built against the other C +runtime fails the link with `/failifmismatch`, and a DLL built against it puts +a second C++ runtime into the process without a word. `crt_linkage` states the +ports' linkage explicitly and wins. + +**Linux.** Linux has two C++ standard libraries that do not link with each +other. The derived triplet names the program's compiler, so the ports use the +program's library on every row: libc++ under mcpp's clang (`std::__1::`), and +the GCC payload's libstdc++ under mcpp's GCC. + +**Upgrading from 0.16.0.** On Linux and macOS, and on Windows with a managed +toolset, the installation moves to a derived triplet, so each port is built once +more (or restored from a binary cache that already holds it); the prefixes of +0.16.0 (`-linux-libcxx`, `x64-linux`) are left where they are. With +Visual Studio and the dynamic C runtime nothing changes: the standard triplet +and the instance vcpkg selects by itself give the same ABI hash. A program that +links the C runtime statically moves to `x64-windows-static`. + +**`detected`** keeps the behaviour of 0.16.0 until 2027-03-28 (a compatibility +unit, `src/compat/detected_toolset.cppm`) and prints a note once per build. On +the Linux libc++ row it names mcpp's clang, as 0.16.0 did. + +**Binary caches.** vcpkg reads `VCPKG_BINARY_SOURCES` and +`VCPKG_DEFAULT_BINARY_CACHE` from the environment, and the installation passes +the environment on; whether to host a shared cache is the project's decision. Not supported: vcpkg's classic mode; the debug libraries under `debug/lib`. @@ -129,10 +189,21 @@ directory; its inputs are the script and the subproject's files, so an edit to t maps its own. `layout` names install directories other than `include/`, `lib/` and `bin/`; `prefix_path` becomes `CMAKE_PREFIX_PATH` (`mcpp::rules::qt::root()` for a subproject that finds Qt); `cache_args` carries `-D…`, `-G …` and a -toolchain file. The subproject is compiled with the toolchain CMake selects by -default unless `cache_args` names a compiler or a toolchain file; on Linux under -a libc++ toolchain the compilers are mcpp's clang, as `deps-vcpkg` builds its -ports (0.15.0). `deploy` places files of the prefix +toolchain file. The subproject is compiled with the toolset mcpp resolved +(0.17.0), by the mechanisms `deps-vcpkg` uses: under `instance` CMake's default +generator (Visual Studio) is kept and pointed at the instance +(`CMAKE_GENERATOR_INSTANCE`) and the toolset version (`-T version=`, when it is +not the instance's default); under `chain` the Ninja generator runs with mcpp's +own ninja (`mcpp::ninja_program()`), the compilers are named by path, and the +action runs with the tools' environment and their directories first on `PATH`. +On the MSVC ABI `CMAKE_MSVC_RUNTIME_LIBRARY` follows the program's C runtime +(policy CMP0091). Each toolset statement configures its own build directory, +because CMake refuses a cache made with another generator or instance; the +directory 0.16.0 configured stays `build/`. A compiler, a toolchain file or a +generator in `cache_args` is the project's decision and wins. The options are +`toolset` and `crt_linkage`, as `deps-vcpkg` takes them, and `generator` +(`default`, or `ninja` for the Ninja generator under an instance as well). +`deploy` places files of the prefix beside the program, as `deps-vcpkg` takes it. ## `deps-archive`: files a program reads at run time, from an archive diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 5fce06f..aa84663 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -29,7 +29,54 @@ mcpp is a general build engine with a framework for build plugins. Plugins come 9. **Every behaviour has a criterion.** Each documented behaviour has a CI check, run on every platform the plugin claims. 10. **Documentation is concise.** A plugin's page opens with one summary paragraph, followed by use, options and behaviour, in plain declarative sentences. The README is an index. -## 3. A new plugin +## 3. Layers, names and the engine floor + +| layer | modules | provided by | +|---|---|---| +| L1 `mcpp.core` | `mcpp.core`, spelled `mcpp` as well (the two are permanently equivalent) | the engine | +| L2 general library | `mcpp.plugins.declare`, `mcpp.plugins.toolset`, `mcpp.plugins.fs`; `mcpp.plugins.testing` | this package, feature `plugins-core` (`plugins-testing` for the kit) | +| L3 plugins | `mcpp.deps.*`, `mcpp.rules.*`, `mcpp.dist.*`, `mcpp.tools.*` here; `mcpp..*` elsewhere | this package, by feature; any package | + +A layer depends only on the layers below it. L2 knows no foreign tool: it turns +the engine's facts into what a plugin needs (`mcpp.plugins.toolset` decides how +a resolved toolset reaches a foreign build system, `instance`, `chain` or +`detected`), and the plugin writes the foreign system's own files. + +**Module names.** A third-party plugin names its modules `mcpp..*`, +where `` is its package's namespace (`mcpp.acme.protobuf`). The +second segments `core`, `plugins`, `deps`, `rules`, `dist` and `tools` belong to +namespace `mcpp`; the engine warns when another package uses them, and +mcpp-index refuses such a package. + +**Using L2 from another package.** + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.17.0", features = ["plugins-core"], host-module = true, reexport = true } +``` + +`reexport = true` is needed when the plugin's consumers import L2 modules in +their own build programs. + +**The engine floor.** A plugin states the first mcpp release it needs in +`[package] mcpp = ">="`; an older engine stops before any other work +and names the upgrade. This package's floor is 2026.9.28.3, the release that +states the build information L2 reads. + +**Testing a plugin.** `mcpp.plugins.testing` runs a plugin function in a child +process against a stated build context (`row::windows_visual_studio()`, +`row::windows_managed()`, `row::linux_gcc()`, `row::linux_libcxx()`, or a +context built with `set` and `file`) and hands the lines it emitted and the +files it wrote to a check. The test is a build program; a failed case fails the +build and prints the report. `tests/plugin-logic` is this package's own use. + +**Compatibility units.** A behaviour kept after the release that replaced it +lives in a `compat/` directory, one unit per behaviour, whose header states +what it keeps, since when, its retirement date (six months later), its +replacement and the note it prints once per build. +`.github/scripts/check-compat-retirement.sh` fails once a date has passed. + +## 4. A new plugin A plugin in this package consists of: - a module file under `rules/`, `tools/`, `dist/` or `deps/`; diff --git a/docs/rules-qt.md b/docs/rules-qt.md index 03bf139..337b024 100644 --- a/docs/rules-qt.md +++ b/docs/rules-qt.md @@ -6,7 +6,7 @@ ```toml [build-dependencies.mcpp] -plugins = { version = "0.16.0", features = ["rules-qt"], host-module = true } +plugins = { version = "0.17.0", features = ["rules-qt"], host-module = true } # The SDK and its version are the project's declaration. [target.'cfg(any(windows, linux, macos))'.xlings.workspace] diff --git a/mcpp.toml b/mcpp.toml index e0e30bf..f66961e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,10 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.16.0" +version = "0.17.0" +# The first mcpp release that states the build information (protocol 14), +# places files in one edge, and renders structured diagnostics (mcpp#734). +mcpp = ">=2026.9.28.3" description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature" license = "Apache-2.0" authors = ["mcpp-community"] @@ -17,21 +20,24 @@ import_std = true # the source set; mcpp compiles every interface unit among the resolved # sources as a host module under the name the unit declares (2026.9.5.3+). [build] -# THE LIB ROOT ALONE, AND `src/declare.cppm` BEHIND A FEATURE. +# THE LIB ROOT ALONE, AND THE GENERAL LIBRARY BEHIND A FEATURE. # -# The two units divide by what they may import: +# The units divide by what they may import: # -# src/plugins.cppm `mcpp.plugins.surface`, importing only `std`, so the -# same code compiles into `mcpp-embed` -- an ordinary -# program, which is what lets the generation be an ACTION -# and a payload be that action's declared input -# src/declare.cppm the build-program half: `mcpp::action`, `mcpp::generated` -# and the decision of how the generation reaches the graph +# src/plugins.cppm `mcpp.plugins`, importing only `std`, so the same code +# compiles into `mcpp-embed` -- an ordinary program, which +# is what lets the generation be an ACTION and a payload +# be that action's declared input +# src/declare.cppm, L2, the official general library for build programs +# src/toolset.cppm, (mcpp#734): `mcpp.plugins.declare`, +# src/fs.cppm `mcpp.plugins.toolset`, `mcpp.plugins.fs` +# src/testing.cppm `mcpp.plugins.testing`, the test kit for plugins # -# The second one imports `mcpp`, which exists only inside a build program, so -# listing it here would break the ORDINARY build of this package -- the build -# that produces the tool. `[features.surface]` scopes it to the consumers that -# have a build program, and every member implies it. +# The L2 units import `mcpp.core` (spelled `mcpp`), which exists only inside a +# build program, so listing them here would break the ORDINARY build of this +# package -- the build that produces the tool. `[features.plugins-core]` +# scopes them to the consumers that have a build program, and every family +# member implies it. # # It also needs mcpp 2026.9.8.1: before that release a package's host modules # were ordered by PATH, so `rules/spirv.cppm` was compiled before @@ -66,21 +72,45 @@ sources = ["src/plugins.cppm"] [features] default = [] -# The build-program half of the surface. Not a member a consumer names: every -# member that embeds a payload implies it, and a consumer that activates none -# of them compiles neither this nor anything that imports `mcpp`. +# L2: THE OFFICIAL GENERAL LIBRARY FOR BUILD PROGRAMS (0.17.0, mcpp#734). +# Every family member implies it, so a project that uses an official plugin +# writes nothing more; a build program that uses only L2, and a third-party +# plugin built on it, name `plugins-core` on the dependency edge. A consumer +# that activates none of them compiles nothing that imports `mcpp`. +# +# mcpp.plugins.declare how a generated surface reaches the build graph +# mcpp.plugins.toolset the resolved toolchain, translated for a foreign +# build system (`instance`, `chain`, `detected`) +# mcpp.plugins.fs deterministic file generation and placement +[features.plugins-core] +sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", + "src/compat/detected_toolset.cppm"] + +# The test kit: a plugin function runs against a stated build context, and its +# directives are compared. Separate from `plugins-core`, so that no build +# program compiles it unless it asks. +[features.plugins-testing] +sources = ["src/testing.cppm"] +implies = ["plugins-core"] + +# COMPATIBILITY UNIT +# keeps: the feature name `surface`, the name of `plugins-core` until 0.16.0 +# since: 0.17.0 (2026-09-28) +# retires: 2027-03-28 +# replacement: `plugins-core` +# note: none: a feature name is resolved before any build program runs [features.surface] -sources = ["src/declare.cppm"] +implies = ["plugins-core"] [features.rules-ascendc] sources = ["rules/ascendc.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.ascendc" device_extensions = [".asc", ".cce"] [features.rules-cuda] sources = ["rules/cuda.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.cuda" device_extensions = [".cu"] @@ -90,7 +120,7 @@ device_extensions = [".cu"] # locates the toolchain and refuses naming the command that found nothing. [features.rules-metal] sources = ["rules/metal.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.metal" device_extensions = [".metal"] @@ -100,19 +130,19 @@ device_extensions = [".metal"] # `rules-metal` gives: the compiler ships with Xcode and the Command Line Tools. [features.rules-swift] sources = ["rules/swift.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.swift" device_extensions = [".swift"] [features.rules-hip] sources = ["rules/hip.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.hip" device_extensions = [".hip"] [features.rules-slang] sources = ["rules/slang.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.slang" device_extensions = [".slang"] @@ -121,7 +151,7 @@ device_extensions = [".slang"] # message than the engine's "no rule compiles it". [features.rules-spirv] sources = ["rules/spirv.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.spirv" device_extensions = [".comp", ".vert", ".frag", ".geom", ".tesc", ".tese", ".mesh", ".task", ".rgen", ".rint", ".rahit", ".rchit", @@ -129,7 +159,7 @@ device_extensions = [".comp", ".vert", ".frag", ".geom", ".tesc", ".tese", [features.rules-sycl] sources = ["rules/sycl.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.sycl" device_extensions = [".sycl"] @@ -141,7 +171,7 @@ device_extensions = [".sycl"] # project's own `sources` names are routed. [features.rules-qt] sources = ["rules/qt.cppm"] -implies = ["surface"] +implies = ["plugins-core"] rule_module = "mcpp.rules.qt" device_extensions = [".ui", ".qrc", ".ts"] @@ -191,23 +221,23 @@ device_extensions = [".ui", ".qrc", ".ts"] # something true -- this member is not a rule. [features.dist-appimage] sources = ["dist/appimage.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-wix] sources = ["dist/wix.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-apple] sources = ["dist/apple.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-web] sources = ["dist/web.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.dist-apk] sources = ["dist/apk.cppm"] -implies = ["surface"] +implies = ["plugins-core"] # `dist-apk` WITH THE KOTLIN COMPILER, AND WITH THE MAVEN RESOLVER (0.11.0). # Features of their own rather than payloads of `dist-apk`, for the reason @@ -241,11 +271,11 @@ implies = ["dist-apk"] # build by name, and its shared-library directory through # `mcpp::runtime_search_dir` -- both from the release that lands mcpp#702. # -# `deps` is the unit both members import, as `surface` is for the rest; a +# `deps` is the unit the members import, as `plugins-core` is for the rest; a # consumer never names it. [features.deps] -sources = ["deps/deps.cppm"] -implies = ["surface"] +sources = ["deps/deps.cppm", "deps/compat/names.cppm"] +implies = ["plugins-core"] [features.deps-vcpkg] sources = ["deps/vcpkg.cppm"] @@ -265,11 +295,11 @@ implies = ["deps"] [features.tools-embed] sources = ["tools/embed.cppm"] -implies = ["surface"] +implies = ["plugins-core"] [features.tools-island] sources = ["tools/island.cppm"] -implies = ["surface"] +implies = ["plugins-core"] # ── The environment each rule needs (mcpp 2026.9.6.6+) ────────────────────── # diff --git a/src/compat/detected_toolset.cppm b/src/compat/detected_toolset.cppm new file mode 100644 index 0000000..a62278f --- /dev/null +++ b/src/compat/detected_toolset.cppm @@ -0,0 +1,47 @@ +// COMPATIBILITY UNIT +// keeps: `toolset = detected` in the deps members: vcpkg and CMake find +// their own toolset, as in 0.16.0 +// since: 0.17.0 (2026-09-28) +// retires: 2027-03-28 +// replacement: the engine's toolchain selection; `msvc@system` makes the +// default `resolved` use that Visual Studio instance +// note: "mcpp.plugins: toolset = detected is kept until 2027-03-28; ..." +// +// Retirement removes this file, its entry in `[features.plugins-core]`, the +// import in `src/toolset.cppm` and the `source::detected` branch there. +// `.github/scripts/check-compat-retirement.sh` fails once the date has passed. + +export module mcpp.plugins.compat.detected_toolset; + +import std; +import mcpp; + +export namespace mcpp::plugins::compat { + +struct detected_answer { + // 0.16.0 named mcpp's own clang on the Linux libc++ row, where the host + // compiler that detection finds uses libstdc++ and the two do not link. + bool name_compilers = false; +}; + +inline detected_answer detected_toolset() { + static bool noted = false; + if (!noted) { + noted = true; + mcpp::report({ + .severity = "note", + .message = "mcpp.plugins: `toolset = detected` is kept until 2027-03-28", + .impact = "vcpkg and CMake choose their own toolset, which may differ from the one " + "mcpp builds the program with", + .hint = "remove the option to use the toolset mcpp resolved; select a Visual Studio " + "instance with the toolchain `msvc@system`", + }); + } + detected_answer a; + a.name_compilers = std::string_view(mcpp::target_os()) == "linux" + && std::string_view(mcpp::cxx_stdlib()) == "libc++" + && std::string_view(mcpp::host()) == std::string_view(mcpp::target()); + return a; +} + +} // namespace mcpp::plugins::compat diff --git a/src/fs.cppm b/src/fs.cppm new file mode 100644 index 0000000..c5edb77 --- /dev/null +++ b/src/fs.cppm @@ -0,0 +1,102 @@ +// mcpp.plugins.fs -- deterministic file generation and placement for build +// programs (L2 of the build-plugin architecture, mcpp#734). +// +// Two things every plugin that writes or places files needs, and that none of +// them should write twice: +// +// 1. A GENERATED FILE KEEPS ITS TIME STAMP WHILE ITS CONTENT DOES NOT CHANGE. +// A build program runs on every plan; a file it rewrites unconditionally +// is a changed input of every action that reads it, and that action runs +// again on every build. `write_if_changed` writes only a changed text. +// +// 2. A FILE THAT AN ACTION PRODUCES IS PLACED BESIDE THE PROGRAM THROUGH A +// COPY THE GRAPH NAMES. `mcpp::deploy` takes a file the build graph knows +// how to produce. A file a `prepare` action installs is not one: its name +// is unknown to the graph until the action has run. `deploy_after` copies +// each named file out of such a directory by an action that names it as +// an output (SPEC-007 R3.2), and deploys the copy. Everything is declared +// before anything exists, so `mcpp emit build-database` plans it on a +// machine that never built. +// +// Until 0.17.0 these lived in `mcpp.deps`, which re-exports them under their +// old names through a compatibility unit (`deps/compat/names.cppm`). + +export module mcpp.plugins.fs; + +import std; +import mcpp; + +export namespace mcpp::plugins::fs { + +inline std::string generic(const std::filesystem::path& p) { + return p.lexically_normal().generic_string(); +} + +// Writes `content` to `file` unless the file already holds it, so a file the +// build program generates keeps its time stamp from one plan to the next and +// the action that reads it does not run again. Returns whether it wrote. +inline bool write_if_changed(const std::filesystem::path& file, const std::string& content) { + namespace stdfs = std::filesystem; + std::error_code ec; + if (stdfs::is_regular_file(file, ec)) { + std::ifstream in(file, std::ios::binary); + const std::string old{ std::istreambuf_iterator(in), std::istreambuf_iterator() }; + if (old == content) return false; + } + stdfs::create_directories(file.parent_path(), ec); + std::ofstream(file, std::ios::binary | std::ios::trunc) << content; + return true; +} + +// One file of a directory to place beside the program: `file` relative to the +// directory (`share/opencc/t2s.json`), `to` the directory beside the program +// it is placed in (`BaseConfig/opencc`; empty or `.` is the program's own +// directory). +struct deploy_entry { + std::string file; + std::string to; +}; + +// A file placed by `deploy_after`: `path`, the copy the build produces (a node +// of the build graph, which a project's own actions may take as an input), and +// `to`, its path beside the program. +struct deployed_file { + std::string path; + std::string to; +}; + +// Copies each entry's file out of `root` once the action that writes `stamp` +// has run, and deploys the copy. `who` names the plugin in action ids. Two or +// more deployed files become one placement edge in the engine (mcpp 2026.9.28.3, +// `mcpp stage --list`), so the count of entries costs no per-file process. +inline std::vector deploy_after(std::string_view who, const std::string& stamp, + const std::filesystem::path& root, + std::span entries) { + namespace stdfs = std::filesystem; + std::vector out; + const stdfs::path base = stdfs::path(mcpp::out_dir()) / "deps-deploy" / std::string(who); + for (auto const& e : entries) { + const stdfs::path rel = stdfs::path(e.file).lexically_normal(); + const std::string src = generic(root / rel); + const std::string copy = generic(base / rel); + // Index loops over a string: GCC 16 fails to inline a string iterator + // inside a module's interface. + std::string key = rel.generic_string(); + for (std::size_t i = 0; i < key.size(); ++i) if (key[i] == '/') key[i] = '.'; + const std::string id = std::format("{}:deploy:{}", who, key); + const std::string desc = std::format("DEPLOY {}", rel.generic_string()); + mcpp::action a; + a.id = id.c_str(); + a.role = mcpp::roles::artifact; + a.description = desc.c_str(); + a.arg("${mcpp.self}").arg("stage").arg("--output").arg(copy.c_str()).arg(src.c_str()) + .input(stamp.c_str()).output(copy.c_str()).submit(); + const std::string dir = e.to.empty() ? std::string(".") : e.to; + mcpp::deploy(copy.c_str(), dir.c_str()); + const std::string to = generic(stdfs::path(dir) / rel.filename()); + out.push_back({copy, to}); + } + return out; +} + +} // namespace mcpp::plugins::fs diff --git a/src/plugins.cppm b/src/plugins.cppm index 91014bc..f1e6b39 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -49,7 +49,7 @@ export namespace mcpp::plugins { // // One package, one version: the number lives in mcpp.toml, and the CI step // `the collection states its own version` compares the two. -inline constexpr std::string_view version = "0.16.0"; +inline constexpr std::string_view version = "0.17.0"; } // namespace mcpp::plugins diff --git a/src/testing.cppm b/src/testing.cppm new file mode 100644 index 0000000..6e1f187 --- /dev/null +++ b/src/testing.cppm @@ -0,0 +1,340 @@ +// mcpp.plugins.testing -- the test kit for build plugins (L2 of the +// build-plugin architecture, mcpp#734; feature `plugins-testing`). +// +// A plugin's logic is a function of the build context, and its effect is the +// set of directives and actions it emits, which are `mcpp:` lines on standard +// output. The kit runs a plugin function against a STATED context -- the +// target, the toolchain facts, the profile, the directories, files the +// function reads -- and hands the emitted lines, and the files the function +// wrote, to a check. No foreign tool is installed or run: a context that +// describes a Visual Studio row is tested on a Linux machine. +// +// A test is a build program: +// +// import std; +// import mcpp.plugins.testing; +// import mcpp.deps.vcpkg; +// namespace t = mcpp::plugins::testing; +// +// int main(int argc, char** argv) { +// return t::run(argc, argv, { +// { "the managed toolset is chained", +// t::row::windows_managed(), +// [] { return mcpp::deps::vcpkg::use({ .libraries = {"fmt"} }) ? 0 : 1; }, +// [](t::result const& r, t::checker& c) { +// c.expect(r.has_line("mcpp:action=", "--host-triplet="), "the host triplet is derived"); +// } }, +// }); +// } +// +// HOW. The build program is run by mcpp as usual; `run` starts the same +// executable once per case (`--mcpp-plugins-testing-case=`) with the case's +// context in its environment and its standard output in a file, and runs the +// check on what it wrote. A separate process per case is what makes the +// context complete: every accessor of `mcpp.core` reads the environment, and +// a function's `static` state starts fresh. A failed check fails the build +// program, which fails the build and prints the report. The verdicts are also +// written to `/plugins-testing/results.txt`, one `PASS` or `FAIL` +// line per case, for a CI step to count. + +module; +#include + +export module mcpp.plugins.testing; + +import std; +import mcpp; + +namespace mcpp::plugins::testing::detail { + +inline void set_env(const std::string& name, const std::string& value) { +#if defined(_WIN32) + ::_putenv_s(name.c_str(), value.c_str()); +#else + if (value.empty()) ::unsetenv(name.c_str()); + else ::setenv(name.c_str(), value.c_str(), 1); +#endif +} + +// Every key an accessor of `mcpp.core` reads that a plugin's logic depends on. +// A case sets each of them -- empty unless its context states a value -- so +// nothing of the real build reaches the function under test. +inline constexpr std::string_view kKeys[] = { + "MCPP_TARGET", "MCPP_TARGET_OS", "MCPP_TARGET_ARCH", "MCPP_TARGET_ENV", "MCPP_HOST", + "MCPP_PROFILE", "MCPP_ACCEL", "MCPP_OUT_DIR", "MCPP_MANIFEST_DIR", "MCPP_TOOLCHAIN_DIR", + "MCPP_TOOLCHAIN_SYSROOT", "MCPP_COMPILER", "MCPP_CXX_STDLIB", "MCPP_TARGET_SYSROOT", + "MCPP_PKG_NAME", "MCPP_PKG_NAMESPACE", "MCPP_PKG_VERSION", "MCPP_PACK_FORMAT", + "MCPP_TOOL_CC", "MCPP_TOOL_CXX", "MCPP_TOOL_LD", "MCPP_TOOL_AR", "MCPP_TOOL_RC", + "MCPP_TOOL_AS", "MCPP_TOOL_MT", + "MCPP_ABI_TOOL_CC", "MCPP_ABI_TOOL_CXX", "MCPP_ABI_TOOL_LD", "MCPP_ABI_TOOL_AR", + "MCPP_ABI_TOOL_RC", "MCPP_ABI_TOOL_AS", "MCPP_ABI_TOOL_MT", + "MCPP_TOOL_ENV", "MCPP_TOOLSET_IDENTITY", "MCPP_MSVC_INSTANCE_DIR", "MCPP_NINJA", + "MCPP_CXX_RUNTIME", "MCPP_MSVC_CRT_LINKAGE", +}; + +inline std::string read_file(const std::filesystem::path& p) { + std::ifstream in(p, std::ios::binary); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +} // namespace mcpp::plugins::testing::detail + +export namespace mcpp::plugins::testing { + +// A stated build context: environment values for the accessors, and files +// that exist before the function runs. `{root}` in a value or a path is the +// case's own scratch directory, created empty for every case. +struct context { + std::vector> values; + std::vector> files; + + context& set(std::string key, std::string value) { + for (auto& kv : values) if (kv.first == key) { kv.second = std::move(value); return *this; } + values.emplace_back(std::move(key), std::move(value)); + return *this; + } + context& file(std::string path, std::string content = {}) { + files.emplace_back(std::move(path), std::move(content)); + return *this; + } + // `xpkg_dir(ns, name)`: the directory of a declared payload. + context& xpkg(std::string_view ns, std::string_view name, std::string dir) { + std::string key = "MCPP_XPKG_"; + auto put = [&](std::string_view s) { + for (std::size_t i = 0; i < s.size(); ++i) { + const char c = s[i]; + key += (c >= 'a' && c <= 'z') ? char(c - 'a' + 'A') + : ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ? c : '_'; + } + }; + if (!ns.empty()) { put(ns); key += '_'; } + put(name); + key += "_DIR"; + return set(std::move(key), std::move(dir)); + } +}; + +// Contexts that describe the rows a plugin meets. Paths are under `{root}`, +// so they exist only where a case creates them. +namespace row { + +inline context linux_gcc() { + return context{} + .set("MCPP_TARGET", "x86_64-linux-gnu").set("MCPP_HOST", "x86_64-linux-gnu") + .set("MCPP_TARGET_OS", "linux").set("MCPP_TARGET_ARCH", "x86_64").set("MCPP_TARGET_ENV", "gnu") + .set("MCPP_PROFILE", "dev").set("MCPP_COMPILER", "gcc").set("MCPP_CXX_STDLIB", "libstdc++") + .set("MCPP_TOOLCHAIN_DIR", "{root}/gcc") + .set("MCPP_TOOL_CC", "{root}/gcc/bin/gcc").set("MCPP_TOOL_CXX", "{root}/gcc/bin/g++") + .set("MCPP_TOOL_LD", "{root}/gcc/bin/g++").set("MCPP_TOOL_AR", "{root}/gcc/bin/gcc-ar") + .set("MCPP_ABI_TOOL_CC", "{root}/gcc/bin/gcc").set("MCPP_ABI_TOOL_CXX", "{root}/gcc/bin/g++") + .set("MCPP_ABI_TOOL_LD", "{root}/gcc/bin/g++").set("MCPP_ABI_TOOL_AR", "{root}/gcc/bin/gcc-ar") + .set("MCPP_TOOLSET_IDENTITY", "gcc 16.1.0").set("MCPP_NINJA", "{root}/ninja/ninja") + .set("MCPP_CXX_RUNTIME", "toolchain-coupled") + .set("MCPP_OUT_DIR", "{root}/out").set("MCPP_MANIFEST_DIR", "{root}/pkg"); +} + +inline context linux_libcxx() { + return linux_gcc() + .set("MCPP_COMPILER", "clang").set("MCPP_CXX_STDLIB", "libc++") + .set("MCPP_TOOLCHAIN_DIR", "{root}/llvm") + .set("MCPP_TOOL_CC", "{root}/llvm/bin/clang").set("MCPP_TOOL_CXX", "{root}/llvm/bin/clang++") + .set("MCPP_TOOL_LD", "{root}/llvm/bin/clang++").set("MCPP_TOOL_AR", "{root}/llvm/bin/llvm-ar") + .set("MCPP_ABI_TOOL_CC", "{root}/llvm/bin/clang").set("MCPP_ABI_TOOL_CXX", "{root}/llvm/bin/clang++") + .set("MCPP_ABI_TOOL_LD", "{root}/llvm/bin/clang++").set("MCPP_ABI_TOOL_AR", "{root}/llvm/bin/llvm-ar") + .set("MCPP_TOOLSET_IDENTITY", "clang 22.1.8"); +} + +// The MSVC ABI with cl.exe from `tools`, the SDK's rc.exe and mt.exe, and the +// environment the engine runs them with. +inline context windows_msvc(const std::string& tools, const std::string& version) { + const std::string bin = tools + "/bin/Hostx64/x64"; + const std::string sdk = "{root}/sdk/bin/10.0.26100.0/x64"; + return context{} + .set("MCPP_TARGET", "x86_64-pc-windows-msvc").set("MCPP_HOST", "x86_64-pc-windows-msvc") + .set("MCPP_TARGET_OS", "windows").set("MCPP_TARGET_ARCH", "x86_64").set("MCPP_TARGET_ENV", "msvc") + .set("MCPP_PROFILE", "dev").set("MCPP_COMPILER", "msvc").set("MCPP_CXX_STDLIB", "msvc-stl") + .set("MCPP_TOOL_CC", bin + "/cl.exe").set("MCPP_TOOL_CXX", bin + "/cl.exe") + .set("MCPP_TOOL_LD", bin + "/link.exe").set("MCPP_TOOL_AR", bin + "/lib.exe") + .set("MCPP_TOOL_RC", sdk + "/rc.exe").set("MCPP_TOOL_MT", sdk + "/mt.exe") + .set("MCPP_ABI_TOOL_CC", bin + "/cl.exe").set("MCPP_ABI_TOOL_CXX", bin + "/cl.exe") + .set("MCPP_ABI_TOOL_LD", bin + "/link.exe").set("MCPP_ABI_TOOL_AR", bin + "/lib.exe") + .set("MCPP_ABI_TOOL_RC", sdk + "/rc.exe").set("MCPP_ABI_TOOL_MT", sdk + "/mt.exe") + .set("MCPP_TOOL_ENV", "INCLUDE=" + tools + "/include;{root}/sdk/Include/ucrt\n" + "LIB=" + tools + "/lib/x64;{root}/sdk/Lib/ucrt/x64\n" + "PATH=" + bin + ";" + sdk) + .set("MCPP_TOOLSET_IDENTITY", "msvc " + version + "; sdk 10.0.26100.0") + .set("MCPP_NINJA", "{root}/ninja/ninja.exe") + .set("MCPP_CXX_RUNTIME", "toolchain-coupled").set("MCPP_MSVC_CRT_LINKAGE", "dynamic") + .set("MCPP_OUT_DIR", "{root}/out").set("MCPP_MANIFEST_DIR", "{root}/pkg"); +} + +// `msvc@system`: the toolset of a Visual Studio instance at `{root}/vs`, which +// selects `default_version` by itself. +inline context windows_visual_studio(const std::string& version = "14.44.35207", + const std::string& default_version = "14.44.35207") { + return windows_msvc("{root}/vs/VC/Tools/MSVC/" + version, version) + .set("MCPP_MSVC_INSTANCE_DIR", "{root}/vs") + .file("vs/VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt", default_version + "\n"); +} + +// A managed MSVC toolset: no Visual Studio instance. +inline context windows_managed(const std::string& version = "14.44.35207") { + return windows_msvc("{root}/msvc/VC/Tools/MSVC/" + version, version); +} + +} // namespace row + +// What one case produced. +struct result { + int exit_code = 0; + std::vector lines; // standard output, one entry per line + std::string errors; // standard error + std::filesystem::path root; // the case's scratch directory + + // A line that starts with `prefix` and contains every one of `parts`. + bool has_line(std::string_view prefix, std::initializer_list parts = {}) const { + return !line(prefix, parts).empty(); + } + bool has_line(std::string_view prefix, std::string_view part) const { + return has_line(prefix, {part}); + } + std::string line(std::string_view prefix, std::initializer_list parts = {}) const { + for (auto const& l : lines) { + if (!l.starts_with(prefix)) continue; + bool all = true; + for (auto p : parts) if (l.find(p) == std::string::npos) { all = false; break; } + if (all) return l; + } + return {}; + } + std::size_t count(std::string_view prefix) const { + std::size_t n = 0; + for (auto const& l : lines) if (l.starts_with(prefix)) ++n; + return n; + } + // A file the function wrote, relative to the case's scratch directory. + std::string file(std::string_view relative) const { + return detail::read_file(root / std::filesystem::path(std::string(relative))); + } + // Every file under a directory of the scratch directory, relative to it. + std::vector files_under(std::string_view relative) const { + std::vector out; + std::error_code ec; + const auto dir = root / std::filesystem::path(std::string(relative)); + for (auto it = std::filesystem::recursive_directory_iterator(dir, ec); + !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) + if (it->is_regular_file(ec)) out.push_back(it->path().lexically_relative(root).generic_string()); + std::ranges::sort(out); + return out; + } +}; + +struct checker { + std::vector failures; + void expect(bool ok, std::string_view what) { + if (!ok) failures.emplace_back(what); + } +}; + +struct test_case { + std::string name; + context ctx; + std::function body; + std::function check; +}; + +namespace detail2 { +inline std::string expand(std::string text, const std::string& root) { + for (std::size_t at = text.find("{root}"); at != std::string::npos; at = text.find("{root}", at + root.size())) + text.replace(at, 6, root); + return text; +} +inline std::string quoted(const std::string& s) { return "\"" + s + "\""; } +} // namespace detail2 + +// Runs `cases` (see the header). Returns the build program's exit code: 0 +// when every case passes. +inline int run(int argc, char** argv, std::span cases) { + namespace fs = std::filesystem; + constexpr std::string_view flag = "--mcpp-plugins-testing-case="; + for (int i = 1; i < argc; ++i) { + const std::string_view a = argv[i]; + if (!a.starts_with(flag)) continue; + const std::size_t n = static_cast(std::stoul(std::string(a.substr(flag.size())))); + return n < cases.size() && cases[n].body ? cases[n].body() : 97; + } + + std::error_code ec; + const fs::path self = fs::absolute(fs::path(argc > 0 ? argv[0] : ""), ec); + const std::string out = mcpp::out_dir(); + const fs::path base = (out.empty() ? fs::temp_directory_path() : fs::path(out)) / "plugins-testing"; + fs::remove_all(base, ec); + fs::create_directories(base, ec); + + // The union of the keys any case states, beside the kit's own list. + std::vector keys; + for (auto k : detail::kKeys) keys.emplace_back(k); + for (auto const& c : cases) + for (auto const& kv : c.ctx.values) + if (std::ranges::find(keys, kv.first) == keys.end()) keys.push_back(kv.first); + + std::string report, verdicts; + int failed = 0; + for (std::size_t n = 0; n < cases.size(); ++n) { + const auto& c = cases[n]; + const fs::path root = base / std::format("case-{}", n); + fs::create_directories(root, ec); + const std::string rootS = root.generic_string(); + for (auto const& [path, content] : c.ctx.files) { + const fs::path f = root / fs::path(detail2::expand(path, rootS)); + fs::create_directories(f.parent_path(), ec); + std::ofstream(f, std::ios::binary) << detail2::expand(content, rootS); + } + for (auto const& k : keys) { + std::string v; + for (auto const& kv : c.ctx.values) if (kv.first == k) v = detail2::expand(kv.second, rootS); + detail::set_env(k, v); + } + const fs::path so = root / "stdout.txt", se = root / "stderr.txt"; + std::string cmd = detail2::quoted(self.string()) + " " + std::string(flag) + std::to_string(n) + + " > " + detail2::quoted(so.string()) + " 2> " + detail2::quoted(se.string()); +#if defined(_WIN32) + cmd = "\"" + cmd + "\""; // cmd.exe strips one outer pair of quotes +#endif + const int status = std::system(cmd.c_str()); + + result r; + r.exit_code = status; + r.root = root; + r.errors = detail::read_file(se); + std::istringstream in(detail::read_file(so)); + for (std::string l; std::getline(in, l); ) { + if (!l.empty() && l.back() == '\r') l.pop_back(); + r.lines.push_back(l); + } + checker ck; + if (c.check) c.check(r, ck); + if (ck.failures.empty()) { + verdicts += std::format("PASS {}\n", c.name); + } else { + ++failed; + verdicts += std::format("FAIL {}\n", c.name); + report += std::format("FAIL: {}\n", c.name); + for (auto const& f : ck.failures) report += std::format(" expected: {}\n", f); + report += std::format(" exit status {}; standard output ({} lines):\n", status, r.lines.size()); + for (auto const& l : r.lines) report += " " + l + "\n"; + if (!r.errors.empty()) report += " standard error:\n" + r.errors + "\n"; + } + } + std::ofstream(base / "results.txt", std::ios::binary) << verdicts; + for (auto const& k : keys) detail::set_env(k, ""); + std::cerr << report + << std::format("mcpp.plugins.testing: {} of {} cases passed\n", cases.size() - failed, cases.size()); + return failed == 0 ? 0 : 1; +} + +inline int run(int argc, char** argv, std::initializer_list cases) { + return run(argc, argv, std::span(cases.begin(), cases.size())); +} + +} // namespace mcpp::plugins::testing diff --git a/src/toolset.cppm b/src/toolset.cppm new file mode 100644 index 0000000..99ae299 --- /dev/null +++ b/src/toolset.cppm @@ -0,0 +1,254 @@ +// mcpp.plugins.toolset -- the resolved toolchain, translated for a foreign +// build system (L2 of the build-plugin architecture, mcpp#734). +// +// A plugin that drives vcpkg, CMake, Meson or make hands it the tools the +// program is built with, so that what it builds links with the program. The +// engine states the facts (mcpp 2026.9.28.3, protocol 14): each role's tool, +// the target ABI's native tools, the environment the engine runs them with, +// a path-free identity of the toolset, the Visual Studio instance it came +// from, and the C++ runtime contract. This module decides HOW those facts +// reach a foreign system, once for every plugin: +// +// instance An MSVC toolset from a Visual Studio instance (`msvc@system`) +// is used through that instance. The foreign system's own +// toolset loading stays in place, pointed at the instance mcpp +// resolved, so MSBuild is present and every kind of project +// builds. +// chain Any other toolset -- a managed MSVC toolset, and every toolset +// on the other rows -- is named: absolute tool paths, the +// environment they run with, and the directories that go first +// on PATH. On the MSVC ABI without an instance no MSBuild exists. +// detected `source::detected`: nothing is named, and the foreign system +// finds its own toolset, as the deps members did before 0.17.0. +// Kept by `src/compat/detected_toolset.cppm` until 2027-03-28. +// +// NONE OF THIS NAMES A FOREIGN SYSTEM. vcpkg's triplet and CMake's generator +// are written by the plugins that drive them (`mcpp.deps.vcpkg`, +// `mcpp.deps.cmake`); this module stops at the tools. + +export module mcpp.plugins.toolset; + +import std; +import mcpp; +import mcpp.plugins.compat.detected_toolset; + +export namespace mcpp::plugins::toolset { + +// Who chooses the tools: the engine's resolution (`resolved`), or the foreign +// system's own detection (`detected`, a compatibility behaviour). +enum class source { resolved, detected }; +// Which compiler `resolved` hands over. `abi_native` is the target ABI's own +// compiler (`cl.exe` on the MSVC ABI whichever driver the row uses; the row's +// compiler elsewhere). `row` is the compiler the row itself runs, for example +// clang on an LLVM row that targets the MSVC ABI. +enum class compiler { abi_native, row }; + +// Embedded in each plugin's options. +struct choice { + source toolset = source::resolved; + compiler cc = compiler::abi_native; +}; + +enum class mechanism { instance, chain, detected }; + +inline std::string_view name(mechanism m) { + return m == mechanism::instance ? "instance" : m == mechanism::chain ? "chain" : "detected"; +} + +struct resolved_tools { + mechanism how = mechanism::detected; + std::string instance_dir; // `instance`: the Visual Studio instance + std::string toolset_version; // `instance`: e.g. 14.44.35207; also set under `chain` on the MSVC ABI + std::string instance_default; // `instance`: the version the instance selects by itself + // Absolute paths, forward slashes. `chain` sets `cc` and `cxx` always and + // the others where the row has them; `detected` sets `cc` and `cxx` only + // where detection cannot agree with the program (the Linux libc++ row). + std::string cc, cxx, ld, ar, rc, mt; + // `chain` on the MSVC ABI: the environment the tools run with, without + // PATH (INCLUDE, LIB, LIBPATH, ...). + std::vector> env; + // `chain`: the directories that go first on PATH, in order, without + // duplicates: the tools' own directories, then the environment's PATH. + std::vector path_dirs; + std::string identity; // toolset_identity(): "msvc 14.44.35207; sdk 10.0.26100.0", "clang 22.1.8" + std::string crt; // msvc_crt_linkage(): "static", "dynamic", or empty off the MSVC ABI + bool msvc_abi = false; +}; + +inline bool msvc_abi() { + return std::string_view(mcpp::target_os()) == "windows" + && std::string_view(mcpp::target_env()) == "msvc"; +} + +inline bool host_is_windows() { + return std::string_view(mcpp::host()).find("windows") != std::string_view::npos; +} + +// `a;b` on a Windows host, `a:b` elsewhere. +inline char path_separator() { return host_is_windows() ? ';' : ':'; } + +inline std::string forward(std::string_view p) { + std::string s(p); + for (std::size_t i = 0; i < s.size(); ++i) if (s[i] == '\\') s[i] = '/'; + return s; +} + +// The toolset version out of an identity "msvc 14.44.35207; sdk 10.0.26100.0". +inline std::string msvc_version_of(std::string_view identity) { + if (!identity.starts_with("msvc ")) return {}; + identity.remove_prefix(5); + const auto end = identity.find(';'); + return std::string(identity.substr(0, end)); +} + +// The toolset version a Visual Studio instance selects when none is named: +// `VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt`, which the +// Visual Studio installer writes and CMake and vcpkg both read. +inline std::string instance_default_version(const std::filesystem::path& instance) { + std::ifstream in(instance / "VC" / "Auxiliary" / "Build" / "Microsoft.VCToolsVersion.default.txt"); + std::string v; + std::getline(in, v); + while (!v.empty() && (v.back() == '\r' || v.back() == ' ' || v.back() == '\n')) v.pop_back(); + return v; +} + +// tool_env() is one KEY=value per line. +inline std::vector> parse_env(std::string_view text) { + std::vector> out; + while (!text.empty()) { + const auto nl = text.find('\n'); + std::string_view line = text.substr(0, nl); + text = nl == std::string_view::npos ? std::string_view{} : text.substr(nl + 1); + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + const auto eq = line.find('='); + if (eq == std::string_view::npos || eq == 0) continue; + out.emplace_back(std::string(line.substr(0, eq)), std::string(line.substr(eq + 1))); + } + return out; +} + +inline bool same_key(std::string_view a, std::string_view b) { + if (a.size() != b.size()) return false; + for (std::size_t i = 0; i < a.size(); ++i) + if (std::tolower(static_cast(a[i])) != std::tolower(static_cast(b[i]))) + return false; + return true; +} + +// The value of `key` in `env`; Windows environment names compare without case. +inline std::string env_value(const std::vector>& env, + std::string_view key) { + for (auto const& [k, v] : env) if (same_key(k, key)) return v; + return {}; +} + +namespace detail { +inline std::string tool_of(compiler which, const char* role) { + return forward(which == compiler::abi_native ? mcpp::abi_tool(role) : mcpp::tool(role)); +} + +inline void add_dir(std::vector& dirs, const std::string& file) { + if (file.empty()) return; + const std::string dir = std::filesystem::path(file).parent_path().generic_string(); + if (dir.empty()) return; + for (auto const& d : dirs) if (same_key(d, dir)) return; + dirs.push_back(dir); +} + +inline std::expected chain(const choice& c, resolved_tools r) { + r.how = mechanism::chain; + r.cc = tool_of(c.cc, "cc"); + r.cxx = tool_of(c.cc, "cxx"); + r.ld = tool_of(c.cc, "ld"); + r.ar = tool_of(c.cc, "ar"); + r.rc = tool_of(c.cc, "rc"); + r.mt = tool_of(c.cc, "mt"); + if (r.cxx.empty()) + return std::unexpected(std::format( + "the resolved toolchain states no {} C++ compiler for target '{}'; the engine " + "states the tools from mcpp 2026.9.28.3 onward.", + c.cc == compiler::abi_native ? "ABI-native" : "row", std::string(mcpp::target()))); + if (r.cc.empty()) r.cc = r.cxx; + if (r.msvc_abi) { + auto env = parse_env(mcpp::tool_env()); + const std::string path = env_value(env, "PATH"); + for (auto const& kv : env) if (!same_key(kv.first, "PATH")) r.env.push_back(kv); + for (auto const* t : { &r.cxx, &r.cc, &r.ld, &r.rc, &r.mt }) add_dir(r.path_dirs, *t); + // The environment's PATH carries the SDK's and the toolset's binary + // directories; its entries follow the tools' own, in its order. + std::string_view rest = path; + while (!rest.empty()) { + const auto semi = rest.find(';'); + const std::string entry = forward(rest.substr(0, semi)); + rest = semi == std::string_view::npos ? std::string_view{} : rest.substr(semi + 1); + if (entry.empty()) continue; + bool seen = false; + for (auto const& d : r.path_dirs) if (same_key(d, entry)) { seen = true; break; } + if (!seen) r.path_dirs.push_back(entry); + } + } else { + for (auto const* t : { &r.cxx, &r.cc }) add_dir(r.path_dirs, *t); + } + return r; +} +} // namespace detail + +// The tools a foreign build system is handed, and how. +inline std::expected resolve(const choice& c = {}) { + resolved_tools r; + r.identity = mcpp::toolset_identity(); + r.crt = mcpp::msvc_crt_linkage(); + r.msvc_abi = msvc_abi(); + if (r.msvc_abi) r.toolset_version = msvc_version_of(r.identity); + + if (c.toolset == source::detected) { + const auto kept = mcpp::plugins::compat::detected_toolset(); + // Where detection cannot agree with the program -- the host compiler + // on Linux uses libstdc++ while the program uses libc++ -- 0.16.0 + // already named mcpp's clang, and so does this. + if (kept.name_compilers) return detail::chain({source::resolved, compiler::row}, r); + r.how = mechanism::detected; + return r; + } + if (r.identity.empty()) + return std::unexpected(std::string( + "the engine states no toolset identity: build information for build programs " + "arrived in mcpp 2026.9.28.3 (protocol 14). Pin \"mcpp\": \"2026.9.28.3\" or newer " + "in .xlings.json, or set the plugin's `toolset` option to `detected`.")); + + const std::string instance = forward(mcpp::msvc_instance_dir()); + if (r.msvc_abi && !instance.empty() && c.cc == compiler::abi_native) { + r.how = mechanism::instance; + r.instance_dir = instance; + r.instance_default = instance_default_version(std::filesystem::path(instance)); + r.cc = r.cxx = detail::tool_of(compiler::abi_native, "cxx"); + return r; + } + return detail::chain(c, r); +} + +// The tools named even where an instance exists: a plugin whose foreign +// system is driven without the instance's own loading (CMake's Ninja +// generator) asks for this. `detected` is answered as by `resolve`. +inline std::expected resolve_named(const choice& c = {}) { + auto r = resolve(c); + if (!r || r->how != mechanism::instance) return r; + resolved_tools base; + base.identity = r->identity; + base.crt = r->crt; + base.msvc_abi = r->msvc_abi; + base.toolset_version = r->toolset_version; + return detail::chain(c, base); +} + +// A one-line account of a resolution, for a plugin's own diagnostics and for +// the identity comment of a generated file. +inline std::string describe(const resolved_tools& r) { + std::string s = std::format("{} ({})", r.identity.empty() ? std::string("unknown toolset") : r.identity, + name(r.how)); + if (r.how == mechanism::instance) s += std::format(", instance {}", r.instance_dir); + if (!r.crt.empty()) s += std::format(", crt {}", r.crt); + return s; +} + +} // namespace mcpp::plugins::toolset diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp new file mode 100644 index 0000000..d63598a --- /dev/null +++ b/tests/plugin-logic/build.mcpp @@ -0,0 +1,278 @@ +// The deps members' decisions, one case per decision (mcpp#734, design §6). +// Each case states a build context, runs the member, and reads the action it +// declared and the files it wrote. Nothing is installed or run. +import std; +import mcpp; +import mcpp.plugins.testing; +import mcpp.plugins.toolset; +import mcpp.deps; // the 0.16.0 names, for the compatibility case +import mcpp.deps.vcpkg; +import mcpp.deps.cmake; + +namespace t = mcpp::plugins::testing; +namespace ts = mcpp::plugins::toolset; + +namespace { + +// A vcpkg root with the three standard triplets the cases use, and a manifest. +t::context with_vcpkg(t::context c) { + const bool win = std::string_view(c.values.front().second).find("windows") != std::string_view::npos; + return c.xpkg("xim", "vcpkg", "{root}/vcpkg") + .file(win ? "vcpkg/vcpkg.exe" : "vcpkg/vcpkg") + .file("vcpkg/triplets/x64-windows.cmake", + "set(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE dynamic)\nset(VCPKG_LIBRARY_LINKAGE dynamic)\n") + .file("vcpkg/triplets/x64-windows-static.cmake", + "set(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE static)\nset(VCPKG_LIBRARY_LINKAGE static)\n") + .file("vcpkg/triplets/x64-linux.cmake", + "set(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE dynamic)\nset(VCPKG_LIBRARY_LINKAGE static)\n" + "set(VCPKG_CMAKE_SYSTEM_NAME Linux)\n") + .file("pkg/vcpkg.json", "{ \"dependencies\": [\"fmt\"] }\n"); +} + +t::context with_subproject(t::context c) { + return c.xpkg("xim", "cmake", "{root}/cmake") + .file("cmake/bin/cmake").file("cmake/bin/cmake.exe") + .file("pkg/greet/CMakeLists.txt", "project(greet C)\n"); +} + +int vcpkg_use(mcpp::deps::vcpkg::options o = {}) { + o.libraries = {"fmt"}; + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} + +int cmake_use(mcpp::deps::cmake::options o = {}) { + o.source = "greet"; + o.libraries = {"greet"}; + return mcpp::deps::cmake::use(o) ? 0 : 1; +} + +// The one generated triplet of a case, by file name. +std::string derived_triplet(const t::result& r) { + for (auto const& f : r.files_under("out/deps-vcpkg/triplets")) + if (f.find("-mcpp-") != std::string::npos) return f; + return {}; +} + +std::string stem(const std::string& path) { + return std::filesystem::path(path).stem().string(); +} + +// The derived triplet's name under two different roots: equal names mean the +// name, and the text it is hashed from, hold no path (design §6.6). +std::string first_linux_name; + +} // namespace + +int main(int argc, char** argv) { + return t::run(argc, argv, { + // ── deps-vcpkg ────────────────────────────────────────────────────── + { "vcpkg: a Visual Studio toolset is selected through its instance", + with_vcpkg(t::row::windows_visual_studio()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the member succeeds"); + const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:x64-windows\""}); + c.expect(!a.empty(), "the installation uses the standard triplet x64-windows"); + c.expect(a.find("VCPKG_VISUAL_STUDIO_PATH=") != std::string::npos, + "the action selects the instance with VCPKG_VISUAL_STUDIO_PATH"); + c.expect(a.find("--host-triplet") == std::string::npos, "the host triplet stays vcpkg's"); + c.expect(derived_triplet(r).empty(), "no triplet is derived"); + } }, + + { "vcpkg: another toolset version of the instance derives a triplet that names it", + with_vcpkg(t::row::windows_visual_studio("14.43.34808", "14.44.35207")), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + const auto f = derived_triplet(r); + c.expect(!f.empty(), "a triplet x64-windows-mcpp- is derived"); + c.expect(r.file(f).find("set(VCPKG_PLATFORM_TOOLSET_VERSION 14.43.34808)") != std::string::npos, + "the derived triplet names the resolved toolset version"); + c.expect(r.file(f).find("VCPKG_CHAINLOAD_TOOLCHAIN_FILE") == std::string::npos, + "the instance's own loading stays in place"); + c.expect(r.has_line("mcpp:action=", {"VCPKG_VISUAL_STUDIO_PATH="}), + "the action still selects the instance"); + } }, + + { "vcpkg: a managed toolset is named in a chain-loaded derived triplet", + with_vcpkg(t::row::windows_managed()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the member succeeds"); + const auto f = derived_triplet(r); + const auto name = stem(f); + c.expect(name.starts_with("x64-windows-mcpp-"), "the derived triplet is x64-windows-mcpp-"); + const auto text = r.file(f); + c.expect(text.find("set(VCPKG_LIBRARY_LINKAGE dynamic)") != std::string::npos, + "the base triplet is inlined, not included"); + c.expect(text.find("\"${CMAKE_CURRENT_LIST_DIR}/mcpp-chain-windows.cmake\"") != std::string::npos, + "the chain-loaded toolchain is found beside the triplet"); + c.expect(text.find("MCPP_VCPKG_CXX") != std::string::npos && text.find(" INCLUDE") != std::string::npos + && text.find(" LIB") != std::string::npos, + "the tools and their environment pass through untracked"); + c.expect(text.find("# toolset: msvc 14.44.35207; sdk 10.0.26100.0 (chain)") != std::string::npos, + "the triplet carries the toolset's identity"); + c.expect(text.find("variable_watch(VCPKG_PLATFORM_TOOLSET") != std::string::npos, + "an MSBuild port is refused by name"); + c.expect(text.find(r.root.generic_string()) == std::string::npos, "the triplet holds no path"); + const auto chain = r.file("out/deps-vcpkg/triplets/mcpp-chain-windows.cmake"); + c.expect(chain.find("file(TO_CMAKE_PATH \"$ENV{MCPP_VCPKG_CXX}\"") != std::string::npos, + "the toolchain reads each tool from the environment through TO_CMAKE_PATH"); + c.expect(chain.find("/scripts/toolchains/windows.cmake\")") != std::string::npos, + "the toolchain includes vcpkg's windows.cmake"); + c.expect(chain.find(r.root.generic_string()) == std::string::npos, "the toolchain holds no path"); + const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:" + name}); + c.expect(!a.empty(), "the installation uses the derived triplet"); + c.expect(a.find("\"--host-triplet=" + name + "\"") != std::string::npos, + "the host triplet is the derived one"); + c.expect(a.find("\"VCPKG_KEEP_ENV_VARS=PATH\"") != std::string::npos, + "vcpkg keeps the PATH the action sets"); + c.expect(a.find("\"PATH=") != std::string::npos && a.find("Hostx64") != std::string::npos, + "the toolset's directories are on that PATH"); + c.expect(a.find("VCPKG_VISUAL_STUDIO_PATH") == std::string::npos, "no instance is selected"); + } }, + + { "vcpkg: a self-contained program's ports link the C runtime statically", + with_vcpkg(t::row::windows_managed() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + const auto f = derived_triplet(r); + c.expect(stem(f).starts_with("x64-windows-static-mcpp-"), "the base triplet is x64-windows-static"); + c.expect(r.file(f).find("set(VCPKG_CRT_LINKAGE static)") != std::string::npos, + "the derived triplet states the static C runtime"); + } }, + + { "vcpkg: a project triplet that contradicts the program's C runtime is refused", + with_vcpkg(t::row::windows_visual_studio() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return vcpkg_use({.triplet = "x64-windows"}); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code != 0, "the member fails"); + c.expect(r.errors.find("the triplet 'x64-windows' links the C runtime dynamic") != std::string::npos + && r.errors.find("links it static (the program's C++ runtime contract, self-contained)") + != std::string::npos, + "the error names both statements"); + c.expect(r.count("mcpp:action=") == 0, "nothing is installed"); + } }, + + { "vcpkg: crt_linkage states the ports' linkage and wins over the contract", + with_vcpkg(t::row::windows_visual_studio() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return vcpkg_use({.triplet = "x64-windows", .crt_linkage = "dynamic"}); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the member succeeds"); + c.expect(r.has_line("mcpp:action=", {"deps-vcpkg:install:x64-windows\""}), + "the project's triplet is used"); + } }, + + { "vcpkg: detected leaves the toolset to vcpkg and says until when", + with_vcpkg(t::row::windows_visual_studio()), + [] { return vcpkg_use({.toolset = {.toolset = ts::source::detected}}); }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("mcpp:action=", {"deps-vcpkg:install:x64-windows\""}), + "the standard triplet is used"); + c.expect(!r.has_line("mcpp:action=", {"VCPKG_VISUAL_STUDIO_PATH"}), "no instance is selected"); + c.expect(r.has_line("mcpp:diagnostic=note", {"kept until 2027-03-28"}), + "the compatibility note is printed"); + } }, + + { "vcpkg: the Linux GCC row is chained with vcpkg's linux.cmake", + with_vcpkg(t::row::linux_gcc()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + const auto f = derived_triplet(r); + const auto name = stem(f); + c.expect(name.starts_with("x64-linux-mcpp-"), "the derived triplet is x64-linux-mcpp-"); + c.expect(r.file("out/deps-vcpkg/triplets/mcpp-chain-linux.cmake") + .find("/scripts/toolchains/linux.cmake\")") != std::string::npos, + "the toolchain includes vcpkg's linux.cmake"); + const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:" + name}); + c.expect(a.find("MCPP_VCPKG_CXX=") != std::string::npos && a.find("gcc/bin/g++") != std::string::npos, + "the row's g++ is handed to vcpkg"); + c.expect(a.find("--host-triplet") == std::string::npos, "host tools keep vcpkg's host triplet"); + c.expect(a.find("\"PATH=") == std::string::npos, "PATH is left alone off the MSVC ABI"); + first_linux_name = name; + } }, + + { "vcpkg: the derived triplet's name does not depend on where the tools are", + with_vcpkg(t::row::linux_gcc()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(!first_linux_name.empty() && stem(derived_triplet(r)) == first_linux_name, + "the same toolset under another root derives the same triplet"); + } }, + + { "vcpkg: the Linux libc++ row hands over mcpp's clang", + with_vcpkg(t::row::linux_libcxx()), + [] { return vcpkg_use(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("mcpp:action=", {"MCPP_VCPKG_CXX=", "llvm/bin/clang++"}), + "clang++ is handed to vcpkg"); + c.expect(stem(derived_triplet(r)) != first_linux_name, "the two toolsets derive two triplets"); + } }, + + // ── deps-cmake ────────────────────────────────────────────────────── + { "cmake: a Visual Studio toolset keeps the Visual Studio generator on that instance", + with_subproject(t::row::windows_visual_studio("14.43.34808", "14.44.35207")), + [] { return cmake_use(); }, + [](const t::result& r, t::checker& c) { + const auto script = r.file("out/deps-cmake/greet/greet.cmake"); + c.expect(script.find("[==[-DCMAKE_GENERATOR_INSTANCE=") != std::string::npos, + "CMAKE_GENERATOR_INSTANCE names the instance"); + c.expect(script.find("[==[version=14.43.34808]==]") != std::string::npos, + "-T version= names a toolset other than the instance's default"); + c.expect(script.find("[==[Ninja]==]") == std::string::npos, "the generator stays CMake's default"); + c.expect(script.find("MultiThreaded$<$:Debug>DLL") != std::string::npos, + "the C runtime follows the program's (dynamic)"); + } }, + + { "cmake: a managed toolset is named, with Ninja and mcpp's ninja", + with_subproject(t::row::windows_managed() + .set("MCPP_CXX_RUNTIME", "self-contained").set("MCPP_MSVC_CRT_LINKAGE", "static")), + [] { return cmake_use(); }, + [](const t::result& r, t::checker& c) { + std::string script; + for (auto const& f : r.files_under("out/deps-cmake/greet")) + if (f.ends_with("greet.cmake")) script = r.file(f); + c.expect(script.find("[==[Ninja]==]") != std::string::npos, "the Ninja generator is used"); + c.expect(script.find("-DCMAKE_MAKE_PROGRAM=") != std::string::npos + && script.find("ninja/ninja.exe") != std::string::npos, "mcpp's ninja runs it"); + c.expect(script.find("-DCMAKE_CXX_COMPILER=") != std::string::npos + && script.find("cl.exe") != std::string::npos, "cl.exe is named"); + c.expect(script.find("MultiThreaded$<$:Debug>]==]") != std::string::npos, + "the C runtime follows the program's (static)"); + const auto a = r.line("mcpp:action=", {"deps-cmake:greet"}); + c.expect(a.find("\"INCLUDE=") != std::string::npos && a.find("\"PATH=") != std::string::npos, + "the tools run with the engine's environment"); + c.expect(script.find("/greet/build-") != std::string::npos, + "the build directory is keyed by the toolset"); + } }, + + { "cmake: compilers in cache_args are the project's decision", + with_subproject(t::row::linux_gcc()), + [] { + mcpp::deps::cmake::options o; + o.cache_args = {"-DCMAKE_CXX_COMPILER=/opt/other/g++"}; + return cmake_use(o); + }, + [](const t::result& r, t::checker& c) { + const auto script = r.file("out/deps-cmake/greet/greet.cmake"); + c.expect(script.find("[==[Ninja]==]") == std::string::npos, "no generator is imposed"); + c.expect(script.find("gcc/bin/g++") == std::string::npos, "the row's compiler is not named"); + } }, + + // ── compatibility ─────────────────────────────────────────────────── + { "compat: program_compilers keeps its 0.16.0 answer and says until when", + t::row::linux_libcxx(), + [] { + const auto cc = mcpp::deps::program_compilers(); + std::printf("answer=%s\n", cc.cxx.c_str()); + return cc ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("answer=", {"llvm/bin/clang++"}), "mcpp's clang++ on the libc++ row"); + c.expect(r.has_line("mcpp:diagnostic=note", {"program_compilers is kept until 2027-03-28"}), + "the compatibility note is printed"); + } }, + }); +} diff --git a/tests/plugin-logic/mcpp.toml b/tests/plugin-logic/mcpp.toml new file mode 100644 index 0000000..7e74781 --- /dev/null +++ b/tests/plugin-logic/mcpp.toml @@ -0,0 +1,24 @@ +# Fixture: the logic of the deps members, tested with `mcpp.plugins.testing` +# against stated build contexts -- a Visual Studio row, a managed MSVC row, a +# Linux GCC row and a Linux libc++ row -- on every host, with no foreign tool +# installed or run. The build program is the test: a failed case fails the +# build and prints the report; `target/**/plugins-testing/results.txt` holds +# one verdict per case. +[package] +name = "plugin-logic" +version = "0.1.0" +description = "Fixture: the deps members' toolset, triplet and runtime logic" +license = "Apache-2.0" +authors = ["mcpp-community"] + +[language] +standard = "c++23" +modules = true +import_std = true + +[build-dependencies.mcpp] +plugins = { path = "../..", features = ["deps-vcpkg", "deps-cmake", "plugins-testing"], host-module = true } + +[targets.plugin-logic] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/plugin-logic/src/main.cpp b/tests/plugin-logic/src/main.cpp new file mode 100644 index 0000000..76e8197 --- /dev/null +++ b/tests/plugin-logic/src/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tests/vcpkg-make-port/build.mcpp b/tests/vcpkg-make-port/build.mcpp new file mode 100644 index 0000000..bf00975 --- /dev/null +++ b/tests/vcpkg-make-port/build.mcpp @@ -0,0 +1,10 @@ +// The installation is the criterion; the program links nothing from it. +import std; +import mcpp; +import mcpp.deps.vcpkg; + +int main() { + mcpp::deps::vcpkg::options o; + o.install_root = "target/vcpkg_installed"; + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} diff --git a/tests/vcpkg-make-port/mcpp.toml b/tests/vcpkg-make-port/mcpp.toml new file mode 100644 index 0000000..a47920a --- /dev/null +++ b/tests/vcpkg-make-port/mcpp.toml @@ -0,0 +1,22 @@ +# Fixture: a port that vcpkg builds with make under msys (icu). Its configure +# script finds `link.exe` on PATH, which vcpkg's clean environment drops; under +# the `chain` mechanism deps-vcpkg puts the toolset's directories first on a +# PATH that vcpkg keeps. Built on the Windows row whose Visual Studio is masked. +[package] +name = "vcpkg-make-port" +version = "0.1.0" +description = "Fixture: a make-based port under a managed MSVC toolset" +license = "Apache-2.0" +authors = ["mcpp-community"] + +[language] +standard = "c++23" +modules = true +import_std = true + +[build-dependencies.mcpp] +plugins = { path = "../..", features = ["deps-vcpkg"], host-module = true } + +[targets.vcpkg-make-port] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/vcpkg-make-port/src/main.cpp b/tests/vcpkg-make-port/src/main.cpp new file mode 100644 index 0000000..c001237 --- /dev/null +++ b/tests/vcpkg-make-port/src/main.cpp @@ -0,0 +1,3 @@ +#include +#include +int main() { std::printf("vcpkg-make-port: icu %s\n", U_ICU_VERSION); return 0; } diff --git a/tests/vcpkg-make-port/vcpkg.json b/tests/vcpkg-make-port/vcpkg.json new file mode 100644 index 0000000..4a9ac50 --- /dev/null +++ b/tests/vcpkg-make-port/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "vcpkg-make-port", + "version": "0.1.0", + "dependencies": ["icu"], + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d" +} diff --git a/tests/vcpkg-msbuild-port/build.mcpp b/tests/vcpkg-msbuild-port/build.mcpp new file mode 100644 index 0000000..bf00975 --- /dev/null +++ b/tests/vcpkg-msbuild-port/build.mcpp @@ -0,0 +1,10 @@ +// The installation is the criterion; the program links nothing from it. +import std; +import mcpp; +import mcpp.deps.vcpkg; + +int main() { + mcpp::deps::vcpkg::options o; + o.install_root = "target/vcpkg_installed"; + return mcpp::deps::vcpkg::use(o) ? 0 : 1; +} diff --git a/tests/vcpkg-msbuild-port/mcpp.toml b/tests/vcpkg-msbuild-port/mcpp.toml new file mode 100644 index 0000000..281a4ff --- /dev/null +++ b/tests/vcpkg-msbuild-port/mcpp.toml @@ -0,0 +1,23 @@ +# Fixture: a port that vcpkg builds with MSBuild (libusb). Under the `chain` +# mechanism -- a managed MSVC toolset, no Visual Studio instance -- no MSBuild +# exists, and deps-vcpkg refuses the port by name instead of letting MSBuild +# fail with `/p:PlatformToolset=external`. Built only on the Windows row whose +# Visual Studio is masked, where the refusal is the criterion. +[package] +name = "vcpkg-msbuild-port" +version = "0.1.0" +description = "Fixture: an MSBuild port under a managed MSVC toolset" +license = "Apache-2.0" +authors = ["mcpp-community"] + +[language] +standard = "c++23" +modules = true +import_std = true + +[build-dependencies.mcpp] +plugins = { path = "../..", features = ["deps-vcpkg"], host-module = true } + +[targets.vcpkg-msbuild-port] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/vcpkg-msbuild-port/src/main.cpp b/tests/vcpkg-msbuild-port/src/main.cpp new file mode 100644 index 0000000..2aa1184 --- /dev/null +++ b/tests/vcpkg-msbuild-port/src/main.cpp @@ -0,0 +1,2 @@ +#include +int main() { std::puts("vcpkg-msbuild-port: built"); return 0; } diff --git a/tests/vcpkg-msbuild-port/vcpkg.json b/tests/vcpkg-msbuild-port/vcpkg.json new file mode 100644 index 0000000..9b31f01 --- /dev/null +++ b/tests/vcpkg-msbuild-port/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "vcpkg-msbuild-port", + "version": "0.1.0", + "dependencies": ["libusb"], + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d" +} From eee568feee43335b45bb10386e5bfab7af3c599a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 21:57:07 +0800 Subject: [PATCH 2/9] ci: the test kit builds with the managed toolset on the row without Visual Studio --- .github/scripts/check-deps-and-qt.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index 1a87fa9..a28ea80 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -369,7 +369,10 @@ plugin_logic() { cd "$ROOT/tests/plugin-logic" rm -rf target mkdir -p target/ci - "$MCPP" build > target/ci/build.log 2>&1 || { cat target/ci/build.log; fail "a plugin-logic case failed"; } + # On the row without Visual Studio the build program itself needs the + # managed toolset, which that job names in MSVC_MANAGED. + "$MCPP" build ${MSVC_MANAGED:+--toolchain "$MSVC_MANAGED"} > target/ci/build.log 2>&1 || + { cat target/ci/build.log; fail "a plugin-logic case failed"; } local results declared passed results=$(find target -path '*plugins-testing/results.txt' | head -1) [ -n "$results" ] || fail "the kit wrote no results.txt" From da44e9991ed696942bb14f29caddf14479ab500a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 22:17:50 +0800 Subject: [PATCH 3/9] deps: the Linux GCC row keeps vcpkg's detection; the MSBuild refusal reads the call stack - mcpp runs its GCC payload with a sysroot, a binutils directory and a link model that only its own command lines carry, so the driver alone is not a complete handover: vcpkg's compiler detection failed with it on CI. On that row the host compiler's libstdc++ is the program's C++ library, and `resolved` answers `detected`, stating why. The clang payloads are complete through their `.cfg` files and stay `chain`. - The MSBuild watch matched the current file, which inside a function is the caller's (the portfile), so it never fired; it now matches the helper in the call stack, by its own directory and file names (measured with CMake 4.4: the helper is refused, a CMake helper's read is not). --- .github/scripts/check-deps-and-qt.sh | 19 +++++++++++----- deps/vcpkg.cppm | 17 ++++++++++---- docs/deps.md | 31 +++++++++++++++++-------- src/toolset.cppm | 31 +++++++++++++++++++++---- tests/plugin-logic/build.mcpp | 34 ++++++++++++++++------------ 5 files changed, 91 insertions(+), 41 deletions(-) diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index a28ea80..f030053 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -72,18 +72,25 @@ vcpkg_consumer() { # THE MECHANISM (0.17.0). On the Visual Studio row the instance mcpp # resolved is selected and the standard triplet is used, so the ABI hash is - # the one vcpkg computes by itself; on the other rows a derived triplet - # names the resolved tools. + # the one vcpkg computes by itself. On the Linux GCC row (this job's + # default) vcpkg's own detection is kept: the payload driver runs with + # flags only mcpp's command lines carry. On the clang rows (macOS here) a + # derived triplet names the resolved tools. if is_windows; then grep -rqs 'VCPKG_VISUAL_STUDIO_PATH=' target --include=build.ninja || fail "the installation does not select the Visual Studio instance mcpp resolved" [ -d target/vcpkg_installed/x64-windows/x64-windows ] || fail "the instance mechanism did not use x64-windows" echo "ok: the installation selects the Visual Studio instance and keeps the standard triplet" - else + elif is_macos; then ls -d target/vcpkg_installed/*-mcpp-*/*-mcpp-* > /dev/null 2>&1 || fail "no derived -mcpp- prefix under target/vcpkg_installed" grep -rqs 'MCPP_VCPKG_CXX=' target --include=build.ninja || fail "the installation does not hand vcpkg the resolved compiler" echo "ok: the installation names the resolved compiler in a derived triplet" + else + ls -d target/vcpkg_installed/*-linux/*-linux > /dev/null 2>&1 || + fail "the GCC row did not keep the standard -linux triplet" + ! grep -rqs 'MCPP_VCPKG_CXX=' target --include=build.ninja || fail "the GCC row named its compiler to vcpkg" + echo "ok: the GCC row keeps vcpkg's detection and the standard triplet" fi if is_windows; then @@ -126,8 +133,8 @@ vcpkg_libcxx() { "$MCPP" run | tee target/ci/default-run.log grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/default-run.log || fail "the default toolchain's program did not print through fmt" [ -f "$lib" ] || fail "the default toolchain's installation removed the $gen prefix" - [ "$(ls -d target/vcpkg_installed/*-mcpp-* | wc -l)" -ge 2 ] || - fail "the default toolchain did not derive a triplet of its own" + [ "$(ls -d target/vcpkg_installed/*/ | wc -l)" -ge 2 ] || + fail "the default toolchain did not install a prefix of its own" local stamp; stamp=$(find target -path '*deps-vcpkg*' -name "$gen.stamp" | head -1) [ -n "$stamp" ] || fail "no $gen installation stamp" touch -r "$stamp" target/ci/before-switch-back @@ -455,7 +462,7 @@ vcpkg_msbuild_refused() { if "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1; then cat target/ci/build.log; fail "an MSBuild port built without Visual Studio" fi - grep -q "builds with MSBuild, which needs a Visual Studio instance" target/ci/build.log || + grep -q "builds with MSBuild" target/ci/build.log || { tail -60 target/ci/build.log; fail "the MSBuild port failed without the plugin's reason"; } echo "ok: an MSBuild port is refused by name without Visual Studio" } diff --git a/deps/vcpkg.cppm b/deps/vcpkg.cppm index 2f60241..910c378 100644 --- a/deps/vcpkg.cppm +++ b/deps/vcpkg.cppm @@ -267,17 +267,24 @@ inline std::string chain_toolchain_text() { } // What an MSBuild port meets under `chain`: vcpkg runs MSBuild with -// `/p:PlatformToolset=external` and fails without naming the cause. The -// MSBuild helpers read `VCPKG_PLATFORM_TOOLSET` to build that argument; the -// watch stops the port there and says why. +// `/p:PlatformToolset=external` and fails without naming the cause ("msbuild: +// no such file or directory" when no Visual Studio exists). The MSBuild +// helpers read `VCPKG_PLATFORM_TOOLSET` to build that argument; the watch +// stops the port there and says why. The helper is recognised in the watch's +// CALL STACK, not its current file: a read inside a function reports the +// caller's file (the portfile), and the stack names the file that defines the +// function (measured with CMake 4.4). The pattern names the helper's own +// directory and files, so a project whose path contains "msbuild" is not +// refused. inline std::string msbuild_refusal_text(std::string_view identity) { return std::format( "# mcpp.deps.vcpkg: a port that builds with MSBuild needs a Visual Studio\n" "# instance, and this toolset comes from none.\n" "if(PORT AND NOT DEFINED Z_MCPP_MSBUILD_WATCH)\n" " set(Z_MCPP_MSBUILD_WATCH 1)\n" - " function(z_mcpp_msbuild_watch variable access value current_file)\n" - " if(access STREQUAL \"READ_ACCESS\" AND current_file MATCHES \"msbuild\")\n" + " function(z_mcpp_msbuild_watch variable access value current_file stack)\n" + " if(access STREQUAL \"READ_ACCESS\" AND stack MATCHES " + "\"/vcpkg-msbuild/|vcpkg_install_msbuild\\\\.cmake|vcpkg_build_msbuild\\\\.cmake\")\n" " message(FATAL_ERROR \"mcpp.deps.vcpkg: the port '${{PORT}}' builds with MSBuild, which " "needs a Visual Studio instance; the toolset mcpp resolved ({}) comes from none. Build with " "the toolchain msvc@system on a machine with Visual Studio, or set the deps-vcpkg option " diff --git a/docs/deps.md b/docs/deps.md index ea6be19..9d4e1fa 100644 --- a/docs/deps.md +++ b/docs/deps.md @@ -108,8 +108,18 @@ vcpkg follows where the toolset came from: | mechanism | when | what vcpkg receives | port kinds that build | |---|---|---|---| | `instance` | an MSVC toolset from a Visual Studio instance (`msvc@system`, the default on a machine with Visual Studio) | `VCPKG_VISUAL_STUDIO_PATH` naming that instance; the standard triplet, or a derived one with `VCPKG_PLATFORM_TOOLSET_VERSION` when the resolved toolset is not the instance's default | CMake, make and MSBuild | -| `chain` | any other toolset: a managed MSVC toolset (`xim:msvc@`), and the toolsets of the Linux and macOS rows | a derived triplet `-mcpp-` that chain-loads a toolchain naming the tools, and the tools' environment | CMake and make; an MSBuild port is refused by name | -| `detected` | `options.toolset.toolset = detected` | nothing: vcpkg finds its own toolset, as in 0.16.0 | as vcpkg's own detection allows | +| `chain` | a managed MSVC toolset (`xim:msvc@`), and the clang toolsets of the Linux and macOS rows | a derived triplet `-mcpp-` that chain-loads a toolchain naming the tools, and the tools' environment | CMake and make; an MSBuild port is refused by name | +| `detected` | `options.toolset.toolset = detected`; and, under `resolved`, the Linux GCC row | nothing: vcpkg finds its own toolset, as in 0.16.0 | as vcpkg's own detection allows | + +**The Linux GCC row.** mcpp runs its GCC payload with a sysroot, a binutils +directory and a link model (the payload's dynamic linker and C library) that +its own command lines add; the driver alone is not a complete toolset, and +vcpkg's compiler detection fails with it on a machine whose host compiler does +not fill the gaps (measured on the plugins' CI). The clang payloads carry their +configuration in their own `.cfg` files and are complete. On the GCC row the +host compiler's libstdc++ is the program's C++ library, so `resolved` keeps +vcpkg's detection there and the member states why in `mcpp.plugins.toolset`'s +`reason`. **The derived triplet.** The base triplet's text is copied into it, not included, because vcpkg hashes a triplet file's content and not the files it @@ -145,14 +155,15 @@ a second C++ runtime into the process without a word. `crt_linkage` states the ports' linkage explicitly and wins. **Linux.** Linux has two C++ standard libraries that do not link with each -other. The derived triplet names the program's compiler, so the ports use the -program's library on every row: libc++ under mcpp's clang (`std::__1::`), and -the GCC payload's libstdc++ under mcpp's GCC. - -**Upgrading from 0.16.0.** On Linux and macOS, and on Windows with a managed -toolset, the installation moves to a derived triplet, so each port is built once -more (or restored from a binary cache that already holds it); the prefixes of -0.16.0 (`-linux-libcxx`, `x64-linux`) are left where they are. With +other. Under mcpp's clang the derived triplet names that clang, so the ports +use libc++ (`std::__1::`) as the program does; under mcpp's GCC the host +compiler's libstdc++ is the program's library, and vcpkg's detection is kept. + +**Upgrading from 0.16.0.** On the clang rows of Linux and macOS, and on Windows +with a managed toolset, the installation moves to a derived triplet, so each +port is built once more (or restored from a binary cache that already holds +it); the prefixes of 0.16.0 (`-linux-libcxx`) are left where they are. The +Linux GCC row is unchanged. With Visual Studio and the dynamic C runtime nothing changes: the standard triplet and the instance vcpkg selects by itself give the same ABI hash. A program that links the C runtime statically moves to `x64-windows-static`. diff --git a/src/toolset.cppm b/src/toolset.cppm index 99ae299..fe69387 100644 --- a/src/toolset.cppm +++ b/src/toolset.cppm @@ -14,13 +14,15 @@ // toolset loading stays in place, pointed at the instance mcpp // resolved, so MSBuild is present and every kind of project // builds. -// chain Any other toolset -- a managed MSVC toolset, and every toolset -// on the other rows -- is named: absolute tool paths, the +// chain Any other toolset -- a managed MSVC toolset, and the clang +// toolsets of the other rows -- is named: absolute tool paths, the // environment they run with, and the directories that go first // on PATH. On the MSVC ABI without an instance no MSBuild exists. -// detected `source::detected`: nothing is named, and the foreign system -// finds its own toolset, as the deps members did before 0.17.0. -// Kept by `src/compat/detected_toolset.cppm` until 2027-03-28. +// detected Nothing is named, and the foreign system finds its own +// toolset. `source::detected` asks for it (kept by +// `src/compat/detected_toolset.cppm` until 2027-03-28); `resolved` +// answers it on the Linux GCC row, whose payload driver is not a +// complete handover (see `resolve`). // // NONE OF THIS NAMES A FOREIGN SYSTEM. vcpkg's triplet and CMake's generator // are written by the plugins that drive them (`mcpp.deps.vcpkg`, @@ -73,6 +75,9 @@ struct resolved_tools { std::string identity; // toolset_identity(): "msvc 14.44.35207; sdk 10.0.26100.0", "clang 22.1.8" std::string crt; // msvc_crt_linkage(): "static", "dynamic", or empty off the MSVC ABI bool msvc_abi = false; + // Why `resolved` answered `detected`: empty unless the resolved toolset + // cannot be handed over by its tools alone. + std::string reason; }; inline bool msvc_abi() { @@ -216,6 +221,22 @@ inline std::expected resolve(const choice& c = {}) "arrived in mcpp 2026.9.28.3 (protocol 14). Pin \"mcpp\": \"2026.9.28.3\" or newer " "in .xlings.json, or set the plugin's `toolset` option to `detected`.")); + // THE GCC PAYLOAD IS NOT A COMPLETE HANDOVER. mcpp runs it with a sysroot, + // a binutils directory and a link model (`--sysroot`, `-B`, the payload's + // dynamic linker and C library) that its own command lines add and a + // foreign build system does not receive; given the driver alone, vcpkg's + // compiler detection failed on CI while it passed on a machine whose host + // compiler filled the gaps. The clang payloads carry their configuration in + // their own `.cfg` files and are complete. On the GCC row the host + // compiler's libstdc++ is the same C++ library, so the foreign system's + // detection agrees with the program, as it did before 0.17.0. + if (!r.msvc_abi && std::string_view(mcpp::compiler()) == "gcc") { + r.how = mechanism::detected; + r.reason = "the GCC payload runs with a sysroot, binutils and a link model that only mcpp's " + "own command lines carry; the host compiler's libstdc++ is the program's C++ library"; + return r; + } + const std::string instance = forward(mcpp::msvc_instance_dir()); if (r.msvc_abi && !instance.empty() && c.cc == compiler::abi_native) { r.how = mechanism::instance; diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp index d63598a..7740860 100644 --- a/tests/plugin-logic/build.mcpp +++ b/tests/plugin-logic/build.mcpp @@ -176,41 +176,45 @@ int main(int argc, char** argv) { "the compatibility note is printed"); } }, - { "vcpkg: the Linux GCC row is chained with vcpkg's linux.cmake", + { "vcpkg: the Linux GCC row keeps vcpkg's detection, and says why", with_vcpkg(t::row::linux_gcc()), + [] { + const auto r = ts::resolve(); + std::printf("reason=%s\n", r ? r->reason.c_str() : ""); + return vcpkg_use(); + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("mcpp:action=", {"deps-vcpkg:install:x64-linux\""}), + "the standard triplet x64-linux is used"); + c.expect(!r.has_line("mcpp:action=", {"MCPP_VCPKG_CXX="}), "no compiler is named"); + c.expect(r.has_line("reason=", {"GCC payload"}), "the resolution states why"); + } }, + + { "vcpkg: the Linux libc++ row hands over mcpp's clang through linux.cmake", + with_vcpkg(t::row::linux_libcxx()), [] { return vcpkg_use(); }, [](const t::result& r, t::checker& c) { - const auto f = derived_triplet(r); - const auto name = stem(f); + const auto name = stem(derived_triplet(r)); c.expect(name.starts_with("x64-linux-mcpp-"), "the derived triplet is x64-linux-mcpp-"); c.expect(r.file("out/deps-vcpkg/triplets/mcpp-chain-linux.cmake") .find("/scripts/toolchains/linux.cmake\")") != std::string::npos, "the toolchain includes vcpkg's linux.cmake"); const auto a = r.line("mcpp:action=", {"deps-vcpkg:install:" + name}); - c.expect(a.find("MCPP_VCPKG_CXX=") != std::string::npos && a.find("gcc/bin/g++") != std::string::npos, - "the row's g++ is handed to vcpkg"); + c.expect(a.find("MCPP_VCPKG_CXX=") != std::string::npos && a.find("llvm/bin/clang++") != std::string::npos, + "clang++ is handed to vcpkg"); c.expect(a.find("--host-triplet") == std::string::npos, "host tools keep vcpkg's host triplet"); c.expect(a.find("\"PATH=") == std::string::npos, "PATH is left alone off the MSVC ABI"); first_linux_name = name; } }, { "vcpkg: the derived triplet's name does not depend on where the tools are", - with_vcpkg(t::row::linux_gcc()), + with_vcpkg(t::row::linux_libcxx()), [] { return vcpkg_use(); }, [](const t::result& r, t::checker& c) { c.expect(!first_linux_name.empty() && stem(derived_triplet(r)) == first_linux_name, "the same toolset under another root derives the same triplet"); } }, - { "vcpkg: the Linux libc++ row hands over mcpp's clang", - with_vcpkg(t::row::linux_libcxx()), - [] { return vcpkg_use(); }, - [](const t::result& r, t::checker& c) { - c.expect(r.has_line("mcpp:action=", {"MCPP_VCPKG_CXX=", "llvm/bin/clang++"}), - "clang++ is handed to vcpkg"); - c.expect(stem(derived_triplet(r)) != first_linux_name, "the two toolsets derive two triplets"); - } }, - // ── deps-cmake ────────────────────────────────────────────────────── { "cmake: a Visual Studio toolset keeps the Visual Studio generator on that instance", with_subproject(t::row::windows_visual_studio("14.43.34808", "14.44.35207")), From 5372bcd067955e47c8615c1a7541f4655f4d741c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 22:37:04 +0800 Subject: [PATCH 4/9] ci: a failed make-based port prints autoconf's config.log --- .github/scripts/check-deps-and-qt.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index f030053..bbcb5fe 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -474,8 +474,15 @@ vcpkg_make_port() { cd "$ROOT/tests/vcpkg-make-port" rm -rf target mkdir -p target/ci - "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1 || - { tail -80 target/ci/build.log; fail "the make-based port did not build with the managed toolset"; } + if ! "$MCPP" build --toolchain "$MSVC_MANAGED" > target/ci/build.log 2>&1; then + tail -80 target/ci/build.log + # autoconf records the invocation and the failing test in config.log, + # which vcpkg's own report does not show. + local cfg + cfg=$(find "$(cygpath -u "$LOCALAPPDATA" 2>/dev/null || echo "$HOME")/vcpkg/mcpp" -path '*icu*' -name config.log 2>/dev/null | head -1) + [ -n "$cfg" ] && { echo "--- $cfg"; head -40 "$cfg"; echo "..."; grep -n "invalid variable\|error" "$cfg" | head -20; } + fail "the make-based port did not build with the managed toolset" + fi "$MCPP" run --toolchain "$MSVC_MANAGED" | tee target/ci/run.log grep -qE '^vcpkg-make-port: icu [0-9]' target/ci/run.log || fail "the program did not read icu's version" echo "ok: a make-based port builds with the managed toolset first on the kept PATH" From 47445568dff5fb9e3ec8678c47c2293bc8fd1517 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 22:48:04 +0800 Subject: [PATCH 5/9] deps: the compatibility names compute 0.16.0's answer without importing the toolset module With mcpp.plugins.toolset in mcpp.deps' re-export chain, GCC 16 emitted no vector::push_back(string&&) for a build program that calls deps::archive::unpack, and its link failed (the Linux archive-consumer row). --- deps/compat/names.cppm | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/deps/compat/names.cppm b/deps/compat/names.cppm index b9042a9..3fbe9e7 100644 --- a/deps/compat/names.cppm +++ b/deps/compat/names.cppm @@ -20,7 +20,6 @@ export module mcpp.deps.compat; import std; import mcpp; import mcpp.plugins.fs; -import mcpp.plugins.toolset; export namespace mcpp::deps { @@ -35,6 +34,10 @@ struct compilers { }; // 0.16.0's answer: mcpp's clang on the Linux libc++ row, nothing elsewhere. +// Computed here rather than through `mcpp.plugins.toolset`: `mcpp.deps` +// re-exports this unit, and with the toolset module in that chain GCC 16 +// emitted no `std::vector::push_back(std::string&&)` for a build +// program calling `mcpp::deps::archive::unpack` (undefined at its link). inline compilers program_compilers() { static bool noted = false; if (!noted) { @@ -51,10 +54,11 @@ inline compilers program_compilers() { || std::string_view(mcpp::cxx_stdlib()) != "libc++" || std::string_view(mcpp::compiler()) != "clang" || std::string_view(mcpp::host()) != std::string_view(mcpp::target())) return {}; - auto r = mcpp::plugins::toolset::resolve({mcpp::plugins::toolset::source::resolved, - mcpp::plugins::toolset::compiler::row}); - if (!r) return {}; - return { r->cc, r->cxx }; + const std::filesystem::path bin = std::filesystem::path(mcpp::toolchain_dir()) / "bin"; + std::error_code ec; + if (!std::filesystem::is_regular_file(bin / "clang++", ec) || !std::filesystem::is_regular_file(bin / "clang", ec)) + return {}; + return { mcpp::plugins::fs::generic(bin / "clang"), mcpp::plugins::fs::generic(bin / "clang++") }; } } // namespace mcpp::deps From 0e7e99d41a4bdf638ec8248d5c319a06ea58397f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 22:48:37 +0800 Subject: [PATCH 6/9] tests: the compatibility case states the clang files its answer checks --- tests/plugin-logic/build.mcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp index 7740860..97a2cb3 100644 --- a/tests/plugin-logic/build.mcpp +++ b/tests/plugin-logic/build.mcpp @@ -267,7 +267,7 @@ int main(int argc, char** argv) { // ── compatibility ─────────────────────────────────────────────────── { "compat: program_compilers keeps its 0.16.0 answer and says until when", - t::row::linux_libcxx(), + t::row::linux_libcxx().file("llvm/bin/clang").file("llvm/bin/clang++"), [] { const auto cc = mcpp::deps::program_compilers(); std::printf("answer=%s\n", cc.cxx.c_str()); From de0dc781e151376985106bd9251c3b4510089a9d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 23:06:37 +0800 Subject: [PATCH 7/9] toolset: the chain's PATH is the tools' own directories, not the engine's whole PATH The engine's tool_env PATH is the cl.exe directory followed by the PATH of the process that ran mcpp. Under Git Bash that carries Git's msys tools and runtime, and icu's configure, run by vcpkg with that PATH kept, failed with 'invalid variable name: 0'. The toolset's and the SDK's binary directories are what a foreign system needs, as the design's measurement M3c kept. --- src/toolset.cppm | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/toolset.cppm b/src/toolset.cppm index fe69387..0e99fea 100644 --- a/src/toolset.cppm +++ b/src/toolset.cppm @@ -69,8 +69,8 @@ struct resolved_tools { // `chain` on the MSVC ABI: the environment the tools run with, without // PATH (INCLUDE, LIB, LIBPATH, ...). std::vector> env; - // `chain`: the directories that go first on PATH, in order, without - // duplicates: the tools' own directories, then the environment's PATH. + // `chain`: the tools' own directories, in order, without duplicates: the + // PATH a foreign system needs for them. std::vector path_dirs; std::string identity; // toolset_identity(): "msvc 14.44.35207; sdk 10.0.26100.0", "clang 22.1.8" std::string crt; // msvc_crt_linkage(): "static", "dynamic", or empty off the MSVC ABI @@ -175,22 +175,16 @@ inline std::expected chain(const choice& c, resolve c.cc == compiler::abi_native ? "ABI-native" : "row", std::string(mcpp::target()))); if (r.cc.empty()) r.cc = r.cxx; if (r.msvc_abi) { - auto env = parse_env(mcpp::tool_env()); - const std::string path = env_value(env, "PATH"); - for (auto const& kv : env) if (!same_key(kv.first, "PATH")) r.env.push_back(kv); + // The environment WITHOUT its PATH. The engine's PATH is the cl.exe + // directory followed by the whole PATH of the process that ran mcpp -- + // under Git Bash that holds Git's own msys tools and runtime, and a + // make-based port configured against them failed (`configure: error: + // invalid variable name: '0'`, icu, the plugins' CI). The tools' + // own directories are the PATH a foreign system needs: the toolset's + // bin (cl, link, lib) and the SDK's (rc, mt). + for (auto const& kv : parse_env(mcpp::tool_env())) + if (!same_key(kv.first, "PATH")) r.env.push_back(kv); for (auto const* t : { &r.cxx, &r.cc, &r.ld, &r.rc, &r.mt }) add_dir(r.path_dirs, *t); - // The environment's PATH carries the SDK's and the toolset's binary - // directories; its entries follow the tools' own, in its order. - std::string_view rest = path; - while (!rest.empty()) { - const auto semi = rest.find(';'); - const std::string entry = forward(rest.substr(0, semi)); - rest = semi == std::string_view::npos ? std::string_view{} : rest.substr(semi + 1); - if (entry.empty()) continue; - bool seen = false; - for (auto const& d : r.path_dirs) if (same_key(d, entry)) { seen = true; break; } - if (!seen) r.path_dirs.push_back(entry); - } } else { for (auto const* t : { &r.cxx, &r.cc }) add_dir(r.path_dirs, *t); } From 913f085076e09635a92ffd8c5a708d421a12fb5c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 28 Sep 2026 23:53:27 +0800 Subject: [PATCH 8/9] ci: consumers build with mcpp 2026.9.28.3, the package floor of 0.17.0 --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 354e94a..1adc803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,11 @@ env: # applies the host's own `[target.]` row (mcpp#704), where the Qt # fixtures state their Linux C++ runtime, and a package with no Qt source # gets no synthesised build program (mcpp#715), which qt-import-only asserts. - MCPP_VERSION: 2026.9.27.1 + # + # 2026.9.28.3 IS WHAT 0.17.0 NEEDS, and the package floor states it: the + # build information `mcpp.plugins.toolset` reads, `mcpp::report`, and one + # placement edge for many files (mcpp#734). + MCPP_VERSION: 2026.9.28.3 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # # Empty on every push and pull request, so the steps run the release above. From 46818f2fa1381b16de7563d94c39321dba326feb Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 29 Sep 2026 01:39:08 +0800 Subject: [PATCH 9/9] No downstream project named: code comments, fixtures, the check script and the design records refer to the validation project Two design records named after it are renamed accordingly. --- .../2026-09-26-deps-vcpkg-rules-qt-design.md | 62 +++++++++---------- .../2026-09-26-deps-vcpkg-rules-qt-plan.md | 16 ++--- ...26-09-26-plugins-0.15-architecture-plan.md | 6 +- ...6-09-27-mcpp-2026.9.27.1-ecosystem-plan.md | 4 +- .../docs/2026-09-27-plugins-0.15.2-plan.md | 20 +++--- ...dation-project-upstream-vs-pr2-outlook.md} | 34 +++++----- ...s-locale-and-validation-project-review.md} | 24 +++---- .github/scripts/check-deps-and-qt.sh | 2 +- deps/vcpkg.cppm | 2 +- tests/qt-consumer/mcpp.toml | 2 +- tests/qt-import-only/mcpp.toml | 2 +- 11 files changed, 87 insertions(+), 87 deletions(-) rename .agents/docs/{2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md => 2026-09-27-validation-project-upstream-vs-pr2-outlook.md} (81%) rename .agents/docs/{2026-09-27-versioning-assets-locale-and-galtranslpp-review.md => 2026-09-27-versioning-assets-locale-and-validation-project-review.md} (92%) diff --git a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md index 8844b72..ca95542 100644 --- a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md +++ b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-design.md @@ -1,10 +1,10 @@ # 通用构建插件设计:`deps-vcpkg`、`rules-qt`、`deps-cmake` 及其 xlings 依赖 状态:第 4 版(实现版;§0.1 的更正块取代正文中与之冲突的内容),实施中 · 2026-09-26 · 实施计划见 `2026-09-26-deps-vcpkg-rules-qt-plan.md` · -基于 mcpp 2026.9.25.1、mcpp-plugins 0.12.0(`origin/main` dea1f09)、xim-pkgindex `origin/main`、GalTranslPP 上游 -`main` 2c7d6bf(2026-09-25)· 起因:在 GalTranslPP(mcpp 工作区 + vcpkg + Qt 6,Windows/MSVC)上调查 +基于 mcpp 2026.9.25.1、mcpp-plugins 0.12.0(`origin/main` dea1f09)、xim-pkgindex `origin/main`、验证工程上游 +`main` 2c7d6bf(2026-09-25)· 起因:在验证工程(mcpp 工作区 + vcpkg + Qt 6,Windows/MSVC)上调查 mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 mcpp 项目都能复用**的构建能力。§1–§6、§8 只设计这批 -通用能力;§7 用 GalTranslPP 做真实案例,核算"插件都做完以后,一个真实项目迁移要花多少、能省多少"。 +通用能力;§7 用验证工程做真实案例,核算"插件都做完以后,一个真实项目迁移要花多少、能省多少"。 标注约定:**【已核实】** 有出处(文档章节、索引文件、发布资产、CI 实测、源码计数);**【待验证】** 是设计依赖但尚未 实测的假设,全部列入 §9 Phase 0。 @@ -35,7 +35,7 @@ mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 | D6 | 2 | 附加模块能否做成一个包 | **能,建议做成一个包 `xim:qt-addons`**(依据与代价见 §6.2;待确认,§10-1) | | D7 | 2 | registry 共享克隆放哪 | vcpkg 的每用户目录(§3.6);第 4 版起不再有托管克隆,见 §0.1 | | D6′ | 3 | D6 确认 | `xim:qt-addons` 为一个包,**自有前缀**(xim 载荷私有),`rules-qt` 同时加入两个前缀 | -| D8 | 3 | 生态通用能力先行 | 通用缺口由 mcpp 以通用机制补上(SPEC-007,mcpp#702);插件按 SPEC-007 实现并等待该发布(§0.1-5);GalTranslPP 的 D 阶段在 fork 上初步验证 | +| D8 | 3 | 生态通用能力先行 | 通用缺口由 mcpp 以通用机制补上(SPEC-007,mcpp#702);插件按 SPEC-007 实现并等待该发布(§0.1-5);验证工程的 D 阶段在 fork 上初步验证 | | D9 | 3 | 免装 VS | 不是目标;删除 `toolchain::msvc_xim`,`xim:msvc` 由 mcpp 自动匹配 | ### 0.1 第 4 版更正(实现时的决定,取代正文中冲突的部分) @@ -43,7 +43,7 @@ mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 1. **registry 不再由插件管理**(取代 §3.6 的托管克隆、按工具检出与文件锁)。vcpkg-tool 每个发布附带 `vcpkg-standalone-bundle.tar.gz`(3.4 MB),其 `vcpkg-bundle.json` 为 `{"deployment":"OneLiner","usegitregistry":true}`, 脚本与该工具版本严格对应。`xim:vcpkg` = 工具 + 该 bundle,安装目录即 `VCPKG_ROOT`。【已核实】本机以此为根,对 - GalTranslPP 的 baseline 执行 `vcpkg install --dry-run`,输出 "Fetching registry information from + 验证工程的 baseline 执行 `vcpkg install --dry-run`,输出 "Fetching registry information from https://github.com/microsoft/vcpkg",解析出 `fmt 12.2.0`,registry 缓存 137 MB。继承的 `VCPKG_ROOT` 不使用, `options::vcpkg_root` 可显式指定。 2. **启动器合为一个 `mcpp-deps`**(取代 `mcpp-vcpkg`、`mcpp-cmake`),子命令 `vcpkg`、`cmake`;消费者写 @@ -92,7 +92,7 @@ mcpp-language-server issue 23 时,整理出一批**与该项目无关、任何 (`docs/30` "Current limitations");`mcpp::action` 是 ninja 边,只在 `mcpp build` 时执行,不受该上限约束 (`docs/30` "Declaring work instead of doing it")。 4. **缺环境不失败**。构建程序遇到缺失的前缀/工具时输出确定的路径并 `mcpp::warning`,真正的失败留给 action 边—— - 这样 IDE(mcppls 通过 emit 取模型)在一台尚未安装依赖的机器上也能拿到可信模型。反例是 GalTranslPP 现状:构建 + 这样 IDE(mcppls 通过 emit 取模型)在一台尚未安装依赖的机器上也能拿到可信模型。反例是验证工程现状:构建 程序缺 Qt 或缺一个 vcpkg `.lib` 就 `return 1`,emit 整体失败(CI 实测 `MCPP_BUILD_DATABASE_PLAN_FAILED`)。 5. **不写别人的东西**。插件只写 `out_dir`、自己管理的目录(§3.6 的托管克隆)和用户显式要求写的位置;用户已有的 vcpkg 克隆、项目源码树默认只读(lupdate 见 §4.3)。 @@ -155,7 +155,7 @@ makes the package's compile edges wait for it")。 管理(§3.6),只是不再承担环境设置。 - 目录隔离:`--x-buildtrees-root`、`--x-packages-root` 指向项目自己的 `out_dir`,`downloads/` 共享。共享克隆的默认 buildtrees/packages 在克隆内,两个项目同时编同一个 port 会互相覆盖;按项目隔离后与 vcpkg 自身的锁语义无关, - 一定安全。(GalTranslPP 的 CI 已经这样用 `--x-buildtrees-root`。) + 一定安全。(验证工程的 CI 已经这样用 `--x-buildtrees-root`。) - 输入:`vcpkg.json`、`vcpkg-configuration.json`、overlay ports/triplets 目录(`input()` + `rerun_if_changed_glob`)。 输出:stamp。只在这些输入变化时重跑;emit 永不执行;不受 600 s 限制。 - 二进制缓存:**不覆盖,用 vcpkg 的默认位置**(【已核实】vcpkg 文档 "Default binary cache":Windows @@ -168,7 +168,7 @@ makes the package's compile edges wait for it")。 (vcpkg 的默认值,已有项目的目录布局不变)。 - 链接:以 `options.libraries` 为准,**一律用确定的完整路径** `mcpp::link_flag(/lib/.lib)`(ELF/Mach-O 上 `lib.a/.so/.dylib`)。链接边排在编译边之后、编译边又等待安装边,所以链接时文件必然已存在,不必在规划期判断 - 存在与否。用完整路径的理由 GalTranslPP 的 `vcpkg_link.hpp` 写过:"An exact file path cannot resolve to a same-named + 存在与否。用完整路径的理由验证工程的 `vcpkg_link.hpp` 写过:"An exact file path cannot resolve to a same-named system library"。之所以要显式列表:首次构建时构建程序先于安装边运行,vcpkg 的 `info/*.list` 尚不存在。 - 校验:`info/*.list` 存在时,比对列表与实际产物,不一致给 `mcpp::warning`;`rerun_if_changed(stamp)` 让安装完成后 下一次构建程序按实际结果重新校验。 @@ -194,12 +194,12 @@ makes the package's compile edges wait for it")。 - 后两项由插件在 `out_dir` 生成 overlay triplet 与 chainload 工具链文件。【已核实】vcpkg 在 Windows 上用干净环境编 port,`INCLUDE`/`LIB` 要经 `VCPKG_ENV_PASSTHROUGH` 传入(文档同页);或者工具链文件直接写 `/I`、`/LIBPATH`。 哪种可行【待验证】。 -- **项目自带自定义 triplet 时**(GalTranslPP 的 `gpp-x64-windows-release` 就是):生成的 triplet 先 `include()` 项目 +- **项目自带自定义 triplet 时**(验证工程的 `gpp-x64-windows-release` 就是):生成的 triplet 先 `include()` 项目 triplet,再追加 chainload 设置,不替换它;triplet 名随之变为 `<原名>-mcpp-<选项>`,安装目录、映射一律按新名。 - 同时检查 triplet 的 `VCPKG_CRT_LINKAGE` 与项目运行库(如 `-fms-runtime-lib=dll`)是否一致,不一致则 warning。 **(5) 工作区**:`blocking` 的顺序是"per package"(【已核实】`docs/30`),被依赖成员的安装边管不到不依赖它的成员 -(GalTranslPP 的 `Updater` 不依赖 core,却要链接 `bit7z`,§7.1)。默认做法:**每个调用 `use()` 的成员各声明一条 +(验证工程的 `Updater` 不依赖 core,却要链接 `bit7z`,§7.1)。默认做法:**每个调用 `use()` 的成员各声明一条 安装边**,命令相同、stamp 各在自己的 `out_dir`。第一条真正安装,其余的 `vcpkg install` 发现已安装、几秒内结束; 并发时由 vcpkg 对安装目录加的锁串行化【待验证:manifest 模式对 `vcpkg_installed` 的锁】。锁不成立时退回"同一个 stamp 由每个成员声明为 `input()`、只由一个成员声明为 `output()`";根本方案是 §8 需求 3。 @@ -250,7 +250,7 @@ manifest 写法处理: | 项目写法 | vcpkg 的要求 | 本设计的做法 | |---|---|---| | `vcpkg-configuration.json` 的 `default-registry` 为 `git` 类型(带 `baseline`) | 工具自己把 registry 取到它的每用户 registries 缓存,所有项目共享 | 什么都不用做(`X_VCPKG_REGISTRIES_CACHE` 可覆盖)【待验证:各平台默认路径】 | -| `vcpkg.json` 的 `builtin-baseline`(GalTranslPP 就是这种) | `VCPKG_ROOT` 必须是含该 baseline 提交的 microsoft/vcpkg 克隆 | 见下 | +| `vcpkg.json` 的 `builtin-baseline`(验证工程就是这种) | `VCPKG_ROOT` 必须是含该 baseline 提交的 microsoft/vcpkg 克隆 | 见下 | `builtin-baseline` 的 `VCPKG_ROOT` 按顺序选: @@ -275,7 +275,7 @@ manifest 写法处理: 它要求的工具、已装工具三个版本,提示升级 `xim:vcpkg`;否则 baseline 一定在这个检出之前,两条都满足。这样**工具版本 决定脚本版本、baseline 决定 port 版本**,不会因为上游 registry 前进而让所有项目一起失败(第 2 版的问题,见 §11)。 -不按发布标签检出:【已核实】GalTranslPP 的 baseline `ea1a7396`(2026-08-08)比最新标签 `2026.07.29` 晚 129 个提交, +不按发布标签检出:【已核实】验证工程的 baseline `ea1a7396`(2026-08-08)比最新标签 `2026.07.29` 晚 129 个提交, 但要求的工具同样是 2026-07-27。按标签检出会把一个完全兼容的项目判为"baseline 太新"。项目在两个标签之间选 baseline 很常见。 @@ -351,7 +351,7 @@ int main() { - **uic / rcc**:`role = "source"`;rcc 的依赖用 `rcc --list` 在规划期取得资源清单作为 `input()`,或 depfile【待验证】。 - **lrelease**:`.ts` → `.qm`,`role = "source"`(产物不参与编译),再 `mcpp::deploy` 到 `translations/`。 - **lupdate**:会**改写源码树里的 `.ts`**,默认关闭;`o.translations.update_sources = true` 时作为 `role = "check"`、 - `blocking = true` 的 action,lrelease 依赖它的 stamp(GalTranslPP 现行做法,§7.2)。与 emit "never writes into the + `blocking = true` 的 action,lrelease 依赖它的 stamp(验证工程现行做法,§7.2)。与 emit "never writes into the project tree"(specs R2.1)的精神一致:只在显式要求的 build 中写。 - **链接**:`include_dir(/include/Qt)`、`link_search(/lib)`、`link_lib("Qt6")`、 `QT__LIB` 等定义;编译器族为 msvc/clang-cl 时加 `/Zc:__cplusplus`(Qt 6 对 MSVC 的要求;clang 驱动不需要); @@ -361,7 +361,7 @@ int main() { `platforms/` 等子目录(【已核实】构建程序的 `mcpp::deploy` 见 `docs/30` "Deploying what the program generated", pack 将其放在 `bin//`,README `dist-apple` 行;清单侧 `[runtime] deploy` 的 `to` 语义见 `docs/04` §2.11)。 `d3dcompiler_47.dll`、`opengl32sw.dll` 按 windeployqt 的默认行为可选部署(`o.deploy_software_gl`)。 -- **库成员的翻译**:库成员(如 GalTranslPP core)生成的 `.qm` 要进可执行成员的 `translations/`。【待验证】依赖方 +- **库成员的翻译**:库成员(如验证工程core)生成的 `.qm` 要进可执行成员的 `translations/`。【待验证】依赖方 构建程序的 `mcpp::deploy` 是否随依赖进入可执行文件的 run/pack 布局;不成立时由可执行成员的 `translations` 选项 直接列出库的 `.ts`。 @@ -374,7 +374,7 @@ int main() { ## 5. `deps-cmake`(第二批) -项目里常见"带一个 CMake 子项目"(submodule/vendored),今天只能靠外部脚本先编好(GalTranslPP 的 ElaWidgetTools +项目里常见"带一个 CMake 子项目"(submodule/vendored),今天只能靠外部脚本先编好(验证工程的 ElaWidgetTools 就是:一个 Python 脚本调 CMake,Qt 路径写死在脚本里,§7.1)。插件把 configure / build / install 作为三条 action (`role = "check"` 链,最后一条 blocking),安装到 `out_dir` 下的前缀,再按 §3.3(2) 的方式映射。 @@ -442,7 +442,7 @@ libraries are available under commercial licenses from The Qt Company, or under 由于规则只链接显式列出的模块,项目不会误用。包说明里写明这一点,并列出 LGPL 与 GPL-only 模块。 - 调试符号包(`debug_information`、`debug_info`)不收(基础包的调试符号解压 4.05 GB)。 -- **首个版本 6.11.1**(GalTranslPP 的要求),平台先做 win64_msvc2022_64,再补 linux、macos、arm64。 +- **首个版本 6.11.1**(验证工程的要求),平台先做 win64_msvc2022_64,再补 linux、macos、arm64。 - 【已核实】下载方式:6.11 起在线仓库按架构分目录(`qt6_6111/qt6_6111_msvc2022_64/Updates.xml`),aqtinstall 3.3.0 不认; 每个归档以安装前缀为根(`bin/`、`include/`、`lib/`)。本设计的验证 CI 已按此方式装成功。 - 许可:xim 只是从 Qt 官方仓库下载;包说明注明 Qt 的 LGPL 义务(动态链接、允许替换)。 @@ -460,14 +460,14 @@ libraries are available under commercial licenses from The Qt Company, or under - 许可:`7z.dll` 为 LGPL,RAR 解压部分带 unRAR 限制;包说明里注明。 - `xim:7zip` 要做的只有一件事:在包说明里写明 Windows 安装目录中 `7z.dll` 的位置是约定(不是偶然),这样项目可以依赖它。 -## 7. 真实案例:GalTranslPP 迁移的成本与收益 +## 7. 真实案例:验证工程迁移的成本与收益 -本节回答:"§3–§6 都做完以后,一个真实项目要花多少才能用上,能省下什么。"数据全部来自 GalTranslPP 上游 `main` -2c7d6bf 的源码计数、它的 `how-to-build.md`、子模块源码和本设计的验证 CI(Sunrisepeak/GalTranslPP PR #1)。 +本节回答:"§3–§6 都做完以后,一个真实项目要花多少才能用上,能省下什么。"数据全部来自验证工程上游 `main` +2c7d6bf 的源码计数、它的 `how-to-build.md`、子模块源码和本设计的验证 CI(验证工程 PR #1)。 ### 7.1 现状(【已核实】源码计数) -工作区 5 个成员:`GalTranslPP`(core,库)、`GPPVersion`(库)、`GPPCLI`、`GPPGUI`、`Updater`(可执行)。依赖关系: +工作区 5 个成员:`验证工程`(core,库)、`GPPVersion`(库)、`cli`、`gui`、`Updater`(可执行)。依赖关系: CLI、GUI 依赖 core;**Updater 不依赖 core,但自己链接 `bit7z`**。 | 部分 | 规模 | 做什么 | @@ -482,7 +482,7 @@ CLI、GUI 依赖 core;**Updater 不依赖 core,但自己链接 `bit7z`**。 | 子模块 ElaWidgetTools | CMake 项目 | 用自带的 `build.py` 编;Qt 路径 `D:/Qt/6.11.1/msvc2022_64` **写死在脚本里**;需要 `WidgetsPrivate` | | 子模块 pybind11(fork) | 附带 Python 头文件与 `python312.lib` | 与仓库内 `Python-3.12.10-embed-amd64.zip` 配套;代码里写死 `BaseConfig/Python-3.12.10-embed-amd64` | | vcpkg | 22 个 port,自定义 triplet `gpp-x64-windows-release`,2 个 overlay port(mecab、proxy) | `builtin-baseline` 写法 | -| Qt 用量 | 45 个含 `Q_OBJECT` 的头文件(全在 GPPGUI)、1 个 `.qrc`、4 个 `.ts`、0 个 `.ui` | 模块:Core、Gui、Widgets、Network;无附加模块 | +| Qt 用量 | 45 个含 `Q_OBJECT` 的头文件(全在 gui)、1 个 `.qrc`、4 个 `.ts`、0 个 `.ui` | 模块:Core、Gui、Widgets、Network;无附加模块 | 新开发者的准备工作(`how-to-build.md`,111 行): @@ -512,10 +512,10 @@ CI(验证 workflow):Qt 靠一段 25 行的内联 Python 从 Qt 仓库下 | vcpkg 克隆 + bootstrap | `xim:vcpkg` + 托管克隆(§3.6) | 无 | | VS Build Tools | mcpp 侧 `sysroot = "xim:msvc@…"`;vcpkg 侧**只有** `toolchain::msvc_xim` 能免装 VS | §3.3(4)【待验证】chainload 细节 | | CMake、Python(开发者手装) | CMake 由插件声明;Python 只剩 embed 包解压(可改成一条用 `xim:7zip` 的 action) | 无 | -| **不替代**:`runtime-stage` + 发布布局代码 | 保留。`Release/GPPCLI`、`GPPGUI`、`GUICORE`、私有镜像目录是这个项目的发布约定,不是通用能力 | 只有改为从 `mcpp pack` 产物派生布局,或引擎提供 §8 需求 2,`runtime-stage` 才能删 | +| **不替代**:`runtime-stage` + 发布布局代码 | 保留。`Release/cli`、`gui`、`GUICORE`、私有镜像目录是这个项目的发布约定,不是通用能力 | 只有改为从 `mcpp pack` 产物派生布局,或引擎提供 §8 需求 2,`runtime-stage` 才能删 | | **不替代**:pybind11 fork 附带的 Python | 保留。换成 `xim:python` 会让头文件(3.12.13)与运行时 embed 包(3.12.10)的补丁版本不一致,而版本号写死在代码里 | 若要换,先把 embed 包也改由 xim 提供,属项目自己的决定 | -### 7.3 迁移成本(插件发布之后,GalTranslPP 一侧,一人) +### 7.3 迁移成本(插件发布之后,验证工程一侧,一人) | 阶段 | 内容 | 人日 | 前提 | |---|---|---|---| @@ -525,7 +525,7 @@ CI(验证 workflow):Qt 靠一段 25 行的内联 Python 从 Qt 仓库下 | D(可选) | 发布改为从 `mcpp pack` 派生,删 `runtime-stage`;vcpkg 改 `toolchain::msvc_xim` 实现免装 VS | 1–2 | §8 需求 2 或 pack 方案;§3.3(4) 验证通过 | | **合计** | A–C 必做 | **3.5–4.5** | D 另计 1–2 | -对比:通用插件本身 27–37 人日(§9),GalTranslPP 只占其中 3.5–4.5。第二个及以后的项目不再分摊插件开发,只付 +对比:通用插件本身 27–37 人日(§9),验证工程只占其中 3.5–4.5。第二个及以后的项目不再分摊插件开发,只付 自己的 A–C。 ### 7.4 收益(A–C 完成后) @@ -557,12 +557,12 @@ CI(验证 workflow):Qt 靠一段 25 行的内联 Python 从 Qt 仓库下 | # | 需求 | 为什么 | 没有它时的退路 | |---|---|---|---| | 1 | 构建程序可感知"规划模式"(或 emit 时构建程序失败只降级) | 原则 3/4 目前靠插件自律;任何项目的构建程序出错都会让 IDE 退回兜底模型 | 插件自律:缺失只 warning | -| 2 | 构建程序声明运行时搜索目录(run 的 PATH、pack 的 PE 闭包),且项目能在 build 时取得闭包结果 | vcpkg/Qt 的 DLL 在各自 `bin/`;GalTranslPP 的 `runtime-stage` 就是在自己补这一块 | 逐个 `mcpp::deploy`;项目保留自己的闭包工具 | +| 2 | 构建程序声明运行时搜索目录(run 的 PATH、pack 的 PE 闭包),且项目能在 build 时取得闭包结果 | vcpkg/Qt 的 DLL 在各自 `bin/`;验证工程的 `runtime-stage` 就是在自己补这一块 | 逐个 `mcpp::deploy`;项目保留自己的闭包工具 | | 3 | blocking action 对依赖方也生效,或工作区级 setup action | 多成员工作区共用一次安装 | 每个成员各一条安装边(§3.3(5)) | | 4 | `mcpp::action` 设置环境变量 | 外部工具普遍靠环境变量配置 | 插件自带启动器(本设计采用) | 另:`[workspace.profile.*]` 继承与本插件无关,但同一消费者会遇到(【已核实】`docs/07` §4.1、specs §9 只允许三类继承; -GalTranslPP 5 个成员重复写了同样的 `[profile.release]`/`[profile.fast-release]`)。 +验证工程5 个成员重复写了同样的 `[profile.release]`/`[profile.fast-release]`)。 ## 9. 计划与工作量(一人估算,含 fixtures 与 CI,Windows 优先) @@ -572,10 +572,10 @@ GalTranslPP 5 个成员重复写了同样的 `[profile.release]`/`[profile.fast- | Phase 1 | `xim:vcpkg`、`xim:qt`(Windows)、`deps-vcpkg` + `mcpp-vcpkg`(registry 选择、按工具检出、锁、目录隔离、三种工具链)+ fixtures | 9–12 | | Phase 2 | `rules-qt` + fixtures;`xim:qt-addons`;`xim:qt*` Linux/macOS | 8–10 | | Phase 3 | `deps-cmake` + `mcpp-cmake` + fixtures | 3.5–5.5 | -| 消费者验证 | GalTranslPP 迁移 A–C(§7.3,fork 分支 + 其 CI) | 3.5–4.5 | +| 消费者验证 |验证工程迁移 A–C(§7.3,fork 分支 + 其 CI) | 3.5–4.5 | | **合计** | | **27–37**(第 2 版 24–33;增加的部分见 §11) | -**验证场已就绪**:Sunrisepeak/GalTranslPP PR #1 的 workflow 已在 windows-2025 上搭好 xlings、mcpp 2026.9.25.1、 +**验证场已就绪**:验证工程 PR #1 的 workflow 已在 windows-2025 上搭好 xlings、mcpp 2026.9.25.1、 LLVM 22.1.8、Qt 6.11.1(直接读 Qt 仓库)、vcpkg(`gpp-x64-windows-release`,22 个 port);【已核实】vcpkg 冷安装 51.6 分钟,结果已缓存。 @@ -595,7 +595,7 @@ LLVM 22.1.8、Qt 6.11.1(直接读 Qt 仓库)、vcpkg(`gpp-x64-windows-rele 1. **托管克隆的检出版本**(§3.6):第 2 版让克隆跟随上游最新、再比较工具版本。上游 registry 一旦要求比 `xim:vcpkg` 更新的工具,**所有项目会同时失败**,直到 xim 跟进。改为检出"主干上与已装工具匹配的最后一个提交"。本版起草时 - 曾写成"与工具匹配的最新发布标签",用 GalTranslPP 的 baseline 实测(比最新标签晚 129 个提交、工具要求相同)后改掉。 + 曾写成"与工具匹配的最新发布标签",用验证工程的 baseline 实测(比最新标签晚 129 个提交、工具要求相同)后改掉。 2. **用户已有的 `VCPKG_ROOT` 改为只读**(§3.6、原则 5):第 2 版会对它 `git fetch`,等于改写用户的仓库。 3. **按项目隔离 buildtrees/packages**(§3.3(1)):第 2 版没提,共享克隆时两个项目同时编同一个 port 会互相覆盖。 4. **二进制缓存改用 vcpkg 默认位置**(§3.3(1)):第 2 版放在 mcpp 缓存目录,与 D7 的"复用用户已有的 vcpkg"矛盾。 @@ -603,9 +603,9 @@ LLVM 22.1.8、Qt 6.11.1(直接读 Qt 仓库)、vcpkg(`gpp-x64-windows-rele 目录位置带来的实际好处是与默认二进制缓存同级。 5. **"免装 VS"的真实条件**(§3.3(4)):第 2 版把"工具链一致"当作纯可选项,并在 §6.1 暗示 `xim:msvc` 就能免装 VS。 已核实 vcpkg 默认路线必须找到 VS 实例,只有 chainload 能绕开;新增 `toolchain::msvc_xim`。 -6. **工作区默认做法**(§3.3(5)):由"共享 stamp 技巧"改为"每个成员各一条安装边",更简单,且 GalTranslPP 的 +6. **工作区默认做法**(§3.3(5)):由"共享 stamp 技巧"改为"每个成员各一条安装边",更简单,且验证工程的 Updater 正好需要。 -7. **§7 重写**:第 2 版的 GalTranslPP 样例写了 `xim:python = 3.12.13`(与 embed 包 3.12.10 不一致)、称可删除约 600 行 +7. **§7 重写**:第 2 版的验证工程样例写了 `xim:python = 3.12.13`(与 embed 包 3.12.10 不一致)、称可删除约 600 行 (`runtime-stage` 和发布布局其实删不掉)、完全没提 ElaWidgetTools(`deps-cmake` 对这个项目不是可有可无)。 8. 附加模块合为一个包(D6),§0、§4.2、§4.3、§6.1、§6.2 同步;新增 `rules-qt-xim-addons`。 9. 小项:自定义 triplet 包装;`use()` 返回前缀;`private_modules`;库成员翻译;rcc 依赖;fixtures 增加 4 个。 diff --git a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md index 4dd1ca1..7ccd70c 100644 --- a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md +++ b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md @@ -7,12 +7,12 @@ | # | 决定 | 对设计的影响 | |---|---|---| | D6 | 附加模块合为一个包 | 确认 `xim:qt-addons`;按 xim 的载荷私有规则,它有**自己的前缀**,不写入 `xim:qt` 的目录 | -| D8 | 先做 mcpp / xlings 生态的通用能力;GalTranslPP 的 D 阶段只在 fork 上做初步验证 | 引擎需求 2 在 mcpp 中实现:构建程序声明运行时库目录 | +| D8 | 先做 mcpp / xlings 生态的通用能力;验证工程的 D 阶段只在 fork 上做初步验证 | 引擎需求 2 在 mcpp 中实现:构建程序声明运行时库目录 | | D9 | 免装 VS 不是目标 | 删除 `toolchain::msvc_xim`;`xim:msvc` 由 mcpp 自动匹配,插件不声明 | 另有一项由实测带来的简化:vcpkg-tool 每个发布附带 `vcpkg-standalone-bundle.tar.gz`(3.4 MB,脚本与该工具 版本严格对应,`vcpkg-bundle.json` 含 `"usegitregistry": true`)。以它为 `VCPKG_ROOT` 时,`builtin-baseline` -清单经 git registry 解析,registry 落在 vcpkg 的每用户 registries 缓存。已在本机实测:对 GalTranslPP 的 +清单经 git registry 解析,registry 落在 vcpkg 的每用户 registries 缓存。已在本机实测:对验证工程的 baseline `ea1a7396` 执行 `vcpkg install --dry-run` 解析出 `fmt 12.2.0`,缓存 137 MB。因此设计第 3 版 §3.6 的托管克隆、 按工具检出与文件锁全部取消。 @@ -24,14 +24,14 @@ baseline `ea1a7396` 执行 `vcpkg install --dry-run` 解析出 `fmt 12.2.0`, | openxlings/xim-pkgindex | `xim:vcpkg` 2026.7.27(工具 + standalone bundle)、`xim:qt` 6.11.1、`xim:qt-addons` 6.11.1;`xim:7zip` 说明 `7z.dll` 位置;测试 | 合入即发布索引;`xlings-res/vcpkg` 资源用 `gh` 与 `gtc` 双端上传 | | mcpp-community/mcpp-plugins | `0.13.0`:`deps-vcpkg` + `mcpp-vcpkg`、`rules-qt` + `rules-qt-xim` + `rules-qt-xim-addons`、`deps-cmake` + `mcpp-cmake`;fixtures;CI 引擎版本;README | tag `v0.13.0`,GitHub release,`gtc` 上传 `mcpp-res/mcpp-plugins` | | mcpp-community/mcpp-index | 登记 `mcpp:plugins 0.13.0` | 合入即发布 | -| Sunrisepeak/GalTranslPP(fork) | 临时 PR 2:迁移 A–C,D 阶段初步验证 | 不合入 | +| 验证工程(fork) | 临时 PR 2:迁移 A–C,D 阶段初步验证 | 不合入 | ## 3. 依赖关系 ``` T2 xim 包(vcpkg/qt/qt-addons)──► T2r 索引合入(xim-pkgindex#878,已合入) T3 插件按 SPEC-007 实现(PR 已开,等待)──┐ -T1 mcpp#702 实现并发布(mcpp 侧)─────────┴──► T3c 插件 CI 全绿 ──► T3r 插件发布 0.13.0 ──► T4 mcpp-index ──► T5 GalTranslPP PR 2 +T1 mcpp#702 实现并发布(mcpp 侧)─────────┴──► T3c 插件 CI 全绿 ──► T3r 插件发布 0.13.0 ──► T4 mcpp-index ──► T5验证工程PR 2 ``` - T1、T2、T3 并行:T3 在本机用 T1 的源码构建验证 Linux 路径;Windows 与 macOS 路径只能由 CI 验证。 @@ -70,9 +70,9 @@ T1 mcpp#702 实现并发布(mcpp 侧)─────────┴──► 位于插件仓库之内,因此其安装位于 `target/` 之下,检查脚本的日志写入 `target/ci/`,第二次构建以 `--profile dev` 绕过快路径,使 Linux 与 Windows、macOS 走同一条规划路径。从索引取得插件的项目不受影响。 -## 6. 0.14.0:运行时数据与精简 Qt(GalTranslPP 验证后的第二轮) +## 6. 0.14.0:运行时数据与精简 Qt(验证工程验证后的第二轮) -GalTranslPP 在 0.13.1 上构建成功后仍有两个手动步骤(解压嵌入式 Python、运行 `Release.py`),Qt 下载量为 +验证工程在 0.13.1 上构建成功后仍有两个手动步骤(解压嵌入式 Python、运行 `Release.py`),Qt 下载量为 完整基础包。第二轮把固定步骤程序化,并补齐 windeployqt 的 Qt 翻译: | 交付 | 仓库 | 机制 | @@ -100,5 +100,5 @@ libstdc++,mcpp 的 clang 使用 libc++,两者的 `std::` 符号互不链接 判据(Linux CI `vcpkg-libcxx`):llvm 下 `libfmt.a` 含 `std::__1::` 符号且程序运行;默认工具链构建后 libc++ 前缀仍在;切回 llvm 不重跑安装;deps-cmake 子项目的 `CMAKE_CXX_COMPILER` 为 mcpp 的 clang。 -GalTranslPP 上游仅支持 Windows(README 所述,代码直接调用 WinAPI),Linux 的判据由本仓库的 fixture 承担; -GalTranslPP PR2 作为 Windows 回归验证。 +验证工程上游仅支持 Windows(README 所述,代码直接调用 WinAPI),Linux 的判据由本仓库的 fixture 承担; +验证工程PR2 作为 Windows 回归验证。 diff --git a/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md b/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md index 1a2338d..903b03a 100644 --- a/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md +++ b/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md @@ -17,7 +17,7 @@ 6. 缺少插件开发规范;README 过长。 约束: -- GalTranslPP 上游仅支持 Windows。Linux 与 macOS 的判据由插件仓库的 fixture 承担,GalTranslPP PR2 作为 Windows 回归。 +-验证工程上游仅支持 Windows。Linux 与 macOS 的判据由插件仓库的 fixture 承担,验证工程PR2 作为 Windows 回归。 - mcpp 最新版本为 2026.9.26.2。 - 0.14.1 与 0.15.0 合并为一个版本 0.15.0。每个仓库一个 PR,仅在无法同时验证时拆分(§8)。 @@ -145,7 +145,7 @@ rules-qt 的 SDK 查找顺序为 `options::root` → `QT_ROOT_DIR`(不采用 ` |---|---| | 删除 `rules-qt-xim*` | 改为 `features = ["rules-qt"]`,并在 `[xlings]` 中声明 `xim:qt-base` 或 `xim:qt` 及版本 | | 删除 `mcpp-deps` | 从依赖边删除 `tools = ["mcpp-deps"]` | -| vcpkg 前缀多一层 triplet | 改用返回的 `prefix`,不硬编码 `vcpkg_installed/`(GalTranslPP `gpp.build` 第 182 行) | +| vcpkg 前缀多一层 triplet | 改用返回的 `prefix`,不硬编码 `vcpkg_installed/`(验证工程`build` 第 182 行) | | 插件不再提供 glib 目录 | 使用外部 Qt 的 Linux 项目自行处理闭包 | 载荷的更新方式:xlings 以磁盘上的载荷与安装记录判定"已安装",不会对已存在的载荷调用配方的 `installed()`。因此含运行时闭包的载荷以新版本号 @@ -164,7 +164,7 @@ mcpp-plugins#32 → 0.15.0(Linux Widgets 与 Windows CRT 判据依赖上一步 │ 三平台 CI 全绿 → tag、GitHub release、gtc 上传 GitCode mcpp-index:登记 0.15.0 │ -GalTranslPP PR2:按 §7 迁移;Windows 回归(含 VC++ 运行时) +验证工程PR2:按 §7 迁移;Windows 回归(含 VC++ 运行时) 并行:mcpp issue E1、E2 ``` diff --git a/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md b/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md index 04d341a..45d4a95 100644 --- a/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md +++ b/.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md @@ -44,7 +44,7 @@ bba733a 在云端从未构建。本地以 mcpp 2026.9.26.2、GCC 16.1 构建。 | e2e 802(#704) | 在默认工具链为 llvm@22.1.8 的机器上失败:测试断言 `NEEDED libstdc++`,而清单未固定工具链。在测试清单中写入 `[toolchain] default = "gcc@16.1.0"` 后三项断言全部通过,说明 #704 的修复正确,缺陷在测试本身 | 逐 issue 审查的结论:12 个 issue 的修复均对应 issue 的原始描述,未发现错误(#706 是已合入上游的文档提交,不在本 PR 中)。需要处理的是以下三点: -- **#714 是行为变更:** 未解析的 `workspace = true` 由静默变为空版本,改为加载时报错。在 mcpp-plugins、mcpp-index、GalTranslPP、mcpp-language-server 中均未发现依赖旧行为的清单。 +- **#714 是行为变更:** 未解析的 `workspace = true` 由静默变为空版本,改为加载时报错。在 mcpp-plugins、mcpp-index、验证工程、mcpp-language-server 中均未发现依赖旧行为的清单。 - **#710 覆盖不足:** 非成员宿主工具包的工具链路径只有单元测试,没有 e2e。 - **note 代码改名:** `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT` 改为 `..._DEFERRED`,没有兼容别名;mcpp-language-server 不读取这两个代码。 @@ -175,7 +175,7 @@ A1 至 A4 可以并行。B3 不依赖 B2:旧版 xlings 忽略 revision 字段 | xlings #620 | 先用 xlings 2026.9.26.3 安装 glibc(revision 0),再升级到 2026.9.27.1 并执行 `xlings install glibc` | 输出重装提示,载荷为 revision 1 | | xlings #621 | 执行 `setlocale(LC_ALL, "C.UTF-8")` 与 `iconv -f UTF-8 -t GBK`;qt-demo 执行 `mcpp run` | 两项调用成功;Qt 不再输出 UTF-8 locale 提示 | -**消费方验证:** GalTranslPP fork 以 mcpp 2026.9.27.1 与 plugins 0.16.0 运行 Windows CI,测量首次 emit、构建后再次 emit,并确认未安装 llvm@20.1.7。预期结果见 `2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md` §4 的 S4 一栏。该验证只更新 fork 分支,不向上游提 PR;是否基于上游最新重做,另行决定。 +**消费方验证:**验证工程fork 以 mcpp 2026.9.27.1 与 plugins 0.16.0 运行 Windows CI,测量首次 emit、构建后再次 emit,并确认未安装 llvm@20.1.7。预期结果见 `2026-09-27-validation-project-upstream-vs-pr2-outlook.md` §4 的 S4 一栏。该验证只更新 fork 分支,不向上游提 PR;是否基于上游最新重做,另行决定。 ### 3.6 阶段 E:收尾 diff --git a/.agents/docs/2026-09-27-plugins-0.15.2-plan.md b/.agents/docs/2026-09-27-plugins-0.15.2-plan.md index 349658e..950b2cf 100644 --- a/.agents/docs/2026-09-27-plugins-0.15.2-plan.md +++ b/.agents/docs/2026-09-27-plugins-0.15.2-plan.md @@ -1,12 +1,12 @@ # plugins 0.15.2 与生态收敛方案 -日期:2026-09-27。依据:`2026-09-27-versioning-assets-locale-and-galtranslpp-review.md` 及其后的讨论结论。 +日期:2026-09-27。依据:`2026-09-27-versioning-assets-locale-and-validation-project-review.md` 及其后的讨论结论。 ## 0. 目标 1. Qt 以上游版本 `6.11.1` 作为唯一版本键,消费方书写上游版本。 2. brotli 资源恢复规范文件名,GitHub 与 GitCode 逐字节一致。 -3. GalTranslPP PR2 达到当前生态能力下的最佳形态:版本单点声明、可配置项有文档、emit 开销可解释且可测。 +3.验证工程PR2 达到当前生态能力下的最佳形态:版本单点声明、可配置项有文档、emit 开销可解释且可测。 4. 需要 mcpp、xlings、xim glibc 改动的事项,以 issue 形式交给各自仓库。 非目标:修改 mcpp、xlings 或 glibc 配方本身。 @@ -46,7 +46,7 @@ D1 的索引改动只是删除冗余键;消费方的切换不依赖它先合 - 验收:三平台 CI 全绿。其中 Linux 的 widgets 打包检查要确认新缓存下装出的 `6.11.1` 带有运行时闭包(libdbus、xkbcommon、fontconfig),这正是 D1 的实证。 - 发布:tag v0.15.2、GitCode 镜像(sha256 比对)、mcpp-index 登记。 -### 2.4 GalTranslPP PR2 +### 2.4 the validation project PR2 | 项 | 内容 | 验收 | |---|---|---| @@ -70,7 +70,7 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 ``` 2.1 brotli 资源 ──► 2.2 xim PR ──► 索引发布 ──► 2.5-1 删除 -r1 -2.3 plugins 0.15.2 ──► 发布 + index 登记 ──► 2.4 GalTranslPP PR2 +2.3 plugins 0.15.2 ──► 发布 + index 登记 ──► 2.4验证工程PR2 ``` 两条链互不阻塞:2.3 与 2.4 使用 `6.11.1` 键,该键今天已存在且内容已修正(见 §1 关键事实)。 @@ -83,7 +83,7 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 | mcpp-community/mcpp#711 | 发布物无法依赖另一个包按目标平台构建的可执行文件,只能借宿主工具边 | 否:本机构建结果正确;交叉编译时才会出错 | | mcpp-community/mcpp#712 | `xpkg_dir` 把裸版本当字面 pin,xlings 则会匹配到四段版本键;已复现(libglvnd `1.7` → 装了 1.7.0.1,`xpkg_dir` 返回空) | 否:写精确版本键即可避开 | | openxlings/xlings#620 | 配方修订号独立于版本号 | 否:没有外部用户时就地修正(D4) | -| openxlings/xlings#621 | glibc 载荷没有 locale 数据,编译进去的 locale 路径不在载荷内 | 否:只是一条提示;GalTranslPP 仅构建 Windows,不受影响 | +| openxlings/xlings#621 | glibc 载荷没有 locale 数据,编译进去的 locale 路径不在载荷内 | 否:只是一条提示;验证工程仅构建 Windows,不受影响 | 不再提交:设置 `QT_ROOT_DIR` 时仍下载声明的 Qt。这是预期行为,按 E4 写进文档。 视 E2、E3 的结果,可能再提交"xlings 条目支持从工作区继承"。 @@ -92,7 +92,7 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 | 风险 | 缓解 | |---|---| -| 删除 `6.11.1.1` 后,仍书写 `6.11.1.1` 的消费方解析失败 | 已知消费方只有 plugins fixture 与 GalTranslPP,二者均在本方案中改为 `6.11.1` | +| 删除 `6.11.1.1` 后,仍书写 `6.11.1.1` 的消费方解析失败 | 已知消费方只有 plugins fixture 与验证工程,二者均在本方案中改为 `6.11.1` | | 某处缓存仍恢复出修正前的 `6.11.1` 载荷 | 两处 CI 缓存 key 均升级;Linux widgets 打包检查会在闭包缺失时失败 | | E2 或 E3 不被 mcpp 支持 | 回退为逐成员声明,不阻塞发布 | @@ -103,14 +103,14 @@ E6 若查明警告来自插件(例如 rules-qt 在不需要 Qt 的构建程序 | 2.1 brotli | GitCode 已上传规范名资源与 `SHA256SUMS`,两端逐字节一致;GitHub 的 `-r1` 已删除;GitCode 的 `-r1` 待用户删除 | | 2.2 xim | openxlings/xim-pkgindex#891 已合入(4c1dc850);发布后的索引中 qt、qt-base、qt-addons 只列出 `6.11.1`;brotli 按规范名安装,带头文件 | | 2.3 plugins | mcpp-community/mcpp-plugins#34 已合入(53c57d5),三平台 CI 全绿;tag v0.15.2;GitCode mcpp-res/mcpp-plugins 0.15.2 与 GitHub 逐字节一致(sha256 `a66e349d…76ad`);mcpplibs/mcpp-index#478 已合入,`latest` 为 0.15.2 | -| E2 | 成员的 `.workspace = true` 可以与 `features`、`host-module` 同用。非成员的 `gpp.build` 不能继承(mcpp#714),改为自带版本;若把它设为成员,根目录的 `mcpp build` 会把它当作目标构建并失败 | +| E2 | 成员的 `.workspace = true` 可以与 `features`、`host-module` 同用。非成员的 `build` 不能继承(mcpp#714),改为自带版本;若把它设为成员,根目录的 `mcpp build` 会把它当作目标构建并失败 | | E3 | 不成立:三种写法(带 cfg 的 target 表、`[target.linux]`、`[xlings.workspace]`)写在根清单时,成员的构建程序都看不到载荷(mcpp#713)。回退为逐成员声明 | -| E6 警告来源 | 21 条 "no Qt SDK" 全部来自 `gpp.build`:它启用 `rules-qt` 只为导入模块,自己没有 build.mcpp,mcpp 仍为它合成规则程序(mcpp#715)。0.15.2 让这种情况不再输出提示;新增 fixture `qt-import-only` | -| Qt 路径单点 | GalTranslPP 的 `gpp::qt_root` 与 `gpp::qt_options()` 汇集到一处:规则、发布布局、ElaWidgetTools 的 `QT_SDK_DIR` 都取同一个 SDK | +| E6 警告来源 | 21 条 "no Qt SDK" 全部来自 `build`:它启用 `rules-qt` 只为导入模块,自己没有 build.mcpp,mcpp 仍为它合成规则程序(mcpp#715)。0.15.2 让这种情况不再输出提示;新增 fixture `qt-import-only` | +| Qt 路径单点 |验证工程的 `gpp::qt_root` 与 `gpp::qt_options()` 汇集到一处:规则、发布布局、ElaWidgetTools 的 `QT_SDK_DIR` 都取同一个 SDK | 新发现并已提交的 mcpp issue:#713(根清单的 xlings 声明对成员不可见)、#714(非成员的 `.workspace = true` 变成空版本)、 #715(没有设备源也合成规则程序)、#716(provisioning 印记在载荷删除后仍然有效)。 -| 2.4 GalTranslPP | Sunrisepeak/GalTranslPP 提交 43ee88f,run 36272623612 全绿。首次 emit 258 s;GPPCLI 6.2 min;GPPGUI 18.2 min;pack 0.9 min;无改动的第二次构建 0.8 min,未触发安装;构建后再次 emit 62 s。所有日志中 "no Qt SDK" 为 0 条;成员记录的 SDK 事实为 `xlings: …/xim-x-qt-base/6.11.1` | +| 2.4验证工程| 验证工程 提交 43ee88f,run 36272623612 全绿。首次 emit 258 s;cli 6.2 min;gui 18.2 min;pack 0.9 min;无改动的第二次构建 0.8 min,未触发安装;构建后再次 emit 62 s。所有日志中 "no Qt SDK" 为 0 条;成员记录的 SDK 事实为 `xlings: …/xim-x-qt-base/6.11.1` | 构建后再次 emit 需要 62 s,原因有二: diff --git a/.agents/docs/2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md b/.agents/docs/2026-09-27-validation-project-upstream-vs-pr2-outlook.md similarity index 81% rename from .agents/docs/2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md rename to .agents/docs/2026-09-27-validation-project-upstream-vs-pr2-outlook.md index 2c7c830..030f871 100644 --- a/.agents/docs/2026-09-27-galtranslpp-upstream-vs-pr2-outlook.md +++ b/.agents/docs/2026-09-27-validation-project-upstream-vs-pr2-outlook.md @@ -1,31 +1,31 @@ -# GalTranslPP:PR2 与上游最新的差异,及后续构建优化的上限 +#验证工程:PR2 与上游最新的差异,及后续构建优化的上限 日期:2026-09-27。 | 对象 | 提交 | 说明 | |---|---|---| | 共同基点 | `8a2c76a` | PR2 分叉处 | -| 上游最新 | julixian/GalTranslPP `e64f93e`(2026-09-27 03:45,"update build system") | 基点之后 2 个提交,48 个文件,+1319/−730 | -| PR2 | Sunrisepeak/GalTranslPP `43ee88f`(`ci/mcpp-plugins-deps-qt`) | 基点之后 15 个提交,21 个文件,+771/−567;plugins 0.15.2,CI run 36272623612 全绿 | +| 上游最新 | 验证工程的上游 `e64f93e`(2026-09-27 03:45,"update build system") | 基点之后 2 个提交,48 个文件,+1319/−730 | +| PR2 | 验证工程 `43ee88f`(`ci/mcpp-plugins-deps-qt`) | 基点之后 15 个提交,21 个文件,+771/−567;plugins 0.15.2,CI run 36272623612 全绿 | ## 1. 结论 1. **构建架构已趋同,依赖获取策略相反。** - - 上游采纳了 PR2 的构建结构:共享构建逻辑作为模块 `gpp.build` 导入,rules-qt 的翻译流程,deps-vcpkg 与 deps-cmake 的机制,以 `${mcpp.self} stage` 动作写出 Release 布局,runtime-stage。两边都删除了旧的 5 个头文件与 `qt-root.txt`。 + - 上游采纳了 PR2 的构建结构:共享构建逻辑作为模块 `build` 导入,rules-qt 的翻译流程,deps-vcpkg 与 deps-cmake 的机制,以 `${mcpp.self} stage` 动作写出 Release 布局,runtime-stage。两边都删除了旧的 5 个头文件与 `qt-root.txt`。 - 分歧在取得方式:上游要求开发者预装 vcpkg、CMake、Qt,并在源码中填写 Qt 路径;Python 解压、`Release.py`、`windeployqt` 保留为构建后的手动步骤。PR2 由 xlings 取得全部工具,构建本身完成部署。 2. **差异规模大,PR2 不能直接变基。** 两边改动重叠 17 个文件;试合并产生 10 处冲突:4 个成员的 `mcpp.toml` 与 `build.mcpp`、`how-to-build.md`、`Release.py`。下一版应基于 `e64f93e` 重做,而不是合并 PR2。 3. **上游有三处优于 PR2,应当吸收:** - - 插件经 `reexport = true` 只在 `gpp.build` 声明一次; + - 插件经 `reexport = true` 只在 `build` 声明一次; - Windows 专属的 defines、系统库、链接选项放进 `[target.windows.*]` 或按驱动生成; - 源码中的 `#pragma comment(lib, …)` 已删除,系统库由清单声明。 4. **上游复制了插件的约 508 行代码(`deps/{tools,vcpkg,cmake}.ixx`),暴露了插件的缺口。** 起因是 `deps-vcpkg`、`deps-cmake` 这两个 feature 无条件声明 `xim:vcpkg`、`xim:cmake`,只要启用就会下载。上游为了用本机工具,只能改用内部 feature `deps`(清单注释写明"消费方不应直接使用"),再在本地复制并修改这两个模块。复制的代码依赖插件内部接口(`absolute_from_root`、`deploy_after`、`link_libraries`),插件升级时可能失效。 -5. **本次新验证的事实:GalTranslPP 今天就能把所有声明集中到一处。** 见 §3。 +5. **本次新验证的事实:验证工程今天就能把所有声明集中到一处。** 见 §3。 ## 2. 差异表 | 维度 | 上游 `e64f93e` | PR2 `43ee88f` | 更优 | |---|---|---|---| -| 插件版本 | 0.15.1;只在 `gpp.build` 声明,`reexport = true` 转交各成员(1 处) | 0.15.2;根 `[workspace.dependencies]`、`gpp.build` 各写一次(2 处);GPPCLI、GPPGUI、Updater 与 `gpp.build` 各列 features | 上游 | +| 插件版本 | 0.15.1;只在 `build` 声明,`reexport = true` 转交各成员(1 处) | 0.15.2;根 `[workspace.dependencies]`、`build` 各写一次(2 处);cli、gui、Updater 与 `build` 各列 features | 上游 | | 插件 features | `deps`(内部 feature)、`rules-qt` | `deps-vcpkg`、`deps-cmake`、`deps-archive`、`rules-qt` | PR2(公开接口) | | vcpkg | 本机安装;`gpp-build.ixx` 中的 `vcpkg_root` 或 PATH;使用复制并修改的模块 | `xim:vcpkg`,可用 `options.root` 改用本机 | 各有取向 | | CMake | 本机安装;`cmake_executable` 或 PATH | `xim:cmake` | 各有取向 | @@ -35,7 +35,7 @@ | BaseConfig、SampleProject | `Release.py` 复制 | 构建部署,并写入 Release 布局 | PR2 | | 7z.dll | 仓库内二进制 `3rdParty/7z.dll`(1.9 MB),`Release.py` 与 runtime-stage 复制 | `xim:7zip`,已从仓库删除 | PR2 | | OpenCC 数据 | 构建复制到 `Release/<成员>/BaseConfig/opencc` | 构建部署到程序旁,并写入 Release 布局 | 相同(PR2 另支持 `mcpp run`) | -| Updater 发布 | Updater 自身的构建程序写入 GPPGUI(`Updater.exe`)与 GUICORE(`Updater_new.exe`) | GPPGUI 取工具边产物 `dep_bin("gpp.updater")` 再复制 | 上游(不依赖宿主工具产物,避开 mcpp#711) | +| Updater 发布 | Updater 自身的构建程序写入 gui(`Updater.exe`)与 GUICORE(`Updater_new.exe`) | gui 取工具边产物 `dep_bin("updater")` 再复制 | 上游(不依赖宿主工具产物,避开 mcpp#711) | | Windows 专属设置 | 各成员 `[target.windows.build]`;`/DEBUG`、`/OPT` 按驱动在代码中生成;runtime-stage 在 `[target.windows.build-dependencies]` | defines 放在 `[workspace.build]`,所有目标通用;`-Wl,/DEBUG` 写在 profile;runtime-stage 无条件声明 | 上游 | | 源码 | 删除 `#pragma comment`;`Tool.cpp` 的 `#ifdef` 改为包住整个函数;Updater 改用 `QCoreApplication` 与 `import boost`;GUI 的 GIL 释放移出 try;ElaWidgetTools 子模块更新 | 未改源码 | 上游 | | vcpkg 目录 | `vcpkg-scripts/{ports,triplets}`,`install_root` 显式 | 原 `ports/`,默认 `install_root` | 上游(目录更清晰) | @@ -45,9 +45,9 @@ | 仓库内构建代码 | 1139 行(其中复制的插件代码 508 行,`Release.py` 43 行) | 741 行 | PR2 | | 首次构建所需手动操作 | 约 13 步 | 4 步 | PR2 | -## 3. 本次验证:声明可集中到 `gpp.build` +## 3. 本次验证:声明可集中到 `build` -**探针**:一个工作区,成员 `app` 以 `host-module` 依赖一个非成员的路径包 `shared`(对应 `gpp.build`)。mcpp 2026.9.26.2,plugins 0.15.2。工程位于 `scratchpad/reexport-probe`。 +**探针**:一个工作区,成员 `app` 以 `host-module` 依赖一个非成员的路径包 `shared`(对应 `build`)。mcpp 2026.9.26.2,plugins 0.15.2。工程位于 `scratchpad/reexport-probe`。 | `shared` 中的声明 | 成员构建程序观察到的结果 | |---|---| @@ -56,10 +56,10 @@ | `[xlings.workspace] "xim:qt-base" = "6.11.1"` | `xpkg_dir("xim", "qt-base")` 与 `rules::qt::root()` 均为 `…/xim-x-qt-base/6.11.1` | | 删除上一行(对照组) | 两者均为空 | -**推论**:插件版本、features、runtime-stage、`xim:qt-base` 与 `xim:7zip` 都可以只在 `gpp.build` 声明一次。 +**推论**:插件版本、features、runtime-stage、`xim:qt-base` 与 `xim:7zip` 都可以只在 `build` 声明一次。 - PR2 为 mcpp#713、#714 所做的逐成员声明因此不再需要。这两个 issue 对根清单仍然成立,但不再阻碍本项目。 -- 附带发现:`gpp.build` 声明了 Qt 之后,插件合成的规则程序会走"有 SDK"的分支。在 Linux 上它会输出 libc++ 提示;在 Windows 上预计不输出。0.15.2 的静默条件只覆盖"无 SDK",应推广为"无 build.mcpp 且无设备源时,不论有无 SDK 都直接返回"(见 §4 的 P2)。 +- 附带发现:`build` 声明了 Qt 之后,插件合成的规则程序会走"有 SDK"的分支。在 Linux 上它会输出 libc++ 提示;在 Windows 上预计不输出。0.15.2 的静默条件只覆盖"无 SDK",应推广为"无 build.mcpp 且无设备源时,不论有无 SDK 都直接返回"(见 §4 的 P2)。 ## 4. 后续优化能做到什么地步 @@ -69,7 +69,7 @@ |---|---|---|---| | S0 | 上游 `e64f93e` 现状 | — | — | | S1 | PR2(参照) | mcpp 2026.9.26.2、plugins 0.15.2 | 已完成 | -| S2 | 基于 `e64f93e` 重做,保留其结构与本机工具模式;吸收 PR2 的自动部署(Python、BaseConfig、SampleProject、Qt 插件与翻译、7z);所有声明集中到 `gpp.build`;Qt 按 `qt_root` → `QT_ROOT_DIR` → `xim:qt-base` 的顺序选取 | 现有发布版即可 | 我方(GalTranslPP 新 PR) | +| S2 | 基于 `e64f93e` 重做,保留其结构与本机工具模式;吸收 PR2 的自动部署(Python、BaseConfig、SampleProject、Qt 插件与翻译、7z);所有声明集中到 `build`;Qt 按 `qt_root` → `QT_ROOT_DIR` → `xim:qt-base` 的顺序选取 | 现有发布版即可 | 我方(验证工程新 PR) | | S3 | S2 + plugins 0.16:P1、P2 | 插件发布 | 我方(mcpp-plugins) | | S4 | S3 + 下一版 mcpp:#705、#707、#710、#711、#716、#712 | mcpp 发布;mcpp main 在 2026.9.26.2 之后只有一个文档提交,尚无相关修复 | mcpp 上游 | | S5 | 长期:不再需要 VS Build Tools(让 vcpkg 通过 chainload 使用 mcpp 的 clang);action 可设环境与工作目录(#708);feature 可声明自身所需的工具(#709) | vcpkg triplet 工作、mcpp 能力 | 生态 | @@ -96,7 +96,7 @@ | 自动模式 ⇄ 本机模式 | 仅本机 | 仅自动(vcpkg/Qt 可指定路径,但仍会下载) | Qt 可切换;vcpkg/CMake 仅本机 | 全部可切换,注释掉一行即可 | 同 S3 | 同 S3 | | `mcpp run` / `mcpp pack` | 否 | 是 | 是 | 是 | 是 | 是 | | 首次 emit(包缓存为空) | 未测 | 258 s(实测) | 约同 S1 | 约同 S1 | 下降(预计):不再安装第二套 LLVM(#710),emit 不再完整构建宿主工具(#707);幅度待实测 | 同 S4 | -| GPPCLI / GPPGUI 编译 | 未测 | 6.2 / 18.2 min(实测) | 约同 S1 | 约同 S1 | 约同 S1 | 约同 S1 | +| cli / gui 编译 | 未测 | 6.2 / 18.2 min(实测) | 约同 S1 | 约同 S1 | 约同 S1 | 约同 S1 | | 无改动的第二次构建 | 未测 | 0.8 min,无安装(实测) | 约同 S1 | 约同 S1 | 约同 S1 | 约同 S1 | | 构建后再次 emit | 约同 S1(同样存在 Updater 工具边) | 62 s(实测) | 约同 S1 | 约同 S1 | 秒级(预计,同 profile 时):#705 修复后,Updater 不再被重建 | 同 S4 | | 删除载荷后的恢复 | — | 需手动删除 provisioning 印记 | 同 S1 | 同 S1 | 自动重新安装(#716) | 同 S4 | @@ -105,15 +105,15 @@ ### 4.3 上限的解读 - **S2 是现有发布版下的上限。** 首次构建只需 4 步(安装 xlings、安装 mcpp、克隆、`mcpp build`),所有版本只写在一处,同时保留上游偏好的本机工具模式。它唯一的缺口是 vcpkg 与 CMake 无法按需改由 xim 取得。 -- **S3 使两种模式对称。** 同一份仓库既能零配置构建,也能使用本机工具,切换方式是注释掉 `gpp.build` 中的一行;上游复制的插件代码可以全部删除。 +- **S3 使两种模式对称。** 同一份仓库既能零配置构建,也能使用本机工具,切换方式是注释掉 `build` 中的一行;上游复制的插件代码可以全部删除。 - **S4 只改变耗时与健壮性,不改变操作步骤。** 收益集中在 emit:构建后的 emit 从 62 s 降到秒级,首次 emit 省去第二套 LLVM。编译时间由源码规模决定,各层不变;要继续缩短,只能靠二进制缓存(vcpkg 已缓存)或减少 LTO/opt,与构建系统无关。 - **S5 才能去掉 VS Build Tools。** 这取决于 vcpkg 的 triplet 能否通过 chainload 使用 mcpp 的 clang,以及 ElaWidgetTools 的 CMake 构建能否同样改用 clang,不在短期范围内。 ## 5. 建议的下一步 -1. **GalTranslPP 新 PR(S2)**: +1. **验证工程新 PR(S2)**: - 从 `e64f93e` 开分支,保留上游结构、源码修正与 `reexport`; - - 把插件、runtime-stage、`xim:qt-base`、`xim:7zip` 集中到 `gpp.build`; + - 把插件、runtime-stage、`xim:qt-base`、`xim:7zip` 集中到 `build`; - Qt 选取顺序:`qt_root` 为空时交给 rules-qt 的默认顺序; - 迁入 PR2 的自动部署,删除 `Release.py`; - `how-to-build.md` 以 `> 注:` 并列两种模式; diff --git a/.agents/docs/2026-09-27-versioning-assets-locale-and-galtranslpp-review.md b/.agents/docs/2026-09-27-versioning-assets-locale-and-validation-project-review.md similarity index 92% rename from .agents/docs/2026-09-27-versioning-assets-locale-and-galtranslpp-review.md rename to .agents/docs/2026-09-27-versioning-assets-locale-and-validation-project-review.md index 89b5212..986f448 100644 --- a/.agents/docs/2026-09-27-versioning-assets-locale-and-galtranslpp-review.md +++ b/.agents/docs/2026-09-27-versioning-assets-locale-and-validation-project-review.md @@ -1,6 +1,6 @@ -# 版本号、资源命名、locale 与 GalTranslPP 构建体验评估 +# 版本号、资源命名、locale 与验证工程构建体验评估 -日期:2026-09-27。范围:0.15.x 交付遗留的三个问题的最佳形态,以及 GalTranslPP PR2 相对 VS 与早期 mcpp 构建的对比。 +日期:2026-09-27。范围:0.15.x 交付遗留的三个问题的最佳形态,以及验证工程PR2 相对 VS 与早期 mcpp 构建的对比。 所有结论均经本地实验或源码核对;证据列于各节末尾。 ## 1. Qt 版本号 6.11.1.1 @@ -18,7 +18,7 @@ ### 1.2 结论(2026-09-27 修订) 6.11.1 与 6.11.1.1 均于 2026-09-26 发布,项目 CI 之外没有消费者。持有修正前 6.11.1 载荷的只有三处: -plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订号的价值在于保护已有用户,此处不存在该前提。 +plugins CI 缓存、验证工程CI 缓存与本机沙箱,均可清除。修订号的价值在于保护已有用户,此处不存在该前提。 - **最佳形态:只保留一个键 `6.11.1`,内容为修正后的配方;删除 `6.11.1.1`。** 消费方写上游版本 `6.11.1`,xlings 与 mcpp 都按字面解析,两侧一致。 - 清理:两处 CI 的缓存 key 升级,本机沙箱删除旧载荷。 @@ -59,7 +59,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 ### 3.2 影响 -- GalTranslPP:无影响(仅 Windows)。 +-验证工程:无影响(仅 Windows)。 - Linux 上的 Qt 程序:Qt 6 在 Unix 上的 local8Bit 固定为 UTF-8,Qt 自身文本处理不受影响;受影响的是程序直接调用的 C 库多字节与宽字符函数(`mbstowcs`、`std::locale("")` 等)。 ### 3.3 最佳形态 @@ -71,7 +71,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 两项完成后无需 `LOCPATH`,`mcpp run` 与 `mcpp pack` 产物行为一致。不采用 `LOCPATH` 注入方案:它对打包后由用户直接启动的程序无效。 -## 4. GalTranslPP:VS → mcpp → 优化后的 mcpp +## 4.验证工程:VS → mcpp → 优化后的 mcpp ### 4.1 首次构建所需操作(Windows) @@ -80,7 +80,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 | 手动安装 | VS 2026 IDE(指定 MSVC v14.50)、CMake、git、vcpkg(克隆、bootstrap、integrate)、Qt 在线安装器(需账户)、Qt VS Tools 插件 | xlings、mcpp、VS Build Tools、CMake、Python、git、vcpkg(克隆、bootstrap、PATH)、Qt 在线安装器 | git、xlings、VS Build Tools | | Qt 路径配置 | 3 处:Qt VS Tools 导入、ElaWidgetTools `CMakeLists.txt`、vcxproj `QtInstall` | 2 处:`build.py`、`qt-root.txt` | 0 处 | | 依赖构建 | 手动运行 `build.bat` 编译 ElaWidgetTools,并检查 3 个产物 | 手动运行 `build.py` 与 `vcpkg install --triplet …` | 由构建完成(`deps-cmake`、`deps-vcpkg`) | -| 构建命令 | IDE 中切换 Release 并逐项目生成 | `mcpp build -p GPPCLI/GPPGUI` | 同左 | +| 构建命令 | IDE 中切换 Release 并逐项目生成 | `mcpp build -p cli/gui` | 同左 | | 运行时部署 | `Release.bat` | 解压 Python、`Release.py`、`windeployqt` ×2 | 由构建完成:Qt 库、插件与翻译、Ela、vcpkg DLL、7z.dll、Python、OpenCC、BaseConfig、VC++ 运行时 | | 分发 | 手动整理 | 手动整理 | `mcpp pack --format dir` | | 手动步骤合计 | 约 12 | 约 13 | 4(安装 xlings、安装 mcpp、克隆、`mcpp build`) | @@ -91,18 +91,18 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 |---|---|---|---| | vcpkg 二进制缓存 | 未命中,独立安装 51 分钟 | 命中 678 MB,缺失部分在 emit 中源码编译(保存后增至 999 MB) | 命中 999 MB,完整 | | `mcpp emit`(全新 checkout) | 0.4 分钟(依赖已预装) | 53 分钟 | 4.8 分钟 | -| GPPCLI(fast-release) | — | 6.5 分钟 | 6.3 分钟 | -| GPPGUI(fast-release) | — | 18.0 分钟 | 19.7 分钟 | +| cli(fast-release) | — | 6.5 分钟 | 6.3 分钟 | +| gui(fast-release) | — | 18.0 分钟 | 19.7 分钟 | | 无改动的第二次构建 | — | 0.8 分钟,无安装 | 0.8 分钟,无安装 | 更正:53 → 4.8 分钟的主要原因是 vcpkg 二进制缓存从部分命中变为完整命中,而非移除 mcpp-deps。 全新 checkout 上 emit 的 4.8 分钟包含: -1. **宿主工具构建**:GPPGUI 以 `tools = ["Updater"]` 取得 Updater.exe,emit 为此以宿主身份完整构建 Updater(Qt 与 vcpkg 程序),runtime_stage 同理。 +1. **宿主工具构建**:gui 以 `tools = ["Updater"]` 取得 Updater.exe,emit 为此以宿主身份完整构建 Updater(Qt 与 vcpkg 程序),runtime_stage 同理。 2. **第二套 LLVM**:宿主工具构建不继承工作区的 `[toolchain] windows = "llvm@22.1.8"`,日志显示 "no toolchain configured — installing llvm@20.1.7",因此额外下载一套 LLVM 并编译其 std 模块。 3. **vcpkg 安装**:Updater 的构建会执行 vcpkg install,此时从二进制缓存恢复约 22 个库。 -4. **构建程序**:5 个成员的 build.mcpp 及其 host 模块(mcpp.plugins、gpp.build)各编译一次。 +4. **构建程序**:5 个成员的 build.mcpp 及其 host 模块(mcpp.plugins、build)各编译一次。 已构建过一次后,宿主工具按"包源码 + 宿主工具链"缓存,vcpkg 前缀已存在,构建程序显示 "up to date (cached)",因此 emit 预计为秒级。 同条件下的"第二次构建"为 0.8 分钟,可作旁证;emit 本身未单独测量。 @@ -130,7 +130,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 | G4 | 仍需手动安装 VS Build Tools(vcpkg 的 MSVC triplet 与 ElaWidgetTools) | 长期项:vcpkg chainload mcpp 的 clang;现阶段保留为文档前提 | | G5 | 成员清单中 7z 的注释位于 Qt 条目之上 | 随 G1 修正 | | G6 | PR2 为验证分支(`ci(temporary)` 提交) | 向上游提交时压缩为一次迁移提交,CI 工作流按上游需要保留或删除 | -| G7 | emit 的首次开销来自宿主工具与第二套 LLVM(§4.2) | mcpp:宿主工具继承工作区工具链;提供目标产物依赖。在此之前,GalTranslPP 可先在 CI 中固定默认工具链 | +| G7 | emit 的首次开销来自宿主工具与第二套 LLVM(§4.2) | mcpp:宿主工具继承工作区工具链;提供目标产物依赖。在此之前,验证工程可先在 CI 中固定默认工具链 | ### 4.5 结论 @@ -156,7 +156,7 @@ plugins CI 缓存、GalTranslPP CI 缓存与本机沙箱,均可清除。修订 | §2 brotli | 两端均为规范名,内容一致;GitHub 的 `-r1` 已删除 | | §3 locale | 已提交 openxlings/xlings#621 | | G1、G5 | 已完成:`how-to-build.md` 用 `> 注:` 列出 Qt、vcpkg、vcpkg 缓存、工具链与插件版本的配置方式 | -| G2 | 部分成立:成员以 `.workspace = true` 继承插件版本;`gpp.build` 不是成员,无法继承(mcpp#714);`xim:qt-base` 仍逐成员声明(mcpp#713) | +| G2 | 部分成立:成员以 `.workspace = true` 继承插件版本;`build` 不是成员,无法继承(mcpp#714);`xim:qt-base` 仍逐成员声明(mcpp#713) | | G3 | 属预期行为:不使用 xim 提供的 Qt 时,注释掉声明,再设置 `QT_ROOT_DIR` 或 `gpp::qt_root` | | G4 | 不变:仍需 VS Build Tools | | G7 | 已提交 mcpp#710(宿主工具不使用工作区工具链)、#711(缺少目标产物依赖) | diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index bbcb5fe..a046e16 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -171,7 +171,7 @@ vcpkg_workspace() { echo "ok: two members that share no dependency both installed and linked one prefix" # The two installations may run at once; vcpkg's own lock on the root makes # the second wait only when asked to, and fails it otherwise ("failed to - # take lock", measured on GalTranslPP under 0.15.0). + # take lock", measured on the validation project under 0.15.0). [ "$(grep -rl -- '--x-wait-for-lock' --include=build.ninja . | wc -l)" -ge 2 ] || fail "a member's installation does not wait for the root's lock" echo "ok: each member's installation waits for another one of the same root" diff --git a/deps/vcpkg.cppm b/deps/vcpkg.cppm index 910c378..e5ac791 100644 --- a/deps/vcpkg.cppm +++ b/deps/vcpkg.cppm @@ -506,7 +506,7 @@ inline prefix use(const options& opt = {}) { // installation root itself (`/vcpkg/vcpkg-running.lock`), and // `--x-wait-for-lock` makes a second installation of the same root -- // two workspace members, run concurrently -- wait for the first - // instead of failing (measured on GalTranslPP under 0.15.0); its build + // instead of failing (measured on the validation project under 0.15.0); its build // and package trees go to a short directory under vcpkg's per-user // directory, because a port's build nests deep and Windows tools still // enforce MAX_PATH. diff --git a/tests/qt-consumer/mcpp.toml b/tests/qt-consumer/mcpp.toml index 55a805a..cbd0418 100644 --- a/tests/qt-consumer/mcpp.toml +++ b/tests/qt-consumer/mcpp.toml @@ -13,7 +13,7 @@ # qt_consumer_de.ts updated from src/ by lupdate -> lrelease, deployed # under translations/ # -# The `.ts` sits in the package root, as it does in GalTranslPP, so lupdate +# The `.ts` sits in the package root, as it does in the validation project, so lupdate # writes one file there and claims no directory. # # and the program prints what each produced: a signal delivered to a slot, a diff --git a/tests/qt-import-only/mcpp.toml b/tests/qt-import-only/mcpp.toml index 4c741a2..38a9bab 100644 --- a/tests/qt-import-only/mcpp.toml +++ b/tests/qt-import-only/mcpp.toml @@ -1,6 +1,6 @@ # Fixture: a package that enables `rules-qt` only to import `mcpp.rules.qt`, # the shape of a library of build logic that other packages' build programs -# import (GalTranslPP's `gpp.build`). It writes no `build.mcpp`, has no `.ui`, +# import (a project's own build module). It writes no `build.mcpp`, has no `.ui`, # `.qrc` or `.ts`, and declares no Qt, so mcpp synthesises no build program # for it and nothing reports a missing SDK (mcpp 2026.9.27.1; mcpp#715). [package]