Skip to content

ci: run fresh-install workflows only in the upstream repo #960

ci: run fresh-install workflows only in the upstream repo

ci: run fresh-install workflows only in the upstream repo #960

Workflow file for this run

name: ci-windows
# Windows CI for mcpp — same flow as Linux (ci-linux.yml) and macOS (ci-macos.yml):
# xlings install mcpp → self-host build → smoke → package
#
# SHAPE: three INDEPENDENT jobs, no `needs:` between them; each restores the
# shared cache lineage (.github/actions/bootstrap-mcpp) and pays one warm
# `mcpp build` to get the PR's own binary. The e2e suite moved to
# ci-windows-e2e.yml (sharded ×2) — it was 9.7 of this job's 20.4 min.
#
# before: build → unit → xlings → stdin → e2e → toolchains → package ≈ 20 min
# after: max(build+unit+package, toolchains+integration) ≈ 8 min
# in parallel with ci-windows-e2e (≈ 8 min)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
concurrency:
group: ci-windows-${{ github.ref }}
cancel-in-progress: true
env:
MCPP_HOME: C:\Users\runneradmin\.mcpp
jobs:
build-test:
name: build + test + package (windows x64, self-host)
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- name: Build mcpp from source (self-host)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build
# Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is
# restored from cache and keeps a directory per build fingerprint,
# so after a version bump the freshly built binary sits alongside
# the previous release's. `find | head -1` returned whichever the
# directory walk hit first — which is how a 0.0.106 build ran the
# 0.0.105 binary and failed 01_help_and_version.
MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
"$MCPP_SELF" --version
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
- name: Unit + integration tests via mcpp test
shell: bash
run: |
export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe")
"$MCPP_SELF" test
- name: Package Windows release zip
id: package
shell: bash
run: |
VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml)
WRAPPER="mcpp-${VERSION}-windows-x86_64"
ZIPNAME="${WRAPPER}.zip"
# Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is
# restored from cache and keeps a directory per build fingerprint,
# so after a version bump the freshly built binary sits alongside
# the previous release's. `find | head -1` returned whichever the
# directory walk hit first — which is how a 0.0.106 build ran the
# 0.0.105 binary and failed 01_help_and_version.
MCPP_BIN=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
test -n "$MCPP_BIN" || { echo "FAIL: no mcpp.exe in target/"; exit 1; }
STAGING=$(mktemp -d)
mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin"
cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp.exe"
printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat"
cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true
cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true
XLINGS_EXE="$USERPROFILE/.xlings/subos/default/bin/xlings.exe"
[ -f "$XLINGS_EXE" ] && cp "$XLINGS_EXE" "$STAGING/$WRAPPER/registry/bin/xlings.exe"
mkdir -p dist
(cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER")
cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME"
(cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256")
echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT"
ls -la dist/
- name: Smoke-test the packaged zip
shell: bash
run: |
ZIPNAME="${{ steps.package.outputs.zipname }}"
WRAPPER="${ZIPNAME%.zip}"
SMOKE=$(mktemp -d)
(cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME")
"$SMOKE/$WRAPPER/bin/mcpp.exe" --version
test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe"
test -f "$SMOKE/$WRAPPER/mcpp.bat"
echo "Smoke-test passed"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: mcpp-windows-x86_64
path: |
dist/*.zip
dist/*.sha256
# Everything that needs a toolchain other than the default, plus the two
# Windows-specific behavioural regressions. Kept on one runner: each leg is
# seconds-to-2-minutes, so per-leg runners would cost more setup than they
# save.
# A Windows machine WITHOUT Visual Studio — the shape of an ordinary user's
# box, and the one shape no GitHub image provides. Every runner ships VS, so
# a bare-Windows regression was structurally invisible here; the fresh-install
# workflow now covers it too, but that one only runs post-release, which is
# far too late to learn that `mcpp new && mcpp build` no longer works on a
# stock machine.
#
# Order matters: mcpp is built while Visual Studio is still present (the
# self-host build uses llvm, which targets the MSVC ABI and needs it), and
# only then is VS masked. e2e 182 opens by asserting that MSVC detection
# FAILS, so an incomplete mask fails the job instead of quietly testing the
# ordinary path.
no-msvc-fallback:
name: "bare Windows: no Visual Studio (windows x64)"
# Takes the binary build-test already produced instead of building here.
# Building in this job means compiling with clang, which reads the MSVC
# STL — the open handles that leaves make the VS directories unrenamable,
# so the masking below silently did nothing.
needs: build-test
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Fetch the PR's mcpp.exe
uses: actions/download-artifact@v4
with:
name: mcpp-windows-x86_64
path: dist
- name: Unpack it
shell: bash
run: |
ZIP=$(ls dist/*.zip | head -1)
test -n "$ZIP" || { echo "FAIL: no zip artifact"; exit 1; }
unzip -q "$ZIP" -d unpacked
MCPP_SELF=$(find unpacked -name "mcpp.exe" | head -1)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe in $ZIP"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
"$MCPP_SELF" --version
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
# Masked before anything else touches Visual Studio, so no process of
# ours is holding a handle into it.
- name: Mask Visual Studio
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'
# All three of msvc.cppm's discovery strategies converge on
# <vsRoot>\VC\Tools\MSVC — vswhere returns an installationPath that
# find_latest_msvc_tools then resolves through it, the env strategy
# checks it explicitly, and the well-known-path scan tests for it.
# So mask the VC directory rather than the Visual Studio root: the
# root is held open on the runner and renaming it is denied, while
# VC one level down renames fine. The runner is disposable, so this
# is both safe and closer to "absent" than any env-only trick.
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }
# -Path with a trailing wildcard segment lists the CONTENTS of the
# matches, not the matches themselves, so `…\*\*\VC` would hand back
# VC's children. Resolve-Path returns the directories themselves.
# Errors are reported, not swallowed: a silent failure here is how
# the first attempt "masked" nothing and still looked fine.
Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
-ErrorAction SilentlyContinue | ForEach-Object {
$p = $_.Path
Write-Host "masking $p"
try { Rename-Item -LiteralPath $p -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
}
# Check the mask's own postcondition here, where the cause is
# obvious, instead of letting it surface three steps later as a
# confusing pass.
$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:"
$left | ForEach-Object { Write-Host " $($_.FullName)" }
exit 1
}
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."
# After masking, so nothing this action does can be holding Visual
# Studio open. It installs xlings (which mcpp resolves the winlibs
# toolchain through) and a released mcpp; neither needs a C++ compiler,
# so a masked VS is irrelevant to it.
- uses: ./.github/actions/bootstrap-mcpp
- name: "No Visual Studio: fallback to winlibs GCC (e2e 182)"
shell: bash
env:
MCPP_VENDORED_XLINGS: ${{ env.XLINGS_BIN }}
run: |
MCPP="$MCPP_SELF" bash tests/e2e/182_windows_no_msvc_fallback.sh
toolchains:
name: "toolchains + regressions (windows x64)"
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- name: Build mcpp from source (self-host)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build
# Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is
# restored from cache and keeps a directory per build fingerprint,
# so after a version bump the freshly built binary sits alongside
# the previous release's. `find | head -1` returned whichever the
# directory walk hit first — which is how a 0.0.106 build ran the
# 0.0.105 binary and failed 01_help_and_version.
MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
# Integration: the mcpp built from THIS PR's source ($MCPP_SELF, the
# self-hosted binary) builds & runs a real external C++ project — xlings
# (openxlings/xlings ships its own mcpp.toml). MCPP_VENDORED_XLINGS only
# supplies the xlings package backend mcpp resolves deps through.
- name: "Integration: mcpp builds & runs xlings (openxlings/xlings)"
shell: bash
env:
XLINGS_NON_INTERACTIVE: '1'
run: |
export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe")
git clone --depth 1 --recurse-submodules \
https://github.com/openxlings/xlings /tmp/xlings-src
cd /tmp/xlings-src
"$MCPP_SELF" self config --mirror GLOBAL
"$MCPP_SELF" build
"$MCPP_SELF" run
# Regression test for the Windows first-run "press Enter to advance" hang.
# Launches mcpp with an OPEN, EMPTY, never-closing stdin pipe. Without
# seal_stdin's Windows fix, any grandchild that reads stdin would inherit
# our pipe and block forever — caught by the timeout below. With the fix,
# every subprocess stdin is redirected from NUL → no possibility of hang.
- name: "Regression: mcpp survives open-empty-stdin (Windows hang fix)"
shell: pwsh
timeout-minutes: 15
env:
MCPP_VENDORED_XLINGS: ${{ env.XLINGS_BIN }}
run: |
$ErrorActionPreference = 'Stop'
# MCPP_SELF was set in a bash step as an MSYS-style path
# (e.g. /d/a/mcpp/...). PowerShell can't exec that — convert it
# to a native Windows path via the git-bash cygpath that ships
# on the runner.
$mcppExe = (& 'C:\Program Files\Git\usr\bin\cygpath.exe' -w $env:MCPP_SELF).Trim()
Write-Host "Resolved MCPP_SELF (Windows form): $mcppExe"
if (-not (Test-Path $mcppExe)) {
throw "MCPP_SELF after cygpath not found: $mcppExe"
}
$tmp = Join-Path $env:RUNNER_TEMP ("stdin-hang-test-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tmp | Out-Null
Set-Location $tmp
& $mcppExe new hello_stdin
Set-Location hello_stdin
function Invoke-McppWithOpenStdin {
param([string]$McppPath, [string]$McppArgs, [int]$TimeoutSeconds = 300)
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = $McppPath
$psi.Arguments = $McppArgs
$psi.WorkingDirectory = (Get-Location).Path
$psi.UseShellExecute = $false
$psi.RedirectStandardInput = $true # parent holds child's stdin
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
# By default the child inherits the parent's env (we did not
# touch $psi.Environment) so MCPP_VENDORED_XLINGS / PATH / etc.
# propagate.
$p = [System.Diagnostics.Process]::Start($psi)
# Async-drain stdout/stderr so a full output buffer doesn't
# itself deadlock the child (separate failure mode from the
# stdin hang we're testing).
$stdoutTask = $p.StandardOutput.ReadToEndAsync()
$stderrTask = $p.StandardError.ReadToEndAsync()
# NEVER write or close $p.StandardInput — the pipe stays open
# and empty for the lifetime of the child. Any grandchild that
# reads stdin will block on this pipe → caught by WaitForExit.
if (-not $p.WaitForExit($TimeoutSeconds * 1000)) {
try { $p.Kill($true) } catch {}
Write-Host "----- captured stdout -----"
Write-Host $stdoutTask.Result
Write-Host "----- captured stderr -----"
Write-Host $stderrTask.Result
throw "REGRESSION: 'mcpp $McppArgs' HUNG with open-empty stdin after ${TimeoutSeconds}s. The Windows seal_stdin fix is not effective."
}
Write-Host "----- stdout -----"
Write-Host $stdoutTask.Result
Write-Host "----- stderr -----"
Write-Host $stderrTask.Result
if ($p.ExitCode -ne 0) {
throw "'mcpp $McppArgs' exited with code $($p.ExitCode) (no hang, but failed)."
}
}
Write-Host '=== T1: mcpp --version (sanity, fast path) ==='
Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs '--version' -TimeoutSeconds 30
Write-Host '=== T2: mcpp build (full bootstrap + toolchain + dep resolve + compile) ==='
Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs 'build' -TimeoutSeconds 600
Write-Host '=== T3: mcpp run (post-build run path) ==='
Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs 'run' -TimeoutSeconds 120
Write-Host 'SUCCESS: mcpp completes with open-empty stdin → Windows seal_stdin fix verified.'
- name: "Toolchain: LLVM — mcpp new → run"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
TMP=$(mktemp -d)
cd "$TMP"
"$MCPP_SELF" new hello_win
cd hello_win
"$MCPP_SELF" run
# MinGW-w64 GCC via the xlings ecosystem (xim:mingw-gcc → xlings-res
# winlibs mirror): install → default → modules build/run → standalone
# exe. Same flow as e2e 97 but as a visible CI step. Payload is cached
# via the mcpp sandbox cache after the first run.
- name: "Toolchain: MinGW — install → build → run (xim:mingw-gcc)"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
MCPP="$MCPP_SELF" bash tests/e2e/97_mingw_toolchain.sh
# windows-latest ships VS 2022 Enterprise with the VC workload, so
# msvc@system detection MUST succeed here — a failure is a regression
# in the discovery/identification chain (vswhere → env → paths).
# Runs BEFORE the LLVM self-host rebuild: that step cleans + rebuilds
# target/, invalidating this job's $MCPP_SELF fingerprint path.
- name: "Toolchain: MSVC — detection & selection (msvc@system)"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
# Neutral cwd: the repo root's mcpp.toml [toolchain] would shadow
# the global default in `toolchain list` / doctor output.
TMP=$(mktemp -d); cd "$TMP"
out=$("$MCPP_SELF" toolchain default msvc); echo "$out"
grep -q "Detected" <<<"$out"
grep -q "msvc@system" <<<"$out"
"$MCPP_SELF" toolchain list | tee tc-list.txt
grep -E '\*\s*msvc' tc-list.txt
"$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true
grep -qi "msvc" doctor.txt
# native cl.exe build: full e2e (modules, import std, incremental)
cd "$GITHUB_WORKSPACE"
MCPP="$MCPP_SELF" bash tests/e2e/99_msvc_native_build.sh
# build.mcpp under cl.exe. `MSVC x build.mcpp` was an empty cell in
# this matrix and the feature was correspondingly at zero — the ten
# build.mcpp e2e all run under clang, whose payload path has no
# spaces in it either. Both gaps closed here.
MCPP="$MCPP_SELF" bash tests/e2e/180_msvc_build_mcpp.sh
# restore the LLVM default for the remaining steps
"$MCPP_SELF" toolchain default llvm@20.1.7
- name: "Toolchain: LLVM — build mcpp (self-host)"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
cp "$MCPP_SELF" /tmp/mcpp-fresh.exe
MCPP=/tmp/mcpp-fresh.exe
"$MCPP" toolchain default llvm@20.1.7
"$MCPP" clean --bmi-cache
"$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log