fix(ci): 去掉 actions/checkout 外网依赖,MSVC 探测改 vswhere 并钉住工具集世代

两个 job 的第 1 步原先是 `uses: actions/checkout@v4`。`uses:` 的解析发生在
Prepare all required actions 阶段——先于任何步骤、无条件联网(默认 github.com),
且每个 job 各自解析进自己的 .cache/act/<hash>、跨 job 不复用。这条链在本机实测
不稳:run 36(10-21)/ 41(10-25)/ 42(10-26)三次都是 ~45 s 后整 job 死亡、
零钩子输出,而同一分钟手工 curl 同一地址返回 200——手工可达 ≠ CI 可用(解析器
跑在 SYSTEM 下)。Gitea 1.25.4 又去掉了自定义 DEFAULT_ACTIONS_URL,服务端没有
可拧的键,唯一归本仓库管的修法就是不再依赖它:两个 job 第 1 步换成原生 git 全量检出。

关键选择(依据与实测表见 docs/CI/远程CI完整指南.md 同文 §5.3,此处只记结论):
- 完整 refspec + 全量,不是 --depth 1:提交信息门禁要解 base..head 的 range;
  且目标 sha 不在历史里就 ::error:: 硬失败,绝不默默构建一棵错的树;
- clean -ffd 而非 -ffdx:-x 会删被忽略文件,而 runner 工作区跨运行复用它们;
- GIT_TERMINAL_PROMPT=0 + 空 credential.helper:否则认证挑战会等人类输入,
  把 30 分钟 job 超时白烧掉,表现得像 CI 挂了;
- 令牌只走 per-invocation -c http.extraheader(Basic + x-access-token,与
  checkout@v4 同拼法),不落 .git/config、不回显;仓库公开时匿名即可;
- 不抓 tag:本仓库 tag 数 0、无脚本用 describe --tags(实测)。

另修 scripts/ci_runner_bootstrap.ps1 的 Find-MsvcCl。旧版只扫 %ProgramFiles%
派生根且取首个命中:本机 C 盘 VS2019 已于今日 01:00 卸载(C 盘 x64 cl.exe 实测
0 个),旧版因此是「找不到 → 只 Warn → 不写」,机器级变量一直指着那条失效路径,
而 harness 对「显式设置但无效」是硬失败、不做自动回退。现在改为 vswhere 取实例
(能看见非默认盘)+ 工具集版本降序 + -MsvcToolset 前缀钉住世代(默认 14.51.,
与覆盖率基线同版本;钉不上连 WARN 再回落最新,绝不静默)+ -MsvcLatest;
-SetEnv 与 -Status 同步纳入 DTU_MS_COVERAGE。

