Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/actions/mask-visual-studio/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Mask Visual Studio
description: >
Make every MSVC discovery strategy of msvc.cppm fail on a runner that ships
Visual Studio, and assert that they do. The runner is disposable, so this is
both safe and closer to "absent" than any env-only trick.

inputs:
attempts:
description: How many times one rename is retried before it counts as denied
required: false
default: "5"
retry-delay-ms:
description: Milliseconds between two attempts at the same rename
required: false
default: "2000"

runs:
using: composite
steps:
- name: Mask Visual Studio
shell: pwsh
env:
MASK_ATTEMPTS: ${{ inputs.attempts }}
MASK_RETRY_DELAY_MS: ${{ inputs.retry-delay-ms }}
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
# masking that directory is what makes this runner answer "no Visual
# Studio" to all three.
#
# The rename is refused outright on some hosts: "Access to the path
# '...\18\Enterprise\VC' is denied", while the same script on the same
# runner image commits the same rename minutes earlier or later (image
# windows-2025-vs2026 20260925.250 renamed it in one run and was
# refused in another). A refusal is therefore retried, and then
# answered one level deeper: a handle into VC does not block renaming
# VC\Tools, and the postcondition below only needs VC\Tools\MSVC gone.
# What outlasts both is reported, never swallowed.
$attempts = [int]$env:MASK_ATTEMPTS
$delay = [int]$env:MASK_RETRY_DELAY_MS

function Mask-Path([string]$Path) {
for ($i = 1; $i -le $attempts; $i++) {
try {
Rename-Item -LiteralPath $Path -NewName "$(Split-Path $Path -Leaf).masked" -ErrorAction Stop
Write-Host "masked $Path"
return $true
} catch {
if ($i -lt $attempts) {
Start-Sleep -Milliseconds $delay
} else {
Write-Host " denied after $attempts attempts: $Path — $($_.Exception.Message)"
}
}
}
return $false
}

$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) { [void](Mask-Path $vswhere) }

# -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.
$vsRoots = @(Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
-ErrorAction SilentlyContinue)
foreach ($p in $vsRoots) { [void](Mask-Path $p.Path) }

# The fallback, and the reason it is enough: vswhere's resolution and
# the well-known-path scan both end at VC\Tools\MSVC, so masking Tools
# answers "no Visual Studio" as well as masking VC does.
$tools = @(Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools" `
-ErrorAction SilentlyContinue)
foreach ($p in $tools) { [void](Mask-Path $p.Path) }

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.
$remaining = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" `
-Directory -ErrorAction SilentlyContinue
if ($remaining) {
Write-Host "FAIL: VC tools still present after masking:"
$remaining | ForEach-Object { Write-Host " $($_.FullName)" }
exit 1
}
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."
39 changes: 6 additions & 33 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -751,40 +751,13 @@ jobs:
steps:
- uses: actions/checkout@v4

# The mask retries a refused rename — "denied" is a state of the host,
# not of the image — and then masks VC\Tools, which is where all three
# of msvc.cppm's discovery strategies end. Its postcondition, no
# VC\Tools\MSVC left, is what keeps an incomplete mask from reading as a
# green no-Visual-Studio install.
- name: Mask Visual Studio
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'

# All three of msvc.cppm's discovery strategies converge on
# <vsRoot>\VC\Tools\MSVC, 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 would be.
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }

Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
-Directory -ErrorAction SilentlyContinue | ForEach-Object {
Rename-Item $_.FullName "$($_.Name).masked" -ErrorAction SilentlyContinue
}

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, rather than letting it surface 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."
uses: ./.github/actions/mask-visual-studio

- name: Install xlings
shell: pwsh
Expand Down
50 changes: 5 additions & 45 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,52 +143,12 @@ jobs:
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.
# ours is holding a handle into it. The mask retries a refused rename
# (a state of the host, not of the image) and then masks VC\Tools; its
# postcondition — no VC\Tools\MSVC left — is what keeps an incomplete
# mask from reading as a green "bare Windows" row.
- 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."
uses: ./.github/actions/mask-visual-studio

# After masking, so nothing this action does can be holding Visual
# Studio open. It installs xlings (which mcpp resolves the winlibs
Expand Down
20 changes: 4 additions & 16 deletions .github/workflows/measure-windows-tool-crt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,12 @@ jobs:
name: measure-candidate
path: candidate

# The same mask as ci-windows.yml's bare-Windows row, applied before
# anything touches Visual Studio.
# The same mask as ci-windows.yml's bare-Windows row — one action, so
# the two rows cannot drift apart — 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/mask-visual-studio

- uses: ./.github/actions/bootstrap-mcpp

Expand Down
23 changes: 20 additions & 3 deletions tests/e2e/845_play_game_restores_the_terminal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
# terminal's mode, and the prepare action does not outlive mcpp;
# G4 with standard input not a terminal the game is off, a line says why,
# and the build succeeds.
#
# The arrows and the Ctrl-C are sent when the status row that carries the
# score first appears: the frame the build itself drew is what the test waits
# for, not a wall-clock delay. A delay sent them into the Programs phase
# whenever the build program took that long, and G1 then failed on the phase
# word while the score was on the screen beside it.
set -e

TMP=$(mktemp -d)
Expand Down Expand Up @@ -69,7 +75,18 @@ if pid == 0:
script = ('trap : INT; stty -g > before.txt; "$0" build --play-game=snake; '
'code=$?; stty -g > after.txt; exit $code')
os.execvpe("sh", ["sh", "-c", script, mcpp], env)
raw, t0, during, sent, status = b"", time.time(), None, False, 0

def strip_ansi(blob):
return re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", blob.decode("utf-8", "replace"))

# The game rides every frame, and the row the assertion below names is the row
# to steer on: nothing is gained by guessing when it is up. The build reaches
# Phase::Building only after its build program has compiled and run (and after
# the toolchain was resolved), which on a loaded runner is a second or more —
# so the arrows and the Ctrl-C wait for the row rather than for the clock.
STEER_ROW = re.compile(r"Building .* · snake \d+")
raw, during, sent, status = b"", None, False, 0
give_up = time.time() + 60 # a build that never draws the row is a failure, not a hang
while True:
r, _, _ = select.select([master], [], [], 0.1)
if r:
Expand All @@ -78,7 +95,7 @@ while True:
except OSError:
chunk = b""
raw += chunk
if time.time() - t0 > 1.5 and not sent:
if not sent and (STEER_ROW.search(strip_ansi(raw)) or time.time() > give_up):
during = termios.tcgetattr(slave)
for key in (b"\x1b[B", b"\x1b[C", b"\x1b[A"):
os.write(master, key)
Expand Down Expand Up @@ -109,7 +126,7 @@ def mode_of(path):
for f in g.split(":"))
return g
before, after = mode_of("before.txt"), mode_of("after.txt")
plain = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", raw.decode("utf-8", "replace")).replace("\r\n", "\n")
plain = strip_ansi(raw).replace("\r\n", "\n")
print(plain)
assert during is not None, "the build ended before the game could be observed"
# G1
Expand Down
Loading