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:
+198
-7
@@ -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
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user