Skip to content

2026.9.29.1: a workspace is one graph per configuration; the planning regression of 2026.9.28.3 fixed at its cause #17

2026.9.29.1: a workspace is one graph per configuration; the planning regression of 2026.9.28.3 fixed at its cause

2026.9.29.1: a workspace is one graph per configuration; the planning regression of 2026.9.28.3 fixed at its cause #17

name: measure windows tool crt
# The measurement of the 2026-09-28 ecosystem design, §2.9 (task M2), which
# gates the removal of the MSVC C++ runtime from xim:qt-base (task I2).
#
# Qt's host tools (moc.exe; lrelease.exe, which loads Qt6Core.dll) are built
# with MSVC and need its C++ runtime to start. Once the payload carries no copy,
# the only copy they can reach is the one the engine puts first on the PATH of
# every action of a build for a Windows target: the toolset's. The system
# directory precedes PATH in the loader's search order, and every GitHub
# Windows image has the VC++ redistributable there, so the system's copy is
# hidden while the legs run; without that the first leg could not fail.
#
# Two rows. On `visual-studio` the toolset's runtime is the Visual Studio
# redistributable. On `bare` Visual Studio is masked and the project names the
# managed toolset `msvc@14.44.35207`, whose payload carries its own.
#
# M-a each tool, started from the CRT-free payload with no runtime
# reachable, fails to start: the measurement can fail.
# M-b each tool, started by the candidate's recorded action edge, starts.
# M-c (a reading) the same edge recorded by the released mcpp; expected to
# fail, as the state before the change.
#
# The legs run the recorded edge with ninja directly, after the build that
# recorded it, so that nothing else (the xlings the engine resolves through,
# for instance) has to start while the system's runtime is hidden.
on:
pull_request:
branches: [ main ]
paths:
- '.github/workflows/measure-windows-tool-crt.yml'
- 'src/build/ninja_backend.cppm'
- 'src/build/runtime_placement.cppm'
- 'src/cli.cppm'
workflow_dispatch:
concurrency:
group: measure-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
candidate:
name: candidate mcpp (windows x64)
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- name: Build the candidate from this commit
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build
exe=$(find target -name mcpp.exe -path '*/bin/*' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2-)
test -n "$exe" || { echo "::error::no mcpp.exe under target/"; exit 1; }
mkdir -p candidate
# The program and what the build placed beside it (its own C++
# runtime among them), so it starts while the system's copy is hidden.
cp "$(dirname "$exe")"/*.exe "$(dirname "$exe")"/*.dll candidate/ 2>/dev/null || cp "$exe" candidate/
ls candidate
candidate/mcpp.exe --version
- uses: actions/upload-artifact@v4
with:
name: measure-candidate
path: candidate
measure:
name: measure (${{ matrix.row }})
needs: candidate
runs-on: windows-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
row: [visual-studio, bare]
env:
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: measure-candidate
path: candidate
# The same mask as ci-windows.yml's bare-Windows row, applied before
# anything touches Visual Studio.
- name: Mask Visual Studio
if: matrix.row == 'bare'
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'
$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 " rename failed: $($_.Exception.Message)" }
}
foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir','VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
}
$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 }
- uses: ./.github/actions/bootstrap-mcpp
with:
cache-target: 'false'
- name: Install xim:qt-base 6.11.1 and remove its copy of the runtime
shell: bash
run: |
"$XLINGS_BIN" install qt-base@6.11.1 -y
QT="$(cygpath -u "$USERPROFILE")/.xlings/data/xpkgs/xim-x-qt-base/6.11.1"
test -x "$QT/bin/moc.exe" || { echo "::error::no moc.exe in $QT/bin"; exit 1; }
# What revision 1 of the recipe removes (task I2). The published
# revision 1 no longer carries them, and `ls` of absent files exits 2,
# which `bash -e` would read as a failed install.
( cd "$QT/bin" && { ls vcruntime140*.dll msvcp140*.dll concrt140.dll vccorlib140.dll 2>/dev/null || true; }; \
rm -f vcruntime140*.dll msvcp140*.dll concrt140.dll vccorlib140.dll )
echo "QT=$QT" >> "$GITHUB_ENV"
- name: Record the tool edges with the candidate and with the released mcpp
shell: bash
run: |
CAND="$PWD/candidate/mcpp.exe"
QTW=$(cygpath -m "$QT")
# The state before this change is the last release. `$MCPP` is the
# bootstrap's shim for this repository's own pin, which refuses to
# run outside the repository, so the release is installed and called
# by its store path. From a neutral directory, so the repository's
# .xlings.json does not select a project scope.
( cd "$RUNNER_TEMP" && "$XLINGS_BIN" install mcpp@2026.9.28.1 -y ) > "$RUNNER_TEMP/released-install.log" 2>&1 || true
REL="$(cygpath -u "$USERPROFILE")/.xlings/data/xpkgs/xim-x-mcpp/2026.9.28.1/bin/mcpp.exe"
toolchain=""
[ "${{ matrix.row }}" = bare ] && toolchain='[toolchain]
windows = "msvc@14.44.35207"'
for who in candidate released; do
d="$RUNNER_TEMP/probe-$who"
mkdir -p "$d/src"
printf 'int main() { return 0; }\n' > "$d/src/main.cpp"
printf 'class Probe : public QObject {\n Q_OBJECT\n};\n' > "$d/probe.h"
printf '[package]\nname = "probe"\nversion = "0.1.0"\n\n%s\n' "$toolchain" > "$d/mcpp.toml"
cat > "$d/build.mcpp" <<EOF
import std;
import mcpp;
int main() {
const std::string root = mcpp::manifest_dir();
const std::string out = mcpp::out_dir();
mcpp::action moc;
moc.id = "tool-moc";
moc.role = mcpp::roles::check;
moc.arg("$QTW/bin/moc.exe").arg((root + "/probe.h").c_str())
.arg("-o").arg((out + "/moc-probe.txt").c_str())
.input((root + "/probe.h").c_str())
.output((out + "/moc-probe.txt").c_str())
.submit();
mcpp::action lr;
lr.id = "tool-lrelease";
lr.role = mcpp::roles::check;
lr.arg("$QTW/bin/lrelease.exe").arg("-version")
.output((out + "/lrelease-probe.stamp").c_str())
.submit();
return 0;
}
EOF
m="$CAND"; [ "$who" = released ] && m="$REL"
if ! ( cd "$d" && "$m" build ); then
if [ "$who" = candidate ]; then
echo "::error::the candidate's build of the probe failed with the system's runtime present"; exit 1
fi
echo "READING M-c: the released mcpp could not record the edge ($(tail -1 "$RUNNER_TEMP/released-install.log"))"
: > "$d/ninja-file"
continue
fi
grep -rl "moc.exe" "$d/target" --include=build.ninja | head -1 > "$d/ninja-file"
test -s "$d/ninja-file" || { echo "::error::no build.ninja records the moc edge ($who)"; exit 1; }
echo "--- $who: the moc edge"
grep -m1 "moc.exe" "$(cat "$d/ninja-file")"
done
NINJA=$(ls "$(cygpath -u "$USERPROFILE")"/.mcpp/registry/data/xpkgs/xim-x-ninja/*/ninja.exe 2>/dev/null | head -1)
test -x "$NINJA" || NINJA=$(command -v ninja || true)
test -x "$NINJA" || { echo "::error::no ninja.exe"; exit 1; }
echo "NINJA=$NINJA" >> "$GITHUB_ENV"
- name: Hide the system's C++ runtime
shell: pwsh
run: |
$names = 'vcruntime140.dll','vcruntime140_1.dll','vcruntime140_threads.dll',
'msvcp140.dll','msvcp140_1.dll','msvcp140_2.dll','msvcp140_atomic_wait.dll',
'msvcp140_codecvt_ids.dll','concrt140.dll','vccorlib140.dll'
foreach ($n in $names) {
$p = Join-Path $env:SystemRoot "System32\$n"
if (Test-Path $p) {
takeown /f $p | Out-Null
icacls $p /grant "Administrators:F" | Out-Null
Rename-Item -LiteralPath $p -NewName "$n.measure-hidden"
if (Test-Path $p) { Write-Host "FAIL: $p is still present"; exit 1 }
Write-Host "hidden: $p"
}
}
- name: "M-a: a tool with no runtime reachable does not start"
shell: bash
run: |
for tool in moc lrelease; do
if out=$(cd /c && PATH="/c/Windows/System32:/c/Windows" "$QT/bin/$tool.exe" -v 2>&1); then
echo "::error::$tool.exe started with no C++ runtime reachable; this measurement cannot fail: $out"
exit 1
fi
echo "ok: M-a $tool.exe does not start without a runtime"
done
# The recorded edges are run again with ninja directly: their outputs
# (the edge targets, absolute paths under target/.build-mcpp/out) are
# removed, so ninja runs exactly those two edges and nothing else.
- name: "M-b: the candidate's action edge starts each tool"
shell: bash
run: |
nf=$(cat "$RUNNER_TEMP/probe-candidate/ninja-file")
bd=$(dirname "$nf")
abs() { case "$1" in [A-Za-z]:*|/*) cygpath -u "$1" ;; *) echo "$bd/$1" ;; esac; }
# `-t targets all` prints `<target>: <rule>`, and a Windows target
# begins with a drive letter and its colon: the rule is cut at the
# LAST `: `, never at the first colon.
"$NINJA" -C "$bd" -t targets all | grep -E 'moc-probe.txt|lrelease-probe.stamp' | sed 's/: [^:]*$//' > "$RUNNER_TEMP/edges"
[ "$(wc -l < "$RUNNER_TEMP/edges")" -eq 2 ] || { cat "$RUNNER_TEMP/edges"; echo "::error::the graph does not hold the two tool edges"; exit 1; }
while IFS= read -r t; do rm -f "$(abs "$t")"; done < "$RUNNER_TEMP/edges"
echo "edges: $(tr '\n' ' ' < "$RUNNER_TEMP/edges")"
PATH="/usr/bin:/c/Windows/System32:/c/Windows" "$NINJA" -C "$bd" $(cat "$RUNNER_TEMP/edges")
moc_out=$(abs "$(grep 'moc-probe.txt' "$RUNNER_TEMP/edges")")
grep -q "Probe" "$moc_out" || { echo "::error::moc.exe ran but wrote no code for Probe ($moc_out)"; exit 1; }
lr_out=$(abs "$(grep 'lrelease-probe.stamp' "$RUNNER_TEMP/edges")")
test -f "$lr_out" || { echo "::error::the lrelease edge left no stamp ($lr_out)"; exit 1; }
echo "ok: M-b moc.exe and lrelease.exe start from the action's PATH (${{ matrix.row }})"
- name: "M-c (reading): the released mcpp's action edge"
shell: bash
run: |
nf=$(cat "$RUNNER_TEMP/probe-released/ninja-file")
[ -n "$nf" ] || { echo "READING M-c: not recorded (see the step above)"; exit 0; }
bd=$(dirname "$nf")
abs() { case "$1" in [A-Za-z]:*|/*) cygpath -u "$1" ;; *) echo "$bd/$1" ;; esac; }
"$NINJA" -C "$bd" -t targets all | grep -E 'moc-probe.txt|lrelease-probe.stamp' | sed 's/: [^:]*$//' > "$RUNNER_TEMP/edges-released"
while IFS= read -r t; do rm -f "$(abs "$t")"; done < "$RUNNER_TEMP/edges-released"
if PATH="/usr/bin:/c/Windows/System32:/c/Windows" "$NINJA" -C "$bd" $(cat "$RUNNER_TEMP/edges-released") > "$RUNNER_TEMP/released.log" 2>&1; then
echo "READING M-c: the released mcpp's edge started the tools (${{ matrix.row }})"
else
echo "READING M-c: the released mcpp's edge did not start the tools (${{ matrix.row }}):"
tail -5 "$RUNNER_TEMP/released.log"
fi
# In bash: Git Bash does not use the MSVC runtime, and pwsh may.
- name: Restore the system's C++ runtime
if: always()
shell: bash
run: |
for f in /c/Windows/System32/*.measure-hidden; do
[ -e "$f" ] && mv "$f" "${f%.measure-hidden}" && echo "restored: ${f%.measure-hidden}"
done
true