Skip to content

feat(windows-sdk): add the Windows SDK as an xlings payload package - #626

Merged
Sunrisepeak merged 13 commits into
mainfrom
feat/msvc-payload-package
Aug 16, 2026
Merged

Sunrisepeak merged 13 commits into
mainfrom
feat/msvc-payload-package

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 16, 2026

Copy link
Copy Markdown
Member

本 PR 只上 windows-sdkmsvc 拆到后续 PR —— 原因见末尾「为什么拆开」。

目标是把 MSVC 变成 xlings 生态内的 payload 包:不装 C 盘、多版本共存、纯命令行、可钉到具体 toolset build。windows-sdk 是其中缺失的那块基础 —— 索引里原本完全没有 Windows SDK,而没有它,任何 MSVC toolset 都编不了一行代码。

设计与全部实测证据:.agents/docs/2026-08-16-msvc-xlings-package-design.md

为什么重写而不是修补

现有 msvc.luatype = "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。

加了什么

本 PR 说明
windows-sdk ✅ 新增 Desktop x64 子集:ucrt/um/shared 头、um 库、rc/mt
msvc ⏭ 后续 toolset 钉到 14.44.35207,已写好并本地验证
vs-buildtools ⏭ 后续 msvc 一起标记 deprecated(它现在唯一的用途就是给旧 msvc 当依赖)

两个包都从 Visual Studio 自己的 channel manifest 取一组钉死的 payload。这是唯一能钉住版本的路径:bootstrapper 永远给当前 bits,而 manifest 给出每个 payload 精确的 version + url + sha256。所有 URL 和 sha256 都写在包里,且包自己校验每个下载 —— 框架的单 url 校验覆盖不到一组 payload。

依赖前先实测过的事实

事实 结果
.vsix 是什么 就是 zip,内容在 Contents/
toolset 解压后布局 已经是 Contents/VC/Tools/MSVC/<ver>/... —— 解包 = 「解 zip,去掉一层」,结果正是消费者已经会读的形状
目录版本 vs 包版本 不同:Tools 35228 / Headers 35220 / CRT 35226,都解到 14.44.35207。所以包版本取目录版本
一份清单里的 toolset 数 16 个(14.29 → 14.44)。多版本共存是微软本来的发布方式,不用自己造
MSI → CAB 映射 清单里查不到(CAB 名是不透明哈希,映射在 MSI 的 Media 表里)。离线解出一次,作为静态数据钉进包:4 个 MSI + 它们引用的 15 个 CAB = 139 MB,整包是 530 MB
Microsoft.Windows.SDK.BuildTools 能否代替 不能 —— 单个 nupkg 不假,但只有 bin/ 工具,无头文件、无库
旧 payload 保留期 VS 2019 的 manifest 与 payload 今天仍可取回。Insiders 会轮换,故意不提供
体积 toolset 83.5 MB + SDK 139 MB = 222.6 MB

env 分两层,不是随意选的

  • INCLUDE / LIB → 每个 shimcl / 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 上跑,我无法在本地验证 —— 这正是 CI windows-test job 要回答的。

本地已通过:

L0 static      1390 passed
L2 isolation    379 passed
lint x3         libpath / blocking-stdin / dep-namespace 全 PASS

第一次 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 found
  • 写裸名 windows-sdk → 能解析,但 dep_install_dir 规则要求带命名空间的坐标(tests/test_dep_query_coordinates.py,理由充分,不该为绕开它而改设计)

check-dep-namespace.lua 的 EXEMPT 注释描述的正是这个两难,出路也在那里:先落 provider,发布,再落 consumer。所以本 PR 只有 windows-sdk

需要 review 拍板的

  1. 许可边界:本 PR 采用「安装时从微软 CDN 下载」,不镜像到 xlings-res(形状与索引里其它需要下载官方分发物的包一致)。若要镜像需另行确认。
  2. 通道:首版只做 release 通道。Insiders 保留期风险最高,而它恰是 build: add a Windows/MSVC mcpp build alongside xmake Sunrisepeak/xrgui#3 的实际需求 —— 这条决定这套包能否直接回写那个 PR。
  3. SDK 拆包粒度:现在 windows-sdk 是单包(139 MB)。若嫌大可拆 headers / libs。
  4. 是否接受对 mcpp 提三个适配需求(文档 §11):VSINSTALLDIR 优先于 vswhere 探测;find_windows_sdk()WindowsSdkDircxx_runtimelinkage 在 MSVC 静态 CRT 上说法不一。
  5. 这个合并、发布之后,我把 msvc 作为后续 PR 提上来。

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.
@Sunrisepeak Sunrisepeak changed the title feat(msvc,windows-sdk): make MSVC an xlings payload package feat(windows-sdk): add the Windows SDK as an xlings payload package Aug 16, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants