Skip to content

2026.9.28.2: one resolver for the runtime beside a Windows program, header tracking on every host, one statement per fact, conditional tables by specificity #1

2026.9.28.2: one resolver for the runtime beside a Windows program, header tracking on every host, one statement per fact, conditional tables by specificity

2026.9.28.2: one resolver for the runtime beside a Windows program, header tracking on every host, one statement per fact, conditional tables by specificity #1

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).
( cd "$QT/bin" && ls vcruntime140*.dll msvcp140*.dll concrt140.dll vccorlib140.dll 2>/dev/null; \
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")
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="$MCPP"
( cd "$d" && "$m" build ) || { echo "::error::the $who build of the probe failed with the system's runtime present"; exit 1; }
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
- 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")
find "$bd" \( -name moc-probe.txt -o -name lrelease-probe.stamp \) -delete
"$NINJA" -C "$bd" -t targets all | grep -E 'moc-probe.txt|lrelease-probe.stamp' | cut -d: -f1 > "$RUNNER_TEMP/edges"
test -s "$RUNNER_TEMP/edges" || { echo "::error::no tool edge in the graph"; exit 1; }
PATH="/usr/bin:/c/Windows/System32:/c/Windows" "$NINJA" -C "$bd" $(cat "$RUNNER_TEMP/edges")
grep -q "Probe" "$(find "$bd" -name moc-probe.txt | head -1)" \
|| { echo "::error::moc.exe ran but wrote no code for Probe"; 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")
bd=$(dirname "$nf")
find "$bd" \( -name moc-probe.txt -o -name lrelease-probe.stamp \) -delete
edges=$("$NINJA" -C "$bd" -t targets all | grep -E 'moc-probe.txt|lrelease-probe.stamp' | cut -d: -f1)
if PATH="/usr/bin:/c/Windows/System32:/c/Windows" "$NINJA" -C "$bd" $edges > "$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