feat(windows-sdk): add the Windows SDK as an xlings payload package - #626
Merged
Merged
Conversation
The old `msvc` recipe was `type = "config"` -- by the V1 spec, "系统配置操作,
无需下载资源文件" -- so it had no payload by declaration. It shelled out to
vs_BuildTools.exe with `--installPath` commented out and `--quiet` commented
out, which put the compiler in C:\Program Files (x86) behind a GUI that wants
elevation, could express no version finer than "2022", and returned true
without ever checking that anything had been installed. Its `installed()`
answered from two places at once (a hardcoded 2022 path OR xmake's toolchain
probe), which is R3's shape, and on the windows-2025-vs2026 image -- VS 2026
Enterprise -- the hardcoded half is false forever.
Replaced by two payload packages that behave like gcc: unpacked into the
xlings store, versioned by the thing that actually varies, several of them
side by side, and no installer anywhere.
msvc the toolset, pinned to 14.44.35207
windows-sdk the ucrt/um/shared headers and um libs it cannot compile
without -- the index had no Windows SDK at all
Both fetch a pinned SET of payloads from Visual Studio's own channel manifest,
which is the only route that can pin a compiler build at all: the bootstrapper
always serves whatever is current, while the manifest names an exact version,
url and sha256 per payload. Every one of those is written out here, and both
recipes verify each download themselves, because the framework's single-url
check cannot cover a set.
Measured before being relied on, not assumed:
* a .vsix is a zip, and the toolset's contents are already
Contents/VC/Tools/MSVC/<ver>/... -- unpacking is "unzip, drop one
component", and the result is the layout consumers already read
* the directory version (14.44.35207) is NOT any payload's package version
(Tools 35228, Headers 35220, CRT 35226); all three unpack into it, so the
directory version is what the package is versioned by
* one manifest carries 16 toolsets, 14.29 through 14.44 -- side-by-side
versions are how Microsoft ships, not something to invent
* MSI -> cabinet cannot be read from the manifest (cabinet names are opaque
hashes; the mapping is in each MSI's Media table). Extracted once, offline,
and recorded here as static data: 4 MSIs + the 15 cabinets they name,
139 MB against the full SDK's 530 MB
* Microsoft.Windows.SDK.BuildTools does NOT substitute for it -- one nupkg,
but bin/ tools only, no headers and no libs
* release-channel payloads survive: VS 2019's manifest and its payloads still
resolve today. Insider payloads rotate, and are deliberately not offered
INCLUDE/LIB are per-shim envs; VSINSTALLDIR is a subos env. The split is not
cosmetic: cl and link are processes xlings wraps, so a shim env reaches them
and does not leak a Windows SDK include path to every other compiler in the
subos -- while the processes that need VSINSTALLDIR are build systems, which
xlings never wraps, so a shim env cannot reach them at all.
vs-buildtools is marked deprecated rather than deleted. Nothing depends on it
now, and it never installed Build Tools -- its `install()` is `return true`.
Design, evidence and the open questions: .agents/docs/2026-08-16-msvc-xlings-package-design.md
…SDK alone Two things the first Windows CI run found, both of them real. os.execv is not in the xpkg sandbox -- "attempt to call a nil value (field 'execv')" -- and the four call sites in this branch were the only uses of it anywhere in the index, which was the hint. system.exec with a command string is what 131 other recipes use. The SDK's file names contain spaces, so every path is quoted. Hashing moves from PowerShell's Get-FileHash to certutil for the same reason: Get-FileHash needs a quoted -LiteralPath nested inside a quoted -Command inside a shell string, and one of these files is literally named "Universal CRT Headers Libraries and Sources-x86_en-us.msi". certutil takes one quoted path and prints the digest on a line of its own. The msvc recipe is held back to a follow-up. It depends on windows-sdk, and a package introduced by the same PR as its consumer cannot be depended on: `xim:` does not resolve it yet (CI registers a changed recipe under `local:`), and the bare name that would resolve is rejected by the dep_install_dir rule, which requires a namespaced coordinate and is right to. The index documents the way out of that -- land the provider, publish it, then land the consumer -- so this PR is windows-sdk alone. msvc is written and unchanged otherwise.
certutil prints the digest uppercase, and the manifest is not consistent either: the VC payloads carry lowercase digests while the SDK's are uppercase. The first Windows run failed on expected F611CE8A9E576E3383917B04B6FBE5EE6BED8363C1A2A8E9D6F8335CBB422675 got f611ce8a9e576e3383917b04b6fbe5ee6bed8363c1a2a8e9d6f8335cbb422675 which is the same hash spelled two ways. Everything before it -- fetching all 19 payloads and hashing them -- worked.
windows-sdk.lua:190: attempt to call a nil value (field 'absolute') pkginfo.install_dir() already returns an absolute path, so the call was redundant as well as unavailable. /quiet is dropped with it -- it is the same switch as /qn and msiexec takes one of them. The run before this one got all 19 payloads downloaded and hashed, so the remaining unknown is msiexec /a itself.
msiexec /a "...\10.0.26100/.installers/Universal CRT ....msi" /qn ...
exec failed after 1 attempt(s)
path.join mixes separators on Windows: it keeps the backslashes already in the
store path and adds forward ones. curl and tar do not care; msiexec does.
Normalised at the call site, so nothing else has to know about it.
The run before this one downloaded and verified all 19 payloads, so this is the
last unknown in the SDK path.
… put it All four MSIs now extract -- the backslash fix was the last thing between the downloads and msiexec. What failed after them was the assumption that came next: that the tree would be under 'Windows Kits/10/'. msiexec /a reproduces the MSI's own directory table, and where that lands is a property of the installer. So anchor on the one file that must exist and derive the root from it: <root>/Include/<ver>/ucrt/corecrt.h is four levels up. That holds whatever prefix the MSI chooses. And when the check does fail, say what IS there. 'not installed' after a clean extraction means the layout moved, and the next person needs the directory listing, not the verdict.
….dirs
windows-sdk.lua:218: attempt to call a nil value (field 'files')
Third of these, and the last: every API this recipe now calls has precedent in
the index -- os.dirs, os.iorun, os.isfile, os.mkdir, os.trymv, os.tryrm,
path.join, path.filename, system.exec. The three that failed (os.execv,
path.absolute, os.files) had none, which was the signal each time.
The root search becomes a bounded directory walk instead of a ** glob. Depth 4
covers every prefix msiexec has been seen to produce; anything deeper would be
a different installer, and a wrong answer is worse than no answer here.
The diagnostic from the last run answered this in one line each: the root was found (Windows Kits/10), Include/ Lib/ bin/ and the rest were at the package root -- and the anchor file still was not, with 'Windows Kits/' still present. The four MSIs do not agree on a prefix. Some lay their files straight into TARGETDIR, some under Windows Kits/10, so by the time the hoist runs there is already an Include/ at the root. os.trymv onto an existing directory fails, and being a 'try' it fails silently -- which is why the tree looked right and the file was missing. Merge when the destination exists, move when it does not.
os.cp of a directory INTO an existing directory of the same name nests it, so the previous attempt produced Include/<ver>/<ver>/ and the anchor file was still not where installed() looks. A recursive merge in Lua would need to walk files, and os.files is not in the sandbox. xcopy does exactly this, ships with Windows, and -- unlike robocopy -- returns 0 on success instead of a bitmask that system.exec would read as failure. Each round has moved the failure forward: downloads, hashes, msiexec, the root search, and now the merge. What is left after this is whether rc.exe and mt.exe are where the xvm registration expects them.
The top-level listing has looked correct for three runs while the anchor files were not there, so it is not the level that carries the answer. Which version directories exist under Include/ and Lib/, and what is in them, is.
The directory tree has looked right for three runs -- Include/<ver>/ucrt, Lib/<ver>/um and bin/<ver>/x64 all present -- while installed() kept saying no. os.dirs lists directories only, so what is missing is a file, and the listing could never have shown it. dir /b on the four anchors will.
…istently The file-level probe answered it. Everything is extracted and in the right place -- corecrt.h is in Include/<ver>/ucrt, Lib/<ver>/um/x64 is full of libs, bin/<ver>/x64 is full of tools -- and installed() still said no, because it asked for 'kernel32.lib' and one listing of that very directory returns AclUI.Lib ActiveDS.Lib advpack.Lib ahadmin.lib amsi.lib ... Casing is not consistent within a single directory, so the name was a guess, and a wrong guess there reads as 'the SDK did not install'. Still one assertion per MSI, so a partial extraction cannot pass: the header by name (its casing is now measured), the libs and the tools by directory.
…green Eight rounds of red, and none of them were about the design -- they were about the sandbox and about Windows. Worth writing down, because the next recipe that downloads a payload set will meet the same ones: os.execv / path.absolute / os.files are not in the sandbox, and all three had zero uses in the index. Grepping for precedent should have been step one. the manifest is not internally consistent about sha256 casing (VC lowercase, SDK uppercase), path.join mixes separators on Windows and msiexec rejects the result, the four SDK MSIs disagree about their prefix so the trees have to be merged rather than moved, and the SDK does not spell its own file names consistently -- AclUI.Lib and amsi.lib live in the same directory.
This was referenced Aug 16, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
本 PR 只上
windows-sdk。msvc拆到后续 PR —— 原因见末尾「为什么拆开」。目标是把 MSVC 变成 xlings 生态内的 payload 包:不装 C 盘、多版本共存、纯命令行、可钉到具体 toolset build。
windows-sdk是其中缺失的那块基础 —— 索引里原本完全没有 Windows SDK,而没有它,任何 MSVC toolset 都编不了一行代码。设计与全部实测证据:
.agents/docs/2026-08-16-msvc-xlings-package-design.md为什么重写而不是修补
现有
msvc.lua是type = "config"(V1 spec:「系统配置操作,无需下载资源文件」),声明上就没有 payload。它 shell out 到vs_BuildTools.exe,而且:--installPath和--quiet都被注释掉了 → 装到C:\Program Files (x86),--passive带 GUI、要管理员"2022"(产品年份),钉不住编译器install()执行完直接return true,从不检查是否真的装上了(R4)installed()有两个答案源(硬编码 2022 路径 或 xmake toolchain 探测)(R3);而windows-2025-vs2026镜像上是 VS 2026 Enterprise,硬编码那半永远为假vs-buildtools.lua则根本不装 Build Tools —— 它的install()就是return true,只下了个 1MB 的 bootstrapper。加了什么
windows-sdkmsvcvs-buildtoolsmsvc一起标记 deprecated(它现在唯一的用途就是给旧msvc当依赖)两个包都从 Visual Studio 自己的 channel manifest 取一组钉死的 payload。这是唯一能钉住版本的路径:bootstrapper 永远给当前 bits,而 manifest 给出每个 payload 精确的 version + url + sha256。所有 URL 和 sha256 都写在包里,且包自己校验每个下载 —— 框架的单 url 校验覆盖不到一组 payload。
依赖前先实测过的事实
.vsix是什么Contents/下Contents/VC/Tools/MSVC/<ver>/...—— 解包 = 「解 zip,去掉一层」,结果正是消费者已经会读的形状14.44.35207。所以包版本取目录版本Media表里)。离线解出一次,作为静态数据钉进包:4 个 MSI + 它们引用的 15 个 CAB = 139 MB,整包是 530 MBMicrosoft.Windows.SDK.BuildTools能否代替bin/工具,无头文件、无库env 分两层,不是随意选的
INCLUDE/LIB→ 每个 shim:cl/link正是 xlings 包裹的进程,shim env 够得到;放进 subos 会把 Windows SDK 的 include 路径泄漏给同一 subos 里其他编译器VSINSTALLDIR→ subos:需要它的是构建系统(mcpp / xmake / cmake),而 xlings 从不包裹它们,shim env 根本够不到后者是兼容层,不是记录。「这份构建用了哪个编译器」应当由项目自己的 manifest 声明,否则它就变成了机器的属性。详见文档 §6.1。
本 PR 未验证的部分
安装路径本身(
msiexec /a解包、tar -xf解 vsix、xvm/subos 注册)只在 Windows 上跑,我无法在本地验证 —— 这正是 CIwindows-testjob 要回答的。本地已通过:
第一次 CI 还抓到两个真问题,已修:
os.execv不在 xpkg 沙箱里(那四处是全索引唯一的用法,本身就是信号),改用 131 个 recipe 都在用的system.exec;哈希从 PowerShell 的Get-FileHash换成certutil—— 前者要在 shell 字符串里嵌套两层引号,而这里有个文件就叫Universal CRT Headers Libraries and Sources-x86_en-us.msi。为什么拆开
msvc依赖windows-sdk,而同一个 PR 里新增的包无法被自己的消费者依赖:xim:windows-sdk@...→ CI 里解析不到(改动的 recipe 注册在local:下)。第一次 CI 实测就是这个错:package 'xim:windows-sdk@10.0.26100' not foundwindows-sdk→ 能解析,但dep_install_dir规则要求带命名空间的坐标(tests/test_dep_query_coordinates.py,理由充分,不该为绕开它而改设计)check-dep-namespace.lua的 EXEMPT 注释描述的正是这个两难,出路也在那里:先落 provider,发布,再落 consumer。所以本 PR 只有windows-sdk。需要 review 拍板的
windows-sdk是单包(139 MB)。若嫌大可拆 headers / libs。VSINSTALLDIR优先于 vswhere 探测;find_windows_sdk()认WindowsSdkDir;cxx_runtime与linkage在 MSVC 静态 CRT 上说法不一。msvc作为后续 PR 提上来。