验证(内层一律 powershell.exe 5.1,忠实复现 runner 执行方式):临时工作区六项
A/B/C/D/E/F——E 是执行**从 ci.yml 里抽出的正文**,测的就是文件那份字节。
A rc=0、commits=665、refspec 完整、分离 HEAD;B 复用工作区后 dirty_files=0、
被忽略文件保留、17ece86..HEAD 解出 18 个提交;C rc=1 且 HEAD 不动;D 坏令牌
fatal: unable to get password 秒失败;令牌改造后再跑 ANON/BOGUS/ANON2=0/1/0。
ci_local 全量:L0.5 = 40 runner、3188/3188 断言、fail=0(MSVC 14.51);
test-coverage 与 ui-golden 均 Passed。.ps1 非 ASCII 字节 0、语法错误 0、无 BOM、
LF;ci.yml 无 BOM 无 CR、13 步、0 个 run 块含非 ASCII、uses: 计数 0。
This commit is contained in:
2026-10-27 08:00:14 +08:00
parent 5869c0307b
commit 2d18193d38
2 changed files with 338 additions and 27 deletions
+198 -7
View File
@@ -125,11 +125,105 @@ jobs:
name: L0 静态门禁 (pre-commit)
runs-on: windows
steps:
- uses: actions/checkout@v4
with:
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
fetch-depth: 0
- name: 检出源码(原生 git;不依赖 github.com 上的 action)
shell: powershell
env:
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
DTU_REPO_REF: ${{ github.ref }}
DTU_REPO_SHA: ${{ github.sha }}
DTU_REPO_TOKEN: ${{ github.token }}
run: |
# Pure ASCII only (see file header).
#
# WHY NOT `uses: actions/checkout`: the runner resolves every `uses:` by
# contacting the actions URL during "Prepare all required actions", BEFORE
# the first step runs. On this intranet runner that call intermittently dies
# with a connect timeout -- runs 36 (10-21) / 41 (10-25) / 42 (10-26) each
# burned ~45 s and produced ZERO hook output, while a manual curl to the same
# URL minutes later returned 200. Manual reachability is not CI health: the
# resolver runs as SYSTEM and the runner caches nothing across jobs (each job
# re-resolves into its own .cache/act/<hash>). Gitea 1.25.4 also removed the
# custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to stop
# depending on the public resolver at all.
#
# KEEP IN SYNC with the same step in the other job. This logic deliberately
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file
# is not checked out yet at the moment it would be needed -- chicken and egg.
#
# NOTE on $ErrorActionPreference: left at Continue on purpose. Under Windows
# PowerShell 5.1 a native command that writes to stderr while EAP is Stop
# raises NativeCommandError -- git legitimately writes progress to stderr.
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
$ws = (Get-Location).Path
Write-Host "workspace: $ws"
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
git init -q .
if ($LASTEXITCODE -ne 0) { Write-Host '::error::git init failed'; exit 1 }
}
$rem = git remote get-url origin 2>$null
if ($LASTEXITCODE -eq 0 -and $rem) {
git remote set-url origin $env:DTU_REPO_URL
} else {
git remote add origin $env:DTU_REPO_URL
}
if ($LASTEXITCODE -ne 0) { Write-Host '::error::configuring remote origin failed'; exit 1 }
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
# ranges and several gates diff against history, so a shallow clone turns
# them into "unknown revision" failures. Tags are skipped on purpose: this
# repository has zero tags and no script uses `git describe --tags`
# (measured 2026-10-27).
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
# Never let a fetch wait for a human: the runner has no console and a
# credential prompt would burn the whole 30 m job timeout while looking like
# a hung CI. Terminal prompts off + no credential helper means an auth
# problem fails immediately with a message instead of blocking. Anonymous
# fetch works here (the repository is public; measured HTTP 200 on
# /info/refs?service=git-upload-pack), so CI must not start depending on a
# credential store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
# The token, when the instance provides one, travels only as a per-invocation
# -c override: it never lands in .git/config and is never echoed in logs.
# Anonymous fetch works too (the repository is public), so CI does not start
# depending on token availability.
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
if ($env:DTU_REPO_TOKEN) {
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
# placeholder user and the token as the password. (Bearer is the GitHub
# *API* style, not the git-http style.) Honest caveat: this branch CANNOT
# be verified here -- the repository is public, so an anonymous fetch
# succeeds and any token we fabricate locally only proves a bad one is
# rejected. If the repository ever goes private this is the first line to
# check; the failure mode is a loud rc=1, never a silent fallback.
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
}
# PS 5.1 wraps each native stderr line into an ErrorRecord, turning two real
# log lines into eight of CategoryInfo/FullyQualifiedErrorId noise. Flattening
# with "$_" keeps the log readable; $LASTEXITCODE is unaffected.
$fetchArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin')
& git @fetchArgs 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host "::error::git fetch origin failed (exit $LASTEXITCODE)"; exit 1 }
# Some refs (PR merge commits, refs outside refs/heads) are not covered by the
# branch refspec -- fall back to an explicit ref fetch, then INSIST the wanted
# SHA is really present, rather than silently building the wrong tree.
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$refArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF)
& git @refArgs 2>&1 | ForEach-Object { "$_" }
}
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history'); exit 1 }
& git checkout -q --detach $env:DTU_REPO_SHA 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host '::error::detached checkout failed'; exit 1 }
git reset --hard -q $env:DTU_REPO_SHA
if ($LASTEXITCODE -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
# -ffd, deliberately not -ffdx: -x would delete IGNORED files, and the runner
# reuses those across runs (pip / pre-commit caches live in the workspace).
git clean -ffd -q
Write-Host ('checkout ok: ' + (git rev-parse --short HEAD) + ' | commits=' + (git rev-list --count HEAD) + ' | dirty_files=' + ((git status --porcelain | Measure-Object).Count))
- name: 环境自检(Python / cppcheck / pre-commit)
shell: powershell
run: |
@@ -399,8 +493,105 @@ jobs:
name: L0.5 主机侧单测 (MSVC)
runs-on: windows
steps:
- uses: actions/checkout@v4
- name: 检出源码(原生 git;与 l0-gates 同一实现口径)
shell: powershell
env:
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
DTU_REPO_REF: ${{ github.ref }}
DTU_REPO_SHA: ${{ github.sha }}
DTU_REPO_TOKEN: ${{ github.token }}
run: |
# Pure ASCII only (see file header).
#
# WHY NOT `uses: actions/checkout`: the runner resolves every `uses:` by
# contacting the actions URL during "Prepare all required actions", BEFORE
# the first step runs. On this intranet runner that call intermittently dies
# with a connect timeout -- runs 36 (10-21) / 41 (10-25) / 42 (10-26) each
# burned ~45 s and produced ZERO hook output, while a manual curl to the same
# URL minutes later returned 200. Manual reachability is not CI health: the
# resolver runs as SYSTEM and the runner caches nothing across jobs (each job
# re-resolves into its own .cache/act/<hash>). Gitea 1.25.4 also removed the
# custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to stop
# depending on the public resolver at all.
#
# KEEP IN SYNC with the same step in the other job. This logic deliberately
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file
# is not checked out yet at the moment it would be needed -- chicken and egg.
#
# NOTE on $ErrorActionPreference: left at Continue on purpose. Under Windows
# PowerShell 5.1 a native command that writes to stderr while EAP is Stop
# raises NativeCommandError -- git legitimately writes progress to stderr.
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
$ws = (Get-Location).Path
Write-Host "workspace: $ws"
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
git init -q .
if ($LASTEXITCODE -ne 0) { Write-Host '::error::git init failed'; exit 1 }
}
$rem = git remote get-url origin 2>$null
if ($LASTEXITCODE -eq 0 -and $rem) {
git remote set-url origin $env:DTU_REPO_URL
} else {
git remote add origin $env:DTU_REPO_URL
}
if ($LASTEXITCODE -ne 0) { Write-Host '::error::configuring remote origin failed'; exit 1 }
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
# ranges and several gates diff against history, so a shallow clone turns
# them into "unknown revision" failures. Tags are skipped on purpose: this
# repository has zero tags and no script uses `git describe --tags`
# (measured 2026-10-27).
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
# Never let a fetch wait for a human: the runner has no console and a
# credential prompt would burn the whole 30 m job timeout while looking like
# a hung CI. Terminal prompts off + no credential helper means an auth
# problem fails immediately with a message instead of blocking. Anonymous
# fetch works here (the repository is public; measured HTTP 200 on
# /info/refs?service=git-upload-pack), so CI must not start depending on a
# credential store it cannot answer.
$env:GIT_TERMINAL_PROMPT = '0'
# The token, when the instance provides one, travels only as a per-invocation
# -c override: it never lands in .git/config and is never echoed in logs.
# Anonymous fetch works too (the repository is public), so CI does not start
# depending on token availability.
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
if ($env:DTU_REPO_TOKEN) {
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
# placeholder user and the token as the password. (Bearer is the GitHub
# *API* style, not the git-http style.) Honest caveat: this branch CANNOT
# be verified here -- the repository is public, so an anonymous fetch
# succeeds and any token we fabricate locally only proves a bad one is
# rejected. If the repository ever goes private this is the first line to
# check; the failure mode is a loud rc=1, never a silent fallback.
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
}
# PS 5.1 wraps each native stderr line into an ErrorRecord, turning two real
# log lines into eight of CategoryInfo/FullyQualifiedErrorId noise. Flattening
# with "$_" keeps the log readable; $LASTEXITCODE is unaffected.
$fetchArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin')
& git @fetchArgs 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host "::error::git fetch origin failed (exit $LASTEXITCODE)"; exit 1 }
# Some refs (PR merge commits, refs outside refs/heads) are not covered by the
# branch refspec -- fall back to an explicit ref fetch, then INSIST the wanted
# SHA is really present, rather than silently building the wrong tree.
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
$refArgs = $auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF)
& git @refArgs 2>&1 | ForEach-Object { "$_" }
}
git rev-parse --verify -q ($env:DTU_REPO_SHA + '^{commit}') 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history'); exit 1 }
& git checkout -q --detach $env:DTU_REPO_SHA 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) { Write-Host '::error::detached checkout failed'; exit 1 }
git reset --hard -q $env:DTU_REPO_SHA
if ($LASTEXITCODE -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
# -ffd, deliberately not -ffdx: -x would delete IGNORED files, and the runner
# reuses those across runs (pip / pre-commit caches live in the workspace).
git clean -ffd -q
Write-Host ('checkout ok: ' + (git rev-parse --short HEAD) + ' | commits=' + (git rev-list --count HEAD) + ' | dirty_files=' + ((git status --porcelain | Measure-Object).Count))
- name: 探测 MSVC(VS BuildTools / Community 等)
id: msvc
shell: powershell
+140 -20
View File
@@ -49,7 +49,11 @@
.PARAMETER SetEnv
Discover and write MACHINE-level DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK /
DTU_MSVC_CL. Existing values are preserved unless -Force.
DTU_MSVC_CL / DTU_MS_COVERAGE. Existing values are preserved unless
-Force. MSVC discovery goes through vswhere (so VS on a non-default
drive is found) and pins the toolset with -MsvcToolset, because the
coverage baseline was recorded with one specific MSVC version -- see
the remote-CI guide (docs/CI/) section 4.4.
.PARAMETER StartNow
Start the task after registering it.
@@ -65,6 +69,16 @@
.PARAMETER Force
With -SetEnv, overwrite existing machine-level values.
.PARAMETER MsvcToolset
Toolset version PREFIX to prefer when writing DTU_MSVC_CL (default
'14.51.'). 'newest available' is deliberately NOT the rule: the host
unit-test coverage baseline was measured with MSVC 14.51 and the
denominator changes with the compiler generation. When no toolset
matches the prefix the script warns and falls back to the newest.
.PARAMETER MsvcLatest
Ignore -MsvcToolset and take the newest MSVC toolset found.
.OUTPUTS
Progress lines. Exit codes: 0 = ok, 1 = precondition failed, 2 = not elevated.
@@ -80,7 +94,9 @@ param(
[switch]$StartNow,
[switch]$Status,
[switch]$DryRun,
[switch]$Force
[switch]$Force,
[string]$MsvcToolset = '14.51.',
[switch]$MsvcLatest
)
$ErrorActionPreference = 'Stop'
@@ -138,24 +154,115 @@ function Find-Cppcheck {
return $null
}
function Find-MsvcCl {
$pf = $env:ProgramFiles
$pf86 = ${env:ProgramFiles(x86)}
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
$roots = @()
foreach ($year in @('2019', '2022', '2026', '18')) {
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
$roots += (Join-Path $pf86 "Microsoft Visual Studio\$year\$edition")
$roots += (Join-Path $pf "Microsoft Visual Studio\$year\$edition")
function Find-VsInstances {
# VS install roots, newest first, via vswhere. This matters because
# vswhere knows about NON-DEFAULT INSTALL DRIVES: this machine keeps
# VS 18 BuildTools under E:, which a %ProgramFiles%-derived probe can
# never see. Returns an empty array when vswhere is unusable so the
# caller can fall back to the classic (incomplete) root scan.
$vswhere = $env:DTU_VSWHERE
if ([string]::IsNullOrWhiteSpace($vswhere)) {
$pf86 = [Environment]::GetFolderPath('ProgramFilesX86')
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
$vswhere = Join-Path $pf86 'Microsoft Visual Studio\Installer\vswhere.exe'
}
if (-not (Test-Path -LiteralPath $vswhere)) { return @() }
$raw = & $vswhere -all -products * -format json 2>$null
if ($LASTEXITCODE -ne 0 -or -not $raw) { return @() }
$list = @()
foreach ($i in @($raw | ConvertFrom-Json)) {
if (-not $i -or -not $i.installationPath) { continue }
if (-not (Test-Path -LiteralPath $i.installationPath)) { continue }
$v = [version]'0.0'
if ($i.installationVersion) { [void][version]::TryParse([string]$i.installationVersion, [ref]$v) }
$list += [pscustomobject]@{ Root = [string]$i.installationPath; Version = $v }
}
return @($list | Sort-Object -Property Version -Descending)
}
function Find-MsvcClCandidates {
# Every x64-host cl.exe on this machine, tagged with its toolset version.
# Comparison operators in PowerShell are case-insensitive by default, so
# -match / -notmatch cover HostX64 / Hostx64 spelling differences.
$cands = @()
$roots = @(Find-VsInstances | ForEach-Object { $_.Root })
if ($roots.Count -eq 0) {
Warn 'vswhere unusable -- falling back to %ProgramFiles% roots; VS installs on other drives are INVISIBLE to that probe'
$pf = [Environment]::GetFolderPath('ProgramFiles')
$pf86 = [Environment]::GetFolderPath('ProgramFilesX86')
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
foreach ($year in @('18', '2026', '2022', '2019')) {
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
foreach ($base in @($pf, $pf86)) {
$r = Join-Path $base ('Microsoft Visual Studio\' + $year + '\' + $edition)
if (Test-Path -LiteralPath $r) { $roots += $r }
}
}
}
}
foreach ($r in $roots) {
if (-not (Test-Path -LiteralPath $r)) { continue }
$cl = Get-ChildItem -LiteralPath $r -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like '*Hostx64\x64*' } |
Select-Object -First 1
if ($cl) { return $cl.FullName }
$msvc = Join-Path $r 'VC\Tools\MSVC'
if (-not (Test-Path -LiteralPath $msvc)) { continue }
foreach ($f in (Get-ChildItem -LiteralPath $msvc -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue)) {
$full = $f.FullName
if ($full -notmatch '\\bin\\Hostx64\\x64\\cl\.exe$') { continue }
$toolset = ''
if ($full -match '\\VC\\Tools\\MSVC\\([^\\]+)\\bin\\') { $toolset = $Matches[1] }
$ver = [version]'0.0'
[void][version]::TryParse($toolset, [ref]$ver)
$cands += [pscustomobject]@{ Cl = $full; Toolset = $toolset; Version = $ver; VsRoot = $r }
}
}
return @($cands)
}
function Find-MsvcCl {
# Rewritten 2026-10-27. The previous version walked a hard-coded root list
# derived from %ProgramFiles% and returned the FIRST hit: with VS2019
# (MSVC 14.29) and a newer VS both present it handed back 14.29, and it
# could not see the E: install at all. "First hit" is never right for a
# compiler: the coverage baseline (scripts/test_coverage_baseline.json)
# was recorded with MSVC 14.51 and the instrumented denominator moves when
# the compiler generation moves (measured: App/util/sha256.c is 105 lines
# under gcc vs 114 under MSVC -- see the host coverage policy doc, 2.1).
# Hence the default is a PINNED toolset prefix, not "newest".
param([string]$ToolsetPrefix = '', [switch]$Latest)
$cands = @(Find-MsvcClCandidates)
if ($cands.Count -eq 0) { return $null }
$sorted = @($cands | Sort-Object -Property Version -Descending)
$seen = (@($sorted | ForEach-Object { $_.Toolset } | Select-Object -Unique)) -join ', '
if (-not $Latest -and $ToolsetPrefix) {
$hit = @($sorted | Where-Object { $_.Toolset -like ($ToolsetPrefix + '*') })
if ($hit.Count -gt 0) {
Info ('MSVC pinned to toolset prefix ' + $ToolsetPrefix + '* -> ' + $hit[0].Toolset + ' (' + $hit.Count + ' of ' + $cands.Count + ' candidate(s); toolsets found: ' + $seen + ')')
return $hit[0].Cl
}
Warn ('no MSVC toolset matching prefix ' + $ToolsetPrefix + ' on this machine (found: ' + $seen + ')')
Warn 'falling back to the NEWEST toolset -- that changes the coverage denominator; read the remote-CI guide (docs/CI/) section 4.4 before accepting it'
}
Info ('using newest MSVC toolset ' + $sorted[0].Toolset + ' of ' + $cands.Count + ' candidate(s); toolsets found: ' + $seen)
return $sorted[0].Cl
}
function Find-CoverageConsole {
# Microsoft.CodeCoverage.Console.exe ships ONLY inside Visual Studio
# (measured 2026-10-27: NuGet candidates contain no such .exe, or only the
# legacy Team Tools CodeCoverage.exe whose CLI differs). Its location
# inside a VS install is a fixed relative path:
param([string]$ClPath = '')
$rel = 'Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe'
$roots = @()
if ($ClPath) {
$vsRoot = ''
# cl.exe = <root>\VC\Tools\MSVC\<ver>\bin\Hostx64\x64\cl.exe
if ($ClPath -match '^(.*)\\VC\\Tools\\MSVC\\') { $vsRoot = $Matches[1] }
if ($vsRoot) { $roots += $vsRoot }
}
foreach ($i in @(Find-VsInstances)) { $roots += $i.Root }
foreach ($r in ($roots | Where-Object { $_ } | Select-Object -Unique)) {
$cand = Join-Path $r $rel
if (Test-Path -LiteralPath $cand) { return $cand }
}
return $null
}
@@ -273,7 +380,7 @@ if ($Status) {
if (Test-Gitea $GiteaUrl) { Ok "Gitea reachable at $GiteaUrl" }
else { Fail "Gitea NOT reachable at $GiteaUrl" }
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL')) {
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL', 'DTU_MS_COVERAGE')) {
$v = Get-MachineEnv $n
if ($v) { Ok ("MACHINE {0} = {1}" -f $n, $v) }
else { Warn "MACHINE $n not set (CI auto-discovery still applies; see scripts/ci_find_python.ps1)" }
@@ -359,8 +466,21 @@ if ($SetEnv) {
if ($cpp) { Set-MachineEnv 'DTU_CPPCHECK' $cpp 'DTU_CPPCHECK' }
else { Warn 'cppcheck not found (install Cppcheck.Cppcheck, or set DTU_CPPCHECK manually)' }
$cl = Find-MsvcCl
if ($cl) { Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL' }
$cl = Find-MsvcCl -ToolsetPrefix $MsvcToolset -Latest:$MsvcLatest
if ($cl) {
Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL'
# Pin the coverage collector too, and pin it INSIDE the same VS as the
# chosen cl.exe: the gate derives it from the compiler ancestor anyway,
# but an explicit machine value also covers the case where somebody
# later points DTU_MSVC_CL at another install. Both vars must be
# changed together (docs/CI/ guide, section 4.4).
$cov = Find-CoverageConsole -ClPath $cl
if ($cov) { Set-MachineEnv 'DTU_MS_COVERAGE' $cov 'DTU_MS_COVERAGE' }
else {
Warn 'Microsoft.CodeCoverage.Console.exe NOT found -- the test-coverage gate FAILS with [ENV] exit 2 (by the 2026-10-24 ruling it does not skip).'
Warn 'install VS BuildTools 17.9+/18.x including Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage, then re-run -SetEnv'
}
}
else { Warn 'MSVC cl.exe (Hostx64\x64) not found -- L0.5 will degrade to "skipped" without it' }
Info 'machine env vars are read when a PROCESS STARTS: the daemon must be restarted below'