ci: run fresh-install workflows only in the upstream repo #960
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |