diff --git a/.agents/docs/2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md b/.agents/docs/2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md new file mode 100644 index 00000000..ce74e8d9 --- /dev/null +++ b/.agents/docs/2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md @@ -0,0 +1,417 @@ +--- +subject: triage +status: active +--- + +# #685、#687 与 Windows clang 的 MSVC STL:三个问题的归属,以及工具链载荷的规范化 + +- 日期:2026-09-24 +- 对象:mcpp-community/mcpp#685、#687;Windows 上 `llvm` 工具链的 STL 来源;工具链载荷的生产与验收 +- 基线:mcpp `origin/main` b30e70c4(`src/`、`modules/` 与 d1f1c98f 相同);openxlings/xim-pkgindex `origin/main` 2646a16;openxlings/libxpkg `origin/main` 385a1e9;d2learn/xim-pkgindex-fromsource `origin/main` bec5139 +- 测量环境:本机 x86_64 Linux;已安装载荷 `xim-x-gcc` 13.3.0 / 15.1.0 / 16.1.0、`xim-x-llvm` 20.1.7 / 22.1.8、`xim-x-glibc` 2.44 +- 本文只做分析,不改代码。文中标注「测量」的结论在本机复现过;标注「代码阅读」的结论来自源码,没有运行;标注「未核实」的是推断 + +--- + +## 0. 结论 + +| 问题 | 性质 | 归属 | 修法要点 | +|---|---|---|---| +| #685 | 引擎缺陷:「产物是不是 macOS 二进制」本应按目标判定,现在按宿主判定 | mcpp | 解析函数去掉 `#if defined(__APPLE__)`,是否适用由各读者按目标决定。共有六个读者,其中 build.mcpp 的宿主编译应继续按宿主判定 | +| #687 | 载荷缺陷,而且已有的修复从未生效。xim-pkgindex 在 2026-08-08 已给 `gcc.lua` 加上 `__prune_stale_fixincludes()`,但它的命令把单引号嵌在单引号里,被 shell 拆开后变成对当前工作目录的 `grep -r`,碰不到载荷 | xim-pkgindex 修配方;mcpp 对已安装载荷给出诊断 | 修正引号,让零命中在日志里可见;补安装后断言测试;已安装的用户需要重装;长期在构建端清空 include-fixed | +| Windows clang 的 STL | 设计缺口。受管载荷 `xim:msvc` / `xim:windows-sdk` 已经存在,但只有 `cl.exe` 使用;`llvm` 这一行由 clang 自己探测机器上的 Visual Studio | mcpp,加上索引里的配对数据 | 不单独分发 STL,分发单位保持「toolset + SDK」。用 `-Xmicrosoft-visualc-tools-root` / `-Xmicrosoft-windows-sdk-root` 把受管 toolset 绑定给 clang,这组参数本机已测可用 | +| 工具链管理规范化 | 需要做,但对象是载荷的生产与验收,不是引擎 | 生态为主(配方仓库、索引 CI);mcpp 提供描述文件和 doctor 检查 | 载荷契约、配方即代码并记录来源、载荷 lint、工具链 × C 库兼容矩阵、安装后不就地改写 | + +提问中的三个问题,直接回答如下。 + +1. **Windows 上的 clang 只依赖 MSVC 的 STL 吗?** 不是。在 MSVC ABI 下,clang 从 VS toolset 取 STL 头、vcruntime,以及 CRT 的导入库和静态库;从 Windows SDK 取 UCRT、Win32 头和导入库。编译器、`lld-link`、`llvm-rc` 和 compiler-rt 来自 llvm 载荷。完整分解见 §3.2。 +2. **要不要通过 xlings 分发 MSVC STL,并做成可配置?** xlings 已经在分发:`xim:msvc` 有 14.44.35207 和 14.52.36629 两个版本,`xim:windows-sdk` 有 10.0.26100,都有 GitCode 镜像。缺的是 clang 这一行没有接上。建议接上,但不单独拆出 STL,因为 STL 头、vcruntime 和 CRT 库必须来自同一个 toolset。配置沿用已有的 `msvc@` 拼写,不新增 SDK 版本键。这与 2026-08-16 三轴设计 §2.4 的决定一致。机器上装了多个 MSVC 时,今天有两个互相独立的选择器,所用版本也就不固定。§3.6 说明怎样收拢成一次选择,以及三种指定方式。 +3. **工具链管理要不要规范化、程序化、可复现?** 需要。#687 就是例证:这类缺陷的修复已经写进了配方,但没有任何程序验证它生效。已发布的三个 gcc 载荷来自两套构建环境,载荷本身也不记录是由哪一版配方构建的。§4 给出具体做法。 + +--- + +## 1. #685:deployment target 按宿主判定 + +### 1.1 核实(代码阅读) + +issue 列出的三处根因全部成立。 + +- `modules/platform/src/macos/macos.cppm:105-116`:`deployment_target()` 在 `#if defined(__APPLE__)` 之外直接返回空串,manifest 里的值被丢弃。 +- `src/build/prepare.cppm:1933-1935`:`min_platform_version()` 对 macOS 目标调用上面的函数,得到空串;随后 `modules/toolchain-model/src/triple.cppm:160` 回落到写死的 `"14.0"`。 +- `src/build/prepare_inputs.cppm:536`:只有在 `if constexpr (mcpp::platform::is_macos)` 成立时,也就是宿主是 macOS 时,这个值才折入指纹。 + +### 1.2 issue 没有列出的读者 + +`deployment_target()` 的每个读者都要单独判断:它问的是目标,还是宿主。 + +| 读者 | 位置 | 问的是 | Linux 宿主交叉到 macOS 时的现状 | +|---|---|---|---| +| 有效三元组 | `prepare.cppm:1878-1936` → `triple.cppm:160` | 目标 | 恒为 14.0(issue 已述) | +| 项目指纹 | `prepare_inputs.cppm:536` | 目标 | 不折入,改了值也不重建(issue 已述) | +| 平台事实 `macos.deployment-target` | `prepare.cppm:8707-8732` | 目标 | 值为空,所以不写入事实表;包里声明的 `macos.deployment-target >= X` 在这种构建里不会被检查(代码阅读) | +| 编译命令里的 `-mmacosx-version-min` | `hostflags.cppm:513`、`flags.cppm:541/727/1795` | 目标 | Linux 宿主上不发这个参数,版本由三元组承载,结果正确 | +| std 模块预编译 | `stdmod.cppm:258-307` | 目标 | 同上 | +| build.mcpp 的宿主编译 | `build_program.cppm:1205-1208` | **宿主** | 正确,因为 build.mcpp 就在宿主上运行 | + +反方向也按宿主判定(代码阅读,外加测量)。macOS 宿主交叉到非 Apple 目标(`x86_64-linux-musl`、wasm、Android)时,`hostflags.cppm:513` 的条件 `mcpp::platform::is_macos && opt.appleSdkRoot.empty()` 成立,编译命令会带上 `-mmacosx-version-min=14.0`,项目指纹也会折入这个值。本机用 clang 22.1.8 对三个非 Apple 目标测过,都是 `warning: argument unused during compilation`,退出码 0。所以这一半的后果只是一条警告(在 `-Werror` 下会变成错误)和不必要的重建,产物不受影响。 + +### 1.3 修法 + +- `deployment_target()` 改成与宿主无关的纯解析,优先级为 env > manifest > 14.0。`docs/04-mcpp-toml.md:703-711` 描述的正是这个优先级,而且没有提宿主限制。 +- 是否适用,由各个读者按 §1.2 的表格分别决定。 + - 三元组:去掉 `#if` 就够了,因为 `min_platform_version()` 只有在 Apple 的非 iOS 目标上才会走到这里。 + - 指纹:不要第二次调用解析函数,改为读取 `min_platform_version()` 的结果。`cache_key.cppm` 的 `minPlatformVersion` 槽已经是这样读的。 + - 平台事实:修好 `min_platform_version()` 后自动正确。 + - `hostflags.cppm:513`:条件改成「目标是 macOS」。 + - build.mcpp 宿主编译:保持按宿主判定。 +- 后续事项(未核实):默认值 14.0 的依据,是官方 LLVM 静态 libc++ 归档的 floor。如果 libc++ 来自这些预构建归档,而 deployment target 设得低于 14.0,产物头部声明的最低系统版本就会低于它所含目标文件的实际要求。mcppls 用的是依赖图里从源码编译的 libc++,不受影响。当 `cxx.prebuilt()` 成立、请求值又低于归档 floor 时要不要警告,需要单独决定。 + +### 1.4 判据 + +- 单测:在任何宿主上,解析函数对同一组 (env, manifest) 给出相同的结果。 +- Linux 宿主上的纯计划测试,不做完整构建: + - `--target aarch64-macos`,manifest 写 `11.0`,`build.ninja` 里出现 `--target=arm64-apple-macos11.0`; + - 改成 `12.0` 后指纹变化; + - 再设 `MACOSX_DEPLOYMENT_TARGET=13.0`,env 优先生效。 +- 在有 macOS 交叉链的环境里(例如 mcppls 的发布流水线):用 llvm 载荷自带的 `llvm-objdump` 检查,产物的 `LC_BUILD_VERSION` 为 `minos 11.0`。 +- 反方向:macOS 宿主构建 `x86_64-linux-musl`,编译命令里不出现 `-mmacosx-version-min`。 + +### 1.5 同类位置 + +这类缺陷的形状是「用宿主分支回答目标问题」。`src/` 与 `modules/` 中共有 107 处 `platform::is_{macos,linux,windows}`,以及 96 处 `#if defined(__APPLE__ | _WIN32 | __linux__)`。 + +- 在 `modules/platform`(进程、文件系统、环境变量)里,按宿主判定是对的。 +- 在构建与工具链代码里,出现次数较多的是 `src/toolchain/registry.cppm`(19)、`src/build/ninja_backend.cppm`(17)、`src/build/prepare.cppm`(13)、`src/build/flags.cppm`(7)、`src/build/plan.cppm`(7)。这些文件里的分支值得逐条标注:它回答的是宿主问题还是目标问题。 + +--- + +## 2. #687:include-fixed 里冻结的 pthread.h,以及一个从未生效的修复 + +### 2.1 复现(测量) + +- 本机 `xim-x-gcc/13.3.0` 与 `15.1.0` 的 `lib/gcc/x86_64-linux-gnu//include-fixed/` 下各有一份 `pthread.h`。两份逐字节相同,大小 48635 字节,文件头写着 `auto-edited by fixincludes from "/home/xlings/.xlings_data/subos/linux/usr/include/pthread.h"`。`16.1.0` 的这个目录里只有 `README`。 +- 用 mcpp 实际使用的参数(`-idirafter /include -idirafter /include`)编译 `#include `:gcc 13.3.0 与 15.1.0 都报 `cannot convert '' to 'unsigned int'`(15.1.0 报在 `bits/std_mutex.h:208:32`),16.1.0 编译通过,与 issue 一致。 +- 头文件搜索顺序依次是 `…/15.1.0/include`、`…/15.1.0/include-fixed`、glibc 2.44、linux-headers。在载荷的副本里删掉 `include-fixed/pthread.h` 后,同一个翻译单元可以编译通过。 +- fixincludes 在这份文件上应用的规则是 `pthread_incomplete_struct_argument`,它把 glibc 的 `struct __jmp_buf_tag __env[1]` 改写成 `*__env`。glibc 2.44 的 `pthread.h:773` 仍然是 `__env[1]`,所以这条规则对 2.44 同样会触发。真正的问题不在这条改写,而在于 fixincludes 复制了整份文件,把 2.39 的 `PTHREAD_COND_INITIALIZER` 也一起冻住了。 + +### 2.2 引擎为什么不能靠调整顺序解决(代码阅读) + +- `include-fixed` 在 gcc 内建的搜索链里,排在任何 `-idirafter` 和系统头目录之前。它本来就是为它所修正的那一份 libc 准备的。 +- mcpp 用 `-idirafter` 注入 glibc(`modules/toolchain-model/src/linkmodel.cppm:76-91`),是为了让 libstdc++ 的 `#include_next ` 链成立。换成 `-isystem`,glibc 会排到 libstdc++ 的包装头之前,这条链就断了。 +- `src/toolchain/hostflags.cppm:452-460` 还记录了一点:GCC 没有 clang `-nostdlibinc` 那样的隔离开关。 + +结论:只要载荷里有从另一份 libc 冻结下来的头文件,引擎怎么排参数都绕不过它。修法只能落在载荷上。 + +### 2.3 修复早已存在,但从未作用到载荷上(测量) + +openxlings/xim-pkgindex 的提交 bd4bff2(2026-08-08,#563,对应 #560)给 `pkgs/g/gcc.lua` 加了 `__prune_stale_fixincludes()`,由 `install()` 调用(`gcc.lua:129`)。它的注释把根因写得完全正确,还明确指出这个问题会在每一次 glibc 升级时重现。问题出在命令的拼法(`gcc.lua:230-232`): + +```lua +local out = os.iorun(string.format( + "sh -c 'grep -rlF %s %s 2>/dev/null || true'", + __shq(banner), __shq(root))) +``` + +`__shq()` 本身会用单引号包住参数,外层又是 `sh -c '…'`,拼出来的命令是: + +``` +sh -c 'grep -rlF 'auto-edited by fixincludes' '/…/lib/gcc' 2>/dev/null || true' +``` + +外层 shell 会把它拆成下面几个词: + +1. `sh` +2. `-c` +3. `grep -rlF auto-edited` +4. `by` +5. `fixincludes /…/lib/gcc 2>/dev/null || true` + +内层 shell 真正执行的只有第 3 个词,也就是 `grep -rlF auto-edited`,没有文件参数。GNU grep 在带 `-r` 又没有文件参数时,会递归搜索当前工作目录。`os.iorun` 有两个实现,libxpkg `src/lua-stdlib/prelude.lua:182` 用的是 `io.popen`,`src/xpkg-executor.cppm:311-339` 用的是 `std::system`,两者都经过 `/bin/sh`,所以上述结论对两者都成立。 + +我把这个函数原样放进 Lua 5.4 执行(`__shq` 和格式串逐字复制),得到三个结果: + +- 工作目录是空目录时,输出为空,`pthread.h` 保留。 +- 工作目录里包含载荷副本时,返回的是相对于工作目录的路径,连模拟脚本自身也被匹配到。 +- 在 mcpp 仓库根目录执行时,这条命令 10 秒内没有结束,因为它在递归扫描整个仓库。 + +也就是说,这个修复做不做事取决于安装钩子的工作目录,通常情况下什么都不做。如果安装时的工作目录很大(例如 `$HOME`),这一步还会变成一次完整的递归扫描。 + +注释里另有两处与实现不符: + +- 注释说「os.iorun raises on a non-zero exit」,但两个实现都不会抛出。 +- 注释说「the recipe would report success having pruned nothing」。实际正是如此,只不过原因不是注释设想的那个。 + +`tests/g/test_gcc.py` 里也没有针对这个函数的测试。 + +这就是 #687 在 2026.9.21.3 上仍能复现的原因。按文件 mtime,本机的 15.1.0 在 08-12 安装、13.3.0 在 08-26 安装,都晚于修复合入;已安装目录里的 `.xpkg.lua` 副本与当前的 `gcc.lua` 逐字节相同;`pthread.h` 仍然存在。不管安装时读到的是哪一版配方,上面的模拟已经说明,这段代码在 sh 下不会删掉这个文件。 + +### 2.4 为什么 16.1.0 没有这个文件(部分未核实) + +三份载荷的 `Configured with:`(测量): + +| 版本 | `--prefix` / `--with-sysroot` | libsanitizer | +|---|---|---| +| 13.3.0、15.1.0 | `/home/xlings/.xlings_data/…`,sysroot 为 `…/subos/linux` | disable | +| 16.1.0 | `/home/speak/workspace/github/openxlings/xim-pkgindex-fromsource/.xlings/…`,sysroot 是该目录下的 project subos;配置行带 `(reconfigured)` | enable | + +- 已排除的解释:15.1.0 与 16.1.0 的 `install-tools/fixincl` 里都有 `pthread_incomplete_struct_argument` 这条规则,所以差别不是 GCC 16 去掉了它。 +- 未核实的部分:16.1.0 的构建 sysroot 现在是一个符号链接目录,`usr/include/*.h` 指向 `xim-x-glibc/2.44/include`。fixincludes 会不会跳过符号链接,构建当时这个目录里是什么内容,都没有查证。 +- 可以确定的部分:16.1.0 干净,是构建环境造成的结果,不是配方里的决定。配方(d2learn/xim-pkgindex-fromsource 的 `pkgs/g/gcc.lua`)固定用 glibc 2.39 构建,没有任何处理 include-fixed 的步骤。 + +### 2.5 影响面 + +- **受影响**:`gcc@13.3.0`、`gcc@15.1.0`,目标 `x86_64-linux-gnu`,实现的 glibc 为 2.44。凡是用到 libstdc++ 线程原语的 C++ 翻译单元都会编译失败。冻结的头文件来自 glibc 2.39,而 `gcc.lua` 的注释在 2026-08-08 就记录了 sysroot 已经是 2.44,所以这个组合至少从那时起就是坏的。 +- **未测量**:`gcc@11.5.0`、`gcc@9.4.0`。它们在索引里,但本机没有安装。13.3.0 与 15.1.0 出自同一台构建机,这两个版本很可能同样受影响。 +- **不受影响(测量)**: + - `musl-gcc` 13.3.0/15.1.0/16.1.0 的 include-fixed 里只有 README。 + - llvm 载荷的 `.cfg` 在安装时被改写成本机路径,没有发现构建机路径泄漏。 +- **其他构建机路径(测量,不影响构建)**:`lib64/*.la`、`plugin/include/configargs.h`、`install-tools/mkheaders.conf` 里也有构建机路径。mcpp 不经 libtool 链接,也不构建 gcc 插件,所以用不到这些文件。 +- **为什么没被发现**:CI 只使用 gcc 16.1.0 和 musl-gcc,「13.3.0 / 15.1.0 × glibc 2.44」这一格从来没有被编译过。 + +### 2.6 修法 + +按见效快慢排序: + +1. **修正配方**(xim-pkgindex,对新安装立即生效)。 + - 引号只保留一层:去掉外层的 `sh -c`,或者去掉内层的 `__shq`。如果去掉的是外层,末尾就不能保留 `|| true`:executor 实现在命令后追加的 `> ""` 只会作用于 `true`,grep 的输出不会被捕获(实测,见设计文档 `2026-09-24-toolchain-selection-and-payload-trust-design.md` §4)。 + - 零命中时写一行 info 日志,说明搜索的根目录,这样「没有要删的」和「命令没跑到」在日志里能区分开。 + - 在 `tests/g/test_gcc.py` 里加断言:安装 13.3.0 或 15.1.0 之后,`include-fixed` 下没有带 fixincludes 横幅的文件。这个断言必须在修复被撤回时变红。 +2. **已经安装的用户。** 索引更新不会让 xlings 重跑 `install()`,所以需要显式重装:先 `mcpp index update`,再 remove + install。 + - mcpp 侧建议在 `mcpp self doctor` 和工具链解析时加一项检查:如果所选 gcc 的 `include-fixed` 里有带 fixincludes 横幅的文件,而横幅里的源路径不属于当前实现的 libc,就打印原因和重装命令。 + - 删除动作本身留在配方里。它针对的是一个具体载荷,按「引擎只做通用能力」的规则不进引擎。 +3. **在构建端根治**(与 §4 一起做)。配方在 `make install` 之后清掉 `include-fixed` 中带横幅的文件,或者用 GCC 官方的 `mkheaders` 针对目标 sysroot 重新生成。重新打包的载荷不能沿用原来的资产名,因为 GitCode 上的资产既不能替换,也不能删除。 +4. **mcpp-index#464 的验证。** 配方修好、载荷重装之前,在 gcc 15 上的验证还会遇到这个问题。issue 里用的临时办法(在一个 `-isystem` 目录里放一个只 `#include` glibc 2.44 `pthread.h` 的替身)只适合做验证,不要放进任何包里。 + +--- + +## 3. Windows 上 clang 与 MSVC STL + +### 3.1 现状(代码阅读,关键点已测量) + +- **默认工具链。** 检测到可用的 MSVC 时,默认是 `llvm@20.1.7`,目标 `x86_64-windows-msvc`;检测不到时,默认改成 `gcc@16.1.0`,目标 `x86_64-windows-gnu`(`modules/toolchain-model/src/triple.cppm:1049-1055`)。 +- **clang 这一行的依赖从哪来。** STL、vcruntime、CRT、UCRT 和 Win32 SDK 都由 clang 驱动自己去探测机器上的 Visual Studio(vswhere、注册表、环境变量),mcpp 不传任何路径。`import std` 用的 `std.ixx` 由 `msvc::find_std_module_source()` 通过系统搜索链定位(`src/toolchain/clang.cppm:202-217`)。 +- **受管载荷没有接到 clang 上。** `xim:msvc`(14.44.35207 为 latest,14.52.36629 为 Insiders 版)和 `xim:windows-sdk`(10.0.26100)已经发布,并有 GitCode 镜像。但 mcpp 里的 `resolve_sdk_for()`、`envOverrides` 这些逻辑只有 `cl.exe` 路径在用,`clang.cppm` 一处都没有引用。所以即使装了受管 MSVC,clang 这一行也不会用它。 +- **Linux 宿主不能交叉编译到 `x86_64-windows-msvc`。** 这两个包的描述文件都只有 `xpm.windows` 一节,`src/toolchain/lifecycle.cppm:703-709` 也写明了这一点。 +- **文档的相关表述。** `docs/20-toolchains.md:1190-1199` 说明 clang 这一行的 CRT 固定是静态的 `libcmt`,`cxx_runtime` 在这一行不生效。`docs/20-toolchains.md:666` 把 MSVC 和 SDK 列为宿主依赖,理由写的是「不可再分发」。 + +### 3.2 clang 在 MSVC ABI 下依赖什么 + +| 组件 | 内容 | 当前来源 | 在受管载荷中的位置 | +|---|---|---|---| +| C++ 标准库 | MSVC STL 头、`modules/std.ixx`、`msvcprt.lib` / `libcpmt.lib` | 机器上的 VS | `xim:msvc` 的 `VC/Tools/MSVC//{include,lib/x64,modules}` | +| 语言运行时 | vcruntime 头、`vcruntime.lib` / `libvcruntime.lib` | 机器上的 VS | 同上 | +| C 运行时 | `libcmt.lib` / `msvcrt.lib`、`oldnames.lib` | 机器上的 VS | 同上 | +| UCRT | `ucrt` 头、`ucrt.lib` | 机器上的 Windows SDK | `xim:windows-sdk` 的 `Include//ucrt`、`Lib//ucrt/x64` | +| Win32 API | `um`、`shared` 头,`kernel32.lib` 等 | 机器上的 Windows SDK | 同上的 `um`、`shared` | +| 编译器、链接器、资源编译器、builtins | clang、`lld-link`、`llvm-rc`、compiler-rt | llvm 载荷 | — | + +STL 不能单独拆出来:STL 头依赖同一个 toolset 里的 vcruntime 头,CRT 导入库也要与之配套。STL 源码虽然是 Apache-2.0 with LLVM exception 许可,vcruntime 和 CRT 库却不是开源的。所以分发单位应当保持「toolset + SDK」,也就是现有的这两个包。 + +### 3.3 不绑定的代价 + +1. **用哪个 STL 由机器决定。** `docs/20-toolchains.md:987-1016` 记录过 clang + MSVC STL 14.51 下 `std::find` 编译失败的问题(microsoft/STL#6294、mcpp#609),文档给出的规避办法之一是「换一个 STL 早于 14.51 的 runner 镜像」。也就是说,选择 STL 的是 CI 镜像,不是项目。 +2. **STL 对 clang 有最低版本要求,VS 一升级就可能让固定的 llvm 版本编译失败。** 下表是从 microsoft/STL 各发布标签的 `yvals_core.h` 里读出的 `STL1000` 下限(测量): + + | STL 标签 | 要求的最低 clang | + |---|---| + | vs-2022-17.12 | Clang 17 | + | vs-2022-17.14(toolset 14.44) | Clang 19 | + | msvc-build-tools-14.50 | Clang 19 | + | msvc-build-tools-14.51 | Clang 20 | + | main(`_MSVC_STL_UPDATE 202609L`) | Clang 22 | + + Windows 默认的 `llvm@20.1.7` 刚好等于 14.51 的下限。main 分支的要求会随下一个 toolset 发布出来。到那时,只要机器上的 VS 自动升级,默认工具链就会在编译第一个翻译单元时报 `STL1000`,而项目本身什么都没改。 +3. **缓存键看不到 STL 版本**(代码阅读,未在 Windows 上实测)。`clang.cppm:189` 把 `stdlibVersion` 设成了 clang 自己的版本号。`cache_key.cppm` 里的头文件集合这一项取自编译命令的 token,而 clang 这一行的 STL 路径不出现在任何 token 里。std 模块的 BMI 另有 `std_module_source_hash` 保护,但依赖缓存键在 VS 升级前后是同一个值。 +4. **没有 VS 的机器上,默认工具链换成了另一种 ABI。** 回落到 `x86_64-windows-gnu` 之后,用户就不能链接 vcpkg 或其他 MSVC ABI 的 `.lib`。而一个可用的受管 MSVC 其实已经在索引里了。 + +### 3.4 所需机制已经具备(测量) + +- **clang 支持的参数。** clang 22.1.8 的 GNU 驱动接受 `-Xmicrosoft-visualc-tools-root`、`-Xmicrosoft-visualc-tools-version`、`-Xmicrosoft-windows-sdk-root`、`-Xmicrosoft-windows-sdk-version`、`-Xmicrosoft-windows-sys-root`。这些参数必须写成分开的两个参数;写成 `=` 连接的形式会报 unknown argument。clang-cl 对应的是 `/vctoolsdir`、`/winsdkdir`、`/winsysroot`。clang 20.1.7 的二进制里也有这些选项名,但没有逐个调用验证。 +- **本机实测。** 我按 `xim:msvc` 和 `xim:windows-sdk` 的目录布局建了一棵空目录树,然后用 `--no-default-config --target=x86_64-pc-windows-msvc` 调用 clang 22.1.8,得到的搜索路径和链接库路径如下: + +``` +#include <...> search starts here: + /lib/clang/22/include + /msvc/VC/Tools/MSVC/14.44.35207/include + /sdk/Include/10.0.26100.0/ucrt + /sdk/Include/10.0.26100.0/shared + /sdk/Include/10.0.26100.0/um + +-libpath:/msvc/VC/Tools/MSVC/14.44.35207/lib/x64 +-libpath:/msvc/VC/Tools/MSVC/14.44.35207/atlmfc/lib/x64 +-libpath:/sdk/Lib/10.0.26100.0/ucrt/x64 +-libpath:/sdk/Lib/10.0.26100.0/um/x64 +``` + +- **这次测量证明了什么、没证明什么。** 它只验证了驱动的路由:clang 能直接使用这两个载荷的目录布局,编译和链接两侧都会用到。用真实文件编译还需要在 Windows 上做。另外,这个路由与宿主无关,在 Linux 宿主上同样成立。 + +### 3.5 建议的做法 + +- **绑定。** 当 clang 这一行(`llvm@X` × `*-windows-msvc`)选定了一个受管 toolset,就在编译、scan-deps、std 模块预编译、链接这四处传入 §3.4 的参数。具体包括: + - `std.ixx` 从同一个载荷里取; + - `stdlibVersion` 记 toolset 的版本; + - `resolution.json` 记录 `msvc-stl@` 和 `ucrt@`。 + + 带版本号的路径进入编译 token 之后,§3.3 第 3 点的缓存键问题也随之解决。 +- **选择。** + - 默认值写在数据里,不写在引擎里:由索引记录 llvm 版本与 toolset 的配对,约束条件是 STL 要求的最低 clang 版本不高于该 llvm 版本。 + - 显式覆盖沿用已有的 `msvc@` 拼写,挂在目标行上。具体键名需要设计,例如 `[target.x86_64-windows-msvc] msvc = "14.44.35207"`,本文不做决定。 + - `msvc@system` 保留今天的行为(由 clang 自己探测),作为显式的逃生口。 + - 不新增 SDK 版本键,SDK 跟着 toolset 的依赖走。这是 2026-08-16 三轴设计 §2.4 已经做出的决定。 +- **在解析阶段拒绝不兼容的组合,不要等编译时报 `STL1000`。** 如果 llvm 版本低于所选 toolset 的 STL 下限,就在解析阶段报错,并列出兼容的 toolset。下限可以作为数据写进 `msvc.lua`,也可以在安装后从 `yvals_core.h` 里读出来。 +- **分阶段推进。** + - 第一阶段:允许显式选择,默认行为不变。 + - 第二阶段:检测不到 VS 时,默认改成 llvm + 受管 MSVC,而不是换一种 ABI。代价是首次下载约 225 MB(据 2026-08-16 设计文档的测量,14.44 toolset 83.5 MB,SDK 139.1 MB)。这一步由维护者决定。 + - 第三阶段:支持 Linux 宿主交叉到 `x86_64-windows-msvc`。前提有两个:一是 `windows-sdk` 要能在 Linux 上解开 MSI/CAB(xwin 的做法);二是要统一再分发的立场。`docs/20-toolchains.md:666` 写的是「不可再分发」,而索引已经在 GitCode 上镜像了这些 vsix 和 MSI/CAB,两者目前不一致,只能由维护者决定。 +- **判据。** 沿用现有那个屏蔽 VS 的 Windows CI job(`.github/workflows/ci-windows.yml:220` 运行 `182_windows_no_msvc_fallback.sh` 的那个): + - 正向:屏蔽 VS 后,用 llvm + 受管 toolset 构建一个 `import std` 程序,断言 `resolution.json` 里记录的 STL 身份。 + - 反向:当机器上的 VS 版本与所选 toolset 不同时,`-v` 输出的搜索列表里只出现载荷路径。 + +### 3.6 机器上有多个 MSVC 时怎么指定(2026-09-24 补充) + +> 本节提出的 `system@14.44` 写法已被取代:维护者提议「带版本号时先匹配系统,没有再用生态包」,设计文档 `2026-09-24-toolchain-selection-and-payload-trust-design.md` §2.2 采纳了这一方案。本节对现状的分析仍然有效。 + +**问题。** clang 这一行目前有两个互相独立的选择器:clang 驱动选头文件和库,mcpp 选 `std.ixx`。两者都读机器状态和 shell 环境,项目本身没有办法指定用哪一个。依据:LLVM llvmorg-22.1.8 的 `clang/lib/Driver/ToolChains/MSVC.cpp:450-460`、`llvm/lib/WindowsDriver/MSVCPaths.cpp`(两处都是代码阅读,20.1.7 的逻辑相同),以及 mcpp 的 `src/toolchain/msvc.cppm:329-468`。 + +| 步骤 | clang 选头文件与库(驱动内部) | mcpp 选 `std.ixx`(`find_std_module_source`) | +|---|---|---| +| 1 | 命令行上的 `/vctoolsdir`、`/winsysroot` | `VSINSTALLDIR` | +| 2 | 环境变量 `VCToolsInstallDir`,其次 `VCINSTALLDIR`,再次 `PATH` 上的第一个 `cl.exe` | `vswhere -latest -prerelease -products * -requires …VC.Tools.x86.x64` | +| 3 | SetupConfig:枚举全部实例(不要求装了 C++ 组件),取安装版本最高的一个,读它的 `Microsoft.VCToolsVersion.default.txt` | `VS*COMNTOOLS` | +| 4 | 注册表(VS 2015 及更早版本) | 固定路径扫描 | +| 实例内的 toolset | 由 `default.txt` 决定 | `VC/Tools/MSVC` 下按字符串比较最大的目录;不读 `VCToolsInstallDir`,也不读 `default.txt` | +| 头文件的其他来源 | 只要设了 `%INCLUDE%` 或 `%EXTERNAL_INCLUDE%`,就整体采用,跳过上面的探测 | — | +| SDK | 注册表 `KitsRoot10` 下版本最高的那个;不读 `WindowsSdkDir`(源码里标着 FIXME) | clang 这一行不使用 mcpp 的 SDK 解析 | +| 链接库路径 | 只要设了 `%LIB%` 就用它 | — | + +**由此导致版本不固定的五种情形**(代码阅读,未在 Windows 上实测): + +1. **两个选择器选中不同的 toolset,`std.ixx` 与头文件的版本不一致。** 例如用 `vcvarsall x64 -vcvars_ver=14.38` 打开 Developer Prompt:clang 读 `VCToolsInstallDir`,用 14.38 的头文件;mcpp 通过 `VSINSTALLDIR` 定位到同一个 VS 实例,再取其中最大的目录 14.44 下的 `std.ixx`。 +2. **最新的 VS 实例没有装 C++ 组件。** 例如装了 VS 2026 却没选 C++ 工作负载。clang 的 SetupConfig 会选中这个实例,读不到 `default.txt`,于是退到注册表,仍然找不到。mcpp 的 vswhere 带了 `-requires`,会找到旧实例里的 `std.ixx`。结果是 mcpp 认为 MSVC 可用,clang 却找不到标准头文件。 +3. **同一个项目、同一台机器,在普通 shell 和 Developer Prompt 里构建,用到的 toolset 和 SDK 可能不同。** +4. **安装新的 VS、VS 自动升级、安装新的 SDK,都会改变默认选择**,而项目本身没有任何改动。 +5. **这些选择既不进缓存键,也不进 `resolution.json`**(见 §3.3 第 3 点)。 + +所以要先把选择收拢到一处,然后才谈得上「怎么指定」。 + +**做法:mcpp 选一次,然后显式告诉 clang。** + +- **在四处传参。** mcpp 自己解析出三样东西:toolset 目录、SDK 根目录、SDK 版本。然后在编译、scan-deps、std 模块预编译、链接这四处传入 `-Xmicrosoft-visualc-tools-root`、`-Xmicrosoft-windows-sdk-root`、`-Xmicrosoft-windows-sdk-version`。 +- **这组参数的效果。** 它们是 `/vctoolsdir`、`/winsdkdir`、`/winsdkversion` 的别名(`clang/include/clang/Options/Options.td:9513-9521`,llvmorg-22.1.8)。带上之后,clang 不再读 `VCToolsInstallDir` 和 `%INCLUDE%`,不再走 SetupConfig;即使设了 `%LIB%`,也会输出显式的 `-libpath`。 +- **`std.ixx` 从同一个 toolset 目录里取。** +- **toolset 和 SDK 必须一起传。** 如果只传 toolset,clang 会因为命令行上出现了 `/vctoolsdir` 而不再读取 `%INCLUDE%`,SDK 就退回到注册表里版本最高的那个,vcvars 选定的 SDK 反而丢了。 +- **剩下的一处缺口。** `lld-link` 自己仍会把 `%LIB%` 追加到搜索路径的末尾(`lld/COFF/Driver.cpp:874`)。显式路径排在前面,只有显式路径里缺某个库时才会用到它。mcpp 可以在链接命令的环境里去掉 `LIB`,把这个缺口也关上。 + +**指定方式。** 下表是语义上的建议,具体键名和拼写留到设计阶段再定: + +| 写法 | 含义 | 固定程度 | +|---|---|---| +| `msvc@14.44.35207`(已有) | 受管载荷,精确到版本;SDK 使用载荷旁边的 `windows-sdk` | 跨机器固定 | +| `system@14.44`(新增) | 在机器上已安装的 toolset 里,选版本以 14.44 开头的最高一个。候选集合是 `vswhere -all -prerelease -products * -format json` 列出的所有实例,乘以各实例下的 `VC/Tools/MSVC/*`。找不到就拒绝构建,并列出机器上实际有哪些 | 在本机固定;换一台机器时,要求对方也装了同一个版本 | +| `system`(已有,继续作为默认) | 选中的 toolset 与今天 clang 自己会选的相同(先看环境变量,再取最新实例的 `default.txt`)。变化只在于:mcpp 选一次、传给 clang、并记录下来 | 不固定,但会打印出来并记录 | + +- **clang 这一行需要一个新键。** 在这一行,编译器是 `llvm@X`,选 MSVC 是另一件独立的事,所以要一个挂在目标行上的新键,例如 `[target.x86_64-windows-msvc] msvc = "14.44.35207"`。`cl.exe` 那一行可以直接在 `msvc@…` 上使用同样的值语法。 +- **项目写明了版本时,以项目为准。** shell 里的 `VCToolsInstallDir`、`VSINSTALLDIR`、`WindowsSdkDir` 会被忽略,并打印一行说明。三轴设计里,受管 toolset 忽略 `WindowsSdkDir` 时就是这样打印 note 的,这里沿用同一做法。 +- **`system@<前缀>` 与 `msvc@<版本>` 分开写,是为了保持「来源」这条轴清晰。** 受管 toolset 绑定载荷自带的 SDK;system toolset 的 SDK 仍然按机器扫描。两者的可复现程度不同,拼写上应该一眼就能看出来。 +- **解析结果写入 `resolution.json` 和缓存键。** `resolution.json` 记录 `msvc-stl@`、`ucrt@` 和来源。 + +**判据:** + +- **正向。** 用受管载荷在 CI 上制造多版本环境:Windows runner 本来就带 VS,再装上 `xim:msvc` 14.44 和 14.52。分别指定 `14.44.35207`、`14.52.36629`、`system`,检查三点:`-v` 输出的搜索列表里只有所选的目录;`resolution.json` 记录了对应的版本;切换版本后指纹随之变化。 +- **环境干扰。** 在用 `vcvarsall -vcvars_ver=<另一个版本>` 打开的环境里再跑一遍。项目写明了版本时,结果不变,并打印忽略说明。 +- **反向。** 撤回修复、去掉显式参数后,测试要能区分出「`std.ixx` 与头文件版本不一致」的情形。 + +--- + +## 4. 工具链管理的规范化 + +### 4.1 回答 + +需要。规范化的对象是**载荷的生产与验收**,具体回答三件事: + +- 这个载荷是谁、在什么环境里、用哪一版配方构建的; +- 它必须满足哪些条件; +- 发布前由什么程序来检查。 + +引擎这一侧已经有描述文件 `.mcpp-toolchain.json`(`src/toolchain/registry.cppm:248-279`,现有字段为 `schema`、`frontend`、`platform_floor`、`std_module_defines`、`runner`),可以用来承载这些信息。 + +### 4.2 依据:根因在载荷生产或安装阶段的缺陷 + +| 缺陷 | 类别 | +|---|---| +| #687:冻结的 `pthread.h`,以及从未生效的清理代码 | 构建机的 libc 内容进入载荷;修复没有验证 | +| gcc 的 `specs` 被历次安装累积改写(2026-08-10) | 安装时就地改写共享状态 | +| 安装后二进制被就地 patch:同一版本的十二个副本有十二个不同的 sha256 | 已安装文件无法与发布物比对 | +| riscv64 musl 载荷的 sysroot 缺内核 UAPI 头(PR#402) | 载荷内容不完整 | +| llvm 22.1.8 slim 载荷漏带 `libatomic.so.1` | 载荷内容不完整 | +| 私有 loader 的默认搜索路径是 glibc 载荷构建时的前缀 | 构建机路径进入载荷 | +| llvm 22.1.8 载荷没有 iOS builtins | 载荷覆盖哪些目标没有声明 | +| 自建载荷的闭包检查输出为空,却被判为通过 | 验收程序本身不可信 | +| 16.1.0 与 13.3.0/15.1.0 来自两套构建环境,配置不同(libsanitizer 开关相反) | 构建不可复现,载荷不记录来源 | + +这些缺陷有一个共同点:载荷对不对,只有等到消费方(mcpp 自己的构建或用户项目)编译失败时才能发现。 + +### 4.3 具体做法 + +1. **载荷契约。** 写进 `docs/specs/`,每一条都要能由程序检查: + - 载荷可重定位,不含构建机的绝对路径。`.la`、`configargs.h` 这类不参与构建的文件可以列为豁免。 + - `include-fixed` 里不含从 libc 冻结下来的头文件。 + - `specs` 和 `.cfg` 保持发布时的内容,安装时不就地改写。确实需要的改写放进单独生成的覆盖文件,例如 mcpp 已经在用的 `-specs=` 形式。 + - 描述文件要声明构建所用的 libc 及其版本下限、适用的目标行,以及是否带 sanitizer 等能力。 +2. **配方即代码,并记录来源。** + - 每个载荷都由一份检入仓库的配方,在固定的构建容器里产出。 + - 构建 sysroot 取自索引发布的 glibc 载荷,而不是某台机器上的 subos。 + - 产物带上来源记录,写进 `.mcpp-toolchain.json`:配方仓库与提交、`Configured with`、上游源码的 sha256、构建所用的 libc。 + - 可复现分三级:能从配方重新构建;能用程序验收;逐字节可复现。前两级是必须的;第三级(`SOURCE_DATE_EPOCH`、`-ffile-prefix-map`、确定性 tar)可以以后再做。 +3. **载荷 lint。** 同一个程序在两处运行:发布前在配方仓库或索引的 CI 里跑,`mcpp self doctor` 也对已安装的载荷跑一次。 + - 检查项:构建机路径扫描、include-fixed 横幅、ELF 的 `PT_INTERP` 与 `RUNPATH`、`specs` 和 `.cfg` 与发布物一致。 + - 每一项都要配一个反向测试:把缺陷放回去,这一项就要变红。§2.3 的修复正是因为缺了这一步,才会看起来生效、实际从未生效。 +4. **工具链 × C 库兼容矩阵。** 在索引 CI 里,从索引本身枚举两个维度:一边是 gcc 和 llvm 的各个版本,另一边是该目标行可解析的各个 libc 版本。每一格编译 ``、``、``,再加一个 `import std` 的最小程序。 + - 格子必须从索引枚举,不能靠手写清单。#687 那一格(gcc 15.1.0 × glibc 2.44)正是手写清单里不会出现的组合。 + - 同一个矩阵也覆盖 §3 里 llvm × msvc toolset 的配对。 +5. **发布顺序。** 改动 glibc 绑定或 `latest` 之前,先让矩阵在新版本上全部通过。这就是「消费者先发布,`latest` 后移动」这条规则在工具链这一层的应用。 + +各部分的归属: + +- 配方和矩阵放在配方仓库与索引 CI。 +- 契约文档和描述文件字段放在 mcpp,因为 mcpp 是消费方。 +- lint 程序最好做成独立工具,两边共用。 +- 引擎只读取描述文件,并在 doctor 里报告结果。 + +--- + +## 5. 建议顺序 + +1. **#687 的配方修正和测试**(xim-pkgindex),同时给 mcpp doctor 加检查。改动小,对新安装立即生效,也能解除 mcpp-index#464 在 gcc 15 上的阻塞。 +2. **#685。** mcpp 侧改动小,mcppls 的下一个版本在等它。 +3. **规范化的第 3、4 项**(lint 和兼容矩阵)。它们能直接防止 #687 这类问题再次出现,也为下面第 4 步提供验收手段。 +4. **Windows clang 的 MSVC 选择收拢为一处(§3.6)**,同时完成 §3.5 的第一阶段。这一步不改变默认结果,只让选择固定下来、可以指定、可以记录。需要先出一份设计,确定键名、`system@<前缀>` 的拼写、配对数据放在哪里,以及四处参数如何注入。 +5. **规范化的第 1、2 项**(契约和配方重建),以及 Windows 的第二、三阶段。 + +## 6. 需要维护者决定的事项 + +维护者于 2026-09-24 答复如下: + +| 事项 | 答复 | +|---|---| +| 已经安装了 gcc 13.3.0 或 15.1.0 的用户,是主动通知重装,还是只靠 doctor 提示 | 暂不通知 | +| 检测不到 VS 时,Windows 的默认工具链是否从 `x86_64-windows-gnu` 改成 llvm + 受管 MSVC | 不改。§3.5 的第二阶段因此搁置;§3.6 里的 `system` 默认值也保持今天的选择结果 | +| MSVC toolset 和 Windows SDK 的再分发立场(文档与 GitCode 镜像的说法不一致) | 未答复,保持现状 | +| 载荷配方和构建 CI 放在哪个仓库 | 以后专门讨论 | + +维护者同时指出:Windows clang 这部分的核心问题是,机器上装有多个 MSVC 版本时如何指定,否则所用版本不固定。§3.6 专门回答这个问题。 + +--- + +## 附:测量记录 + +| 结论 | 做法 | +|---|---| +| 冻结的头文件使 `` 编译失败;删掉后编译通过 | 从 mcpplibs/mcpp-index `tests/examples/cli11/compile_commands.json` 取 gcc 15.1.0 的实际参数,`-fsyntax-only` 编译 `#include `;再在载荷副本里删掉 `include-fixed/pthread.h`,重跑同一条命令 | +| 清理函数在 sh 下什么都不删 | 把 `gcc.lua` 的 `__shq` 和格式串逐字复制进 Lua 5.4,加上 libxpkg 的 `os.iorun`(`io.popen`),分别在空目录、载荷副本目录、mcpp 仓库根目录下执行 | +| 应用的是哪条 fixincludes 规则,两个版本都有这条规则 | 对比冻结头与 glibc 2.44 头中的 `__jmp_buf_tag` 声明;对 15.1.0、16.1.0 的 `install-tools/fixincl` 执行 `strings \| grep pthread_incomplete_struct_argument` | +| 三个 gcc 载荷的构建来源 | 各载荷 `bin/gcc -v` 的 `Configured with:` | +| clang 接受受管载荷的布局 | 按 `xim:msvc` / `xim:windows-sdk` 布局建空目录树,用 clang 22.1.8 带 `--no-default-config --target=x86_64-pc-windows-msvc -Xmicrosoft-*` 调用 `-v -fsyntax-only` 和 `-###` | +| 在非 Apple 目标上,`-mmacosx-version-min` 只产生警告 | clang 22.1.8,`--target` 分别为 `x86_64-linux-musl`、`wasm32-wasi`、`aarch64-linux-android24`,`-fsyntax-only` | +| STL 要求的最低 clang 版本 | `gh api repos/microsoft/STL/contents/stl/inc/yvals_core.h?ref=` 读取 `STL1000` 行 | +| clang 的 MSVC 与 SDK 选择顺序、`-Xmicrosoft-*` 的别名关系(代码阅读) | llvmorg-22.1.8 的 `clang/lib/Driver/ToolChains/MSVC.cpp`、`llvm/lib/WindowsDriver/MSVCPaths.cpp`、`clang/include/clang/Options/Options.td`、`lld/COFF/Driver.cpp`;20.1.7 的 `MSVCPaths.cpp` 与 22.1.8 的差异只在命名空间写法 | diff --git a/.agents/docs/2026-09-24-toolchain-selection-and-payload-trust-design.md b/.agents/docs/2026-09-24-toolchain-selection-and-payload-trust-design.md new file mode 100644 index 00000000..1fd0c922 --- /dev/null +++ b/.agents/docs/2026-09-24-toolchain-selection-and-payload-trust-design.md @@ -0,0 +1,408 @@ +--- +subject: design +status: active +--- + +# MSVC toolset 的选择、#685、#687 与工具链管理规范:总体设计 + +- 日期:2026-09-24(v2,同日修订;与 v1 的差异见 §12) +- 依据:`2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md`(下称「分析」)。分析里已经论证过的内容,本文只引用。 +- 规范:`docs/specs/toolchain-management.md`(SPEC-006,草案 v0.1),与本文同时写成。 +- 基线:mcpp `origin/main` b30e70c4 +- 状态:待 review。 + +## 0. 维护者的决定 + +| 事项 | 决定 | +|---|---| +| 已安装 gcc 13.3.0 / 15.1.0 的用户 | 不主动通知;只在 doctor 里诊断 | +| Windows 默认工具链 | 不变 | +| MSVC 再分发立场 | 保持现状 | +| 生态包的标记 | 用 `xim:` 前缀表示只取生态包 | +| STL 对 clang 的版本下限 | 不做预检查,等编译期的 `STL1000` 报错 | +| `cl.exe` 行的边界变化 | 只要比现在更好,就接受 | +| 载荷规范化 | 先写一份核心 spec,暂不实现;等 LLVM 发新版本、一起加入新工具链时,再实现、验证和调整。配方与构建 CI 放在哪个仓库,以后再定 | + +## 1. 摘要 + +| 部分 | 设计要点 | 仓库 | 时机 | +|---|---|---|---| +| A. MSVC toolset 的选择 | 在 MSVC ABI 目标上,**编译器是工具链,MSVC toolset 是 sysroot**。clang 行用 `[target.x86_64-windows-msvc] sysroot = "msvc@…"` 指定;带版本时先匹配机器上的 toolset,`xim:` 前缀表示只取生态包。mcpp 只选一次,把结果显式传给 clang,同时打印并记录 | mcpp | review 通过后 | +| B. #685 | 计算 deployment target 时不再看宿主;是否适用按目标判定 | mcpp | 先做 | +| C. #687 | 修正 `gcc.lua` 的引号并补测试;mcpp doctor 给出诊断 | xim-pkgindex、mcpp | 先做 | +| D. 工具链管理规范 | 写成 SPEC-006 草案;实现跟随下一批 LLVM 工具链 | mcpp(spec),生态仓库(实现,待定) | spec 现在写,实现以后做 | + +## 2. 原则 + +1. **一个问题只有一个回答者。** 选择在 prepare 阶段做一次,结果存进 `Toolchain`,所有读者读这个结果。 +2. **声明压过探测,探测的结果必须看得见。** +3. **目标的属性按目标判定。** 只有在宿主上运行的东西按宿主判定。 +4. **每个修复都要有判据:把修复撤回,判据就变红。** + +--- + +## 3. A 部分:MSVC toolset 的选择 + +### 3.1 语义:编译器是工具链,MSVC 是 sysroot + +v1 给 clang 行设计的是一个叫 `msvc` 的新键,读起来分不清这一行到底是 clang 还是 msvc。问题的根源是把两条轴混在了一起: + +- **工具链轴**:由谁来编译。可以是 `cl.exe`,也可以是 clang。 +- **目标轴**:对着什么编译。三元组 `x86_64-windows-msvc` 的第三段本身就说明了这一点:对着 MSVC 环境编译,包括 STL、CRT、UCRT 和 Win32 SDK。 + +mcpp 已经有目标轴上现成的写法:`[target.].sysroot`。SPEC-004 §4.1 把它定义为目标轴的条目,含义是「被编译对着的东西」。LLVM 也用同样的名字称呼这个目录:clang-cl 的 `/winsysroot`,以及 GNU 驱动的 `-Xmicrosoft-windows-sys-root`。所以: + +```toml +[toolchain] +windows = "llvm@22.1.8" # 编译器:clang + +[target.x86_64-windows-msvc] +sysroot = "msvc@14.44.35207" # clang 对着编译的 MSVC 环境 +``` + +读法是「编译器 llvm 22,sysroot 为 MSVC 14.44」。这与 Linux 上「编译器 gcc,sysroot 为 glibc」是同一个结构。 + +`cl.exe` 行没有第二个选择:编译器本身就是 toolset,它的 sysroot 就是它自己。 + +```toml +[toolchain] +windows = "msvc@14.44.35207" # 编译器与 sysroot 是同一个 toolset +``` + +`cl.exe` 行如果另外写了 `sysroot`,并且与编译器不是同一个 toolset,就拒绝构建。 + +### 3.2 写法 + +同一套写法用在两个位置:`cl.exe` 行的工具链字符串,和 clang 行的 `sysroot`。 + +| 写法 | 含义 | +|---|---| +| `msvc@system`;clang 行不写 `sysroot` 时等同于此 | 机器上的默认 toolset,顺序见 §3.4 | +| `msvc@14.44.35207` | 先在机器上找目录名与之完全相同的 toolset;找不到就用生态包,未安装时自动安装 | +| `msvc@14.44` | 按版本分量做前缀匹配。机器上有就取匹配的最高版本,没有就取生态包里匹配的最高版本 | +| `xim:msvc@14.44.35207` | 只取生态包,不看机器 | + +规则: + +- **前缀按分量匹配。** `14.4` 匹配 `14.4.x`,不匹配 `14.44`。「最高」按数字元组比较,不按字符串比较。今天的 `find_latest_msvc_tools()` 用的是字符串比较。 +- **`xim:` 用在 `sysroot` 里**,就是这个键现有的 xpkg 引用语法(`:@`)。不带命名空间的 `msvc@…` 是新增的形式,只在 `*-windows-msvc` 行上有效。 +- **`xim:` 用在工具链字符串里**,需要解析器支持去掉这个前缀。gcc、llvm 等族没有系统来源,对它们来说 `xim:` 与不带前缀等价。 +- **只有 MSVC 有系统来源。** `gcc@system` 继续被拒绝。 + +「先匹配系统」之所以可行:完整版本号对应微软的同一次构建,`xim:msvc` 解包的是 VS 自己的 channel manifest 里的同一批 vsix,所以两种来源的 toolset 内容按构造相同。这一点尚未逐字节核对,放在 §3.10 的判据里。 + +### 3.3 来源不同带来的差异 + +| 差异 | 处理 | +|---|---| +| SDK 跟随来源:生态包用载荷自带的 `windows-sdk`,系统来源用机器扫描的结果 | 沿用 `resolve_sdk_for()`。SDK 版本以 `ucrt@` 进入缓存键,并在构建开头打印。要连 SDK 一起固定,用 `xim:` | +| 部分版本在不同机器上可能解析到不同的补丁版本 | 打印解析出的完整版本;文档建议 CI 写完整版本,或写 `xim:` | +| `msvc@<版本>` 过去一律用生态包,现在机器上有同版本就用机器的 | 写进迁移说明(§3.8) | +| 机器上匹配到的 toolset 不完整:缺 `lib/x64`;需要 `import std` 时缺 `modules/std.ixx`;`cl.exe` 行缺 `cl.exe` | 不完整的不算匹配,跳过并打印一行说明,然后回落到生态包 | + +### 3.4 候选集合与解析算法 + +**系统候选。** 取以下来源的每个实例: + +- `vswhere -all -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -format text -utf8`(文本格式,理由见 §11); +- `VSINSTALLDIR` / `VCINSTALLDIR` 指向的实例; +- 没有 vswhere 时,扫描固定路径。 + +每个实例再展开其下的 `VC/Tools/MSVC/`,记下实例路径、toolset 版本、产品名,以及它是否为 `VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt` 指定的默认版本。加 `-utf8` 是因为用户名里可能有非 ASCII 字符。 + +**生态包候选。** 已安装的 `xim-x-msvc/`,以及索引里可安装的版本。 + +**解析函数 `select_toolset()` 是纯函数。** 输入是候选集合、环境变量快照、写法,以及本次构建的需求(是否要 `cl.exe`、是否要 std 模块)。输出是 `MsvcSelection`(来源、toolset 目录、版本、实例与产品名、SDK、说明文字),或者一个列出全部候选的错误。只有枚举候选这一步依赖 Windows。 + +**`msvc@system` 按顺序取第一个完整的候选:** + +1. `VCToolsInstallDir` 指向的 toolset; +2. `VSINSTALLDIR` / `VCINSTALLDIR` 所指实例的默认 toolset; +3. `PATH` 上 `cl.exe` 所在的 toolset; +4. 带 C++ 组件、且 `installationVersion` 最高的实例的默认 toolset; +5. 固定路径扫描(只在没有 vswhere 时)。 + +这就是 clang 驱动和 mcpp 两条现有选择链合并后的顺序。 + +**带版本的写法:** + +1. 在系统候选里找完整且匹配的,取最高的; +2. 找不到就在生态包候选里找(`xim:` 写法直接从这一步开始),已安装的优先,其次从索引安装; +3. 仍然没有就报错,列出三类候选。 + +写了版本时,`VCToolsInstallDir`、`VSINSTALLDIR`、`WindowsSdkDir` 都不参与选择;如果它们本来会选出别的结果,打印一行说明。 + +**结果只算一次。** 在 prepare 里算出,存进 `Toolchain` 的只读字段:`msvcToolsDir`、`msvcToolsVersion`、`msvcOrigin`、`windowsSdkRoot`、`windowsSdkVersion`,写法与 `appleSdkRoot` 相同。 + +**原生构建也读得到。** `sysroot` 按实际构建目标去查目标行(`src/build/prepare.cppm:1371-1391` 的 `find_target_entry` / `sysroot_override`,调用处在 `:10682`),所以在 Windows 上直接 `mcpp build` 也会命中 `[target.x86_64-windows-msvc]`。目标行上的 `toolchain` 则只按 `--target` 去查(`:2801`)。 + +build.mcpp 在 Windows 宿主上的宿主编译,读的是宿主三元组那一行,也就是同一行。 + +### 3.5 交给编译器 + +**`cl.exe` 行。** 机制不变,仍由 `build_env_for_cl()` 合成 `INCLUDE` / `LIB`,只是输入改为 `MsvcSelection`。 + +**clang 行。** + +- **注入位置。** `modules/toolchain-model/src/linkmodel.cppm:373` 目前对 MSVC 目标返回空模型,改为返回三组 token,每组都是分开的两个参数: + - `-Xmicrosoft-visualc-tools-root ` + - `-Xmicrosoft-windows-sdk-root ` + - `-Xmicrosoft-windows-sdk-version ` + + 这些 token 经由已有的读者到达各处: + - `host_compile_tokens`(`src/toolchain/hostflags.cppm:247`),它服务普通编译、std 模块预编译和 build.mcpp 宿主编译; + - 缓存键(`src/build/cache_key.cppm:499`); + - 链接一侧(`hostflags.cppm:581` 的 `host_link_tokens`,以及 `src/build/flags.cppm:1684` 起的 PeLld 分支)。 + + MinGW 分支继续返回空模型。 +- **scan-deps。** 它读到的是否就是这条编译命令,实现时要核对。 +- **`std.ixx`** 从 `/modules/std.ixx` 取(`src/toolchain/clang.cppm:202-217`)。`stdlibVersion` 改记 toolset 版本(`clang.cppm:189`)。 +- **toolset 和 SDK 必须一起传。** 否则 clang 会放弃 `%INCLUDE%`,SDK 退回注册表里的最高版本。 +- **链接命令的环境里去掉 `LIB`**,这样就关上了 `lld-link` 的剩余路径(`lld/COFF/Driver.cpp:874`)。 +- **路径含空格。** 系统 toolset 一定在 `C:\Program Files\…` 下,token 必须经过现有的转义渲染。 + +这组参数都是 `/vctoolsdir` 等选项的别名(`Options.td:9513-9521`,llvmorg-22.1.8)。它们的路由效果已经在本机实测过(分析 §3.4)。 + +### 3.6 记录与可见性 + +- **构建开头打印一行**,例如:`MSVC 14.44.35207 (system: Visual Studio 2022 Community) · Windows SDK 10.0.26100.0 (machine)`。 +- **`resolution.json`** 增加两个字段: + - `msvc_toolset`:`version`、`origin`、`root`、`product`; + - `windows_sdk`:`version`、`root`、`origin`。 + + runtime identity 为 `msvc-stl@` 加 `ucrt@`。 +- **目标侧报告。** c++ 层报告为 `msvc-stl`,c-abi 层报告为 `ucrt`,来源都是这个 sysroot。目前 sysroot 只接到 c-abi 层(`modules/manifest/src/targetside_model.cppm:716-718`),在 MSVC 目标上要同时接到 c++ 层。 +- **`mcpp toolchain list`** 在 Windows 上列出机器上的 toolset。 +- **兼容性检查。** 不做 STL 下限的预检查,由编译期的 `STL1000` 报错。 + +### 3.7 诊断 + +``` +error: msvc@14.38 matches no toolset on this machine or in the index + on this machine: 14.44.35207 Visual Studio 2022 Community (default) + 14.51.36014 Visual Studio 18 Insiders (default) + installed by mcpp: 14.52.36629 + in the index: 14.44.35207, 14.52.36629 +``` + +``` +note: sysroot msvc@14.44.35207 is pinned; VCToolsInstallDir (14.38.33130) is ignored +``` + +其他情形: + +- 写的是 `cl` 的版本号(例如 `19.44`):沿用 `cl_version_spelling_hint()`。 +- `cl.exe` 行同时写了 `toolchain = "msvc@A"` 和 `sysroot = "msvc@B"`,A 与 B 不同:拒绝构建。 + +### 3.8 兼容与迁移 + +- **旧引擎读到 `sysroot = "msvc@…"`**(不带命名空间)时,按现有解析规则会整份清单报 `is not an xpkg reference`(`modules/manifest/src/toml.cppm:3229-3237`)。失败是响亮的,但报错文字会把人引向 xpkg 语法。 +- **旧引擎读到 `sysroot = "xim:msvc@…"`** 时会接受。它接下来做什么没有测过,必须用已发布的上一版引擎实测:是忽略,还是把它当成 C 库去安装。 + - 实现时的代码阅读结论是:旧引擎会把它当作 xpkg 引用去安装,多下载一份 toolset。 + - 实测(2026-09-24,临时分支 `probe/old-engine-msvc-sysroot`,windows-latest,已发布的 2026.9.21.3,同一个 `mcpp new` 工程写四种清单)推翻了其中一半:`xim:msvc@14.44.35207` 被接受,构建与运行都成功,**什么都没有安装**(registry 前后无新增),编译用的是机器上的 toolset,而构建输出把它列为 `c-abi msvc (xim:msvc@14.44.35207, prebuilt)`。也就是说,旧引擎静默地报告了一个它没有使用的 sysroot。`msvc@system` 与 `msvc@14.44.35207` 两种写法按预期让整份清单被拒(`is not an xpkg reference`,退出码 2)。不写 `sysroot` 的对照构建成功。这些写进了 CHANGELOG 与 `docs/20` 的「更早的引擎」一段,补救是把 mcpp 固定在 2026.9.24.1 或更高。 +- **最低版本只能写在文档里。** 项目清单里没有「最低引擎版本」这个键(未找到),所以在 `docs/20` 里写明新写法需要的 mcpp 版本。 +- **行为变化。** `msvc@system` 除 §3.9 的边界情形外结果不变。`msvc@<版本>` 在装有同版本 VS 的机器上改用系统的那一份,这一条写进 `docs/20-toolchains.md` 和 CHANGELOG。 + +### 3.9 与今天不同的边界情形 + +维护者的决定是「更好就接受」。每一条都说明了为什么更好: + +| 情形 | 今天 | 设计之后 | 更好的原因 | +|---|---|---|---| +| `cl.exe` 行,实例里另装了一个比默认更新的 side-by-side toolset | 取目录名最大的 | 取 `default.txt` 指定的 | 与 VS 自己的 vcvars / MSBuild 默认一致;更新的 side-by-side 往往是预览版 | +| `cl.exe` 行,在 `vcvarsall -vcvars_ver=X` 的环境里 | 忽略 `VCToolsInstallDir` | 使用 X | 声明压过探测,与 `VSINSTALLDIR` 优先的既有决定(xrgui#3)同一原则 | +| clang 行,`std.ixx` 与头文件来自不同 toolset | 可能发生 | 不会发生 | 两者一致是正确性要求 | +| 最新的实例没有 C++ 组件 | clang 找不到标准头 | 跳过该实例 | 今天这种情况必然失败 | +| 系统 SDK 里版本最高的不完整 | clang 仍取它 | 取最高的完整版本 | 避免 LNK1104 这类问题(三轴设计里记录过) | +| 版本比较 | 字符串比较 | 数字元组比较 | `14.9` 与 `14.10` 这类情形用字符串比较会排错 | + +### 3.10 测试与判据 + +**单测(在 Linux 上,被测对象是 `select_toolset()` 纯函数)。** 在临时目录里造出以下环境:两个实例、side-by-side toolset、`default.txt`、一个不完整的 toolset、一个生态包 store,以及几份环境变量快照。覆盖的用例: + +- 完整版本命中系统; +- 前缀按分量匹配(`14.4` 不匹配 `14.44`); +- 回落到生态包;`xim:` 跳过系统候选; +- 找不到时报错并列出三类候选; +- 写了版本时忽略环境变量并打印说明; +- `msvc@system` 按五步顺序选择; +- 不完整的候选被跳过; +- `cl.exe` 行的 `toolchain` 与 `sysroot` 冲突时拒绝。 + +反向:去掉「先匹配系统」这一步,「完整版本命中系统」的用例必须变红。 + +**Windows e2e。** 用 runner 自带的 VS,再装上 `xim:msvc` 14.44.35207 和 14.52.36629: + +1. `sysroot = "msvc@14.52.36629"`(runner 上没有这个版本):`-v` 只显示生态包路径,`origin = managed`。 +2. 写 runner 上 VS 的完整版本:用系统路径,不触发安装,`origin = system`。同时对比这一份和生态包里同版本的 `include/yvals_core.h`、`lib/x64/msvcprt.lib` 的哈希。 +3. `sysroot = "xim:msvc@"`:走生态包,`origin = managed`。 +4. 在 `vcvarsall -vcvars_ver=<另一个版本>` 的环境里重跑第 1、2 条:结果不变,并打印说明。 +5. 不写 `sysroot`:`std.ixx` 与头文件出自同一个 toolset 目录。 +6. 路径含空格:第 2 条本身就覆盖了。 + +**旧引擎:** 用已发布的上一版 mcpp 分别跑两种写法,记录它的实际行为(§3.8)。 + +### 3.11 代码改动点 + +| 位置 | 改动 | +|---|---| +| `src/toolchain/msvc.cppm:329-468` | 新增候选枚举和 `select_toolset()`;`find_vs_install_path()` 这条链改为调用它 | +| `src/toolchain/registry.cppm:519-570`(`parse_toolchain_spec`) | 支持 `xim:` 前缀 | +| `src/toolchain/registry.cppm:1192-1200` | 带版本的 msvc 写法,来源由解析结果决定,不再单凭拼写判定 | +| `src/build/prepare.cppm:3520-3528` | 带版本的 msvc 写法先走 `select_toolset()` | +| `modules/manifest/src/toml.cppm:3229-3237` | `*-windows-msvc` 行的 `sysroot` 接受 `msvc@…` | +| `modules/manifest/src/targetside_model.cppm:716` | MSVC 目标上 sysroot 同时供给 c-abi 与 c++ 两层 | +| `modules/toolchain-model/src/model.cppm:234` 附近 | 在 `Toolchain` 上新增只读字段 | +| `modules/toolchain-model/src/linkmodel.cppm:373` | MSVC 目标返回 `-Xmicrosoft-*` token | +| `src/toolchain/clang.cppm:186-217` | `std.ixx` 与 `stdlibVersion` 取自 `MsvcSelection` | +| `src/build/prepare.cppm:14967` | `resolution.json` 新增字段 | +| `src/toolchain/lifecycle.cppm` | `toolchain list` 列出系统 toolset | +| 文档 | `docs/20-toolchains.md`、`docs/22-target-side.md`(`sysroot` 一节)、`docs/04-mcpp-toml.md`,以及 `docs/zh/` 对应译文;SPEC-006 的实现状态 | + +### 3.12 不做 + +- SDK 版本键; +- 改变没有 VS 的机器上的默认工具链; +- Linux 宿主交叉编译到 `x86_64-windows-msvc`; +- llvm × toolset 的默认配对数据; +- STL 下限预检查。 + +--- + +## 4. B 部分:#685 + +- **解析与宿主无关。** `modules/platform/src/macos/macos.cppm:105-116` 去掉 `#if defined(__APPLE__)`,优先级为 env > manifest > 14.0。 +- **各读者按目标判定:** + - 三元组:`prepare.cppm:1933`; + - 指纹:`prepare_inputs.cppm:536` 改为读 `min_platform_version()` 的结果; + - 平台事实:`prepare.cppm:8707`; + - `-mmacosx-version-min`:`hostflags.cppm:513`; + - build.mcpp 宿主编译(`build_program.cppm:1205`)保持按宿主判定。 +- **判据:** + - 单测:解析结果与宿主无关; + - Linux 宿主上的纯计划测试(`--target aarch64-macos`,版本 `11.0`):`build.ninja` 里出现 `arm64-apple-macos11.0`;改值后指纹变化;env 优先于 manifest; + - 反向:macOS 宿主构建 `x86_64-linux-musl`,命令里不出现 `-mmacosx-version-min`。 + +## 5. C 部分:#687 + +**xim-pkgindex:** + +- `pkgs/g/gcc.lua:230-232` 改为只有一层引号,且不带 `|| true`: + + ```lua + local out = os.iorun(string.format("grep -rlF %s %s", + __shq(banner), __shq(root))) + ``` + + `os.iorun` 的两个实现都经过 `/bin/sh`,零命中时也都不报错。executor 实现会在命令末尾追加 ` 2>/dev/null > ""`,这个重定向只作用于 `|| true` 里的 `true`,grep 的输出就不会被捕获(实测)。 +- 每次都写一行日志,包含搜索的根目录和删除的数量,数量为零时也写。 +- 修正注释里与实现不符的两处。 +- `tests/g/test_gcc.py` 断言:安装 13.3.0 或 15.1.0 之后,`include-fixed` 下没有带横幅的文件。撤回修正后这条断言要变红。 +- 顺带核对 11.5.0 和 9.4.0。 + +**mcpp:** + +- 只在 `mcpp self doctor` 里加一项检查:已安装 gcc 的 `include-fixed` 里有带 fixincludes 横幅的文件时,列出文件、横幅里的源路径,以及重装命令(`mcpp index update`,然后 remove + install)。 +- 判据:用 15.1.0 载荷的副本触发,用 16.1.0 不触发。 + +## 6. D 部分:工具链管理规范 + +写成 `docs/specs/toolchain-management.md`(SPEC-006,草案 v0.1)。每一条都标注了实现状态。内容: + +1. 身份与写法,包括 `xim:` 前缀和 MSVC 的系统来源; +2. 来源与选择:一次选择、声明优先、结果可见、MSVC ABI 目标的 sysroot、目标属性按目标判定; +3. 载荷契约:可重定位、不含构建环境的 C 库内容、安装时的改写限于清单、完整性、描述文件的来源字段、修订与资产名; +4. 构建:配方入库、固定的构建环境、可复现等级; +5. 验收:载荷 lint、兼容矩阵、准入门、已安装载荷的诊断; +6. 发布顺序。 + +spec 以已有机制为基础,而不是另起一套: + +- `docs/32` 的描述符规则; +- `docs/91` §4 的安装后修正管线与 `.mcpp-fixup.json`; +- `.mcpp-toolchain.json`; +- xim-pkgindex 的准入脚本 `verify-toolchain.sh`; +- CI 工作流 `toolchain-consumer-smoke.yml`。 + +spec 也写明了现有的准入门为什么没能拦住 #687: + +- 准入门不在 CI 里运行,默认使用 glibc 2.39,测试程序也不含线程头; +- smoke 工作流只测描述符改动后解析出的那一个版本(latest)。 + +实现(lint、矩阵、配方)跟随下一批 LLVM 工具链一起做,到时候按 spec 实现、验证,并据实测调整 spec。 + +## 7. 实施顺序 + +1. xim-pkgindex:修 `gcc.lua` 并补测试(C 部分)。 +2. mcpp:#685(B 部分)。 +3. mcpp:#687 的 doctor 诊断(C 部分)。 +4. mcpp:MSVC toolset 选择(A 部分),在本设计 review 通过后开始,需要 Windows CI。 +5. 生态:SPEC-006 的实现,跟随下一批 LLVM 工具链。 + +除纯文档外,所有改动都走 PR;合入后以 `origin/main` HEAD 上那次 run 为准。 + +## 8. 风险 + +| 风险 | 缓解 | +|---|---| +| 系统优先让不同机器用到不同的 SDK | 打印并记录 `ucrt@`;需要固定时用 `xim:` | +| 旧引擎对 `sysroot = "xim:msvc@…"` 的行为未知 | 已实测(§3.8):接受而不生效,静默报告一个没有使用的 sysroot;文档写明并建议固定 mcpp 版本 | +| vswhere 的文本输出有变化 | 解析器只读 `instanceId`、`installationPath`、`installationVersion`、`displayName` 四个键,单测覆盖 CRLF 与缺路径的实例;vswhere 失败时回落到固定路径 | +| 系统 toolset 与生态包内容相同这一点未逐字节验证 | e2e 第 2 条核对 | +| `linkmodel` 的 PE 分支变成非空,影响 MinGW | MinGW 继续返回空模型,单测断言 | + +## 9. 自审 + +我对 v1 做了自审,找到的问题和处理如下: + +| # | v1 的问题 | 处理 | +|---|---|---| +| 1 | clang 行的 `msvc` 新键把编译器轴和目标轴混在一起,读起来分不清是 clang 还是 msvc | 改用目标轴已有的 `sysroot`(§3.1) | +| 2 | v1 设想新键「与 `toolchain` 一样」,但目标行上的 `toolchain` 只在交叉构建时读取(`prepare.cppm:2801`),照这个样子做,Windows 上直接 `mcpp build` 时就读不到 | `sysroot` 按实际构建目标去查,原生构建也生效(§3.4 末尾) | +| 3 | v1 说旧引擎对新键「只警告」 | 对 `sysroot = "msvc@…"`,旧引擎会拒绝整份清单;`xim:` 形式的行为未知,列为发布前的实测项(§3.8) | +| 4 | v1 的 `gcc.lua` 修正示例带 `|| true`,在 executor 实现下捕获为空 | 去掉 `|| true`,附实测(§5) | +| 5 | v1 说默认结果不变,但系统 SDK 的选择规则与 clang 不完全相同 | 加入边界表(§3.9) | +| 6 | v1 断言 scan-deps 会拿到这些 token,没有核对 | 改为实现时核对(§3.5) | +| 7 | v1 没有说明 build.mcpp 宿主编译怎样拿到同一个选择 | 宿主三元组那一行就是同一行(§3.4) | +| 8 | v1 的 D 部分没有读 xim-pkgindex 里已有的准入门和 smoke 工作流,等于把同一个决定又写了一遍 | spec 以它们为基础,并说明它们为什么没拦住 #687(§6) | +| 9 | v1 的前缀匹配和「最高」没有定义比较方式 | 按分量前缀匹配,按数字元组比较(§3.2) | +| 10 | v1 的 vswhere 调用没有处理编码 | 加 `-utf8`(§3.4) | +| 11 | v1 没有说明目标侧报告里 sysroot 供给哪几层 | MSVC 目标上同时供给 c-abi 与 c++(§3.6) | +| 12 | spec 初稿写的是「`ucrt@` 身份已实现」,没有区分行 | 改为 `cl.exe` 行已实现、clang 行未实现:e2e 241 为此专门钉了 `msvc@system` | +| 13 | spec 初稿把「副本加原子重命名」标为已实现 | 这只对 mcpp 的修正管线成立,xlings 安装时的改写没有核对过,改为部分实现 | +| 14 | spec 初稿说 smoke 工作流只测「一个版本」,没有说明是哪一个 | 读了原文:不带版本号安装,也就是 `latest`(16.1.0)。加清理逻辑的 PR(#563)正是在 16.1.0 上变绿的,而 16.1.0 本来就没有冻结文件,清理代码生效与否都是绿,这条绿什么也没有测到 | + +## 10. 待 review 的决策点 + +1. **`sysroot` 的语义**(§3.1):在 MSVC ABI 目标上,编译器是工具链,MSVC toolset 是 sysroot。clang 行写 `sysroot = "msvc@…"`,`cl.exe` 行不写。 +2. **`xim:` 前缀接受的范围**:对所有工具链族都接受(没有系统来源的族等价于不写),还是只接受 msvc。建议全部接受,因为这是生态包的统一标记。 +3. **SPEC-006 的范围与粒度**:选择规则和载荷契约放在同一份 spec 里,还是拆成两份。 + +## 11. 实现记录(2026-09-24) + +实现时与本文前面各节不同的地方,以及原因: + +| 位置 | 设计 | 实现 | 原因 | +|---|---|---|---| +| §3.5 链接环境 | 去掉 `LIB` | 不去掉 | `envOverrides` 作用于整个 ninja 进程;用户可能用 `LIB` 提供自己的库目录,清掉它有兼容性风险。显式 `-libpath` 排在前面,剩余路径只在显式路径缺库时才会用到 | +| §3.4 写了版本时的 SDK | `WindowsSdkDir` 不参与 | 系统来源的 toolset 仍按机器扫描选 SDK,`WindowsSdkDir` 优先 | 沿用三轴设计对 `msvc@system` 的决定:机器上的东西只能靠查找,声明压过扫描。版本进入 `ucrt@` 身份和缓存键,所以结果可见 | +| §3.6 目标侧报告 | c++ 与 c-abi 两层报告来源为 sysroot | 两层仍记为预制来源(SPEC-002 不变),所选 toolset 与 SDK 写入 `resolution.json` | 不改 SPEC-002 的层语义;`resolution.json` 已经给出同样的信息 | +| §3.7 cl.exe 行的冲突 | 拒绝 | 已实现:cl.exe 行的 `sysroot` 若指向另一个 toolset,报错并说明两种做法 | — | +| 新增 | — | `mcpp toolchain default msvc@` 在机器已有该 toolset 时直接写入默认值,不要求先装载荷 | 与构建对同一写法的理解保持一致 | +| 新增 | — | `flags.cppm` 的路径转义:含空白的路径加引号,其余路径的写法逐字节不变 | 系统 toolset 总在 `C:\Program Files` 下;此前经这条通道的路径都不含空格 | +| 新增 | — | std 模块命令的路径转义:Windows 用平台引号,POSIX 保持单引号 | Windows 经 cmd.exe 执行这条命令,cmd.exe 不认单引号 | +| 新增 | — | cl.exe 行不再从别的 toolset 借用 `std.ixx` | 同一个「两个选择器」问题在 cl.exe 行上的另一处 | +| e2e 239 | — | 改用 `xim:msvc@` | 裸写法现在会先选 runner 上的同版本 toolset,测试就不再测载荷 | +| 新增 | — | `CompileUnit::providesModule` 从 `std::optional` 改为 `std::string`(空串表示不是模块接口) | 第一轮 Windows CI 在 clang + MSVC STL 下报 `_SMF_control` 无匹配构造函数,报在 `plan.cppm` 的 `CompileUnit` 上;这是已知的那类问题:模块接口一有扰动就可能触发,删掉这个成员类型才能根除 | +| §3.4 系统候选 | vswhere `-format json` | vswhere `-format text`,由 `parse_vswhere_text` 解析 | 只读四个键,不需要 JSON 解析器,`msvc.cppm` 保持原有的导入。这一改动最初是作为第二轮 Windows 失败的修法提出的(假设:新增的 `import mcpp.libs.json` 是触发者),第三轮 CI 把这个假设否掉了,见下一行 | +| 新增 | — | `RuntimeBinding::libc`、`hostLibc` 从 `std::optional` 改为 `std::string`(空串表示没有) | 第二、三轮 Windows CI 报在 `RuntimeBinding` 的隐式复制上(`prepare.cppm` 的 `make_shared`),而这个结构体与这次复制在 main 上都能编译。用两个只含 Windows 编译步骤的临时分支并行测量:只改这两个成员的分支编译通过,只把 `toml.cppm` 新增的 `optional` 返回值改掉的分支仍报同一错误。所有读者都把「没有」与空串等同处理(`value_or("")`、非空才赋值、JSON 解码时空串即没有),语义不变 | +| §5 `std.ixx` 同源 | 连同 `std.compat.ixx` 一起换成所选 toolset 的 | 只换 `std.ixx`,`std.compat` 保持检测的结果(这一行上一直为空) | 第四轮 Windows CI(构建首次通过):`import std` 的预编译报 `std.compat.pcm` 不存在。main 上 clang 的 MSVC 行从来没有 `std.compat` 源;clang 的 compat 构建命令不带 `-x c++-module`,clang 把 `.ixx` 当作链接器输入,`--precompile` 什么都不写且退出 0,下一条命令才失败。给这一行加上 `std.compat` 是另一项功能,不在本次范围内 | + +Linux 上的零差异检查:用已发布的 2026.9.21.3 与本分支的二进制,在同一目录分别构建 `examples/01-hello` 与 `examples/04-workspace`,四份 `build.ninja` 除 mcpp 自身路径那一行外逐字节相同,指纹也相同。 + +## 12. 修订记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| v1 | 2026-09-24 | 初版:`msvc` 新键;系统优先;D 部分只列路线 | +| v2 | 2026-09-24 | 按维护者决定改为 `sysroot` 语义,接受 `xim:`,取消 STL 下限预检查,C 部分只做 doctor,D 部分改为 SPEC-006 草案;并入 §9 的自审结果 | diff --git a/.agents/docs/2026-09-24-toolchain-selection-implementation-plan.md b/.agents/docs/2026-09-24-toolchain-selection-implementation-plan.md new file mode 100644 index 00000000..98dd7b48 --- /dev/null +++ b/.agents/docs/2026-09-24-toolchain-selection-implementation-plan.md @@ -0,0 +1,67 @@ +--- +subject: plan +status: active +--- + +# 工具链选择与载荷可信度:实施计划 + +- 日期:2026-09-24 +- 设计:`2026-09-24-toolchain-selection-and-payload-trust-design.md`(v2) +- 规范:`docs/specs/toolchain-management.md`(SPEC-006) +- 维护者对设计 §10 的答复: + - `sysroot` 语义接受; + - `xim:` 前缀对所有工具链族都接受; + - SPEC-006 保持一份,不拆分。 + +## 1. 约束 + +| 角度 | 约束 | +|---|---| +| 架构 | MSVC 的选择只在 prepare 里做一次,结果存进 `Toolchain`,所有读者都读它。选择逻辑是纯函数,只有枚举候选依赖 Windows。deployment target 的解析函数以目标三元组为参数,调用方无法再按宿主判定 | +| 稳定性 | 不写版本时,默认结果与今天一致,例外只有设计 §3.9 列出的几种(每一种都更好)。任何回落都打印一行说明 | +| 简洁 | 不新增清单键;clang 行复用 `[target.].sysroot`。三组 `-Xmicrosoft-*` 参数由 `LinkModel` 这一个地方产生 | +| 用户体验 | 构建开头打印一行,说明用的是哪个 toolset、来自哪里、SDK 版本。找不到时列出三类候选。写了版本而环境变量被忽略时,打印说明 | +| 兼容性 | `msvc@system`、`msvc@<版本>`、`gcc@…`、`llvm@…` 这些写法都继续有效。`msvc@<版本>` 在机器上有同版本时改用系统的那一份,这一点写进 CHANGELOG。缓存键的变化只影响 Windows clang 行 | +| 跨平台 | 选择逻辑、写法解析、清单解析的单测都在 Linux 上运行;Windows 行为由 Windows CI 的 e2e 覆盖。#685 的修正对 macOS 宿主保持原有输出 | +| 一致性 | `cl.exe` 行与 clang 行共用同一个选择函数。「最高」统一按数字元组比较 | +| 无感升级 | 已有项目不需要改清单。新写法在旧引擎上是响亮的失败,不会被静默忽略;文档写明所需的最低版本 | +| 测试覆盖 | 每个判据都配反向用例(撤回修复就变红)。纯函数单测覆盖边界情形;e2e 覆盖真实二进制 | + +## 2. 任务 + +| 编号 | 仓库 | 内容 | 主要文件 | 依赖 | +|---|---|---|---|---| +| T1 | mcpp | #685:deployment target 与宿主无关,是否适用按目标判定 | `modules/platform/src/macos/macos.cppm`、`src/build/prepare.cppm`(deployment target 相关位置)、`src/build/prepare_inputs.cppm`、`src/toolchain/hostflags.cppm`(`-mmacosx-version-min`)、`src/toolchain/stdmod.cppm`、`src/build/flags.cppm`、`src/build/build_program.cppm`、对应单测 | 无 | +| T2 | mcpp | #687 的 doctor 诊断:已安装 gcc 的 `include-fixed` 里带横幅的文件 | `src/doctor.cppm`、单测 | 无 | +| T3 | mcpp | 工具链写法的 `xim:` 前缀(所有族) | `src/toolchain/registry.cppm`、`src/toolchain/compat.cppm`、单测 | 无 | +| T4 | mcpp | MSVC 候选枚举与纯函数 `select_toolset()`;`cl.exe` 行的 `msvc@system` 与带版本的写法改用它 | `src/toolchain/msvc.cppm`、`src/build/prepare.cppm`(工具链解析段)、单测 | T3 | +| T5 | mcpp | clang 行的 MSVC sysroot:清单解析、在 prepare 中解析、`Toolchain` 字段、`-Xmicrosoft-*` token、`std.ixx`、`stdlibVersion`、链接环境、`resolution.json`、构建开头那一行 | `modules/manifest/src/toml.cppm`、`modules/toolchain-model/src/model.cppm`、`linkmodel.cppm`、`src/toolchain/hostflags.cppm`(链接 token)、`src/toolchain/clang.cppm`、`src/build/flags.cppm`(链接一侧与含空格路径的转义)、`src/build/cache_key.cppm`、`src/build/prepare.cppm` | T4 | +| T6 | mcpp | `mcpp toolchain list` 在 Windows 上列出机器上的 toolset | `src/toolchain/lifecycle.cppm` | T4 | +| T7 | mcpp | Windows e2e:多版本、`xim:`、环境变量被忽略、`std.ixx` 与头文件同源 | `tests/e2e/`、`.github/workflows/ci-windows*.yml` | T5 | +| T8 | mcpp | 文档:`docs/20`、`docs/22`、`docs/04` 及 `docs/zh/` 对应译文;SPEC-006 的实现状态;CHANGELOG;版本号 | 文档与版本文件 | T1 到 T7 | +| T9 | xim-pkgindex | `gcc.lua` 清理代码的引号修正与日志;`tests/g/test_gcc.py` 断言 | `pkgs/g/gcc.lua`、`tests/g/test_gcc.py` | 无 | +| T10 | — | 两个 PR 的 CI 全绿后合入:xim-pkgindex 先合,mcpp 后合 | — | T1 到 T9 | +| T11 | mcpp、xlings-res、xim-pkgindex | 发布:打 tag、四平台构建、本地 gtc 补 GitCode、核验镜像、合入 bot 的索引 PR、在 PR 里前移 bootstrap pin | — | T10 | +| T12 | — | 生态验证:在 xlings subos 沙箱里(mcpp 与 xlings 都配 CN 镜像)装已发布的 mcpp,逐项验证 #685、#687 以及 mcpp-index 的若干包 | — | T11 | + +并行方式: + +- T1 与 T2 交给一个子代理,在 worktree `mcpp-685` 里做; +- T9 交给另一个子代理,在 xim-pkgindex 的 worktree 里做; +- T3 到 T8 由主线在 `mcpp-tcsel` 里做; +- T1、T2 做完后合入主线分支,形成一个 PR。 + +文件归属按上表划分。`src/build/prepare.cppm` 由两边在互不重叠的区段修改:T1 只动 deployment target 相关的函数与调用处,T4、T5 只动工具链解析段与 `resolution.json`。 + +## 3. 判据 + +| 编号 | 判据 | 反向 | +|---|---|---| +| T1 | 在 Linux 宿主上,`aarch64-macos` 目标的 `build.ninja` 带有 manifest 里写的版本;改值后指纹变化;env 优先于 manifest | macOS 宿主构建非 Apple 目标时不出现 `-mmacosx-version-min` | +| T2 | 15.1.0 载荷副本触发诊断 | 16.1.0 不触发 | +| T3 | `xim:gcc@16.1.0` 与 `gcc@16.1.0` 解析结果相同;`xim:msvc@system` 被拒绝 | — | +| T4 | 纯函数单测覆盖设计 §3.10 列出的全部用例 | 去掉「先匹配系统」后,对应用例变红 | +| T5 | 单测:MSVC 目标的 link model 产出三组 token,并进入缓存键;Windows CI 构建一个 `import std` 程序 | MinGW 目标的 token 为空 | +| T7 | Windows e2e 的五条 | — | +| T9 | 安装后 `include-fixed` 里没有带横幅的文件 | 撤回修正后断言变红 | +| T12 | 沙箱里所有检查通过,并把没有运行的检查单独列出 | — | diff --git a/.agents/docs/2026-09-24-toolchain-selection-verify.sh b/.agents/docs/2026-09-24-toolchain-selection-verify.sh new file mode 100755 index 00000000..13d7f590 --- /dev/null +++ b/.agents/docs/2026-09-24-toolchain-selection-verify.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# Ecosystem verification for mcpp#685, mcpp#687 and the MSVC toolset selection, +# against the PUBLISHED mcpp and index, inside a SubOS sandbox with CN mirrors +# for xlings and mcpp. +# +# B64=$(base64 -w0 .agents/docs/2026-09-24-toolchain-selection-verify.sh) +# xlings subos new v924 +# xlings subos use v924 --sandbox --cmd \ +# "echo $B64 | base64 -d > /tmp/v.sh && MCPP_VERIFY_VERSION=2026.9.24.1 bash /tmp/v.sh" +# +# Run it once against the previous release first (MCPP_VERIFY_VERSION=2026.9.21.3): +# the sections marked CHANGE must fail there and pass on the new release; the +# sections marked GUARD must pass on both. The #687 recipe fix lives in the index +# (openxlings/xim-pkgindex#870), so its section is a GUARD for mcpp and holds for +# both releases once the index carries the fix. +# +# The MSVC toolset selection is Windows-only and is verified by the Windows CI +# jobs (e2e 760, e2e 239); this Linux sandbox lists it under NOT RUN. +# +# The sandbox shares the xlings data directory, so a published mcpp is addressed +# by its store path and exact version. Its $HOME content persists between runs of +# one SubOS, so every section clears its own probe directory first. +set -u + +VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}" +STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}" +LLVM="${MCPP_VERIFY_LLVM:-22.1.8}" + +fails=0 +skipped="" +fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); } +ok() { printf 'ok: %s\n' "$1"; } +section() { printf '\n== %s ==\n' "$1"; } +skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped + - $1"; } +unset XLINGS_ACTIVE_SUBOS + +root="$HOME/verify-924" +rm -rf "$root"; mkdir -p "$root" + +section "A. GUARD: the published mcpp answers for itself, with CN mirrors for xlings and mcpp" +if [ ! -x "$STORE" ]; then + fail "no mcpp at $STORE"; printf '\nfails=%d (nothing else can run)\n' "$fails"; exit 1 +fi +got=$("$STORE" --version 2>&1 | head -1) +case "$got" in *"$VER"*) ok "mcpp --version says $got" ;; *) fail "mcpp --version says '$got', expected $VER" ;; esac +if command -v xlings >/dev/null 2>&1; then + xlings config --mirror CN >/dev/null 2>&1 && ok "xlings config --mirror CN" || fail "xlings config --mirror CN" +fi +"$STORE" self config --mirror CN >/dev/null 2>&1 && ok "mcpp self config --mirror CN" || fail "mcpp self config --mirror CN" +grep -q '"mirror": *"CN"' "$HOME/.mcpp/registry/.xlings.json" 2>/dev/null \ + && ok "mcpp's xlings reads mirror CN" || fail "mcpp's xlings does not read mirror CN" +timeout 900 "$STORE" index update > "$root/index-update.log" 2>&1 \ + && ok "mcpp index update" || { fail "mcpp index update"; tail -5 "$root/index-update.log"; } + +section "B. GUARD: a new project builds and runs with the default toolchain" +d=$root/b; mkdir -p "$d"; cd "$d" +"$STORE" new hello > /dev/null 2>&1 +cd hello +if timeout 1800 "$STORE" run > run.log 2>&1 && grep -q 'Hello' run.log; then + ok "mcpp new + run" +else + fail "mcpp new + run"; tail -15 run.log +fi + +section "C. GUARD (index, #687): a fresh gcc@15.1.0 has no frozen header, and compiles" +d=$root/c; mkdir -p "$d/src"; cd "$d" +printf '[package]\nname = "mtx"\nversion = "0.1.0"\n[toolchain]\nlinux = "gcc@15.1.0"\n' > mcpp.toml +printf '#include \n#include \nint main() { std::mutex m; std::lock_guard g(m); return *std::make_unique(0); }\n' > src/main.cpp +if timeout 2400 "$STORE" build > build.log 2>&1; then + ok "gcc@15.1.0 compiles and against glibc 2.44" +else + fail "gcc@15.1.0 build"; grep -m3 -E 'error' build.log +fi +g15="$HOME/.mcpp/registry/data/xpkgs/xim-x-gcc/15.1.0" +if [ -d "$g15" ]; then + frozen=$(grep -rlF 'auto-edited by fixincludes' "$g15"/lib/gcc/*/*/include-fixed 2>/dev/null | wc -l) + [ "$frozen" -eq 0 ] && ok "the installed gcc@15.1.0 carries no fixincludes-frozen header" \ + || fail "$frozen frozen header(s) remain in the installed gcc@15.1.0" +else + fail "gcc@15.1.0 was not installed under $g15" +fi + +section "D. CHANGE (#687): mcpp self doctor reports a frozen header in an installed gcc payload" +if [ -d "$g15" ]; then + fx=$(ls -d "$g15"/lib/gcc/*/*/include-fixed | head -1) + printf '/* DO NOT EDIT THIS FILE.\n\n It has been auto-edited by fixincludes from:\n\n\t"/verify/usr/include/probe.h"\n*/\n' > "$fx/verify-probe.h" + "$STORE" self doctor > "$root/doctor.log" 2>&1 + if grep -q 'gcc@15.1.0' "$root/doctor.log" && grep -q 'verify-probe.h' "$root/doctor.log"; then + ok "doctor names the payload and the frozen file" + else + fail "doctor did not report the planted frozen header"; grep -i -m3 'include-fixed\|fixincludes' "$root/doctor.log" + fi + rm -f "$fx/verify-probe.h" + "$STORE" self doctor > "$root/doctor2.log" 2>&1 + grep -q 'verify-probe.h' "$root/doctor2.log" && fail "doctor still reports a removed file" \ + || ok "doctor is silent once the file is gone" +else + skip "D: no gcc@15.1.0 payload (section C did not install it)" +fi + +section "E. CHANGE (#685): the macOS deployment target reaches an aarch64-macos build from Linux" +d=$root/e; mkdir -p "$d/src"; cd "$d" +printf 'int main() { return 0; }\n' > src/main.cpp +cat > mcpp.toml < cfg.log 2>&1; then + cdb=$(find target -name compile_commands.json | head -1) + if [ -n "$cdb" ] && grep -q 'arm64-apple-macos11.0' "$cdb"; then + ok "the manifest value reaches the triple (arm64-apple-macos11.0)" + else + fail "the triple does not carry 11.0"; grep -o -- '--target=[^ "]*' "$cdb" 2>/dev/null | sort -u + fi + MACOSX_DEPLOYMENT_TARGET=12.0 timeout 900 "$STORE" build --target aarch64-macos --configure-only > cfg2.log 2>&1 + cdb=$(ls -t $(find target -name compile_commands.json) | head -1) + grep -q 'arm64-apple-macos12.0' "$cdb" && ok "MACOSX_DEPLOYMENT_TARGET overrides the manifest" \ + || fail "MACOSX_DEPLOYMENT_TARGET did not reach the triple" +else + fail "configure for aarch64-macos"; tail -8 cfg.log +fi + +section "F. CHANGE: xim: is the one toolchain namespace, and xim:msvc@system is not a spelling" +# Earlier releases stripped ANY `:` prefix, so `xim:gcc@16.1.0` already built +# and is a GUARD here; what changed is that another namespace is refused where it +# is read, and that `xim:msvc@system` is refused as a spelling (before, it failed +# later, on Linux as "only available on Windows hosts"). +cd "$root/b/hello" +if timeout 1800 "$STORE" build --toolchain "xim:gcc@16.1.0" > xim.log 2>&1; then + ok "GUARD: --toolchain xim:gcc@16.1.0 builds" +else + fail "--toolchain xim:gcc@16.1.0"; tail -5 xim.log +fi +if "$STORE" build --toolchain "foo:gcc@16.1.0" > ximfoo.log 2>&1; then + fail "foo:gcc@16.1.0 was accepted (the namespace was stripped)" +else + grep -q 'is not a toolchain namespace' ximfoo.log && ok "foo:gcc@16.1.0 is refused as a namespace" \ + || { fail "foo:gcc@16.1.0 refused for another reason"; head -3 ximfoo.log; } +fi +if "$STORE" build --toolchain "xim:msvc@system" > ximsys.log 2>&1; then + fail "xim:msvc@system was accepted" +else + grep -q 'xim:msvc@' ximsys.log && ok "xim:msvc@system is refused as a spelling, naming both meanings" \ + || { fail "xim:msvc@system refused for another reason"; head -3 ximsys.log; } +fi + +section "G. GUARD (mcpp-index): consumers of published packages build and pass their tests" +consumer() { # name toolchain dep-table dep-line test-b64 + local name=$1 tc=$2 tbl=$3 dep=$4 b64=$5 + local d=$root/g-$name; mkdir -p "$d/tests" "$d/src"; cd "$d" + printf 'int main() { return 0; }\n' > src/main.cpp + { printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$name" + [ -n "$tc" ] && printf '[toolchain]\nlinux = "%s"\n' "$tc" + printf '[dependencies.%s]\n%s\n' "$tbl" "$dep"; } > mcpp.toml + echo "$b64" | base64 -d > tests/t.cpp + if timeout 3000 "$STORE" test > test.log 2>&1; then + ok "$name: mcpp test passed${tc:+ ($tc)}" + else + fail "$name: mcpp test${tc:+ ($tc)}"; grep -m5 -E 'error|FAIL|failed' test.log + fi +} +consumer yaml-cpp-gcc16 "" compat 'yaml-cpp = "0.9.0"' "Ly8gQmVoYXZpb3JhbCB0ZXN0IGZvciBjb21wYXQueWFtbC1jcHAgMC45LjAuCi8vCi8vIEVhY2ggY2hlY2sgcmVhY2hlcyBzeW1ib2xzIGRlZmluZWQgaW4gdGhlIHBhY2thZ2UncyBvd24gLmNwcCBmaWxlcywgc28gYQovLyBwYWNrYWdlIHRoYXQgY29tcGlsZWQgbm90aGluZyBjYW5ub3QgcGFzcyBieSBiZWluZyBoZWFkZXItb25seSBieSBhY2NpZGVudDoKLy8KLy8gICBZQU1MOjpMb2FkLCBub2RlIGFjY2VzcywgY29udmVyc2lvbnMgIC0+IHBhcnNlLmNwcCwgbm9kZV9kYXRhLmNwcCwgY29udmVydC5jcHAKLy8gICBhbmNob3JzIGFuZCBhbGlhc2VzICAgICAgICAgICAgICAgICAgIC0+IG5vZGVidWlsZGVyLmNwcCwgc2luZ2xlZG9jcGFyc2VyLmNwcAovLyAgIFlBTUw6OkxvYWRBbGwgICAgICAgICAgICAgICAgICAgICAgICAgLT4gcGFyc2UuY3BwLCBwYXJzZXIuY3BwCi8vICAgWUFNTDo6RW1pdHRlciwgWUFNTDo6RHVtcCAgICAgICAgICAgICAtPiBlbWl0dGVyLmNwcCwgZW1pdHRlcnN0YXRlLmNwcCwgZW1pdC5jcHAKLy8gICBtYWxmb3JtZWQgaW5wdXQgICAgICAgICAgICAgICAgICAgICAgIC0+IHNjYW5uZXIuY3BwLCBleGNlcHRpb25zLmNwcAovLyAgIFlBTUw6OkJ1aWxkR3JhcGhPZk5leHREb2N1bWVudCAgICAgICAgLT4gY29udHJpYi9ncmFwaGJ1aWxkZXIqLmNwcAovLyAgIFlBTUw6OkZwVG9TdHJpbmcgICAgICAgICAgICAgICAgICAgICAgLT4gZnB0b3N0cmluZy5jcHAgKG5ldyBpbiAwLjkuMCkKLy8KLy8gUmV0dXJucyBub24temVybyBvbiBhbnkgbWlzbWF0Y2guCiNpbmNsdWRlIDx5YW1sLWNwcC95YW1sLmg+CiNpbmNsdWRlIDx5YW1sLWNwcC9jb250cmliL2dyYXBoYnVpbGRlci5oPgojaW5jbHVkZSA8eWFtbC1jcHAvZnB0b3N0cmluZy5oPgoKI2luY2x1ZGUgPGNzdGRpbz4KI2luY2x1ZGUgPHNzdHJlYW0+CiNpbmNsdWRlIDxzdHJpbmc+CiNpbmNsdWRlIDx2ZWN0b3I+CgovLyBUaGUgcGFja2FnZSBidWlsZHMgb2JqZWN0cywgbm90IGEgc2hhcmVkIGxpYnJhcnksIHNvIGV2ZXJ5IHlhbWwtY3BwCi8vIGRlY2xhcmF0aW9uIHRoaXMgVFUgc2VlcyBtdXN0IGJlIHBsYWluIC0tIG5vdCBkbGxpbXBvcnQuIFRoZSBwYWNrYWdlCi8vIGRlbGl2ZXJzIHRoYXQgd2l0aCBhIHNoaW0gaW4gZnJvbnQgb2YgeWFtbC1jcHAvZGxsLmgsIGJlY2F1c2Ugbm8gZGVzY3JpcHRvcgovLyBrZXkgY2FycmllcyBhIGRlZmluZSB0byBhIGNvbnN1bWVyJ3MgVFVzLiBPbiBMaW51eCB0aGUgZGlmZmVyZW5jZSBpcyBvbmx5IGEKLy8gdmlzaWJpbGl0eSBhdHRyaWJ1dGUgYW5kIGV2ZXJ5dGhpbmcgbGlua3MgYW55d2F5OyBvbiB0aGUgTVNWQyBBQkkgaXQgaXMgYQovLyBsaW5rIGZhaWx1cmUuIEFzc2VydGluZyBpdCBhdCBjb21waWxlIHRpbWUgZmFpbHMgZXZlcnl3aGVyZSB0aGUgbW9tZW50IHRoZQovLyBzaGltIHN0b3BzIGJlaW5nIHJlYWNoZWQuCiNpZm5kZWYgWUFNTF9DUFBfU1RBVElDX0RFRklORQojICBlcnJvciAiWUFNTF9DUFBfU1RBVElDX0RFRklORSBkaWQgbm90IHJlYWNoIHRoZSBjb25zdW1lcjogdGhlIHlhbWwtY3BwL2RsbC5oIHNoaW0gd2FzIG5vdCBmb3VuZCBmaXJzdCIKI2VuZGlmCgpuYW1lc3BhY2UgewoKY29uc3QgY2hhciAqY29uc3Qga0RvYyA9IFIiKApuYW1lOiBtY3BwLWluZGV4CnZlcnNpb246IDMKcmF0aW86IDAuMjUKZW5hYmxlZDogdHJ1ZQp0YWdzOiBbeWFtbCwgY3BwLCAicXVvdGVkLCB3aXRoIGNvbW1hIl0KYmFzZTogJmJhc2UKICBob3N0OiBsb2NhbGhvc3QKICBwb3J0OiA4MDgwCmRlcml2ZWQ6CiAgPDw6ICpiYXNlCiAgYWxpYXNfb2ZfYmFzZTogKmJhc2UKdGV4dDogfAogIGxpbmUgb25lCiAgbGluZSB0d28KKSI7Cgpib29sIGxvYWRzX2FuZF9jb252ZXJ0cygpIHsKICAgIFlBTUw6Ok5vZGUgcm9vdCA9IFlBTUw6OkxvYWQoa0RvYyk7CiAgICBpZiAoIXJvb3QuSXNNYXAoKSkgcmV0dXJuIGZhbHNlOwogICAgaWYgKHJvb3RbIm5hbWUiXS5hczxzdGQ6OnN0cmluZz4oKSAhPSAibWNwcC1pbmRleCIpIHJldHVybiBmYWxzZTsKICAgIGlmIChyb290WyJ2ZXJzaW9uIl0uYXM8aW50PigpICE9IDMpIHJldHVybiBmYWxzZTsKICAgIGlmIChyb290WyJyYXRpbyJdLmFzPGRvdWJsZT4oKSAhPSAwLjI1KSByZXR1cm4gZmFsc2U7CiAgICBpZiAoIXJvb3RbImVuYWJsZWQiXS5hczxib29sPigpKSByZXR1cm4gZmFsc2U7CgogICAgY29uc3QgWUFNTDo6Tm9kZSB0YWdzID0gcm9vdFsidGFncyJdOwogICAgaWYgKCF0YWdzLklzU2VxdWVuY2UoKSB8fCB0YWdzLnNpemUoKSAhPSAzKSByZXR1cm4gZmFsc2U7CiAgICBpZiAodGFnc1syXS5hczxzdGQ6OnN0cmluZz4oKSAhPSAicXVvdGVkLCB3aXRoIGNvbW1hIikgcmV0dXJuIGZhbHNlOwogICAgaWYgKHRhZ3MuYXM8c3RkOjp2ZWN0b3I8c3RkOjpzdHJpbmc+PigpLmZyb250KCkgIT0gInlhbWwiKSByZXR1cm4gZmFsc2U7CgogICAgaWYgKHJvb3RbInRleHQiXS5hczxzdGQ6OnN0cmluZz4oKSAhPSAibGluZSBvbmVcbmxpbmUgdHdvXG4iKSByZXR1cm4gZmFsc2U7CgogICAgLy8gQSBtaXNzaW5nIGtleSBpcyByZXBvcnRlZCBhcyB1bmRlZmluZWQgYW5kIGEgZmFsbGJhY2sgYXBwbGllczsgYW4KICAgIC8vIGV4cGxpY2l0IGNvbnZlcnNpb24gb2YgdGhlIHdyb25nIGtpbmQgdGhyb3dzLgogICAgaWYgKHJvb3RbImFic2VudCJdLklzRGVmaW5lZCgpKSByZXR1cm4gZmFsc2U7CiAgICBpZiAocm9vdFsiYWJzZW50Il0uYXM8aW50Pig0MikgIT0gNDIpIHJldHVybiBmYWxzZTsKICAgIHRyeSB7CiAgICAgICAgKHZvaWQpcm9vdFsibmFtZSJdLmFzPGludD4oKTsKICAgICAgICByZXR1cm4gZmFsc2U7CiAgICB9IGNhdGNoIChjb25zdCBZQU1MOjpCYWRDb252ZXJzaW9uICYpIHsKICAgIH0KICAgIHJldHVybiB0cnVlOwp9Cgpib29sIHJlc29sdmVzX2FsaWFzZXMoKSB7CiAgICBZQU1MOjpOb2RlIHJvb3QgPSBZQU1MOjpMb2FkKGtEb2MpOwogICAgY29uc3QgWUFNTDo6Tm9kZSBhbGlhcyA9IHJvb3RbImRlcml2ZWQiXVsiYWxpYXNfb2ZfYmFzZSJdOwogICAgaWYgKGFsaWFzWyJwb3J0Il0uYXM8aW50PigpICE9IDgwODApIHJldHVybiBmYWxzZTsKICAgIC8vIEFuIGFsaWFzIGlzIHRoZSBTQU1FIG5vZGUgYXMgaXRzIGFuY2hvciwgbm90IGEgY29weSBvZiBpdC4KICAgIHJldHVybiBhbGlhcy5pcyhyb290WyJiYXNlIl0pOwp9Cgpib29sIGxvYWRzX2V2ZXJ5X2RvY3VtZW50KCkgewogICAgY29uc3Qgc3RkOjp2ZWN0b3I8WUFNTDo6Tm9kZT4gZG9jcyA9IFlBTUw6OkxvYWRBbGwoImE6IDFcbi0tLVxuYjogMlxuLS0tXG4tIDNcbiIpOwogICAgcmV0dXJuIGRvY3Muc2l6ZSgpID09IDMgJiYgZG9jc1swXVsiYSJdLmFzPGludD4oKSA9PSAxCiAgICAgICAgICAgJiYgZG9jc1sxXVsiYiJdLmFzPGludD4oKSA9PSAyICYmIGRvY3NbMl1bMF0uYXM8aW50PigpID09IDM7Cn0KCmJvb2wgZW1pdHNfYW5kX3JvdW5kX3RyaXBzKCkgewogICAgWUFNTDo6RW1pdHRlciBvdXQ7CiAgICBvdXQgPDwgWUFNTDo6QmVnaW5NYXA7CiAgICBvdXQgPDwgWUFNTDo6S2V5IDw8ICJsaXN0IiA8PCBZQU1MOjpWYWx1ZSA8PCBZQU1MOjpGbG93CiAgICAgICAgPDwgWUFNTDo6QmVnaW5TZXEgPDwgMSA8PCAyIDw8IDMgPDwgWUFNTDo6RW5kU2VxOwogICAgb3V0IDw8IFlBTUw6OktleSA8PCAibmVzdGVkIiA8PCBZQU1MOjpWYWx1ZSA8PCBZQU1MOjpCZWdpbk1hcAogICAgICAgIDw8IFlBTUw6OktleSA8PCAiayIgPDwgWUFNTDo6VmFsdWUgPDwgInYiIDw8IFlBTUw6OkVuZE1hcDsKICAgIG91dCA8PCBZQU1MOjpFbmRNYXA7CiAgICBpZiAoIW91dC5nb29kKCkpIHJldHVybiBmYWxzZTsKICAgIGlmIChzdGQ6OnN0cmluZyhvdXQuY19zdHIoKSkgIT0gImxpc3Q6IFsxLCAyLCAzXVxubmVzdGVkOlxuICBrOiB2IikgcmV0dXJuIGZhbHNlOwoKICAgIFlBTUw6Ok5vZGUgYWdhaW4gPSBZQU1MOjpMb2FkKG91dC5jX3N0cigpKTsKICAgIGlmIChhZ2FpblsibGlzdCJdWzJdLmFzPGludD4oKSAhPSAzKSByZXR1cm4gZmFsc2U7CgogICAgLy8gQSBub2RlIGJ1aWx0IGluIGNvZGUgYW5kIGR1bXBlZCBjb21lcyBiYWNrIHdpdGggdGhlIHNhbWUgY29udGVudC4KICAgIFlBTUw6Ok5vZGUgYnVpbHQ7CiAgICBidWlsdFsieCJdID0gNzsKICAgIGJ1aWx0WyJ5Il0ucHVzaF9iYWNrKCJmaXJzdCIpOwogICAgYnVpbHRbInkiXS5wdXNoX2JhY2soInNlY29uZCIpOwogICAgWUFNTDo6Tm9kZSBiYWNrID0gWUFNTDo6TG9hZChZQU1MOjpEdW1wKGJ1aWx0KSk7CiAgICByZXR1cm4gYmFja1sieCJdLmFzPGludD4oKSA9PSA3ICYmIGJhY2tbInkiXS5zaXplKCkgPT0gMgogICAgICAgICAgICYmIGJhY2tbInkiXVsxXS5hczxzdGQ6OnN0cmluZz4oKSA9PSAic2Vjb25kIjsKfQoKYm9vbCByZWplY3RzX21hbGZvcm1lZF9pbnB1dCgpIHsKICAgIHRyeSB7CiAgICAgICAgKHZvaWQpWUFNTDo6TG9hZCgia2V5OiBbdW50ZXJtaW5hdGVkXG5vdGhlcjogMVxuIik7CiAgICB9IGNhdGNoIChjb25zdCBZQU1MOjpQYXJzZXJFeGNlcHRpb24gJmUpIHsKICAgICAgICAvLyBUaGUgbWFyayBwb2ludHMgaW50byB0aGUgZG9jdW1lbnQgcmF0aGVyIHRoYW4gYmVpbmcgbGVmdCB1bnNldC4KICAgICAgICByZXR1cm4gZS5tYXJrLmxpbmUgPj0gMCAmJiAhc3RkOjpzdHJpbmcoZS53aGF0KCkpLmVtcHR5KCk7CiAgICB9CiAgICByZXR1cm4gZmFsc2U7Cn0KCn0gIC8vIG5hbWVzcGFjZQoKLy8gVXBzdHJlYW0gZGVjbGFyZXMgR3JhcGhCdWlsZGVySW50ZXJmYWNlJ3MgZGVzdHJ1Y3RvciBwdXJlIHZpcnR1YWwgYW5kCi8vIGRlZmluZXMgaXQgbm93aGVyZSAoMC44LjAgYW5kIDAuOS4wIGFsaWtlKSwgc28gZXZlcnkgY2xhc3MgZGVyaXZlZCBmcm9tIGl0Ci8vIC0tIHVwc3RyZWFtJ3Mgb3duIEdyYXBoQnVpbGRlcjxJbXBsPiBpbmNsdWRlZCAtLSBuZWVkcyB0aGUgZGVmaW5pdGlvbiBmcm9tCi8vIGl0cyB1c2VyLiBUaGlzIGlzIHRoYXQgZGVmaW5pdGlvbiwgYXMgYW55IGNvbnN1bWVyIG9mIHRoZSBjb250cmliIEFQSSBtdXN0Ci8vIHdyaXRlIGl0LiBCdWlsZEdyYXBoT2ZOZXh0RG9jdW1lbnQgYmVsb3cgc3RpbGwgY29tZXMgb25seSBmcm9tCi8vIHNyYy9jb250cmliL2dyYXBoYnVpbGRlci5jcHAsIHNvIHRoZSBjaGVjayBzdGlsbCBwcm92ZXMgY29udHJpYiB3YXMgYnVpbHQuCllBTUw6OkdyYXBoQnVpbGRlckludGVyZmFjZTo6fkdyYXBoQnVpbGRlckludGVyZmFjZSgpID0gZGVmYXVsdDsKCm5hbWVzcGFjZSB7CgovLyBDb3VudHMgd2hhdCB0aGUgY29udHJpYiBHcmFwaEJ1aWxkZXIgaXMgdG9sZCwgd2hpY2ggb25seSB0aGUgdHdvCi8vIHNyYy9jb250cmliLyouY3BwIHRyYW5zbGF0aW9uIHVuaXRzIGltcGxlbWVudC4Kc3RydWN0IENvdW50ZXIgOiBZQU1MOjpHcmFwaEJ1aWxkZXJJbnRlcmZhY2UgewogICAgaW50IHNjYWxhcnMgPSAwLCBzZXF1ZW5jZXMgPSAwLCBtYXBzID0gMDsKICAgIHZvaWQgKk5ld051bGwoY29uc3QgWUFNTDo6TWFyayAmLCB2b2lkICopIG92ZXJyaWRlIHsgcmV0dXJuIHRoaXM7IH0KICAgIHZvaWQgKk5ld1NjYWxhcihjb25zdCBZQU1MOjpNYXJrICYsIGNvbnN0IHN0ZDo6c3RyaW5nICYsIHZvaWQgKiwKICAgICAgICAgICAgICAgICAgICBjb25zdCBzdGQ6OnN0cmluZyAmKSBvdmVycmlkZSB7CiAgICAgICAgKytzY2FsYXJzOwogICAgICAgIHJldHVybiB0aGlzOwogICAgfQogICAgdm9pZCAqTmV3U2VxdWVuY2UoY29uc3QgWUFNTDo6TWFyayAmLCBjb25zdCBzdGQ6OnN0cmluZyAmLCB2b2lkICopIG92ZXJyaWRlIHsKICAgICAgICArK3NlcXVlbmNlczsKICAgICAgICByZXR1cm4gdGhpczsKICAgIH0KICAgIHZvaWQgQXBwZW5kVG9TZXF1ZW5jZSh2b2lkICosIHZvaWQgKikgb3ZlcnJpZGUge30KICAgIHZvaWQgU2VxdWVuY2VDb21wbGV0ZSh2b2lkICopIG92ZXJyaWRlIHt9CiAgICB2b2lkICpOZXdNYXAoY29uc3QgWUFNTDo6TWFyayAmLCBjb25zdCBzdGQ6OnN0cmluZyAmLCB2b2lkICopIG92ZXJyaWRlIHsKICAgICAgICArK21hcHM7CiAgICAgICAgcmV0dXJuIHRoaXM7CiAgICB9CiAgICB2b2lkIEFzc2lnbkluTWFwKHZvaWQgKiwgdm9pZCAqLCB2b2lkICopIG92ZXJyaWRlIHt9CiAgICB2b2lkIE1hcENvbXBsZXRlKHZvaWQgKikgb3ZlcnJpZGUge30KICAgIHZvaWQgKkFuY2hvclJlZmVyZW5jZShjb25zdCBZQU1MOjpNYXJrICYsIHZvaWQgKm5vZGUpIG92ZXJyaWRlIHsgcmV0dXJuIG5vZGU7IH0KfTsKCmJvb2wgYnVpbGRzX2FfZ3JhcGgoKSB7CiAgICBzdGQ6OmlzdHJpbmdzdHJlYW0gaW4oImE6IFsxLCAyXVxuYjoge2M6IDN9XG4iKTsKICAgIFlBTUw6OlBhcnNlciBwYXJzZXIoaW4pOwogICAgQ291bnRlciBjb3VudGVyOwogICAgaWYgKFlBTUw6OkJ1aWxkR3JhcGhPZk5leHREb2N1bWVudChwYXJzZXIsIGNvdW50ZXIpID09IG51bGxwdHIpIHJldHVybiBmYWxzZTsKICAgIC8vIGtleXMgYSwgYiwgYyBhbmQgdmFsdWVzIDEsIDIsIDMKICAgIHJldHVybiBjb3VudGVyLnNjYWxhcnMgPT0gNiAmJiBjb3VudGVyLnNlcXVlbmNlcyA9PSAxICYmIGNvdW50ZXIubWFwcyA9PSAyOwp9CgovLyAwLjkuMCBmb3JtYXRzIGZsb2F0aW5nIHBvaW50IGJ5IHRoZSBzaG9ydGVzdCByZXByZXNlbnRhdGlvbiB0aGF0IHJlYWRzCi8vIGJhY2sgdG8gdGhlIHNhbWUgdmFsdWU7IDAuOC4wIHByaW50ZWQgbWF4X2RpZ2l0czEwIGRpZ2l0cwovLyAoIjAuMTAwMDAwMDAwMDAwMDAwMDEiKS4gVGhlIGVtaXR0ZXIgdXNlcyB0aGUgc2FtZSByb3V0aW5lLgpib29sIGZvcm1hdHNfZmxvYXRzX3Nob3J0ZXN0KCkgewogICAgaWYgKFlBTUw6OkZwVG9TdHJpbmcoMC4xKSAhPSAiMC4xIikgcmV0dXJuIGZhbHNlOwogICAgaWYgKFlBTUw6OkZwVG9TdHJpbmcoMS41ZikgIT0gIjEuNSIpIHJldHVybiBmYWxzZTsKICAgIFlBTUw6OkVtaXR0ZXIgb3V0OwogICAgb3V0IDw8IDAuMTsKICAgIHJldHVybiBzdGQ6OnN0cmluZyhvdXQuY19zdHIoKSkgPT0gIjAuMSI7Cn0KCn0gIC8vIG5hbWVzcGFjZQoKaW50IG1haW4oKSB7CiAgICBzdHJ1Y3QgQ2hlY2sgewogICAgICAgIGNvbnN0IGNoYXIgKm5hbWU7CiAgICAgICAgYm9vbCAoKnJ1bikoKTsKICAgIH0gY2hlY2tzW10gPSB7CiAgICAgICAgeyJsb2Fkc19hbmRfY29udmVydHMiLCBsb2Fkc19hbmRfY29udmVydHN9LAogICAgICAgIHsicmVzb2x2ZXNfYWxpYXNlcyIsIHJlc29sdmVzX2FsaWFzZXN9LAogICAgICAgIHsibG9hZHNfZXZlcnlfZG9jdW1lbnQiLCBsb2Fkc19ldmVyeV9kb2N1bWVudH0sCiAgICAgICAgeyJlbWl0c19hbmRfcm91bmRfdHJpcHMiLCBlbWl0c19hbmRfcm91bmRfdHJpcHN9LAogICAgICAgIHsicmVqZWN0c19tYWxmb3JtZWRfaW5wdXQiLCByZWplY3RzX21hbGZvcm1lZF9pbnB1dH0sCiAgICAgICAgeyJidWlsZHNfYV9ncmFwaCIsIGJ1aWxkc19hX2dyYXBofSwKICAgICAgICB7ImZvcm1hdHNfZmxvYXRzX3Nob3J0ZXN0IiwgZm9ybWF0c19mbG9hdHNfc2hvcnRlc3R9LAogICAgfTsKICAgIGJvb2wgb2sgPSB0cnVlOwogICAgZm9yIChjb25zdCBDaGVjayAmYyA6IGNoZWNrcykgewogICAgICAgIGNvbnN0IGJvb2wgcGFzc2VkID0gYy5ydW4oKTsKICAgICAgICBzdGQ6OnByaW50ZigiJXMgJXNcbiIsIHBhc3NlZCA/ICJvayAgIiA6ICJGQUlMIiwgYy5uYW1lKTsKICAgICAgICBvayA9IG9rICYmIHBhc3NlZDsKICAgIH0KICAgIHJldHVybiBvayA/IDAgOiAxOwp9Cg==" +consumer yaml-cpp-gcc15 "gcc@15.1.0" compat 'yaml-cpp = "0.9.0"' "Ly8gQmVoYXZpb3JhbCB0ZXN0IGZvciBjb21wYXQueWFtbC1jcHAgMC45LjAuCi8vCi8vIEVhY2ggY2hlY2sgcmVhY2hlcyBzeW1ib2xzIGRlZmluZWQgaW4gdGhlIHBhY2thZ2UncyBvd24gLmNwcCBmaWxlcywgc28gYQovLyBwYWNrYWdlIHRoYXQgY29tcGlsZWQgbm90aGluZyBjYW5ub3QgcGFzcyBieSBiZWluZyBoZWFkZXItb25seSBieSBhY2NpZGVudDoKLy8KLy8gICBZQU1MOjpMb2FkLCBub2RlIGFjY2VzcywgY29udmVyc2lvbnMgIC0+IHBhcnNlLmNwcCwgbm9kZV9kYXRhLmNwcCwgY29udmVydC5jcHAKLy8gICBhbmNob3JzIGFuZCBhbGlhc2VzICAgICAgICAgICAgICAgICAgIC0+IG5vZGVidWlsZGVyLmNwcCwgc2luZ2xlZG9jcGFyc2VyLmNwcAovLyAgIFlBTUw6OkxvYWRBbGwgICAgICAgICAgICAgICAgICAgICAgICAgLT4gcGFyc2UuY3BwLCBwYXJzZXIuY3BwCi8vICAgWUFNTDo6RW1pdHRlciwgWUFNTDo6RHVtcCAgICAgICAgICAgICAtPiBlbWl0dGVyLmNwcCwgZW1pdHRlcnN0YXRlLmNwcCwgZW1pdC5jcHAKLy8gICBtYWxmb3JtZWQgaW5wdXQgICAgICAgICAgICAgICAgICAgICAgIC0+IHNjYW5uZXIuY3BwLCBleGNlcHRpb25zLmNwcAovLyAgIFlBTUw6OkJ1aWxkR3JhcGhPZk5leHREb2N1bWVudCAgICAgICAgLT4gY29udHJpYi9ncmFwaGJ1aWxkZXIqLmNwcAovLyAgIFlBTUw6OkZwVG9TdHJpbmcgICAgICAgICAgICAgICAgICAgICAgLT4gZnB0b3N0cmluZy5jcHAgKG5ldyBpbiAwLjkuMCkKLy8KLy8gUmV0dXJucyBub24temVybyBvbiBhbnkgbWlzbWF0Y2guCiNpbmNsdWRlIDx5YW1sLWNwcC95YW1sLmg+CiNpbmNsdWRlIDx5YW1sLWNwcC9jb250cmliL2dyYXBoYnVpbGRlci5oPgojaW5jbHVkZSA8eWFtbC1jcHAvZnB0b3N0cmluZy5oPgoKI2luY2x1ZGUgPGNzdGRpbz4KI2luY2x1ZGUgPHNzdHJlYW0+CiNpbmNsdWRlIDxzdHJpbmc+CiNpbmNsdWRlIDx2ZWN0b3I+CgovLyBUaGUgcGFja2FnZSBidWlsZHMgb2JqZWN0cywgbm90IGEgc2hhcmVkIGxpYnJhcnksIHNvIGV2ZXJ5IHlhbWwtY3BwCi8vIGRlY2xhcmF0aW9uIHRoaXMgVFUgc2VlcyBtdXN0IGJlIHBsYWluIC0tIG5vdCBkbGxpbXBvcnQuIFRoZSBwYWNrYWdlCi8vIGRlbGl2ZXJzIHRoYXQgd2l0aCBhIHNoaW0gaW4gZnJvbnQgb2YgeWFtbC1jcHAvZGxsLmgsIGJlY2F1c2Ugbm8gZGVzY3JpcHRvcgovLyBrZXkgY2FycmllcyBhIGRlZmluZSB0byBhIGNvbnN1bWVyJ3MgVFVzLiBPbiBMaW51eCB0aGUgZGlmZmVyZW5jZSBpcyBvbmx5IGEKLy8gdmlzaWJpbGl0eSBhdHRyaWJ1dGUgYW5kIGV2ZXJ5dGhpbmcgbGlua3MgYW55d2F5OyBvbiB0aGUgTVNWQyBBQkkgaXQgaXMgYQovLyBsaW5rIGZhaWx1cmUuIEFzc2VydGluZyBpdCBhdCBjb21waWxlIHRpbWUgZmFpbHMgZXZlcnl3aGVyZSB0aGUgbW9tZW50IHRoZQovLyBzaGltIHN0b3BzIGJlaW5nIHJlYWNoZWQuCiNpZm5kZWYgWUFNTF9DUFBfU1RBVElDX0RFRklORQojICBlcnJvciAiWUFNTF9DUFBfU1RBVElDX0RFRklORSBkaWQgbm90IHJlYWNoIHRoZSBjb25zdW1lcjogdGhlIHlhbWwtY3BwL2RsbC5oIHNoaW0gd2FzIG5vdCBmb3VuZCBmaXJzdCIKI2VuZGlmCgpuYW1lc3BhY2UgewoKY29uc3QgY2hhciAqY29uc3Qga0RvYyA9IFIiKApuYW1lOiBtY3BwLWluZGV4CnZlcnNpb246IDMKcmF0aW86IDAuMjUKZW5hYmxlZDogdHJ1ZQp0YWdzOiBbeWFtbCwgY3BwLCAicXVvdGVkLCB3aXRoIGNvbW1hIl0KYmFzZTogJmJhc2UKICBob3N0OiBsb2NhbGhvc3QKICBwb3J0OiA4MDgwCmRlcml2ZWQ6CiAgPDw6ICpiYXNlCiAgYWxpYXNfb2ZfYmFzZTogKmJhc2UKdGV4dDogfAogIGxpbmUgb25lCiAgbGluZSB0d28KKSI7Cgpib29sIGxvYWRzX2FuZF9jb252ZXJ0cygpIHsKICAgIFlBTUw6Ok5vZGUgcm9vdCA9IFlBTUw6OkxvYWQoa0RvYyk7CiAgICBpZiAoIXJvb3QuSXNNYXAoKSkgcmV0dXJuIGZhbHNlOwogICAgaWYgKHJvb3RbIm5hbWUiXS5hczxzdGQ6OnN0cmluZz4oKSAhPSAibWNwcC1pbmRleCIpIHJldHVybiBmYWxzZTsKICAgIGlmIChyb290WyJ2ZXJzaW9uIl0uYXM8aW50PigpICE9IDMpIHJldHVybiBmYWxzZTsKICAgIGlmIChyb290WyJyYXRpbyJdLmFzPGRvdWJsZT4oKSAhPSAwLjI1KSByZXR1cm4gZmFsc2U7CiAgICBpZiAoIXJvb3RbImVuYWJsZWQiXS5hczxib29sPigpKSByZXR1cm4gZmFsc2U7CgogICAgY29uc3QgWUFNTDo6Tm9kZSB0YWdzID0gcm9vdFsidGFncyJdOwogICAgaWYgKCF0YWdzLklzU2VxdWVuY2UoKSB8fCB0YWdzLnNpemUoKSAhPSAzKSByZXR1cm4gZmFsc2U7CiAgICBpZiAodGFnc1syXS5hczxzdGQ6OnN0cmluZz4oKSAhPSAicXVvdGVkLCB3aXRoIGNvbW1hIikgcmV0dXJuIGZhbHNlOwogICAgaWYgKHRhZ3MuYXM8c3RkOjp2ZWN0b3I8c3RkOjpzdHJpbmc+PigpLmZyb250KCkgIT0gInlhbWwiKSByZXR1cm4gZmFsc2U7CgogICAgaWYgKHJvb3RbInRleHQiXS5hczxzdGQ6OnN0cmluZz4oKSAhPSAibGluZSBvbmVcbmxpbmUgdHdvXG4iKSByZXR1cm4gZmFsc2U7CgogICAgLy8gQSBtaXNzaW5nIGtleSBpcyByZXBvcnRlZCBhcyB1bmRlZmluZWQgYW5kIGEgZmFsbGJhY2sgYXBwbGllczsgYW4KICAgIC8vIGV4cGxpY2l0IGNvbnZlcnNpb24gb2YgdGhlIHdyb25nIGtpbmQgdGhyb3dzLgogICAgaWYgKHJvb3RbImFic2VudCJdLklzRGVmaW5lZCgpKSByZXR1cm4gZmFsc2U7CiAgICBpZiAocm9vdFsiYWJzZW50Il0uYXM8aW50Pig0MikgIT0gNDIpIHJldHVybiBmYWxzZTsKICAgIHRyeSB7CiAgICAgICAgKHZvaWQpcm9vdFsibmFtZSJdLmFzPGludD4oKTsKICAgICAgICByZXR1cm4gZmFsc2U7CiAgICB9IGNhdGNoIChjb25zdCBZQU1MOjpCYWRDb252ZXJzaW9uICYpIHsKICAgIH0KICAgIHJldHVybiB0cnVlOwp9Cgpib29sIHJlc29sdmVzX2FsaWFzZXMoKSB7CiAgICBZQU1MOjpOb2RlIHJvb3QgPSBZQU1MOjpMb2FkKGtEb2MpOwogICAgY29uc3QgWUFNTDo6Tm9kZSBhbGlhcyA9IHJvb3RbImRlcml2ZWQiXVsiYWxpYXNfb2ZfYmFzZSJdOwogICAgaWYgKGFsaWFzWyJwb3J0Il0uYXM8aW50PigpICE9IDgwODApIHJldHVybiBmYWxzZTsKICAgIC8vIEFuIGFsaWFzIGlzIHRoZSBTQU1FIG5vZGUgYXMgaXRzIGFuY2hvciwgbm90IGEgY29weSBvZiBpdC4KICAgIHJldHVybiBhbGlhcy5pcyhyb290WyJiYXNlIl0pOwp9Cgpib29sIGxvYWRzX2V2ZXJ5X2RvY3VtZW50KCkgewogICAgY29uc3Qgc3RkOjp2ZWN0b3I8WUFNTDo6Tm9kZT4gZG9jcyA9IFlBTUw6OkxvYWRBbGwoImE6IDFcbi0tLVxuYjogMlxuLS0tXG4tIDNcbiIpOwogICAgcmV0dXJuIGRvY3Muc2l6ZSgpID09IDMgJiYgZG9jc1swXVsiYSJdLmFzPGludD4oKSA9PSAxCiAgICAgICAgICAgJiYgZG9jc1sxXVsiYiJdLmFzPGludD4oKSA9PSAyICYmIGRvY3NbMl1bMF0uYXM8aW50PigpID09IDM7Cn0KCmJvb2wgZW1pdHNfYW5kX3JvdW5kX3RyaXBzKCkgewogICAgWUFNTDo6RW1pdHRlciBvdXQ7CiAgICBvdXQgPDwgWUFNTDo6QmVnaW5NYXA7CiAgICBvdXQgPDwgWUFNTDo6S2V5IDw8ICJsaXN0IiA8PCBZQU1MOjpWYWx1ZSA8PCBZQU1MOjpGbG93CiAgICAgICAgPDwgWUFNTDo6QmVnaW5TZXEgPDwgMSA8PCAyIDw8IDMgPDwgWUFNTDo6RW5kU2VxOwogICAgb3V0IDw8IFlBTUw6OktleSA8PCAibmVzdGVkIiA8PCBZQU1MOjpWYWx1ZSA8PCBZQU1MOjpCZWdpbk1hcAogICAgICAgIDw8IFlBTUw6OktleSA8PCAiayIgPDwgWUFNTDo6VmFsdWUgPDwgInYiIDw8IFlBTUw6OkVuZE1hcDsKICAgIG91dCA8PCBZQU1MOjpFbmRNYXA7CiAgICBpZiAoIW91dC5nb29kKCkpIHJldHVybiBmYWxzZTsKICAgIGlmIChzdGQ6OnN0cmluZyhvdXQuY19zdHIoKSkgIT0gImxpc3Q6IFsxLCAyLCAzXVxubmVzdGVkOlxuICBrOiB2IikgcmV0dXJuIGZhbHNlOwoKICAgIFlBTUw6Ok5vZGUgYWdhaW4gPSBZQU1MOjpMb2FkKG91dC5jX3N0cigpKTsKICAgIGlmIChhZ2FpblsibGlzdCJdWzJdLmFzPGludD4oKSAhPSAzKSByZXR1cm4gZmFsc2U7CgogICAgLy8gQSBub2RlIGJ1aWx0IGluIGNvZGUgYW5kIGR1bXBlZCBjb21lcyBiYWNrIHdpdGggdGhlIHNhbWUgY29udGVudC4KICAgIFlBTUw6Ok5vZGUgYnVpbHQ7CiAgICBidWlsdFsieCJdID0gNzsKICAgIGJ1aWx0WyJ5Il0ucHVzaF9iYWNrKCJmaXJzdCIpOwogICAgYnVpbHRbInkiXS5wdXNoX2JhY2soInNlY29uZCIpOwogICAgWUFNTDo6Tm9kZSBiYWNrID0gWUFNTDo6TG9hZChZQU1MOjpEdW1wKGJ1aWx0KSk7CiAgICByZXR1cm4gYmFja1sieCJdLmFzPGludD4oKSA9PSA3ICYmIGJhY2tbInkiXS5zaXplKCkgPT0gMgogICAgICAgICAgICYmIGJhY2tbInkiXVsxXS5hczxzdGQ6OnN0cmluZz4oKSA9PSAic2Vjb25kIjsKfQoKYm9vbCByZWplY3RzX21hbGZvcm1lZF9pbnB1dCgpIHsKICAgIHRyeSB7CiAgICAgICAgKHZvaWQpWUFNTDo6TG9hZCgia2V5OiBbdW50ZXJtaW5hdGVkXG5vdGhlcjogMVxuIik7CiAgICB9IGNhdGNoIChjb25zdCBZQU1MOjpQYXJzZXJFeGNlcHRpb24gJmUpIHsKICAgICAgICAvLyBUaGUgbWFyayBwb2ludHMgaW50byB0aGUgZG9jdW1lbnQgcmF0aGVyIHRoYW4gYmVpbmcgbGVmdCB1bnNldC4KICAgICAgICByZXR1cm4gZS5tYXJrLmxpbmUgPj0gMCAmJiAhc3RkOjpzdHJpbmcoZS53aGF0KCkpLmVtcHR5KCk7CiAgICB9CiAgICByZXR1cm4gZmFsc2U7Cn0KCn0gIC8vIG5hbWVzcGFjZQoKLy8gVXBzdHJlYW0gZGVjbGFyZXMgR3JhcGhCdWlsZGVySW50ZXJmYWNlJ3MgZGVzdHJ1Y3RvciBwdXJlIHZpcnR1YWwgYW5kCi8vIGRlZmluZXMgaXQgbm93aGVyZSAoMC44LjAgYW5kIDAuOS4wIGFsaWtlKSwgc28gZXZlcnkgY2xhc3MgZGVyaXZlZCBmcm9tIGl0Ci8vIC0tIHVwc3RyZWFtJ3Mgb3duIEdyYXBoQnVpbGRlcjxJbXBsPiBpbmNsdWRlZCAtLSBuZWVkcyB0aGUgZGVmaW5pdGlvbiBmcm9tCi8vIGl0cyB1c2VyLiBUaGlzIGlzIHRoYXQgZGVmaW5pdGlvbiwgYXMgYW55IGNvbnN1bWVyIG9mIHRoZSBjb250cmliIEFQSSBtdXN0Ci8vIHdyaXRlIGl0LiBCdWlsZEdyYXBoT2ZOZXh0RG9jdW1lbnQgYmVsb3cgc3RpbGwgY29tZXMgb25seSBmcm9tCi8vIHNyYy9jb250cmliL2dyYXBoYnVpbGRlci5jcHAsIHNvIHRoZSBjaGVjayBzdGlsbCBwcm92ZXMgY29udHJpYiB3YXMgYnVpbHQuCllBTUw6OkdyYXBoQnVpbGRlckludGVyZmFjZTo6fkdyYXBoQnVpbGRlckludGVyZmFjZSgpID0gZGVmYXVsdDsKCm5hbWVzcGFjZSB7CgovLyBDb3VudHMgd2hhdCB0aGUgY29udHJpYiBHcmFwaEJ1aWxkZXIgaXMgdG9sZCwgd2hpY2ggb25seSB0aGUgdHdvCi8vIHNyYy9jb250cmliLyouY3BwIHRyYW5zbGF0aW9uIHVuaXRzIGltcGxlbWVudC4Kc3RydWN0IENvdW50ZXIgOiBZQU1MOjpHcmFwaEJ1aWxkZXJJbnRlcmZhY2UgewogICAgaW50IHNjYWxhcnMgPSAwLCBzZXF1ZW5jZXMgPSAwLCBtYXBzID0gMDsKICAgIHZvaWQgKk5ld051bGwoY29uc3QgWUFNTDo6TWFyayAmLCB2b2lkICopIG92ZXJyaWRlIHsgcmV0dXJuIHRoaXM7IH0KICAgIHZvaWQgKk5ld1NjYWxhcihjb25zdCBZQU1MOjpNYXJrICYsIGNvbnN0IHN0ZDo6c3RyaW5nICYsIHZvaWQgKiwKICAgICAgICAgICAgICAgICAgICBjb25zdCBzdGQ6OnN0cmluZyAmKSBvdmVycmlkZSB7CiAgICAgICAgKytzY2FsYXJzOwogICAgICAgIHJldHVybiB0aGlzOwogICAgfQogICAgdm9pZCAqTmV3U2VxdWVuY2UoY29uc3QgWUFNTDo6TWFyayAmLCBjb25zdCBzdGQ6OnN0cmluZyAmLCB2b2lkICopIG92ZXJyaWRlIHsKICAgICAgICArK3NlcXVlbmNlczsKICAgICAgICByZXR1cm4gdGhpczsKICAgIH0KICAgIHZvaWQgQXBwZW5kVG9TZXF1ZW5jZSh2b2lkICosIHZvaWQgKikgb3ZlcnJpZGUge30KICAgIHZvaWQgU2VxdWVuY2VDb21wbGV0ZSh2b2lkICopIG92ZXJyaWRlIHt9CiAgICB2b2lkICpOZXdNYXAoY29uc3QgWUFNTDo6TWFyayAmLCBjb25zdCBzdGQ6OnN0cmluZyAmLCB2b2lkICopIG92ZXJyaWRlIHsKICAgICAgICArK21hcHM7CiAgICAgICAgcmV0dXJuIHRoaXM7CiAgICB9CiAgICB2b2lkIEFzc2lnbkluTWFwKHZvaWQgKiwgdm9pZCAqLCB2b2lkICopIG92ZXJyaWRlIHt9CiAgICB2b2lkIE1hcENvbXBsZXRlKHZvaWQgKikgb3ZlcnJpZGUge30KICAgIHZvaWQgKkFuY2hvclJlZmVyZW5jZShjb25zdCBZQU1MOjpNYXJrICYsIHZvaWQgKm5vZGUpIG92ZXJyaWRlIHsgcmV0dXJuIG5vZGU7IH0KfTsKCmJvb2wgYnVpbGRzX2FfZ3JhcGgoKSB7CiAgICBzdGQ6OmlzdHJpbmdzdHJlYW0gaW4oImE6IFsxLCAyXVxuYjoge2M6IDN9XG4iKTsKICAgIFlBTUw6OlBhcnNlciBwYXJzZXIoaW4pOwogICAgQ291bnRlciBjb3VudGVyOwogICAgaWYgKFlBTUw6OkJ1aWxkR3JhcGhPZk5leHREb2N1bWVudChwYXJzZXIsIGNvdW50ZXIpID09IG51bGxwdHIpIHJldHVybiBmYWxzZTsKICAgIC8vIGtleXMgYSwgYiwgYyBhbmQgdmFsdWVzIDEsIDIsIDMKICAgIHJldHVybiBjb3VudGVyLnNjYWxhcnMgPT0gNiAmJiBjb3VudGVyLnNlcXVlbmNlcyA9PSAxICYmIGNvdW50ZXIubWFwcyA9PSAyOwp9CgovLyAwLjkuMCBmb3JtYXRzIGZsb2F0aW5nIHBvaW50IGJ5IHRoZSBzaG9ydGVzdCByZXByZXNlbnRhdGlvbiB0aGF0IHJlYWRzCi8vIGJhY2sgdG8gdGhlIHNhbWUgdmFsdWU7IDAuOC4wIHByaW50ZWQgbWF4X2RpZ2l0czEwIGRpZ2l0cwovLyAoIjAuMTAwMDAwMDAwMDAwMDAwMDEiKS4gVGhlIGVtaXR0ZXIgdXNlcyB0aGUgc2FtZSByb3V0aW5lLgpib29sIGZvcm1hdHNfZmxvYXRzX3Nob3J0ZXN0KCkgewogICAgaWYgKFlBTUw6OkZwVG9TdHJpbmcoMC4xKSAhPSAiMC4xIikgcmV0dXJuIGZhbHNlOwogICAgaWYgKFlBTUw6OkZwVG9TdHJpbmcoMS41ZikgIT0gIjEuNSIpIHJldHVybiBmYWxzZTsKICAgIFlBTUw6OkVtaXR0ZXIgb3V0OwogICAgb3V0IDw8IDAuMTsKICAgIHJldHVybiBzdGQ6OnN0cmluZyhvdXQuY19zdHIoKSkgPT0gIjAuMSI7Cn0KCn0gIC8vIG5hbWVzcGFjZQoKaW50IG1haW4oKSB7CiAgICBzdHJ1Y3QgQ2hlY2sgewogICAgICAgIGNvbnN0IGNoYXIgKm5hbWU7CiAgICAgICAgYm9vbCAoKnJ1bikoKTsKICAgIH0gY2hlY2tzW10gPSB7CiAgICAgICAgeyJsb2Fkc19hbmRfY29udmVydHMiLCBsb2Fkc19hbmRfY29udmVydHN9LAogICAgICAgIHsicmVzb2x2ZXNfYWxpYXNlcyIsIHJlc29sdmVzX2FsaWFzZXN9LAogICAgICAgIHsibG9hZHNfZXZlcnlfZG9jdW1lbnQiLCBsb2Fkc19ldmVyeV9kb2N1bWVudH0sCiAgICAgICAgeyJlbWl0c19hbmRfcm91bmRfdHJpcHMiLCBlbWl0c19hbmRfcm91bmRfdHJpcHN9LAogICAgICAgIHsicmVqZWN0c19tYWxmb3JtZWRfaW5wdXQiLCByZWplY3RzX21hbGZvcm1lZF9pbnB1dH0sCiAgICAgICAgeyJidWlsZHNfYV9ncmFwaCIsIGJ1aWxkc19hX2dyYXBofSwKICAgICAgICB7ImZvcm1hdHNfZmxvYXRzX3Nob3J0ZXN0IiwgZm9ybWF0c19mbG9hdHNfc2hvcnRlc3R9LAogICAgfTsKICAgIGJvb2wgb2sgPSB0cnVlOwogICAgZm9yIChjb25zdCBDaGVjayAmYyA6IGNoZWNrcykgewogICAgICAgIGNvbnN0IGJvb2wgcGFzc2VkID0gYy5ydW4oKTsKICAgICAgICBzdGQ6OnByaW50ZigiJXMgJXNcbiIsIHBhc3NlZCA/ICJvayAgIiA6ICJGQUlMIiwgYy5uYW1lKTsKICAgICAgICBvayA9IG9rICYmIHBhc3NlZDsKICAgIH0KICAgIHJldHVybiBvayA/IDAgOiAxOwp9Cg==" +consumer cli11 "" compat 'CLI11 = "2.7.2"' "Ly8gQmVoYXZpb3JhbCB0ZXN0OiBwYXJzZSBzeW50aGV0aWMgYXJndiB3aXRoIENMSTExIGFuZCBhc3NlcnQgd2hhdCBjYW1lIG91dC4KLy8gQ292ZXJzIHRoZSBmb3VyIHRoaW5ncyBhIHBhcnNlciBoYXMgdG8gZ2V0IHJpZ2h0IOKAlCB2YWx1ZXMgYW5kIGRlZmF1bHRzLAovLyBmbGFncywgYSBtdWx0aS12YWx1ZSBvcHRpb24sIGEgc3ViY29tbWFuZCDigJQgcGx1cyB0aGUgdHdvIGZhaWx1cmUgcGF0aHMKLy8gKHVua25vd24gb3B0aW9uLCBmYWlsZWQgdmFsaWRhdG9yKSwgd2hpY2ggaXMgd2hhdCBwcm92ZXMgdGhlIHBhcnNlIGFjdHVhbGx5Ci8vIHJhbiByYXRoZXIgdGhhbiBzaWxlbnRseSBkb2luZyBub3RoaW5nLiBSZXR1cm5zIG5vbi16ZXJvIG9uIGFueSBtaXNtYXRjaC4KLy8KLy8gSXQgYWxzbyBjYWxscyB0aGUgcGFja2FnZSdzIGFuY2hvciBzeW1ib2wuIGNvbXBhdC5DTEkxMSBpcyBoZWFkZXItb25seSwgc28KLy8gaXRzIGxpYiB0YXJnZXQgaG9sZHMgZXhhY3RseSB0aGF0IG9uZSB0cmFuc2xhdGlvbiB1bml0OiBpZiB0aGUgcmVmZXJlbmNlCi8vIHJlc29sdmVzLCB0aGUgcGFja2FnZSByZWFsbHkgd2FzIGJ1aWx0IGFuZCBsaW5rZWQsIG5vdCBqdXN0IGluY2x1ZGVkLgojaW5jbHVkZSA8Q0xJL0NMSS5ocHA+CgojaW5jbHVkZSA8c3RyaW5nPgojaW5jbHVkZSA8dmVjdG9yPgoKaW50IG1jcHBfY29tcGF0X2NsaTExX2hlYWRlcnNfYW5jaG9yKHZvaWQpOwoKbmFtZXNwYWNlIHsKCi8vIFZhbHVlcywgZGVmYXVsdCwgZmxhZywgbXVsdGktdmFsdWUgb3B0aW9uIGFuZCBhIHN1YmNvbW1hbmQsIGluIG9uZSBwYXJzZS4KYm9vbCBwYXJzZXNfYV9mdWxsX2NvbW1hbmRfbGluZSgpIHsKICAgIENMSTo6QXBwIGFwcHsibWNwcC1pbmRleCBDTEkxMSBtZW1iZXIifTsKCiAgICBzdGQ6OnN0cmluZyBuYW1lOwogICAgYXBwLmFkZF9vcHRpb24oIi1uLC0tbmFtZSIsIG5hbWUsICJhIG5hbWUiKS0+cmVxdWlyZWQoKTsKCiAgICBpbnQgY291bnQgPSAxOyAgLy8gbGVmdCBhbG9uZSBieSB0aGUgYXJndiBiZWxvdzogYXNzZXJ0cyB0aGUgZGVmYXVsdCBob2xkcwogICAgYXBwLmFkZF9vcHRpb24oIi1jLC0tY291bnQiLCBjb3VudCwgImhvdyBtYW55Iik7CgogICAgYm9vbCB2ZXJib3NlID0gZmFsc2U7CiAgICBhcHAuYWRkX2ZsYWcoIi12LC0tdmVyYm9zZSIsIHZlcmJvc2UsICJjaGF0dHkiKTsKCiAgICBzdGQ6OnZlY3RvcjxpbnQ+IG51bXM7CiAgICBhcHAuYWRkX29wdGlvbigiLS1udW1zIiwgbnVtcywgIm51bWJlcnMiKS0+ZXhwZWN0ZWQoMyk7CgogICAgYXV0byAqc3ViID0gYXBwLmFkZF9zdWJjb21tYW5kKCJydW4iLCAiZG8gdGhlIHRoaW5nIik7CiAgICBzdGQ6OnN0cmluZyB0YXJnZXQ7CiAgICBzdWItPmFkZF9vcHRpb24oInRhcmdldCIsIHRhcmdldCwgIndoYXQgdG8gcnVuIiktPnJlcXVpcmVkKCk7CgogICAgY29uc3QgY2hhciAqYXJndltdID0geyJwcm9nIiwgIi0tbmFtZSIsICJtY3BwIiwgIi12IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAiLS1udW1zIiwgIjEiLCAiMiIsICIzIiwgInJ1biIsICJpbmRleCJ9OwogICAgdHJ5IHsKICAgICAgICBhcHAucGFyc2Uoc3RhdGljX2Nhc3Q8aW50PihzaXplb2YoYXJndikgLyBzaXplb2YoYXJndlswXSkpLCBhcmd2KTsKICAgIH0gY2F0Y2ggKGNvbnN0IENMSTo6UGFyc2VFcnJvciAmKSB7CiAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgfQoKICAgIHJldHVybiBuYW1lID09ICJtY3BwIiAmJiBjb3VudCA9PSAxICYmIHZlcmJvc2UKICAgICAgICAgICAmJiBudW1zID09IHN0ZDo6dmVjdG9yPGludD57MSwgMiwgM30KICAgICAgICAgICAmJiBhcHAuZ290X3N1YmNvbW1hbmQoc3ViKSAmJiB0YXJnZXQgPT0gImluZGV4IjsKfQoKLy8gQW4gb3B0aW9uIG5vYm9keSBkZWNsYXJlZCBtdXN0IGJlIHJlamVjdGVkLCBub3QgaWdub3JlZC4KYm9vbCByZWplY3RzX2FuX3Vua25vd25fb3B0aW9uKCkgewogICAgQ0xJOjpBcHAgYXBwOwogICAgaW50IGNvdW50ID0gMDsKICAgIGFwcC5hZGRfb3B0aW9uKCItYywtLWNvdW50IiwgY291bnQpOwoKICAgIGNvbnN0IGNoYXIgKmFyZ3ZbXSA9IHsicHJvZyIsICItLW5vcGUifTsKICAgIHRyeSB7CiAgICAgICAgYXBwLnBhcnNlKDIsIGFyZ3YpOwogICAgfSBjYXRjaCAoY29uc3QgQ0xJOjpQYXJzZUVycm9yICYpIHsKICAgICAgICByZXR1cm4gdHJ1ZTsKICAgIH0KICAgIHJldHVybiBmYWxzZTsKfQoKLy8gVmFsaWRhdG9ycyBsaXZlIGluIHRoZWlyIG93biBoZWFkZXI7IGEgdmFsdWUgb3V0c2lkZSB0aGUgcmFuZ2UgbXVzdCB0aHJvdy4KYm9vbCBlbmZvcmNlc19hX3ZhbGlkYXRvcigpIHsKICAgIENMSTo6QXBwIGFwcDsKICAgIGludCBjb3VudCA9IDA7CiAgICBhcHAuYWRkX29wdGlvbigiLWMsLS1jb3VudCIsIGNvdW50KS0+Y2hlY2soQ0xJOjpSYW5nZSgxLCAxMCkpOwoKICAgIGNvbnN0IGNoYXIgKmFyZ3ZbXSA9IHsicHJvZyIsICItYyIsICI5OSJ9OwogICAgdHJ5IHsKICAgICAgICBhcHAucGFyc2UoMywgYXJndik7CiAgICB9IGNhdGNoIChjb25zdCBDTEk6OlZhbGlkYXRpb25FcnJvciAmKSB7CiAgICAgICAgcmV0dXJuIHRydWU7CiAgICB9IGNhdGNoIChjb25zdCBDTEk6OlBhcnNlRXJyb3IgJikgewogICAgICAgIHJldHVybiBmYWxzZTsKICAgIH0KICAgIHJldHVybiBmYWxzZTsKfQoKfSAgLy8gbmFtZXNwYWNlCgppbnQgbWFpbigpIHsKICAgIGNvbnN0IGJvb2wgb2sgPSBwYXJzZXNfYV9mdWxsX2NvbW1hbmRfbGluZSgpCiAgICAgICAgICAgICAgICAgICAgJiYgcmVqZWN0c19hbl91bmtub3duX29wdGlvbigpCiAgICAgICAgICAgICAgICAgICAgJiYgZW5mb3JjZXNfYV92YWxpZGF0b3IoKQogICAgICAgICAgICAgICAgICAgICYmIHN0ZDo6c3RyaW5nKENMSTExX1ZFUlNJT04pID09ICIyLjcuMiIKICAgICAgICAgICAgICAgICAgICAmJiBtY3BwX2NvbXBhdF9jbGkxMV9oZWFkZXJzX2FuY2hvcigpID09IDA7CiAgICByZXR1cm4gb2sgPyAwIDogMTsKfQo=" +consumer fmt "" fmtlib 'fmt = "12.2.0"' "Ly8gQmVoYXZpb3JhbCB0ZXN0OiBjb25zdW1lIHtmbXR9J3MgY29tcGlsZWQgaW1wbGVtZW50YXRpb24gdGhyb3VnaCB0aGUgQysrMjMKLy8gbW9kdWxlIGBmbXRgIChgaW1wb3J0IGZtdDtgLCBubyAjaW5jbHVkZSkgYW5kIGFzc2VydCB0aGUgcmVzdWx0cy4gRXhlcmNpc2VzCi8vIHNyYy9mb3JtYXQuY2MgdmlhIHRoZSBtb2R1bGUgdW5pdCdzIHRhaWwgaW5jbHVkZXMg4oCUIG5vdCBqdXN0IGhlYWRlciBpbmxpbmVzLgovLyBSZXR1cm5zIG5vbi16ZXJvIG9uIGFueSBtaXNtYXRjaC4KaW1wb3J0IHN0ZDsKaW1wb3J0IGZtdDsKCmludCBtYWluKCkgewogICAgc3RkOjpzdHJpbmcgYSA9IGZtdDo6Zm9ybWF0KCJ7fSArIHt9ID0ge30iLCAyLCAzLCAyICsgMyk7CiAgICBzdGQ6OnN0cmluZyBiID0gZm10Ojpmb3JtYXQoIns6MDguM2Z9IiwgMy4xNDE1OSk7CiAgICBzdGQ6OnN0cmluZyBjID0gZm10Ojpmb3JtYXQoInswfS17MX0tezB9IiwgIngiLCAieSIpOwogICAgc3RkOjpzdHJpbmcgZCA9IGZtdDo6Zm9ybWF0KCJ7OiN4fSIsIDI1NSk7CgogICAgYm9vbCBvayA9IGEgPT0gIjIgKyAzID0gNSIKICAgICAgICAgICAgICAmJiBiID09ICIwMDAzLjE0MiIKICAgICAgICAgICAgICAmJiBjID09ICJ4LXkteCIKICAgICAgICAgICAgICAmJiBkID09ICIweGZmIjsKICAgIHJldHVybiBvayA/IDAgOiAxOwp9Cg==" +consumer fmt-llvm "llvm@$LLVM" fmtlib 'fmt = "12.2.0"' "Ly8gQmVoYXZpb3JhbCB0ZXN0OiBjb25zdW1lIHtmbXR9J3MgY29tcGlsZWQgaW1wbGVtZW50YXRpb24gdGhyb3VnaCB0aGUgQysrMjMKLy8gbW9kdWxlIGBmbXRgIChgaW1wb3J0IGZtdDtgLCBubyAjaW5jbHVkZSkgYW5kIGFzc2VydCB0aGUgcmVzdWx0cy4gRXhlcmNpc2VzCi8vIHNyYy9mb3JtYXQuY2MgdmlhIHRoZSBtb2R1bGUgdW5pdCdzIHRhaWwgaW5jbHVkZXMg4oCUIG5vdCBqdXN0IGhlYWRlciBpbmxpbmVzLgovLyBSZXR1cm5zIG5vbi16ZXJvIG9uIGFueSBtaXNtYXRjaC4KaW1wb3J0IHN0ZDsKaW1wb3J0IGZtdDsKCmludCBtYWluKCkgewogICAgc3RkOjpzdHJpbmcgYSA9IGZtdDo6Zm9ybWF0KCJ7fSArIHt9ID0ge30iLCAyLCAzLCAyICsgMyk7CiAgICBzdGQ6OnN0cmluZyBiID0gZm10Ojpmb3JtYXQoIns6MDguM2Z9IiwgMy4xNDE1OSk7CiAgICBzdGQ6OnN0cmluZyBjID0gZm10Ojpmb3JtYXQoInswfS17MX0tezB9IiwgIngiLCAieSIpOwogICAgc3RkOjpzdHJpbmcgZCA9IGZtdDo6Zm9ybWF0KCJ7OiN4fSIsIDI1NSk7CgogICAgYm9vbCBvayA9IGEgPT0gIjIgKyAzID0gNSIKICAgICAgICAgICAgICAmJiBiID09ICIwMDAzLjE0MiIKICAgICAgICAgICAgICAmJiBjID09ICJ4LXkteCIKICAgICAgICAgICAgICAmJiBkID09ICIweGZmIjsKICAgIHJldHVybiBvayA/IDAgOiAxOwp9Cg==" + +section "H. NOT RUN here" +skip "MSVC toolset selection (Windows only): e2e 760 and 239 in ci-windows-e2e / ci-windows-msvc-xlings" + +printf '\n== summary ==\nfails=%d\n' "$fails" +[ -n "$skipped" ] && printf 'not run:%s\n' "$skipped" +[ "$fails" -eq 0 ] diff --git a/.agents/docs/README.md b/.agents/docs/README.md index fcb19197..d6fae69a 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -303 records. +306 records. ## By subject @@ -30,6 +30,7 @@ Records that declare one. Everything else is listed by date below. ### design +- [MSVC toolset 的选择、#685、#687 与工具链管理规范:总体设计](2026-09-24-toolchain-selection-and-payload-trust-design.md) — active - [openkal 生态:能力的时刻模型,以及 C 环境方案空间的划分](2026-09-20-openkal-c-environment-ecosystem-design.md) — active - [#674:`presents = "posix"` 在 Windows 上兑现契约的下半段](2026-09-19-issue-674-cenv-posix-preinclude-design.md) — superseded - [The build database of #636, and two defects on the way to the latest xlings](2026-09-14-636-build-database-and-the-latest-xlings.md) — active @@ -52,6 +53,7 @@ Records that declare one. Everything else is listed by date below. ### plan +- [工具链选择与载荷可信度:实施计划](2026-09-24-toolchain-selection-implementation-plan.md) — active - [openkal 生态:完整性收尾与验收方案](2026-09-21-openkal-ecosystem-completion-and-acceptance.md) — active - [C 环境生态方案:执行计划](2026-09-20-ecosystem-execution-plan.md) — active @@ -75,6 +77,7 @@ Records that declare one. Everything else is listed by date below. ### triage +- [#685、#687 与 Windows clang 的 MSVC STL:三个问题的归属,以及工具链载荷的规范化](2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md) — active - [运行时绑定方案 v3:让 mcpp 真正安装它所声明的运行时](2026-09-17-runtime-binding-multi-repo-plan.md) — landed - [#662:目标侧由依赖图提供时,编译器的隐式头文件搜索仍指向宿主](2026-09-17-issue-662-graph-target-header-isolation-plan.md) — active - [Issue #660 分析:`glibc@2.44` 绑定在 2.44.3 发布后解析失败](2026-09-17-issue-660-glibc-line-binding-analysis.md) — landed @@ -94,6 +97,9 @@ Records that declare one. Everything else is listed by date below. ### 2026-09 +- [工具链选择与载荷可信度:实施计划](2026-09-24-toolchain-selection-implementation-plan.md) — active +- [MSVC toolset 的选择、#685、#687 与工具链管理规范:总体设计](2026-09-24-toolchain-selection-and-payload-trust-design.md) — active +- [#685、#687 与 Windows clang 的 MSVC STL:三个问题的归属,以及工具链载荷的规范化](2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md) — active - [openkal 生态:完整性收尾与验收方案](2026-09-21-openkal-ecosystem-completion-and-acceptance.md) — active - [本轮生态级自审](2026-09-20-wave-self-review.md) — active - [openkal 生态:能力的时刻模型,以及 C 环境方案空间的划分](2026-09-20-openkal-c-environment-ecosystem-design.md) — active diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7bb482..60873548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,90 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.9.24.1] - 2026-09-24 + +### MSVC ABI 上的 toolset:只选一次,可以指定,记录在案 + +clang 以 `*-windows-msvc` 为目标时,编译所针对的 MSVC 环境(STL、CRT、Windows SDK)由两个互不相关的 +选择器决定:clang 驱动自己探测头文件与库(`VCToolsInstallDir`、`PATH`、最新实例的默认 toolset, +`%INCLUDE%` 存在时整体采用它),mcpp 另行按 `VSINSTALLDIR`、`vswhere -latest`、目录名最大者定位 +`std.ixx`。机器上装有多个 toolset 时,两者可能指向不同的 toolset;项目无法指定用哪一个;选择结果 +既不进缓存键,也不出现在任何记录里。 + +现在 MSVC toolset 是这一行的 sysroot,由 `[target.].sysroot` 指定,prepare 解析一次: + +```toml +[toolchain] +windows = "llvm@22.1.8" + +[target.x86_64-windows-msvc] +sysroot = "msvc@14.44.35207" # 或 "msvc@system"(默认),或 "xim:msvc@14.44.35207" +``` + +结果以 `-Xmicrosoft-visualc-tools-root`、`-Xmicrosoft-windows-sdk-root`、 +`-Xmicrosoft-windows-sdk-version` 传给编译、链接与 `std` 模块预编译(这几个是 clang-cl +`/vctoolsdir` 等选项的别名;带上它们后,clang 不再读 `VCToolsInstallDir` 与 `%INCLUDE%`), +`std.ixx` 取自同一个 toolset。构建打印一行 `Resolved sysroot msvc@system → MSVC <版本> (...)`, +`resolution.json` 新增 `msvc_toolset` 与 `windows_sdk`,toolset 目录与 SDK 版本进入缓存键, +SDK 版本也成为这一行的运行时身份 `ucrt@<版本>`。 + +写法在 cl.exe 行与 clang 行上相同: + +| 写法 | 含义 | +|---|---| +| `msvc@system` | 本机默认:`VCToolsInstallDir` → `VSINSTALLDIR` 实例的默认 toolset → `PATH` 上的 `cl.exe` → 带 C++ 组件的最新实例的默认 toolset | +| `msvc@` | 本机已装的同版本 toolset 优先(所有实例中查找),没有时安装载荷;环境变量不参与,被忽略时打印说明 | +| `xim:msvc@` | 只用载荷,SDK 随载荷固定 | + +`xim:` 是唯一的工具链命名空间。gcc、llvm 的工具链总来自载荷,`xim:gcc@16.1.0` 与 `gcc@16.1.0` 等价, +这一点与过去相同;过去任何 `:` 前缀都被静默剥掉,现在其他命名空间在读取处被拒绝。`xim:msvc@system` +不是一种写法,同样在读取处被拒绝(过去它在更晚的阶段失败,Linux 上报的是「只在 Windows 宿主可用」)。 + +**行为变化。** `msvc@` 过去一律使用载荷;现在机器上有同版本时直接用机器的那一份,SDK +随之取机器上的。需要载荷(连同它的 SDK)的项目改写为 `xim:msvc@`。cl.exe 行的 +`msvc@system` 改为取实例的默认 toolset(`Microsoft.VCToolsVersion.default.txt`)而不是目录名最大者, +并开始读取 `VCToolsInstallDir`;某个 toolset 缺少 `std.ixx` 时,不再借用另一个 toolset 的。 +cl.exe 行上指向另一个 toolset 的 `sysroot` 被拒绝。`mcpp toolchain list` 在 Windows 上列出 +本机已装的 toolset。 + +**旧引擎读新写法。** 在 Windows runner 上用 2026.9.21.3 实测:MSVC 行上的 `sysroot = "msvc@system"` +与 `"msvc@"` 让整份清单被拒(「is not an xpkg reference」);`"xim:msvc@"` 被接受 +而不生效:不安装任何东西,clang 针对机器上的 toolset 编译,构建输出却把这个值列为 c-abi 层。 +依赖所写 toolset 的项目应把 mcpp 固定在 2026.9.24.1 或更高。 + +### `macos_deployment_target` 按目标生效,不再按宿主(#685) + +在 Linux 或 Windows 宿主上 `mcpp build --target aarch64-macos`,产物的 `LC_BUILD_VERSION minos` 恒为 +14.0:`[build] macos_deployment_target` 与 `MACOSX_DEPLOYMENT_TARGET` 都被忽略,改了值也不重建。 +解析函数只在 `#if defined(__APPLE__)` 下读这两个输入,其他宿主上返回空,三元组于是回落到内置的 +`14.0`;指纹也只在宿主是 macOS 时折入这个值;平台事实 `macos.deployment-target` 同样为空,包里 +针对它的版本要求在这种构建里不被检查。反方向同理:macOS 宿主交叉到非 Apple 目标时,编译命令里 +仍带 `-mmacosx-version-min`。 + +现在解析(env > manifest > 14.0)与宿主无关,是否适用由构建的目标决定:三元组、指纹、平台事实、 +`-mmacosx-version-min` 与 std 模块预编译都按目标判定;build.mcpp 的宿主编译按它自己的目标(即宿主) +判定。macOS 宿主构建 macOS 目标时输出不变。判据是 `tests/e2e/746_…`:在 Linux 宿主上用 +`[target.aarch64-macos] toolchain = "llvm@…"` 走 `--configure-only`,不需要 macOS SDK,断言 +`--target=arm64-apple-macos11.0`、改值后指纹变化、env 优先于 manifest;把修复撤回,三条全红。 + +### `mcpp self doctor` 报告 gcc 载荷里冻结的 fixincludes 头(#687) + +gcc 13.3.0、15.1.0(以及 11.5.0)的 x86_64-linux-gnu 载荷在 `include-fixed/` 里带着构建机上 glibc +头的 fixincludes 副本(`pthread.h` 等)。它们在搜索顺序上排在构建所用的 glibc 2.44 之前,于是 +``、`` 编译失败。索引里本有一段安装时删除它们的清理代码,但单引号嵌套让 grep 搜索的是 +当前目录而不是载荷,从未生效;配方的修正在 openxlings/xim-pkgindex#870,新安装会被清理。 + +已安装的载荷不会因为索引更新而重新清理。doctor 新增一项检查:列出每个 gcc 载荷里带 +`auto-edited by fixincludes` 横幅的文件、横幅里的源路径,以及重装命令 +(`mcpp index update`,然后 `mcpp toolchain remove gcc@` 与 `mcpp toolchain install gcc@`)。 +这是警告,不是失败。 + +### SPEC-006:工具链管理(草案) + +`docs/specs/toolchain-management.md`:身份与写法、来源与选择、载荷契约、构建、验收与发布顺序, +每条标注实现状态。载荷契约与验收(载荷 lint、编译器 × C 库兼容矩阵、准入门进 CI)计划与下一批 +LLVM 工具链一同实现。 + ## [2026.9.21.3] - 2026-09-21 ### `mcpp test --no-run`:为一个跑不了的目标构建测试,并把这当成答案 diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index c48709fc..e58c5d7f 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -708,7 +708,10 @@ cargo/rustc, cc, etc.) > this field (the project default, similar to SwiftPM's `platforms:`) > the **built-in default `14.0`** (rustc-style — every target has a baseline, and 14.0 is the floor of LLVM's official static libraries themselves). This value enters the BMI fingerprint, so switching targets automatically rebuilds -the module cache. +the module cache. Resolution and application both follow the TARGET, not the +machine running mcpp: `mcpp build --target aarch64-macos` honors this field +(and the environment variable) the same way on Linux or Windows as it does on +a Mac, and a non-macOS target never sees it. ### Build concurrency (`jobs`) and module scheduling (`bmi_schedule`) diff --git a/docs/09-commands-by-scenario.md b/docs/09-commands-by-scenario.md index 6d88751e..5538ccc1 100644 --- a/docs/09-commands-by-scenario.md +++ b/docs/09-commands-by-scenario.md @@ -224,8 +224,9 @@ $ mcpp xpkg parse mcpp.plugins.lua --json ## Environment diagnosis `mcpp self doctor` checks the toolchain, the `std` module, the registry, cache -health and the last runtime-closure verdict, and reports what it found rather -than only what failed: +health, the last runtime-closure verdict, and installed GCC payloads for +fixincludes-frozen headers left over from the build machine that produced +them, and reports what it found rather than only what failed: ``` $ mcpp self doctor diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index 74f0f266..30eb8c67 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -300,8 +300,9 @@ spec** says which: | Spec | Origin | Compiler resolved | |---|---|---| -| `msvc@system` (or bare `msvc`) | the machine's own Visual Studio | whatever is installed here | -| `msvc@` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the named toolset, identically on every machine | +| `msvc@system` (or bare `msvc`) | the machine's own Visual Studio | the machine's default toolset (order below) | +| `msvc@` (e.g. `msvc@14.44.35207`) | an installed toolset of that version, else an xlings payload mcpp installs (2026.9.24.1+) | the named toolset | +| `xim:msvc@` | an xlings payload mcpp installs (2026.9.24.1+) | the named toolset, with the SDK installed beside it | They are not alternatives to pick between once — they answer different questions. `msvc@system` asks *"use what this developer already has"*; @@ -377,13 +378,21 @@ installs, updates, or removes one. mcpp toolchain default msvc ``` -mcpp auto-locates it in this order: +mcpp takes the first complete toolset in this order (2026.9.24.1+): -1. **`VSINSTALLDIR`** — set by a developer command prompt or by a CI step that - ran `vcvarsall`. A declared answer, so it outranks the probes below. -2. `vswhere.exe` (including prerelease/Insiders instances) -3. `VS*COMNTOOLS` -4. the standard `Program Files\Microsoft Visual Studio\\` paths +1. the toolset **`VCToolsInstallDir`** names — set by a developer command + prompt, including one opened with `vcvarsall … -vcvars_ver=`; +2. the default toolset of the instance **`VSINSTALLDIR`** (or `VCINSTALLDIR`) + names; +3. the toolset of the first `cl.exe` on `PATH`; +4. the default toolset of the newest Visual Studio instance that has the C++ + tools, as `vswhere.exe` reports them (prerelease/Insiders included); +5. without `vswhere.exe`, the standard + `Program Files\Microsoft Visual Studio\\` paths. + +An instance's default toolset is the one its +`VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt` names. A toolset is +complete when it has `include\`, `lib\x64\` and, for cl.exe builds, `cl.exe`. It then identifies the versions involved and persists the stable spec `msvc@system`: @@ -414,16 +423,31 @@ mcpp toolchain list --available msvc # what can be pinned mcpp toolchain install msvc 14.44.35207 ``` -This works like `gcc@16.1.0` in every respect: the payload is downloaded into -mcpp's own store, several toolsets coexist, `mcpp toolchain remove -msvc@` uninstalls one, and a manifest that names one gets it -installed automatically on first build. +A pinned toolset that this machine already has is used where it is installed +(2026.9.24.1+): mcpp looks through every Visual Studio instance for a complete +toolset of that version, and only when none has it does it install the +payload. A partial version (`msvc@14.44`) takes the highest match. The +environment (`VCToolsInstallDir`, `VSINSTALLDIR`) takes no part in a pinned +choice, and a variable that would have chosen a different toolset is reported +in a `note:`. `mcpp toolchain list` shows the machine's toolsets under +`installed toolsets`. + +`xim:msvc@` asks for the payload only, whatever the machine has. +The payload is downloaded into mcpp's own store, several toolsets coexist, +`mcpp toolchain remove msvc@` uninstalls one, and a manifest that +names one gets it installed automatically on first build. ```toml [toolchain] -windows = "msvc@14.44.35207" +windows = "msvc@14.44.35207" # an installed 14.44.35207 first, else the payload +# windows = "xim:msvc@14.44.35207" # the payload only ``` +`xim:` is accepted on every family; for gcc and llvm, whose toolchains always +come from payloads, `xim:gcc@16.1.0` and `gcc@16.1.0` are the same toolchain. +It is the only namespace a toolchain spelling accepts: earlier releases +stripped any `:` prefix silently, and another namespace is now refused. + **The version is the toolset directory name** (`14.44.35207` — what `VC\Tools\MSVC\` is named and what `-vcvars_ver` takes), *not* the cl banner version (`19.44.35211`) and not the product year. Nothing needs to be @@ -449,8 +473,8 @@ different questions and so must the SDK: | origin | SDK selection | |---|---| -| `msvc@` | the `xim:windows-sdk` payload installed **with that toolset**, in mcpp's own store. `WindowsSdkDir` / `WindowsSdkVersion` in the environment are **ignored**, and mcpp prints a `note:` saying so. | -| `msvc@system` | **`WindowsSdkDir`** (+ `WindowsSdkVersion`) if declared, then `C:\Program Files (x86)\Windows Kits\10`. | +| a payload toolset | the `xim:windows-sdk` payload installed **with that toolset**, in mcpp's own store. `WindowsSdkDir` / `WindowsSdkVersion` in the environment are **ignored**, and mcpp prints a `note:` saying so. | +| an installed toolset (`msvc@system`, or `msvc@` found on the machine) | **`WindowsSdkDir`** (+ `WindowsSdkVersion`) if declared, then `C:\Program Files (x86)\Windows Kits\10`. | The asymmetry is the point. A pinned toolset is a promise that two machines compile the same source against the same headers; an environment variable that @@ -495,6 +519,45 @@ is built per project and cl bakes `_MSVC_MT`/`_MSVC_MD` into it, so a per-role override (`cxx_runtime = { tests = … }`) is refused with a message saying so rather than producing a module mismatch inside the ucrt headers. +### Clang on the MSVC ABI: the toolset is the sysroot + +When the compiler is clang (`windows = "llvm@"`, the default on a +machine with Visual Studio), the MSVC toolset is what clang compiles against: +its STL, its CRT and the Windows SDK that follows it. The target row names it +with `sysroot`, using the same spellings (2026.9.24.1+): + +```toml +[toolchain] +windows = "llvm@22.1.8" + +[target.x86_64-windows-msvc] +sysroot = "msvc@14.44.35207" # or "msvc@system" (the default), or "xim:msvc@14.44.35207" +``` + +mcpp resolves the toolset and its SDK once, by the rules above, and passes +them to clang as `-Xmicrosoft-visualc-tools-root`, +`-Xmicrosoft-windows-sdk-root` and `-Xmicrosoft-windows-sdk-version` on every +compile, link and `std` module precompile; `std.ixx` comes from the same +toolset. The build prints the choice: + +``` + Resolved sysroot msvc@system → MSVC 14.44.35207 (system: Visual Studio Community 2022) · Windows SDK 10.0.26100.0 +``` + +and records it in `resolution.json` (`msvc_toolset`, `windows_sdk`). The +toolset directory and the SDK version are part of the build cache key, and the +SDK version is the row's runtime identity (`ucrt@`), as on the cl.exe +row. On a cl.exe row a `sysroot` that names a different toolset than the +compiler is refused. + +**Earlier engines** (measured with 2026.9.21.3 on a Windows runner) refuse the +whole manifest for `msvc@system` and `msvc@`, with "is not an xpkg +reference". They accept `xim:msvc@` but do not act on it: nothing is +installed, clang builds against the machine's toolset, and the build output +names the value as the c-abi layer. A project that depends on the toolset it +names pins mcpp 2026.9.24.1 or later, for example in the `.xlings.json` +workspace pin. + ## SDK Toolchains (`emsdk`, `android-ndk`) Two of the five toolchain spellings name an **SDK** rather than a bare @@ -663,7 +726,7 @@ reason it cannot. | **an assembler (`nasm`)** | the pinned `xim:nasm` first; the host's only when that route could not serve, and **named in the build report** when it is used | An offline machine that already has a usable assembler can still build. It used to be the other way round -- see below. | | **a C++ compiler on PATH (`$CXX`, else `g++`)** | `mcpp doctor` only | That command's job is to report on the host. The build path sets the compiler from a resolved payload at every branch that reaches the probe, and refuses when the payload cannot be resolved rather than falling through to PATH. | | **a command interpreter (`/bin/sh`, `cmd.exe` on Windows)** | `run_shell_deadline` for `[hooks]`, `run_streaming_bounded` for the xlings CLI, and the detached codegen command | A hook is a line the USER wrote in shell syntax. Shipping a shell would change the language that line is read in, so the thing mcpp depends on here is not a tool it could package -- it is the host's agreement about what that line means. | -| **the MSVC toolset and the Windows SDK** | `msvc@system`, which a user names; or a managed toolset that has no SDK payload beside it | Not redistributable, the same category as the Apple SDK. A managed toolset BINDS its SDK and ignores `WindowsSdkDir` even when set, because a pin the environment can overwrite is not a pin; the fallback to the machine's SDK works, is not reproducible, and carries a note saying so (`SdkChoice::note`, which the caller must surface). | +| **the MSVC toolset and the Windows SDK** | `msvc@system`, which a user names; a pinned `msvc@` found installed on the machine; or a managed toolset that has no SDK payload beside it | Not redistributable, the same category as the Apple SDK. A managed toolset BINDS its SDK and ignores `WindowsSdkDir` even when set, because a pin the environment can overwrite is not a pin; the fallback to the machine's SDK works, is not reproducible, and carries a note saying so (`SdkChoice::note`, which the caller must surface). | That list is meant to be exhaustive, and it is derived rather than remembered. Four sweeps over `src/` and `modules/` reach it. Every `fs::which` call -- diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 3039ef48..a9f37759 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -1105,6 +1105,13 @@ The value is an xpkg reference or the empty string; a bare name is rejected when the manifest is parsed, because accepting it would install nothing and then fail much later naming a missing libc. +On an MSVC-ABI row (`*-windows-msvc`) the sysroot is the MSVC toolset, and the +value is an msvc spelling (2026.9.24.1+): `msvc@system` (the default when the +key is absent), `msvc@` (an installed toolset of that version, else +the payload), or `xim:msvc@` (the payload only). Any other value on +such a row is rejected when the manifest is parsed. See +[20 — Clang on the MSVC ABI](20-toolchains.md#clang-on-the-msvc-abi-the-toolset-is-the-sysroot). + A build program can ask which C library **payload** supplies the sysroot: `mcpp::target_libc()` returns that package's name and `mcpp::target_libc_profile()` the sub-directory for the target's ISA profile. diff --git a/docs/README.md b/docs/README.md index 0cdb72ed..fb7766ae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -161,3 +161,4 @@ downstream tooling. - [SPEC-003 — The exit-code contract](specs/exit-codes.md) - [SPEC-004 — `mcpp.toml` planes, conditioning shape, resolution axes and naming](specs/manifest-semantics.md) - [SPEC-005 — The build database `mcpp emit build-database` prints](specs/build-database.md) + - [SPEC-006 — Toolchain management: identity, origin, selection and the payload contract](specs/toolchain-management.md) diff --git a/docs/specs/README.md b/docs/specs/README.md index b2c00a89..6df63769 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -35,6 +35,7 @@ | [SPEC-003](exit-codes.md) | 退出码契约(分类、语义、稳定性承诺) | 评审中 v1.0 | 2026-09-01 | mcpp >= 2026.9.1.1 | | [SPEC-004](manifest-semantics.md) | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | 草案 v1.4 | 2026-09-15 | 条件化形状 mcpp >= 2026.8.29.1;目标轴 mcpp >= 2026.9.6.4;`linkage` 默认值 mcpp >= 2026.9.15.2 | | [SPEC-005](build-database.md) | 构建数据库:`mcpp emit build-database` 的内容、取值规则与不写工程目录的保证 | 评审中 v1.0 | 2026-09-15 | mcpp >= 2026.9.15.1 | +| [SPEC-006](toolchain-management.md) | 工具链管理:身份、来源、选择与载荷契约 | 草案 v0.2 | 2026-09-24 | 逐条标注;已实现条款 mcpp >= 2026.9.24.1 | ## 文档约定 diff --git a/docs/specs/toolchain-management.md b/docs/specs/toolchain-management.md new file mode 100644 index 00000000..958e3d48 --- /dev/null +++ b/docs/specs/toolchain-management.md @@ -0,0 +1,230 @@ +# SPEC-006:工具链管理 + +| 项 | 值 | +|---|---| +| 规范编号 | SPEC-006 | +| 标题 | 工具链管理:身份、来源、选择与载荷契约 | +| 状态 | 草案 v0.2 | +| 最后修改 | 2026-09-24 | +| 对应实现 | 逐条标注;标为「已实现」的条款对应 mcpp >= 2026.9.24.1。标为「未实现」的条款计划与下一批 LLVM 工具链一同落地,届时按实测修订本规范 | +| 相关设计文档 | `.agents/docs/2026-09-24-toolchain-selection-and-payload-trust-design.md`、`.agents/docs/2026-09-24-685-687-msvc-stl-and-toolchain-payloads.md` | +| 相关 issue | mcpp#685、mcpp#687 | +| 使用文档 | [docs/20 - 工具链](../zh/20-toolchains.md)、[docs/32 - 编写载荷](../zh/32-authoring-a-payload.md)、[docs/91 - 工具链内部](../zh/91-toolchain-internals.md) | + +本规范定义 mcpp 对工具链的命名、选择和使用方式,以及一个工具链载荷在发布前必须满足的条件。 +目标侧的层模型见 [SPEC-002](target-side.md);清单的平面划分与解析轴见 [SPEC-004](manifest-semantics.md)。 + +用语按 RFC 2119:**必须 / 禁止**(强制)、**应当**(强烈建议)、**可以**(可选)。 + +--- + +## 1. 术语 + +| 术语 | 含义 | +|---|---| +| 工具链 | 执行编译的程序及其随附部分:编译器驱动、编译器运行时(builtins 与展开器)、随编译器发布的 C++ 标准库、汇编器、链接器与归档工具 | +| 载荷 | 由 xlings 从索引安装的预构建目录树,身份为命名空间、名字与版本 | +| 来源 | 工具链或 sysroot 的出处:`managed`(生态包)或 `system`(在本机定位) | +| sysroot | 编译时所针对的目标环境。Linux 目标上是 C 库载荷;MSVC ABI 目标上是 MSVC toolset(STL、vcruntime、CRT)及其 Windows SDK | +| 构建环境 | 产出一个载荷的机器或容器,以及其中的 C 库 | + +--- + +## 2. 身份与写法 + +### 2.1 基本写法 已实现 + +工具链**必须**写作 `<族>@<版本>`,族为 `gcc`、`llvm`、`msvc`、`emsdk`、`android-ndk`。 +部分版本**必须**解析为匹配的最高版本。 + +### 2.2 系统来源 已实现 + +只有 `msvc` 有系统来源,写作 `msvc@system`。其他族写 `@system`,**必须**在读取处被拒绝。 +不带族的 `system`(PATH 上的编译器)**必须**被拒绝,拒绝信息给出可用的写法。 + +### 2.3 生态包前缀 已实现 + +`xim:<族>@<版本>` 表示只取生态包。对没有系统来源的族,它与不带前缀的写法等价,规范写法去掉前缀; +对 msvc,前缀改变含义,规范写法保留前缀。`xim:msvc@system` 与 `xim:` 之外的命名空间**必须**被拒绝。 + +### 2.4 带版本的 msvc 写法 已实现 + +`msvc@<版本>` **必须**先在本机已安装的 toolset 中匹配,匹配不到再取生态包;规则见 §3.4。 +`mcpp toolchain default msvc@<版本>` 在本机已有该 toolset 时**必须**直接接受,不要求先安装生态包。 + +### 2.5 版本匹配 部分实现 + +前缀**必须**按版本分量匹配:`14.4` 匹配 `14.4.x`,不匹配 `14.44`。「最高」**必须**按数字元组比较。 + +当前:本机 MSVC toolset 的匹配与比较按本条实现;其他族的前缀匹配方式未逐一核对。 + +--- + +## 3. 来源与选择 + +### 3.1 一次选择 已实现 + +一次构建中,工具链及其 sysroot **必须**只解析一次;编译、依赖扫描、标准库模块、链接与缓存键**必须**读取同一个结果。 +clang 以 MSVC ABI 为目标时,所选 toolset 与 SDK 以 `-Xmicrosoft-visualc-tools-root`、`-Xmicrosoft-windows-sdk-root`、 +`-Xmicrosoft-windows-sdk-version` 显式交给编译器驱动,`std.ixx` 取自同一个 toolset。 + +### 3.2 声明优先于探测 部分实现 + +项目写明的版本**必须**优先于环境变量与本机扫描;被忽略的环境变量**必须**以一行说明报告。 + +| 条目 | 状态 | +|---|---| +| 受管 MSVC 忽略 `WindowsSdkDir` 并报告 | 已实现 | +| `VSINSTALLDIR` 优先于 vswhere | 已实现 | +| 写明版本时忽略 `VCToolsInstallDir` 与 `VSINSTALLDIR`,并报告 | 已实现 | + +### 3.3 结果可见 已实现 + +凡由探测得到的选择,其来源、版本与 SDK **必须**打印在构建输出中,写入 `resolution.json`,并进入缓存键。 +MSVC ABI 目标上:SDK 以 `ucrt@<版本>` 进入运行时身份;clang 行的 toolset 与 SDK 写入 `resolution.json` 的 +`msvc_toolset` 与 `windows_sdk`,toolset 目录与 SDK 版本进入缓存键,`stdlibVersion` 记为 toolset 的版本。 + +### 3.4 MSVC 的候选与顺序 已实现 + +本机候选**必须**覆盖全部 VS 实例(含预发布版)及每个实例下的全部 toolset。 + +`msvc@system` **必须**按以下顺序取第一个完整的候选: + +1. `VCToolsInstallDir` 所指的 toolset; +2. `VSINSTALLDIR` 或 `VCINSTALLDIR` 所指实例的默认 toolset; +3. `PATH` 上 `cl.exe` 所在的 toolset; +4. 带 C++ 组件、且安装版本最高的实例的默认 toolset; +5. 没有 vswhere 时,扫描固定路径得到的候选。 + +带版本的写法**必须**依次尝试本机候选、已安装的生态包、索引;三者都没有时,**必须**报错并列出三类候选。 + +「完整」指本次构建所需的文件都在:库目录;需要 `import std` 时的 `modules/std.ixx`;`cl.exe` 行的 `cl.exe`。 +不完整的候选**必须**被跳过,并报告。 + +`cl.exe` 行与 clang 行使用同一个选择。 + +### 3.5 MSVC ABI 目标的 sysroot 已实现 + +在 `*-windows-msvc` 目标上,编译器是工具链,MSVC toolset 是 sysroot。 + +- `[target.<三元组>].sysroot` 在这些行上**可以**写作 `msvc@system`、`msvc@<版本>` 或 `xim:msvc@<版本>`,含义同 §2。 +- 未写时**必须**等同于 `msvc@system`。 +- 编译器为 `cl.exe` 时,sysroot **必须**是该编译器所属的 toolset;另写一个不同的 sysroot,**必须**被拒绝。 +- SDK **必须**跟随 toolset 的来源:生态包 toolset 用随其安装的 `windows-sdk` 载荷,本机 toolset 用本机扫描的结果。 +- 目标侧报告([SPEC-002](target-side.md))中,c-abi 与 c++ 两层记为预制来源;具体的 toolset 与 SDK 记入 `resolution.json`(§3.3)。 +- 该值**禁止**作为 C 库包进入项目环境的安装集合。 + +### 3.6 目标属性按目标判定 已实现 + +描述产物的属性(最低系统版本、三元组中的版本段)**必须**按目标判定,与宿主无关; +只有在宿主上执行的编译(build.mcpp)按宿主判定。macOS 的 deployment target 在任何宿主上都按目标解析与施加。 + +--- + +## 4. 载荷契约 + +### 4.1 可重定位 部分实现 + +载荷在使用时**禁止**依赖构建环境中的路径。以下文件含构建环境的路径,但不参与编译与链接,**可以**豁免: +libtool 的 `.la`;gcc 的 `plugin/include/configargs.h` 与 `install-tools/mkheaders.conf`。 + +### 4.2 不含构建环境的 C 库内容 部分实现 + +gcc 载荷的 `lib/gcc/<三元组>/<版本>/include-fixed/` **禁止**含带 fixincludes 横幅(`auto-edited by fixincludes`)的头文件。 +这类头文件是构建环境 C 库的冻结副本,在搜索顺序上排在构建所用的 C 库之前。gcc 自己生成、不带横幅的头文件不受此限。 + +当前:gcc 13.3.0、15.1.0 与 11.5.0 的 x86_64-linux-gnu 发布归档含这类文件(mcpp#687)。索引中的 gcc 配方在安装时删除它们(openxlings/xim-pkgindex#870 之后生效); +在此之前安装的载荷由 `mcpp self doctor` 报告(§6.4)。故标为部分实现:安装后的载荷满足本条,发布归档不满足。 + +### 4.3 安装时的改写 部分实现 + +| 要求 | 状态 | +|---|---| +| 安装后的改写**必须**限于一张列出的清单:ELF 的 `PT_INTERP` 与 `RUNPATH`、clang 的 `.cfg` | 部分实现:改写集中在单一入口,清单未写成规范 | +| 改写**必须**由标记文件 `.mcpp-fixup.json` 记录 | 已实现 | +| 改写**必须**经副本与原子重命名完成 | 部分实现:mcpp 的安装后修正管线如此;xlings 安装时所做的改写未核对 | +| 清单之外的文件**必须**与发布归档一致 | 未实现:没有检查 | +| gcc 的 `specs` **禁止**在安装时改写 | 已实现 | + +### 4.4 完整性 部分实现 + +载荷**必须**包含其声明的能力所需的全部文件,例如 llvm 载荷的 `share/libc++/v1/std.cppm`,以及 libc++ 运行期依赖的 `libatomic.so.1`。 + +当前:准入脚本检查 llvm 载荷的这几项,但它不在 CI 中运行。 + +### 4.5 描述文件 部分实现 + +载荷根目录的 `.mcpp-toolchain.json` 已支持 `schema`、`frontend`、`platform_floor`、`std_module_defines`、`runner`(已实现)。 + +它**应当**另外记录来源(未实现):配方所在仓库与提交、编译器的配置行、上游源码的 sha256、构建所用的 C 库及其版本。 + +### 4.6 修订与资产名 已实现 + +一个版本在索引中的内容由 sha256 固定。内容有任何变化,**必须**使用新的资产名,因为 GitCode 上的发布资产既不能替换,也不能删除。 + +--- + +## 5. 构建 + +### 5.1 配方入库 部分实现 + +每个发布的载荷**必须**能由一份检入仓库的配方重新构建。 + +当前:llvm 子包、musl、glibc 等由 xim-pkgindex 中的构建脚本产出;gcc 由 `fromsource` 配方产出。已发布的三个 gcc 载荷出自两个不同的构建环境,载荷本身不记录构建环境。配方与构建 CI 所在的仓库待定。 + +### 5.2 构建环境 未实现 + +构建**必须**在固定的容器中进行;构建 sysroot **必须**取自索引发布的 C 库载荷,**禁止**取自某台机器的 subos。 + +### 5.3 可复现等级 部分实现 + +| 等级 | 含义 | 要求 | +|---|---|---| +| 可重建 | 由配方与固定输入重新得到功能等价的载荷 | **必须** | +| 可验收 | 由第 6 节的程序判定合格与否 | **必须** | +| 逐字节可复现 | 固定时间戳、路径前缀映射与归档顺序 | **可以** | + +--- + +## 6. 验收 + +### 6.1 载荷 lint 未实现 + +**必须**有一个程序对一个载荷目录执行 §4.1 至 §4.4 的全部检查,并在发布前运行。 +每一项检查**必须**有反向测试:把对应的缺陷放回载荷,该项检查必须失败。 + +### 6.2 兼容矩阵 部分实现 + +载荷发布或 C 库绑定变化时,**必须**编译一个矩阵:一维是该目标行在索引中可解析的全部编译器版本,另一维是全部 C 库版本。 +格子**必须**由索引枚举得到,**禁止**使用手写清单。 +每格至少编译一个使用 ``、``、`` 的程序,以及一个 `import std` 的程序。 +在 MSVC ABI 目标上,矩阵的另一维是 toolset 版本。 + +当前:描述符变化时,CI 只安装不带版本号时解析出的那一个版本(即 `latest`),并编译一个 `import std` 程序。gcc 描述符的改动因此只在 16.1.0 上验证过,13.3.0 与 15.1.0 未被覆盖。 + +### 6.3 准入门 部分实现 + +xim-pkgindex 的准入脚本 `verify-toolchain.sh` 对一个载荷归档做一次真实的编译、链接与运行,并对 llvm 载荷检查完整性与 CRT 的解析位置(已实现)。 + +它**必须**在 CI 中运行(未实现),并**必须**覆盖 §6.2 的程序(未实现)。当前它默认使用 glibc 2.39,测试程序不含线程头文件。 + +### 6.4 已安装载荷的诊断 部分实现 + +`mcpp self doctor` **必须**对已安装的载荷执行 §6.1 中不依赖归档的检查;发现问题时,**必须**给出重装命令。 + +当前:doctor 执行 §4.2 的检查(gcc 载荷 `include-fixed/` 中带 fixincludes 横幅的文件);§4.1、§4.3、§4.4 的检查未实现。 + +--- + +## 7. 发布顺序 未实现 + +移动 C 库绑定或某个载荷的 `latest` 之前,§6.2 的矩阵**必须**在新版本上全部通过。 + +--- + +## 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| v0.1 | 2026-09-24 | 初版草案:身份与写法、来源与选择(含 MSVC ABI 目标的 sysroot)、载荷契约、构建、验收、发布顺序 | +| v0.2 | 2026-09-24 | 随 mcpp 2026.9.24.1 更新实现状态:§2.3、§2.4、§3.1 至 §3.6 已实现;§4.2、§6.4 部分实现;§2.2 更正:不带族的 `system` 被拒绝 | diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index a9f42ed6..6b9e4d28 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -684,7 +684,10 @@ minos`)里记录的最低系统版本,也就是这个二进制能运行的 调用的显式覆盖,cargo/rustc、cc 等同样这样处理)> 这个字段(工程默认 值,类似 SwiftPM 的 `platforms:`)> **内置默认值 `14.0`**(rustc 风格——每个目标都有一个基线,而 14.0 正是 LLVM 官方静态库自身的下限)。 -这个值进入 BMI 指纹,所以切换目标会自动重建模块缓存。 +这个值进入 BMI 指纹,所以切换目标会自动重建模块缓存。解析与应用都 +按 TARGET 判定,不按运行 mcpp 的那台机器判定:`mcpp build --target +aarch64-macos` 在 Linux 或 Windows 上和在 Mac 上一样遵从这个字段 +(以及环境变量),非 macOS 目标则永远不会看到它。 ### 构建并发(`jobs`)与模块调度(`bmi_schedule`) diff --git a/docs/zh/09-commands-by-scenario.md b/docs/zh/09-commands-by-scenario.md index d94d1d83..cc93afd0 100644 --- a/docs/zh/09-commands-by-scenario.md +++ b/docs/zh/09-commands-by-scenario.md @@ -207,8 +207,9 @@ $ mcpp xpkg parse mcpp.plugins.lua --json ## 环境诊断 -`mcpp self doctor` 检查工具链、`std` 模块、registry、缓存健康与最近一次 -运行期闭包判定,并报告它查到了什么,而不只报告失败的部分: +`mcpp self doctor` 检查工具链、`std` 模块、registry、缓存健康、最近一次 +运行期闭包判定,以及已安装的 GCC 载荷里是否留有构建它的那台机器自己的 +fixincludes 冻结头文件,并报告它查到了什么,而不只报告失败的部分: ``` $ mcpp self doctor diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index e426272f..5199c893 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -287,8 +287,9 @@ target,Targets 一栏里没有的,就是这台机器确实服务不了(实 | Spec | 来源 | 解析出的编译器 | |---|---|---| -| `msvc@system`(或裸 `msvc`) | 这台机器自己的 Visual Studio | 这里装了什么就是什么 | -| `msvc@`(如 `msvc@14.44.35207`) | mcpp 安装的 xlings 载荷 | 指名的那一个,在每台机器上一致 | +| `msvc@system`(或裸 `msvc`) | 这台机器自己的 Visual Studio | 这台机器的默认 toolset(顺序见下文) | +| `msvc@`(如 `msvc@14.44.35207`) | 已安装的同版本 toolset,没有时为 mcpp 安装的 xlings 载荷(2026.9.24.1+) | 指名的那一个 | +| `xim:msvc@` | mcpp 安装的 xlings 载荷(2026.9.24.1+) | 指名的那一个,连同随它安装的 SDK | 它们不是二选一,而是回答不同的问题。`msvc@system` 问的是「用这位开发者已经 有的东西」;`msvc@14.44.35207` 问的是「用恰好这个编译器构建本工程」。pinned @@ -356,13 +357,21 @@ mcpp 只负责定位并识别已安装的 Visual Studio / Build Tools,**从不 mcpp toolchain default msvc ``` -mcpp 按下面这个顺序自动定位: +mcpp 按下面的顺序取第一个完整的 toolset(2026.9.24.1+): -1. **`VSINSTALLDIR`**——由开发者命令提示符,或跑过 `vcvarsall` 的 CI 步骤 - 设置。这是一个**声明**而不是一次探测,所以排在下面几种探测之前。 -2. `vswhere.exe`(含 prerelease / Insiders 实例) -3. `VS*COMNTOOLS` -4. 标准的 `Program Files\Microsoft Visual Studio\\` 路径 +1. **`VCToolsInstallDir`** 指向的 toolset——由开发者命令提示符设置,包括用 + `vcvarsall … -vcvars_ver=` 打开的那种; +2. **`VSINSTALLDIR`**(或 `VCINSTALLDIR`)指向的实例的默认 toolset; +3. `PATH` 上第一个 `cl.exe` 所在的 toolset; +4. `vswhere.exe` 报告的、装有 C++ 工具的最新 Visual Studio 实例的默认 + toolset(含 prerelease / Insiders 实例); +5. 没有 `vswhere.exe` 时,标准的 + `Program Files\Microsoft Visual Studio\\` 路径。 + +实例的默认 toolset 是它的 +`VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt` 所指的那一个。 +一个 toolset 具备 `include\`、`lib\x64\`,对 cl.exe 构建还要具备 `cl.exe`, +才算完整。 随后识别涉及的各个版本,并持久化为稳定的 spec `msvc@system`: @@ -392,15 +401,27 @@ mcpp toolchain list --available msvc # what can be pinned mcpp toolchain install msvc 14.44.35207 ``` -这在每个方面都和 `gcc@16.1.0` 一样:载荷下载进 mcpp 自己的 store,多个 -toolset 可以共存,`mcpp toolchain remove msvc@` 卸载其中一个, -manifest 里点名的那个会在首次构建时自动安装。 +pin 的 toolset 如果这台机器已经装有,就直接使用已安装的那一份(2026.9.24.1+): +mcpp 在所有 Visual Studio 实例中查找该版本的完整 toolset,只有都没有时才安装 +载荷。部分版本(`msvc@14.44`)取匹配的最高版本。环境变量(`VCToolsInstallDir`、 +`VSINSTALLDIR`)不参与 pin 的选择;如果某个变量本来会选出别的 toolset,会打印 +一行 `note:`。`mcpp toolchain list` 在 `installed toolsets` 下列出这台机器的 +toolset。 + +`xim:msvc@` 只要载荷,不看机器上有什么。载荷下载进 mcpp 自己的 +store,多个 toolset 可以共存,`mcpp toolchain remove msvc@` 卸载其中 +一个,manifest 里点名的那个会在首次构建时自动安装。 ```toml [toolchain] -windows = "msvc@14.44.35207" +windows = "msvc@14.44.35207" # an installed 14.44.35207 first, else the payload +# windows = "xim:msvc@14.44.35207" # the payload only ``` +所有工具链族都接受 `xim:`;gcc 与 llvm 的工具链总是来自载荷,所以 +`xim:gcc@16.1.0` 与 `gcc@16.1.0` 是同一个工具链。它是工具链写法唯一接受的 +命名空间:早先的版本会静默剥掉任何 `:` 前缀,现在其他命名空间被拒绝。 + **这里的版本是 toolset 目录名**(`14.44.35207`——即 `VC\Tools\MSVC\` 下的 目录名,也是 `-vcvars_ver` 接受的值),**不是** cl banner 的版本 (`19.44.35211`),也不是产品年份。机器上不需要预装任何东西:载荷带来编译器、 @@ -425,8 +446,8 @@ staging 成 `.ifc` BMI,用 `/interface /TP /ifcOutput` 编译 `.cppm` 模块 | 来源 | SDK 的选定方式 | |---|---| -| `msvc@` | **随该 toolset 一起装进 mcpp store 的** `xim:windows-sdk` 载荷。环境里的 `WindowsSdkDir` / `WindowsSdkVersion` 会被**忽略**,mcpp 会打印一行 `note:` 说明这一点。 | -| `msvc@system` | 先看 **`WindowsSdkDir`**(与 `WindowsSdkVersion`),没有声明时再看 `C:\Program Files (x86)\Windows Kits\10`。 | +| 载荷 toolset | **随该 toolset 一起装进 mcpp store 的** `xim:windows-sdk` 载荷。环境里的 `WindowsSdkDir` / `WindowsSdkVersion` 会被**忽略**,mcpp 会打印一行 `note:` 说明这一点。 | +| 已安装的 toolset(`msvc@system`,或在机器上找到的 `msvc@`) | 先看 **`WindowsSdkDir`**(与 `WindowsSdkVersion`),没有声明时再看 `C:\Program Files (x86)\Windows Kits\10`。 | 这种不对称正是要点所在。pin 一个 toolset,是在承诺「两台机器用同一套头文件 编译同一份源码」;一个能被环境悄悄改写的变量,会把这条承诺降格成一种偏好。 @@ -468,6 +489,40 @@ cxx_runtime = "self-contained" # the C++ runtime axis (`cxx_runtime = { tests = … }`)会被拒绝,并给出一条说明,而不是在 ucrt 头文件内部产生一次模块不匹配。 +### MSVC ABI 上的 clang:toolset 就是 sysroot + +编译器是 clang 时(`windows = "llvm@"`,装有 Visual Studio 的机器上的 +默认值),MSVC toolset 是 clang 编译时所针对的东西:它的 STL、CRT,以及随它 +而来的 Windows SDK。目标行用 `sysroot` 指定它,写法相同(2026.9.24.1+): + +```toml +[toolchain] +windows = "llvm@22.1.8" + +[target.x86_64-windows-msvc] +sysroot = "msvc@14.44.35207" # or "msvc@system" (the default), or "xim:msvc@14.44.35207" +``` + +mcpp 按上面的规则解析一次 toolset 与它的 SDK,并在每一次编译、链接和 `std` +模块预编译中以 `-Xmicrosoft-visualc-tools-root`、`-Xmicrosoft-windows-sdk-root` +与 `-Xmicrosoft-windows-sdk-version` 传给 clang;`std.ixx` 也取自同一个 toolset。 +构建会打印这次选择: + +``` + Resolved sysroot msvc@system → MSVC 14.44.35207 (system: Visual Studio Community 2022) · Windows SDK 10.0.26100.0 +``` + +并记录在 `resolution.json` 里(`msvc_toolset`、`windows_sdk`)。toolset 目录与 +SDK 版本都进入构建缓存键,SDK 版本也是这一行的运行时身份(`ucrt@`), +与 cl.exe 行相同。在 cl.exe 行上,指向与编译器不同 toolset 的 `sysroot` 会被 +拒绝。 + +**更早的引擎**(在 Windows runner 上用 2026.9.21.3 实测)对 `msvc@system` 与 +`msvc@` 拒绝整份清单,报「is not an xpkg reference」。它们接受 +`xim:msvc@`,但不按它行事:什么都不安装,clang 针对机器上的 toolset +编译,构建输出却把这个值列为 c-abi 层。依赖所写 toolset 的项目应把 mcpp 固定在 +2026.9.24.1 或更高,例如写在 `.xlings.json` 的 workspace pin 里。 + ## SDK 工具链(`emsdk`、`android-ndk`) 五种工具链拼法里,有两种命名的是一个 **SDK** 而不是一个裸编译器:`emsdk` @@ -624,7 +679,7 @@ mcpp build --target aarch64-ios-sim # resolves llvm@22.1.8 + the Simulator SD | **一个汇编器(`nasm`)** | 先取被钉住的 `xim:nasm`;只有那条路服务不了时才取宿主的,且**在构建报告里点名**用到的是哪一个 | 一台离线、但本来就装了可用汇编器的机器仍然能构建。它此前是反过来的——见下文。 | | **PATH 上的 C++ 编译器(`$CXX`,否则 `g++`)** | 只有 `mcpp doctor` | 那个命令的职责就是报告宿主的状况。构建这条路上,在每一个到得了这个探针的分支上都从解析出的载荷设定编译器,载荷解析不了时**拒绝**,而不是落到 PATH。 | | **一个命令解释器(`/bin/sh`,Windows 上是 `cmd.exe`)** | `[hooks]` 走 `run_shell_deadline`,xlings CLI 走 `run_streaming_bounded`,还有一个分离式的代码生成命令 | 一条 hook 是**用户自己**用 shell 语法写下的那一行。自带一个 shell 会改变那一行被解读所用的语言,所以这里依赖的不是一个能打包的工具,而是宿主对那一行含义的约定。 | -| **MSVC 工具集与 Windows SDK** | 用户点名的 `msvc@system`;或者一个受管工具集旁边没有 SDK 载荷时 | 不可再分发,与 Apple SDK 同一类。受管工具集**绑定**自己的 SDK,即使 `WindowsSdkDir` 被设置也不理会——一个环境能覆盖的 pin 就不是 pin;回落到机器自己那份能用,但不可复现,因此会带一句说明(`SdkChoice::note`,调用方必须把它呈现出来)。 | +| **MSVC 工具集与 Windows SDK** | 用户点名的 `msvc@system`;在机器上找到的 pin 版本 `msvc@`;或者一个受管工具集旁边没有 SDK 载荷时 | 不可再分发,与 Apple SDK 同一类。受管工具集**绑定**自己的 SDK,即使 `WindowsSdkDir` 被设置也不理会——一个环境能覆盖的 pin 就不是 pin;回落到机器自己那份能用,但不可复现,因此会带一句说明(`SdkChoice::note`,调用方必须把它呈现出来)。 | 这张表意在穷举,而它是**推导出来的**,不是靠记忆写出来的。四次扫描 `src/` 与 `modules/` 就能重新得到它。每一处 `fs::which` 调用——恰好五处,每一处都 diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 8fd76a1b..86b47c0f 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -931,6 +931,12 @@ sysroot = "" # no C library at all 取值是一个 xpkg 引用或空字符串;裸名在解析清单时即被拒绝,因为接受它会 导致什么都不安装,然后在很晚的时候以「缺少 libc」失败。 +在 MSVC ABI 行(`*-windows-msvc`)上,sysroot 是 MSVC toolset,取值是 msvc 的 +写法(2026.9.24.1+):`msvc@system`(键缺席时的默认)、`msvc@`(已安装的 +同版本 toolset,没有时为载荷)或 `xim:msvc@`(只要载荷)。这类行上的 +其他取值在解析清单时即被拒绝。见 [20 —— 工具链管理](20-toolchains.md) 中 +「MSVC ABI 上的 clang:toolset 就是 sysroot」一节。 + 构建程序可以询问供给 sysroot 的是哪个 C 库**载荷**:`mcpp::target_libc()` 返回该包的名字,`mcpp::target_libc_profile()` 返回目标 ISA 档位对应的 子目录。零 libc 档上两者均为空。见 diff --git a/docs/zh/README.md b/docs/zh/README.md index ec880757..738861e1 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -156,3 +156,4 @@ - [SPEC-003 —— 退出码契约](../specs/exit-codes.md) - [SPEC-004 —— `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约](../specs/manifest-semantics.md) - [SPEC-005 —— `mcpp emit build-database` 输出的构建数据库](../specs/build-database.md) + - [SPEC-006 —— 工具链管理:身份、来源、选择与载荷契约](../specs/toolchain-management.md) diff --git a/mcpp.toml b/mcpp.toml index 860ea730..fd30b216 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.21.3" +version = "2026.9.24.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 5d145694..9d976d69 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -662,6 +662,44 @@ parse_c_abi_absent(const t::Value& v, return std::nullopt; } +// `[target.].sysroot`: the refusal for a value the row cannot take, or +// nullopt. Kept out of `parse_string` for the reason the helper above states. +// +// ON AN MSVC-ABI ROW THE SYSROOT IS AN MSVC TOOLSET. The compiler is the +// toolchain (`cl.exe` or clang) and the toolset -- its STL, its CRT and the +// Windows SDK that follows it -- is what the compiler builds against, which is +// what this key names on every other row too. The spellings are the msvc +// family's own: `msvc@system`, `msvc@` (an installed toolset first, +// then the package) and `xim:msvc@` (the package only). The row is +// recognised by its spelling because this module does not parse triples; +// `x86_64-windows-msvc` and `x86_64-pc-windows-msvc` both end in `-msvc`. +std::optional sysroot_refusal(std::string_view triple, std::string_view value) { + const bool msvcRow = triple.find("windows") != std::string_view::npos + && triple.ends_with("-msvc"); + if (msvcRow) { + const bool pkg = value.starts_with("xim:"); + const std::string_view spec = pkg ? value.substr(4) : value; + const bool ok = spec.starts_with("msvc@") && spec.size() > 5 + && !(pkg && spec == "msvc@system"); + if (ok) return std::nullopt; + return std::format( + "[target.{}].sysroot = '{}': on an MSVC-ABI row the sysroot is an " + "MSVC toolset, written \"msvc@system\", \"msvc@\" (an " + "installed toolset first, then the package) or " + "\"xim:msvc@\" (the package only).", + triple, value); + } + if (!value.empty() && value.find(':') == std::string_view::npos) { + return std::format( + "[target.{}].sysroot = '{}' is not an xpkg reference; " + "expected `:[@]` (e.g. " + "\"xim:picolibc-riscv@1.8.12\"), or \"\" for a target " + "that takes no prebuilt C library directory.", + triple, value); + } + return std::nullopt; +} + } // namespace @@ -3228,14 +3266,8 @@ std::expected parse_string(std::string_view content, // get picolibc back. if (auto it = body.find("sysroot"); it != body.end() && it->second.is_string()) { std::string s = it->second.as_string(); - if (!s.empty() && s.find(':') == std::string::npos) { - return std::unexpected(error(origin, std::format( - "[target.{}].sysroot = '{}' is not an xpkg reference; " - "expected `:[@]` (e.g. " - "\"xim:picolibc-riscv@1.8.12\"), or \"\" for a target " - "that takes no prebuilt C library directory.", - triple, s))); - } + if (auto why = sysroot_refusal(triple, s)) + return std::unexpected(error(origin, *why)); e.sysroot = std::move(s); e.sysrootDeclared = true; } diff --git a/modules/platform/src/macos/macos.cppm b/modules/platform/src/macos/macos.cppm index a63e34c2..7b51243c 100644 --- a/modules/platform/src/macos/macos.cppm +++ b/modules/platform/src/macos/macos.cppm @@ -94,25 +94,41 @@ inline constexpr std::string_view default_deployment_target = "14.0"; // MACOSX_DEPLOYMENT_TARGET env var (explicit per-invocation override, // the convention cargo/rustc/cc honor) wins over `manifestValue` (the // [build] macos_deployment_target project default), which wins over -// the built-in default floor — the result is never empty on macOS. -// THE single source of truth — flags.cppm, the BMI fingerprint rule -// and the std-module prebuild must all consume this same resolution, -// or cached std.pcm modules drift from the TUs (config-mismatch / -// unstaged-module failures observed on macos CI). -std::string deployment_target(std::string_view manifestValue); +// the built-in default floor. +// +// WHETHER IT APPLIES IS A TARGET QUESTION, NOT A HOST QUESTION, so +// `targetIsMacos` is a parameter rather than a `#if defined(__APPLE__)` +// read inside this function. Every reader that folds this value into a +// compiler flag, a triple or the build fingerprint builds for a +// particular target (native or `--target`-crossed), and that target's +// OS -- not the machine running mcpp -- decides whether a macOS floor +// makes sense. The resolution itself (env > manifest > 14.0) never +// consults the host either, which is what makes `mcpp build --target +// aarch64-macos` on Linux honour `macos_deployment_target` at all: the +// old `#if defined(__APPLE__)` guard answered empty on every non-Apple +// host regardless of what the target was. +// +// `modules/platform` does not import `mcpp.toolchain.triple` (layering: +// the triple model is built on top of platform, not the reverse), so +// each caller answers the "is the target macOS" question itself -- +// typically `parsedTriple.os == "macos"` -- and passes the bool rather +// than a triple this module cannot name. +// +// Result is never empty when `targetIsMacos` is true; always empty +// when it is false. THE single source of truth — flags.cppm, the BMI +// fingerprint rule and the std-module prebuild must all consume this +// same resolution, or cached std.pcm modules drift from the TUs +// (config-mismatch / unstaged-module failures observed on macos CI). +std::string deployment_target(bool targetIsMacos, std::string_view manifestValue); // Return macOS-specific runtime library directories for LLVM toolchains. -std::string deployment_target(std::string_view manifestValue) { -#if defined(__APPLE__) +std::string deployment_target(bool targetIsMacos, std::string_view manifestValue) { + if (!targetIsMacos) return {}; if (const char* dt = std::getenv("MACOSX_DEPLOYMENT_TARGET"); dt && *dt) return dt; if (!manifestValue.empty()) return std::string(manifestValue); return std::string(default_deployment_target); -#else - (void)manifestValue; - return {}; -#endif } std::vector diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index 440d2f28..85b7ff4f 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -73,6 +73,14 @@ struct ToolchainLinkModel { std::vector systemIncludes; // Rendering knobs derived from the toolchain at resolve time. + // CLANG ON THE MSVC ABI: the toolset and SDK the build compiles against, + // copied from the toolchain. Rendered by `msvc_driver_tokens`, not by + // `compile_tokens`/`link_tokens`: those describe a C library located in a + // prebuilt directory, and none of their modes applies to this row. + std::filesystem::path msvcToolsDir; + std::filesystem::path winSdkRoot; + std::string winSdkVersion; + bool clangDriver = false; // clang: -isystem headers; gcc: -idirafter // gcc: -idirafter (…#include_next), -B/-L only bool clangWithCfg = false; // sibling .cfg exists (bundled LLVM) @@ -101,6 +109,28 @@ struct ToolchainLinkModel { return render_tokens(compile_tokens(esc)); } + // `-Xmicrosoft-*` as separate words: clang's GNU driver accepts only the + // separated form, and each is an alias of the clang-cl option that names + // the directory (`/vctoolsdir`, `/winsdkdir`, `/winsdkversion`). With them + // on the command line the driver neither reads VCToolsInstallDir or + // %INCLUDE% nor asks the installer for the newest instance. The toolset + // and the SDK always travel together: given only the toolset, the driver + // stops reading %INCLUDE% and takes the registry's newest SDK instead. + // Read by the compile line, the link line and the cache key alike. + std::vector msvc_driver_tokens(const PathEscape& esc) const { + std::vector out; + if (msvcToolsDir.empty()) return out; + out.push_back("-Xmicrosoft-visualc-tools-root"); + out.push_back(esc(msvcToolsDir)); + if (!winSdkRoot.empty() && !winSdkVersion.empty()) { + out.push_back("-Xmicrosoft-windows-sdk-root"); + out.push_back(esc(winSdkRoot)); + out.push_back("-Xmicrosoft-windows-sdk-version"); + out.push_back(winSdkVersion); + } + return out; + } + // Link-side flags as argv tokens. `-B` is the CRT-discovery fix for #195: // the driver resolves crt objects through -B prefixes and sysroot paths, // never through -L. @@ -370,7 +400,14 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { // both want CLibMode::None. Keyed on the TARGET (not the host) so the // ELF resolution below stays testable anywhere and a future // cross-compile resolves by what it builds FOR. - if (is_msvc_target(tc) || is_mingw_target(tc)) return lm; + if (is_msvc_target(tc) || is_mingw_target(tc)) { + if (is_msvc_target(tc) && tc.compiler == CompilerId::Clang) { + lm.msvcToolsDir = tc.msvcToolsDir; + lm.winSdkRoot = tc.windowsSdkRoot; + lm.winSdkVersion = tc.windowsSdkVersion; + } + return lm; + } // AND A TARGET WHOSE TOOLCHAIN SHIPS ITS OWN SYSROOT, for the same reason // one sentence further up: nothing here describes its C library. diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index 80ff7a9c..a6bf1046 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -270,6 +270,18 @@ struct Toolchain { // the runtime contract hash). Deriving the second from a second search is // how the SDK came to have no identity in the first place. std::string windowsSdkVersion; + + // THE MSVC TOOLSET A CLANG `*-windows-msvc` BUILD COMPILES AGAINST, and the + // SDK that follows it: the row's sysroot. Chosen once by prepare from + // `[target.].sysroot` (default `msvc@system`) and handed to the + // driver explicitly by the link model, so clang does not search the + // machine a second time. Empty on every other row, where nothing changes; + // the cl.exe row carries the same answer in its own path and envOverrides. + std::filesystem::path msvcToolsDir; // /VC/Tools/MSVC/ + std::string msvcToolsVersion; // "14.44.35207" + std::string msvcOrigin; // "system" | "managed" + std::string msvcProduct; // for the one printed line + std::filesystem::path windowsSdkRoot; // /10 or the payload // Something about HOW this toolchain was resolved that the user has to be // told, but which is not a failure. Non-empty ⇒ the caller MUST surface it. // diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 3e42d572..8fe9f5f9 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.21.3"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.24.1"; } // namespace mcpp diff --git a/src/build/build_database.cppm b/src/build/build_database.cppm index e905909e..429595aa 100644 --- a/src/build/build_database.cppm +++ b/src/build/build_database.cppm @@ -450,7 +450,7 @@ Rendered render(std::span members, auto& set = set_for(member.setPrefix + package + (isTest ? ":test" : ""), package, kind); nlohmann::json provides = nlohmann::json::object(); - if (cu.providesModule) provides[*cu.providesModule] = ""; + if (!cu.providesModule.empty()) provides[cu.providesModule] = ""; nlohmann::json requires_ = nlohmann::json::array(); for (auto const& name : cu.imports) requires_.push_back(name); r.compileCommands.push_back(nlohmann::json{ diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index 8b807fb0..90095f2e 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -1202,9 +1202,23 @@ std::expected run_build_program( // One resolution of the deployment target, used by every compile below // and by the std module it asks stdmod to build — they must agree or // clang rejects the BMI. + // + // LEGITIMATELY HOST-KEYED, unlike the main build's readers (#685). `tc` + // here is always the HOST toolchain (see this function's own doc above + // `run_build_program` / `host_base_flags`'s header) -- build.mcpp is + // compiled AND run on the machine doing the build, so "the target" this + // one compile is for IS the host, and `tc.targetTriple` already names + // it. Asking `tc`'s own resolved target keeps this correct without + // reading a compile-time `__APPLE__`/`is_macos` constant, which would + // silently disagree with `tc` the day build.mcpp gains a host toolchain + // resolved for something other than the machine mcpp itself runs on. + const bool buildProgramTargetIsMacos = [&] { + auto bpTt = mcpp::toolchain::triple::parse(tc.targetTriple); + return bpTt && bpTt->os == "macos"; + }(); const std::string macosDeploymentTarget = mcpp::platform::macos::deployment_target( - m.buildConfig.macosDeploymentTarget); + buildProgramTargetIsMacos, m.buildConfig.macosDeploymentTarget); auto base = host_base_flags(tc, macosDeploymentTarget); // The host compile has always been spelled in GNU driver syntax with no diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index 2374869f..bdaff087 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -499,6 +499,14 @@ BuildAxes build_axes(const mcpp::toolchain::Toolchain& tc, for (auto& t : mcpp::toolchain::resolve_link_model(tc) .compile_tokens(relativize)) b.targetHeaderSet.push_back(t); + // THE MSVC TOOLSET AND SDK OF THE CLANG ROW. Their directories carry + // the versions (`.../MSVC/14.44.35207`, `-Xmicrosoft-windows-sdk-version + // 10.0.26100.0`), so two toolsets are two keys. Before these were on + // the command line the key could not see the STL at all: the driver + // found it, and `stdlibVersion` held clang's own version. + for (auto& t : mcpp::toolchain::resolve_link_model(tc) + .msvc_driver_tokens(relativize)) + b.targetHeaderSet.push_back(t); } b.cppStandard = rootManifest.package.standard; diff --git a/src/build/configure.cppm b/src/build/configure.cppm index e3c93ad4..87b95cb7 100644 --- a/src/build/configure.cppm +++ b/src/build/configure.cppm @@ -59,18 +59,18 @@ stage_configure_prerequisites(const BuildPlan& plan) { const auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); for (const auto& unit : plan.compileUnits) { - if (!unit.servedFromCache || !unit.providesModule + if (!unit.servedFromCache || unit.providesModule.empty() || unit.cachedBmi.empty()) continue; std::string fileName; - fileName.reserve(unit.providesModule->size() + traits.bmiExt.size()); - for (char ch : *unit.providesModule) + fileName.reserve(unit.providesModule.size() + traits.bmiExt.size()); + for (char ch : unit.providesModule) fileName.push_back(ch == ':' ? '-' : ch); fileName += traits.bmiExt; auto result = stage_one( unit.cachedBmi, plan.outputDir / traits.bmiDir / fileName, - *unit.providesModule); + unit.providesModule); if (!result) return std::unexpected(result.error()); } diff --git a/src/build/flags.cppm b/src/build/flags.cppm index e0c07bed..4afe81d7 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -538,8 +538,19 @@ CompileFlags compute_flags(const BuildPlan& plan) { // override, the convention cargo/rustc/cc honor) > the manifest's // [build] macos_deployment_target (project default, SwiftPM-style) > // empty (toolchain/SDK default). + // + // TARGET-KEYED, NOT HOST-KEYED (#685). `targetIsMacos` asks + // `plan.toolchain.targetTriple` -- the target THIS compile is for -- + // rather than the machine mcpp runs on, so `mcpp build --target + // aarch64-macos` from a Linux host honours the manifest key instead of + // the empty answer the old host-gated resolver gave unconditionally on + // any non-Apple host. Same discriminator `peTarget` below uses. + const bool targetIsMacos = [&] { + auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple); + return t && t->os == "macos"; + }(); std::string macosDeploymentTarget = mcpp::platform::macos::deployment_target( - plan.manifest.buildConfig.macosDeploymentTarget); + targetIsMacos, plan.manifest.buildConfig.macosDeploymentTarget); f.cxxBinary = plan.toolchain.binaryPath; f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain); @@ -626,8 +637,17 @@ CompileFlags compute_flags(const BuildPlan& plan) { // --no-default-config and provide everything explicitly. const auto dm = mcpp::toolchain::resolve_clang_driver(plan.toolchain); const auto lm = mcpp::toolchain::resolve_link_model(plan.toolchain); + // ONE WORD PER PATH, QUOTED WHEN IT HAS TO BE. `escape_path` escapes for + // ninja and does not quote, so a path with a space reached the command as + // two words. No path this line carries on Linux or macOS had one; the MSVC + // toolset on the clang row always has one (`C:\Program Files\...`). A + // path without whitespace keeps its old spelling byte for byte. const mcpp::toolchain::PathEscape ninjaEsc = - [](const std::filesystem::path& p) { return escape_path(p); }; + [](const std::filesystem::path& p) { + const auto s = p.string(); + return s.find_first_of(" \t") == std::string::npos + ? escape_path(p) : ninja_command_word(s); + }; std::string compile_toolchain_flags; std::string link_toolchain_flags; @@ -853,7 +873,6 @@ CompileFlags compute_flags(const BuildPlan& plan) { link_toolchain_flags_c = crossTarget; f.sysroot = link_toolchain_flags; } - // Binutils -B flag — a GCC/libstdc++ payload concern (musl and MinGW-w64 // cross both bundle their own as/ld; Clang and MSVC never take an external // binutils). MinGW must not get the Linux binutils -B — its PE/SEH output @@ -1744,8 +1763,13 @@ CompileFlags compute_flags(const BuildPlan& plan) { // where every other host produces a static one. ⇒ Whole-program static // linkage is a property of the TARGET (`target_supports_full_static` // plus the manifest's `linkage`), so it belongs on every host's line. - f.ld = std::format("{} -fuse-ld=lld{}{}{}", full_static, link_intent_ld, - user_ldflags, link_extra); + // CLANG ON THE MSVC ABI: the link half of what the compile line says. + // The driver derives the toolset's and the SDK's library directories + // from these rather than from the machine; empty without a toolset. + const auto msvcSysroot = + mcpp::toolchain::render_tokens(lm.msvc_driver_tokens(ninjaEsc)); + f.ld = std::format("{} -fuse-ld=lld{}{}{}{}", full_static, msvcSysroot, + link_intent_ld, user_ldflags, link_extra); f.ldC = f.ld; // no C++ runtime token on this line } else if (linkShape == LinkShape::AppleSdk) { // macOS. The C++ runtime itself is decided by the contract table above diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 2da375ed..6226cb6b 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1754,7 +1754,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // silent miscompile. std::unordered_map byModule; for (auto& cu : plan.compileUnits) - if (cu.providesModule) byModule.emplace(*cu.providesModule, &cu); + if (!cu.providesModule.empty()) byModule.emplace(cu.providesModule, &cu); auto reaches_std = [&](const CompileUnit& start) { std::vector stack{&start}; @@ -1864,13 +1864,13 @@ std::string emit_ninja_string(const BuildPlan& plan) { // answered here, once, from `cu.providesModule` — the same field `bmi_out` // is bound from, so the flag and the binding can no longer disagree. auto module_edge_vars = [&](const mcpp::build::CompileUnit& cu) -> std::string { - if (!cu.providesModule) + if (cu.providesModule.empty()) return std::format(" module_lang ={}\n", traits.moduleImplLangFlag); std::string v = std::format(" module_lang ={}\n", traits.moduleInterfaceLangFlag); if (traits.needsExplicitModuleOutput) v += std::format(" module_output ={}{}\n", traits.moduleOutputPrefix, - bmi_path(*cu.providesModule)); + bmi_path(cu.providesModule)); return v; }; @@ -1936,8 +1936,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { escape_ninja_path(cu.cachedObject))); append(" verify = --verify size\n"); staged.push_back(obj); - if (cu.providesModule && !cu.cachedBmi.empty()) { - auto bmi = bmi_path(*cu.providesModule); + if (!cu.providesModule.empty() && !cu.cachedBmi.empty()) { + auto bmi = bmi_path(cu.providesModule); append(std::format("build {} : stage_file {}\n", bmi, escape_ninja_path(cu.cachedBmi))); append(" verify = --verify size\n"); @@ -2056,8 +2056,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { // existed there from an earlier build — a fresh project failed. append(std::format(" compile_target = {}\n", escape_ninja_path(cu.object))); append(std::format(" deps_target = {}\n", - splitBmi && cu.providesModule - ? bmi_path(*cu.providesModule) + splitBmi && !cu.providesModule.empty() + ? bmi_path(cu.providesModule) : escape_ninja_path(cu.object))); if (auto includes = local_include_flags(cu, dial); !includes.empty()) append(std::format(" local_includes ={}\n", includes)); @@ -2109,8 +2109,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (!cu.scanOverridden && !verifyAll) continue; auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi"; std::string exp; - if (cu.providesModule) - exp += std::format("--expect-provides {}", *cu.providesModule); + if (!cu.providesModule.empty()) + exp += std::format("--expect-provides {}", cu.providesModule); if (!cu.imports.empty()) { std::string csv; for (auto& m : cu.imports) { @@ -2132,7 +2132,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { for (auto& cu : plan.compileUnits) { if (cu.servedFromCache) continue; if (is_scan_exempt(cu)) continue; - if (!cu.providesModule) continue; + if (cu.providesModule.empty()) continue; if (cu.kind != mcpp::SourceKind::ModuleInterface) continue; two_phase_ddi.insert( (cu.object.parent_path() / cu.source.filename()).string() + ".ddi"); @@ -2156,9 +2156,9 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (cu.servedFromCache) continue; // a stage_file edge owns these outputs std::string rule = pick_rule(cu); - if (splitBmi && cu.providesModule && + if (splitBmi && !cu.providesModule.empty() && cu.kind == mcpp::SourceKind::ModuleInterface) { - const auto bmi = bmi_path(*cu.providesModule); + const auto bmi = bmi_path(cu.providesModule); const auto obj = escape_ninja_path(cu.object); const auto slot = obj + ".sched"; const auto ddi = (cu.object.parent_path() / cu.source.filename()) @@ -2221,9 +2221,9 @@ std::string emit_ninja_string(const BuildPlan& plan) { // shape rather than emitting a BMI edge nothing can order. } - if (twoPhase && cu.providesModule && + if (twoPhase && !cu.providesModule.empty() && cu.kind == mcpp::SourceKind::ModuleInterface) { - const auto bmi = bmi_path(*cu.providesModule); + const auto bmi = bmi_path(cu.providesModule); const auto obj = escape_ninja_path(cu.object); const auto ddi = (cu.object.parent_path() / cu.source.filename()) .string() + ".ddi"; @@ -2254,8 +2254,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { } std::string out_line = "build " + escape_ninja_path(cu.object); - if (cu.providesModule) { - out_line += " | " + bmi_path(*cu.providesModule); + if (!cu.providesModule.empty()) { + out_line += " | " + bmi_path(cu.providesModule); } out_line += std::format(" : {} {}", rule, escape_ninja_path(cu.source)); if (!is_scan_exempt(cu)) { @@ -2266,8 +2266,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { out_line += order_only_for(cu); out_line += "\n dyndep = " + it->second; // P2: set bmi_out for the copy_if_different logic in cxx_module. - if (cu.providesModule) { - out_line += "\n bmi_out = " + bmi_path(*cu.providesModule); + if (!cu.providesModule.empty()) { + out_line += "\n bmi_out = " + bmi_path(cu.providesModule); } out_line += "\n"; if (rule == "cxx_module") out_line += module_edge_vars(cu); @@ -2319,10 +2319,10 @@ std::string emit_ninja_string(const BuildPlan& plan) { } std::string out_line = "build " + escape_ninja_path(cu.object); - if (cu.providesModule) { + if (!cu.providesModule.empty()) { // Use implicit output (|) so $out only contains the .o file. // GCC writes BMI implicitly; Clang uses -fmodule-output=$bmi_out. - out_line += " | " + bmi_path(*cu.providesModule); + out_line += " | " + bmi_path(cu.providesModule); } out_line += std::format(" : {} {}", rule, escape_ninja_path(cu.source)); if (!implicit.empty()) @@ -2342,8 +2342,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { out_line += " unit_cxxflags =" + flags + "\n"; } // Clang needs $bmi_out to emit -fmodule-output=$bmi_out - if (cu.providesModule) { - out_line += " bmi_out = " + bmi_path(*cu.providesModule) + "\n"; + if (!cu.providesModule.empty()) { + out_line += " bmi_out = " + bmi_path(cu.providesModule) + "\n"; } if (rule == "cxx_module") out_line += module_edge_vars(cu); append(std::move(out_line)); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index b1155203..cec7ecfe 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -49,7 +49,13 @@ struct CompileUnit { std::vector packageCflags; std::vector packageCxxflags; std::vector packageAsmflags; // per-glob asmflags (G4) - std::optional providesModule; // logical name, if .cppm export + // The logical module name, when this unit is a module interface; empty + // otherwise (a module name is never empty). A plain string and not an + // `std::optional`: that member type does not copy under + // clang with the MSVC STL once the module interface around it changes + // (`_SMF_control` has no matching constructor), and the error names this + // struct from whichever unit copies a CompileUnit first. + std::string providesModule; std::vector imports; // logical names imported // Unit came from a scan_overrides declaration — plan-vs-ddi // verification is mandatory for it (ninja_backend emits --expect-*). @@ -1757,7 +1763,7 @@ make_plan(const mcpp::manifest::Manifest& manifest, // unit that reads and writes no BMI. if (auto it = implementationStandardFlag.find(cu.packageName); it != implementationStandardFlag.end() - && cu.kind == mcpp::SourceKind::Cxx && !cu.providesModule + && cu.kind == mcpp::SourceKind::Cxx && cu.providesModule.empty() && cu.imports.empty() && cu.declaration == mcpp::modgraph::ModuleDeclaration::None) { cu.packageCxxflags.push_back(it->second); @@ -1796,8 +1802,8 @@ make_plan(const mcpp::manifest::Manifest& manifest, // 2. Build map of module-name → compile unit (for inter-unit dep resolution) std::map producerOf; for (std::size_t i = 0; i < plan.compileUnits.size(); ++i) { - if (plan.compileUnits[i].providesModule) { - producerOf[*plan.compileUnits[i].providesModule] = i; + if (!plan.compileUnits[i].providesModule.empty()) { + producerOf[plan.compileUnits[i].providesModule] = i; } } diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index d99f9fc4..8d1d8f06 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -1390,6 +1390,178 @@ sysroot_override(const mcpp::manifest::Manifest& m, return (e && e->sysrootDeclared) ? &e->sysroot : nullptr; } +// THE MSVC TOOLSET A CLANG `*-windows-msvc` BUILD COMPILES AGAINST. +// +// On an MSVC-ABI row the compiler is the toolchain and the MSVC toolset -- its +// STL and CRT, and the Windows SDK that follows it -- is the sysroot, named by +// `[target.].sysroot` (default `msvc@system`). Until this existed the +// clang driver searched the machine for headers and libraries while mcpp +// searched it again for `std.ixx`, by a different order, so a machine with two +// installations could compile one toolset's `std.ixx` against another's +// headers, and the choice reached neither the cache key nor any report. The +// choice is made here once, recorded on the toolchain, and handed to the +// driver by the link model. +// +// `msvc@system` that finds nothing returns without binding, so the build +// reaches the "targeting the MSVC ABI without a usable MSVC" diagnosis that +// already names the alternatives. +std::expected +bind_msvc_sysroot(mcpp::toolchain::Toolchain& tc, + const mcpp::manifest::Manifest& m, + const std::function()>& cfgOf) +{ + namespace msvc = mcpp::toolchain::msvc; + auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); + if (!tt) return {}; + const std::string* declared = sysroot_override(m, *tt); + const std::string text = declared ? *declared : std::string("msvc@system"); + auto spec = mcpp::toolchain::parse_toolchain_spec(text); + if (!spec) + return std::unexpected(std::format( + "[target.{}].sysroot = '{}': {}", tt->str(), text, spec.error())); + if (spec->family != mcpp::toolchain::Family::Msvc) + return std::unexpected(std::format( + "[target.{}].sysroot = '{}': on an MSVC-ABI row the sysroot is an " + "MSVC toolset (msvc@system, msvc@ or xim:msvc@)", + tt->str(), text)); + + msvc::ToolsetNeeds needs; + needs.cl = false; // clang compiles against the toolset; it does not run cl.exe + const bool systemSel = spec->version.empty() || spec->version == "system"; + std::vector instances; + std::optional choice; + if (!spec->ecosystemOnly) { + instances = msvc::enumerate_vs_instances(); + choice = msvc::select_system_toolset( + instances, msvc::msvc_env_snapshot(), + systemSel ? std::string_view("system") : std::string_view(spec->version), + needs); + if (!choice && systemSel) return {}; + } + + std::string origin = "system"; + if (!choice) { + // THE PACKAGE: `xim:` asked for it, or no installed toolset matched. + auto cfg = cfgOf(); + if (!cfg) return std::unexpected(cfg.error()); + mcpp::toolchain::ToolchainSpec pkgSpec = *spec; + pkgSpec.target = {}; + auto pkg = mcpp::toolchain::to_xim_package(pkgSpec); + mcpp::fetcher::Fetcher fetcher(**cfg); + mcpp::fetcher::InstallProgressHandler progress; + auto payload = fetcher.resolve_xpkg_path(pkg.target(), /*autoInstall=*/true, + &progress); + if (!payload) { + // `xim:` never looked at the machine, so the refusal does not + // report on it. + if (spec->ecosystemOnly) + return std::unexpected(std::format( + "[target.{}].sysroot = '{}': the package could not be " + "provided: {}\n" + " packages: `mcpp toolchain list --available msvc`", + tt->str(), text, payload.error().message)); + std::string onMachine; + for (auto const& line : msvc::describe_system_toolsets(instances, needs)) + onMachine += "\n " + line; + return std::unexpected(std::format( + "[target.{}].sysroot = '{}' matches no toolset on this machine, " + "and the package could not be provided: {}\n" + " installed on this machine:{}\n" + " packages: `mcpp toolchain list --available msvc`", + tt->str(), text, payload.error().message, + onMachine.empty() ? std::string(" none") : onMachine)); + } + auto inst = mcpp::toolchain::resolve_managed_msvc( + mcpp::config::make_xlings_env(**cfg), pkg, /*identifyVersion=*/false); + if (!inst) return std::unexpected(inst.error()); + choice.emplace(); + choice->vsRoot = inst->vsRoot; + choice->version = inst->toolsVersion; + choice->toolsDir = inst->vsRoot / "VC" / "Tools" / "MSVC" / inst->toolsVersion; + choice->product = "xim:msvc@" + inst->toolsVersion; + choice->via = "package"; + origin = "managed"; + } + for (auto const& n : choice->notes) mcpp::ui::info("note", n); + + // THE SDK FOLLOWS THE TOOLSET'S ORIGIN: a package binds the windows-sdk + // installed with it, a machine's toolset takes the machine's SDK by the + // search `msvc@system` has always used. The same function the cl.exe row + // uses, asked about the toolset directory rather than a cl.exe. + auto sdk = msvc::resolve_sdk_for(choice->toolsDir / "bin"); + if (!sdk.note.empty()) mcpp::ui::info("note", sdk.note); + + tc.msvcToolsDir = choice->toolsDir; + tc.msvcToolsVersion = choice->version; + tc.msvcOrigin = origin; + tc.msvcProduct = choice->product; + if (sdk.sdk) { + tc.windowsSdkRoot = sdk.sdk->root; + tc.windowsSdkVersion = sdk.sdk->version; + } + // The STL is this toolset's, so its version is the standard library's. + tc.stdlibVersion = choice->version; + + // THE STD MODULE OF THIS TOOLSET, replacing the `std.ixx` detection found + // by its own search. A toolset without one leaves `import std` unavailable + // rather than borrowing another toolset's. + // + // Only `std` is rebound. Detection never gave this row a `std.compat` + // source, and the clang builder for it passes the file without + // `-x c++-module`: given `std.compat.ixx`, clang takes it for linker + // input, `--precompile` writes nothing and exits 0, and the next command + // fails on the missing BMI (measured on the Windows runners). + std::error_code ec; + const auto ixx = choice->toolsDir / "modules" / "std.ixx"; + const bool msvcStl = tc.stdModuleSource.empty() + || tc.stdModuleSource.filename() == "std.ixx"; + if (msvcStl && std::filesystem::exists(ixx, ec)) { + tc.stdModuleSource = ixx; + tc.hasImportStd = true; + tc.importStdMinLevel = msvc::std_module_min_level_for_stl(ixx); + } else if (msvcStl && !tc.stdModuleSource.empty()) { + tc.stdModuleSource.clear(); + tc.hasImportStd = false; + } + + mcpp::ui::info("Resolved", std::format( + "sysroot {} → MSVC {} ({}: {}){}", spec->spec_str(), choice->version, + origin, choice->product, + tc.windowsSdkVersion.empty() + ? std::string{} + : std::format(" · Windows SDK {}", tc.windowsSdkVersion))); + return {}; +} + +// ON THE CL.EXE ROW THE COMPILER IS ITS OWN SYSROOT: cl.exe cannot compile +// against another toolset's STL. A declared sysroot is therefore a second +// statement of the compiler's toolset, and one that names a different +// toolset is refused rather than silently ignored. +std::expected +check_cl_row_sysroot(const mcpp::toolchain::Toolchain& tc, + const mcpp::manifest::Manifest& m) +{ + auto tt = mcpp::toolchain::triple::parse(tc.targetTriple); + if (!tt) return {}; + const std::string* declared = sysroot_override(m, *tt); + if (!declared) return {}; + auto spec = mcpp::toolchain::parse_toolchain_spec(*declared); + if (!spec || spec->version.empty() || spec->version == "system") return {}; + // /bin/Host//cl.exe → is named by the toolset. + const auto toolset = tc.binaryPath.parent_path().parent_path() + .parent_path().parent_path().filename().string(); + if (mcpp::toolchain::msvc::toolset_version_matches(spec->version, toolset)) + return {}; + return std::unexpected(std::format( + "[target.{}].sysroot = '{}' names a different toolset than the " + "compiler ({}, toolset {}). With cl.exe the compiler is its own " + "sysroot: pin the toolset in the toolchain (`msvc@`) and " + "drop `sysroot`, or build with clang to compile against another " + "toolset.", + tt->str(), *declared, tc.binaryPath.string(), toolset)); +} + // The target-facing answers a `build.mcpp` may ask the engine for. // // ONE function because there are TWO call sites — the root project and each @@ -1931,8 +2103,15 @@ std::string min_platform_version(const mcpp::manifest::Manifest& m, // default is not the SDK's. Bionic rejects the unversioned triple // outright, which is the other half of the asymmetry. if (t.is_ios()) return m.buildConfig.iosDeploymentTarget; - return mcpp::platform::macos::deployment_target( - m.buildConfig.macosDeploymentTarget); + // AND ONLY FOR A macOS TARGET. `deployment_target` itself no longer + // consults the host (#685); the discriminator is `t.os`, which is this + // function's own target parameter and is available regardless of what + // machine mcpp runs on. A non-Apple target (Linux, Windows, wasm, + // freestanding) answers empty here, same as it always has. + if (t.os == "macos") + return mcpp::platform::macos::deployment_target( + /*targetIsMacos=*/true, m.buildConfig.macosDeploymentTarget); + return {}; } std::string with_index_cause(std::string msg) { @@ -2510,7 +2689,7 @@ prepare_build(bool print_fingerprint, return bin.string(); }(); - auto runtimePayload = runtimeBindingSnapshot.libc.value_or(""); + auto runtimePayload = runtimeBindingSnapshot.libc; auto runtimeLibDir = runtimeBindingSnapshot.libraryDirs.empty() ? std::filesystem::path{} : runtimeBindingSnapshot.libraryDirs.front(); @@ -2530,7 +2709,7 @@ prepare_build(bool print_fingerprint, if (!cfgP) return; if (!mcpp::toolchain::ensure_declared_runtime(**cfgP, runtimeBindingSnapshot)) return; - runtimePayload = runtimeBindingSnapshot.libc.value_or(""); + runtimePayload = runtimeBindingSnapshot.libc; runtimeLibDir = runtimeBindingSnapshot.libraryDirs.empty() ? std::filesystem::path{} : runtimeBindingSnapshot.libraryDirs.front(); }; @@ -3425,7 +3604,30 @@ prepare_build(bool print_fingerprint, tcSpecIsMsvc = parsedSpec && tcOriginAxis == mcpp::toolchain::Origin::SystemMsvc; - if (tcSpecIsMsvc) { + // A PINNED TOOLSET THIS MACHINE ALREADY HAS IS USED WHERE IT IS. + // + // `msvc@14.44.35207` names one Microsoft build, and the ecosystem package + // of that version unpacks the same installer payloads Visual Studio does, + // so an installed copy is the same toolset without a download. `xim:` + // opts out: it asks for the package, whose SDK is pinned with it. + std::optional installedPin; + std::vector installedPinNotes; + if constexpr (mcpp::platform::is_windows) { + if (parsedSpec && !tcSpecIsMsvc + && parsedSpec->family == mcpp::toolchain::Family::Msvc + && !parsedSpec->ecosystemOnly && !parsedSpec->version.empty()) + installedPin = mcpp::toolchain::msvc::system_installation_matching( + parsedSpec->version, mcpp::toolchain::msvc::ToolsetNeeds{}, + &installedPinNotes); + } + + if (installedPin) { + for (auto const& n : installedPinNotes) mcpp::ui::info("note", n); + explicit_compiler = installedPin->clPath; + mcpp::ui::info("Resolved", std::format( + "{} → msvc {} (installed: {})", parsedSpec->display(), + installedPin->display_version(), installedPin->clPath.string())); + } else if (tcSpecIsMsvc) { if (!mcpp::platform::is_windows) { return std::unexpected(std::format( "toolchain '{}' is only available on Windows hosts", *tcSpec)); @@ -4224,6 +4426,17 @@ prepare_build(bool print_fingerprint, } } + // THE MSVC TOOLSET OF THE CLANG ROW, chosen once and recorded before the + // runtime identity below reads its SDK version. See bind_msvc_sysroot. + if (tc->compiler == mcpp::toolchain::CompilerId::Clang + && mcpp::toolchain::is_msvc_target(*tc)) { + auto bound = bind_msvc_sysroot(*tc, *m, [&] { return get_cfg(); }); + if (!bound) return std::unexpected(bound.error()); + } else if (tc->compiler == mcpp::toolchain::CompilerId::MSVC) { + if (auto ok = check_cl_row_sysroot(*tc, *m); !ok) + return std::unexpected(ok.error()); + } + // The Windows runtime identity, flowing BACK into the contract. // // Everything else about the runtime is known before a toolchain is @@ -4688,7 +4901,11 @@ prepare_build(bool print_fingerprint, auto tt = overrides.target_triple.empty() ? std::optional{mcpp::toolchain::triple::host_triple()} : mcpp::toolchain::triple::parse(overrides.target_triple); - if (tt) + // Not on an MSVC-ABI row: there the key names an MSVC toolset, which + // `bind_msvc_sysroot` locates or installs itself -- an installed + // toolset of the pinned version must win over a download, and + // `msvc@system` is not a package at all. + if (tt && !tt->is_msvc_env()) targetSysroot = mcpp::toolchain::triple::effective_sysroot( *tt, sysroot_override(*m, *tt)); } @@ -10680,7 +10897,12 @@ prepare_build(bool print_fingerprint, // `sysroot = ""` and "no sysroot key" are different answers and // must not be collapsed: the first says this project wants no // prebuilt C library, the second says it did not say. - if (auto const* ovr = sysroot_override(*m, *tt); ovr && ovr->empty()) + // On an MSVC-ABI row the key names the MSVC toolset, which the + // toolchain binding consumes (`bind_msvc_sysroot`); it is not a + // C library package for this model to report as one. + if (tt->is_msvc_env()) + ; + else if (auto const* ovr = sysroot_override(*m, *tt); ovr && ovr->empty()) in.sysrootDeclaredEmpty = true; else in.sysrootXpkg = mcpp::toolchain::triple::effective_sysroot( @@ -12608,8 +12830,13 @@ prepare_build(bool print_fingerprint, hopt.cAbiPrebuilt = true; hopt.cxxFromGraph = true; hopt.appleSdkRoot = tc->appleSdkRoot; + // Target-keyed, not host-keyed (#685) — see `min_platform_version`. + const bool cAbiTargetIsMacos = [&] { + auto cAbiTt = mcpp::toolchain::triple::parse(tc->targetTriple); + return cAbiTt && cAbiTt->os == "macos"; + }(); hopt.macosDeploymentTarget = mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget); + cAbiTargetIsMacos, m->buildConfig.macosDeploymentTarget); for (auto& t : mcpp::toolchain::host_compile_tokens( *tc, hopt, mcpp::toolchain::no_escape)) { const auto q = " " + mcpp::xlings::shq(t); @@ -12772,8 +12999,17 @@ prepare_build(bool print_fingerprint, mcpp::toolchain::FingerprintInputs fpi; fpi.toolchain = *tc; fpi.cppStandard = m->package.standard; - fpi.compileFlags = canonical_compile_flags(*m) - + canonical_package_build_metadata(packages); + // Target-keyed, not host-keyed (#685): the fingerprint must fold + // `macos_deployment_target` whenever THIS BUILD's resolved toolchain + // targets macOS, whether mcpp itself is running on Linux, Windows or + // macOS — see the discriminator comment on `min_platform_version` and + // on `canonical_compile_flags`. + const bool fpTargetIsMacos = [&] { + auto fpTt = mcpp::toolchain::triple::parse(tc->targetTriple); + return fpTt && fpTt->os == "macos"; + }(); + fpi.compileFlags = canonical_compile_flags(*m, fpTargetIsMacos) + + canonical_package_build_metadata(packages, fpTargetIsMacos); // [c-abi] REALISATION AND `__OPENKAL__` PARTICIPATE IN THE FINGERPRINT // (design 2026-09-18 §3.4, gap #4 of the design's own self-review). Two // builds whose C library declares `data-model = "lp64"` and `"llp64"` @@ -12865,13 +13101,21 @@ prepare_build(bool print_fingerprint, const auto stdCrt = mcpp::toolchain::msvc_crt_flag( stdDialect, mcpp::toolchain::msvc_wants_static_crt( m->buildConfig.linkage, m->buildConfig.cxxRuntime)); + // Whether THIS build's resolved toolchain targets macOS — the same + // target-not-host discriminator `min_platform_version` uses, parsed + // locally because `tc` (not a `triple::Triple`) is what is in scope + // here (#685). + const bool stdTargetIsMacos = [&] { + auto stdTt = mcpp::toolchain::triple::parse(tc->targetTriple); + return stdTt && stdTt->os == "macos"; + }(); if (overrides.plan_only) { // Described, not compiled: the paths and commands are the ones // ensure_built would use, from the one derivation in stdmod.cppm. auto described = mcpp::toolchain::describe_std_module( *tc, m->package.standard, stdFlagAndDialect, mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget), + stdTargetIsMacos, m->buildConfig.macosDeploymentTarget), mcpp::toolchain::default_cache_root(), stdCrt); if (!described) { refusal::record(refusal::Code::StdModulePrecompile); @@ -12886,7 +13130,7 @@ prepare_build(bool print_fingerprint, auto sm = mcpp::toolchain::ensure_built( *tc, m->package.standard, stdFlagAndDialect, mcpp::platform::macos::deployment_target( - m->buildConfig.macosDeploymentTarget), + stdTargetIsMacos, m->buildConfig.macosDeploymentTarget), mcpp::toolchain::default_cache_root(), stdCrt); if (!sm) { // THE ONE CODE IN THE TAXONOMY THAT NOTHING WROTE. @@ -14480,9 +14724,9 @@ prepare_build(bool print_fingerprint, // one extra compile. if (cu.packageObjectRel.empty()) { addressable = false; break; } - if (cu.providesModule) { + if (!cu.providesModule.empty()) { std::string bmi; - for (char c : *cu.providesModule) + for (char c : cu.providesModule) bmi.push_back(c == ':' ? '-' : c); bmi += std::string(bmiT.bmiExt); arts.bmiFiles.push_back(std::move(bmi)); @@ -14510,9 +14754,9 @@ prepare_build(bool print_fingerprint, cu.servedFromCache = true; cu.cachedObject = mcpp::bmi_cache::cached_obj_path( key, cu.packageObjectRel.generic_string()); - if (cu.providesModule) { + if (!cu.providesModule.empty()) { std::string bmi; - for (char c : *cu.providesModule) + for (char c : cu.providesModule) bmi.push_back(c == ':' ? '-' : c); bmi += std::string(bmiT.bmiExt); cu.cachedBmi = mcpp::bmi_cache::cached_bmi_path(key, bmi); @@ -14962,6 +15206,22 @@ prepare_build(bool print_fingerprint, {"artifacts", nlohmann::json::array()}, }}, }; + // THE MSVC SYSROOT OF THE CLANG ROW: which toolset and SDK the build + // compiled against, and where each came from. Absent on every other + // row, so a reader can tell "not this row" from "not recorded". + if (!ctx.plan.toolchain.msvcToolsDir.empty()) { + const auto& tcr = ctx.plan.toolchain; + j["msvc_toolset"] = { + {"version", tcr.msvcToolsVersion}, + {"origin", tcr.msvcOrigin}, + {"product", tcr.msvcProduct}, + {"root", tcr.msvcToolsDir.generic_string()}, + }; + j["windows_sdk"] = { + {"version", tcr.windowsSdkVersion}, + {"root", tcr.windowsSdkRoot.generic_string()}, + }; + } std::error_code ec; std::filesystem::create_directories(ctx.plan.outputDir, ec); auto path = ctx.plan.outputDir / "resolution.json"; diff --git a/src/build/prepare_inputs.cppm b/src/build/prepare_inputs.cppm index 829809ba..78ef7cc1 100644 --- a/src/build/prepare_inputs.cppm +++ b/src/build/prepare_inputs.cppm @@ -517,7 +517,8 @@ std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc, // Exported so the "every build-variant knob is in here" invariant is machine- // checkable: the profile knobs were absent for a long time precisely because // nothing could assert on this string. -std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { +std::string canonical_compile_flags(const mcpp::manifest::Manifest& m, + bool targetIsMacos = false) { std::string s; s += "-std="; s += m.package.standard; s += " -fmodules"; @@ -528,18 +529,27 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { // into the fingerprint so switching targets rebuilds the BMI cache // instead of dying with a module config mismatch. // + // TARGET-KEYED, NOT HOST-KEYED (#685). This used to read + // `if constexpr (mcpp::platform::is_macos)`, i.e. the platform mcpp + // itself was BUILT for, which left the fingerprint blind to + // `macos_deployment_target` whenever the BUILD ran on a non-Apple host — + // so `mcpp build --target aarch64-macos` on Linux kept the same output + // directory no matter what the manifest key was edited to. The caller + // answers whether THIS build's target is macOS (the same discriminator + // `min_platform_version` uses); it defaults to false so a caller that + // has no target in hand (a manifest-only unit test) gets today's + // non-macOS behaviour rather than silently guessing. + // // The built-in default floor (rustc-style) lives in the single // resolver (platform::macos::deployment_target), so this rule, the // flags and the std-module prebuild always agree — the 0.0.50-era // attempt to inject a default here alone left the test build's // std.pcm unstaged (import std failed wholesale on macos CI). - if constexpr (mcpp::platform::is_macos) { - auto dtv = mcpp::platform::macos::deployment_target( - m.buildConfig.macosDeploymentTarget); - if (!dtv.empty()) { - s += " macos_deployment_target="; - s += dtv; - } + if (auto dtv = mcpp::platform::macos::deployment_target( + targetIsMacos, m.buildConfig.macosDeploymentTarget); + !dtv.empty()) { + s += " macos_deployment_target="; + s += dtv; } if (!m.buildConfig.cStandard.empty()) { s += " c_standard="; @@ -615,7 +625,8 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { } std::string canonical_package_build_metadata( - const std::vector& packages) + const std::vector& packages, + bool targetIsMacos = false) { std::string s; for (auto const& pkg : packages) { @@ -653,7 +664,7 @@ std::string canonical_package_build_metadata( // already folds; serialising it twice is harmless and keeps this loop // one rule rather than one rule and an exception. s += ' '; - s += canonical_compile_flags(pkg.manifest); + s += canonical_compile_flags(pkg.manifest, targetIsMacos); // The level a C++-layer provider compiles its implementation units at // (`make_plan`). Appended only when there is one, so every other // output directory keeps its identity. diff --git a/src/doctor.cppm b/src/doctor.cppm index 7bd2848a..f57a9bf6 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -80,6 +80,89 @@ export std::vector parse_readelf_runpath(std::string_view dump) { return out; } +// A frozen copy of the BUILD MACHINE's own libc header, found in a GCC +// payload's `include-fixed`. +// +// GCC's `fixincludes` step runs once, at BUILD time, against whatever glibc +// happened to be on the machine that produced the payload, and writes the +// edited copy into `lib/gcc///include-fixed` — which +// `-idirafter` (mcpp's own C-library search order) puts AHEAD of the +// realised glibc every project actually builds against. A header the +// payload should never have needed to fix (glibc moved on since) is a +// stale, frozen snapshot sitting in front of the one mcpp resolved, and +// mcpp#687 was ``/`` breaking against it. +export struct FixincludesFinding { + std::filesystem::path file; // relative to `payloadRoot` + std::string source; // the banner's own path, or empty if unparsed +}; + +// Scan one installed toolchain payload for fixincludes-frozen headers. +// +// PURE AND PAYLOAD-ROOT SCOPED, so a unit test can point it at a synthetic +// directory tree instead of the real registry (`tests/unit/ +// test_doctor_fixincludes.cpp`), and `doctor_report` below is its one real +// caller. `payloadRoot` is a version directory (e.g. `/data/xpkgs/ +// xim-x-gcc/13.3.0`) — not the whole payload store — so the same function +// scans a native, a musl-gcc, or a cross-gcc payload alike; the directory +// SHAPE (`lib/gcc///include-fixed`) is a GCC-family +// constant this does not need to be told. +// +// THE FIRST ~2 KiB IS ENOUGH. fixincludes writes its banner as the file's +// very first comment, before the copyright notice it also carries — reading +// a bounded prefix instead of the whole file keeps this cheap to run over +// an entire registry, and a header that legitimately mentions the phrase +// past that point (none does; it is fixincludes' own wording) is not this +// function's concern. +export std::vector +find_fixincludes_banners(const std::filesystem::path& payloadRoot) { + constexpr std::string_view kBanner = "auto-edited by fixincludes"; + constexpr std::size_t kHeadBytes = 2048; + + std::vector out; + std::error_code ec; + for (auto it = std::filesystem::recursive_directory_iterator( + payloadRoot, std::filesystem::directory_options::skip_permission_denied, ec); + !ec && it != std::filesystem::recursive_directory_iterator{}; + it.increment(ec)) { + std::error_code isDirEc; + if (!it->is_directory(isDirEc) || isDirEc) continue; + if (it->path().filename() != "include-fixed") continue; + + std::error_code innerEc; + for (auto& fe : std::filesystem::directory_iterator(it->path(), innerEc)) { + std::error_code isFileEc; + if (innerEc || !fe.is_regular_file(isFileEc) || isFileEc) continue; + + std::ifstream in(fe.path(), std::ios::binary); + if (!in) continue; + std::string head(kHeadBytes, '\0'); + in.read(head.data(), static_cast(head.size())); + head.resize(static_cast(std::max(in.gcount(), 0))); + + auto pos = head.find(kBanner); + if (pos == std::string::npos) continue; + + FixincludesFinding f; + std::error_code relEc; + f.file = std::filesystem::relative(fe.path(), payloadRoot, relEc); + if (relEc || f.file.empty()) f.file = fe.path(); + + // The banner's shape (see the fixture in the unit test and the + // real payloads under xim-x-gcc): + // It has been auto-edited by fixincludes from: + // "/quoted/source/path" + // The source is the first double-quoted span after the banner. + if (auto q1 = head.find('"', pos); q1 != std::string::npos) + if (auto q2 = head.find('"', q1 + 1); q2 != std::string::npos) + f.source = head.substr(q1 + 1, q2 - q1 - 1); + + out.push_back(std::move(f)); + } + } + std::ranges::sort(out, {}, [](auto const& f) { return f.file.string(); }); + return out; +} + // `mcpp self env`. export int env_report() { auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback()); @@ -532,6 +615,81 @@ export int doctor_report() { } #endif + // ─── Fixincludes-frozen headers in installed GCC payloads (mcpp#687) ──── + // + // NOT GATED ON HOST OS, unlike the RUNPATH block above: this reads no + // ELF and spawns no `readelf`, so it runs the same way on every host + // that has a GCC-family payload installed, including a cross gcc/ + // musl-gcc payload on a Linux, macOS or Windows mcpp. + mcpp::ui::status("Checking", "gcc include-fixed headers"); + if (cfg) { + auto pkgsDir = (*cfg).xlingsHome() / "data" / "xpkgs"; + std::error_code ec; + bool sawAnyGcc = false; + bool anyFrozen = false; + + if (std::filesystem::exists(pkgsDir, ec)) { + // Same enumeration as the RUNPATH block: every `xim-x-` + // directory `identify_xim_payload` recognises, filtered to the + // GCC family (native, musl-gcc, mingw-(cross-)gcc, and any + // `-gcc` cross payload all answer Family::Gcc). + for (auto& entry : std::filesystem::directory_iterator(pkgsDir, ec)) { + auto name = entry.path().filename().string(); + if (name.rfind("xim-x-", 0) != 0) continue; + auto id = mcpp::toolchain::identify_xim_payload( + name.substr(std::string("xim-x-").size())); + if (!id || id->family != mcpp::toolchain::Family::Gcc) continue; + + for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) { + if (!vEntry.is_directory(ec)) continue; + sawAnyGcc = true; + auto findings = find_fixincludes_banners(vEntry.path()); + if (findings.empty()) continue; + anyFrozen = true; + + mcpp::toolchain::ToolchainSpec s; + s.family = id->family; + s.version = vEntry.path().filename().string(); + s.target = id->target; + auto specStr = s.spec_str(); + auto targetFlag = id->target.empty() + ? std::string{} + : std::format(" --target {}", id->target.str()); + + std::string files; + std::string sources; + for (auto& f : findings) { + if (!files.empty()) files += ", "; + files += f.file.string(); + if (!f.source.empty() + && sources.find(f.source) == std::string::npos) { + if (!sources.empty()) sources += ", "; + sources += f.source; + } + } + warn(std::format( + "{}: {} carries a fixincludes copy of the BUILD " + "MACHINE's own header ({}), frozen from {}. mcpp's " + "`-idirafter` puts this ahead of the C library the " + "project actually resolves, so a header this frozen " + "can disagree with it (observed: a frozen " + "`pthread.h` breaking ``/``, " + "mcpp#687). Remedy: `mcpp index update`, then " + "`mcpp toolchain remove {}{}` and " + "`mcpp toolchain install {}{}`.", + s.display(), findings.size() == 1 ? "a file" : + std::format("{} files", findings.size()), + files, sources.empty() ? "an unrecorded source" : sources, + specStr, targetFlag, specStr, targetFlag)); + } + } + } + if (sawAnyGcc && !anyFrozen) + ok("no fixincludes-frozen headers in installed gcc payloads"); + else if (!sawAnyGcc) + ok("no installed gcc-family payloads to check"); + } + // ── Build-policy knobs that are otherwise invisible ──────────────────── // // Both of these change behaviour without changing anything a user can see diff --git a/src/runtime/binding.cppm b/src/runtime/binding.cppm index 6cdbc44c..b4b5e303 100644 --- a/src/runtime/binding.cppm +++ b/src/runtime/binding.cppm @@ -49,8 +49,11 @@ struct RuntimeBinding { std::string runtimeId; std::string contractHash; std::optional loader; - std::optional libc; - std::optional hostLibc; + // Empty means absent. Plain strings rather than `std::optional`: + // that member type does not copy under clang with the MSVC STL once the + // importers' module graph changes (see `TargetEntry::sysroot`). + std::string libc; + std::string hostLibc; // IMMUTABLE payload directories (`/xim-x-glibc/2.39/lib64`). std::vector libraryDirs; // The SubOS symlink farm (`/lib`) — a union view of everything @@ -171,8 +174,8 @@ std::string canonical_contract(const RuntimeBinding& binding) { append_field(out, binding.selection.subosName); append_field(out, binding.provenance); append_field(out, binding.loader ? binding.loader->generic_string() : ""); - append_field(out, binding.libc.value_or("")); - append_field(out, binding.hostLibc.value_or("")); + append_field(out, binding.libc); + append_field(out, binding.hostLibc); for (auto const& p : binding.libraryDirs) append_field(out, p.generic_string()); // The farm participates in the hash because it participates in the @@ -480,8 +483,8 @@ std::string serialize_runtime_binding(const RuntimeBinding& binding) { j["runtime_id"] = binding.runtimeId; j["contract_hash"] = binding.contractHash; j["loader"] = binding.loader ? binding.loader->generic_string() : ""; - j["libc"] = binding.libc.value_or(""); - j["host_libc"] = binding.hostLibc.value_or(""); + j["libc"] = binding.libc; + j["host_libc"] = binding.hostLibc; j["library_dirs"] = nlohmann::json::array(); for (auto const& path : binding.libraryDirs) j["library_dirs"].push_back(path.generic_string()); diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 97af1732..1ca76021 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -201,7 +201,11 @@ void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) { #if defined(_WIN32) // Fallback: if libc++ std.cppm not found, look for MSVC STL's std.ixx. - // Uses msvc.cppm which searches via vswhere, env vars, and known paths. + // This is the machine's default toolset by the one selection both rows + // use (msvc::select_system_toolset, selector `system`). A build replaces it + // with the toolset its row names (`[target.].sysroot`, see + // prepare's bind_msvc_sysroot); this answer serves what only detects, such + // as `mcpp toolchain list`. if (!tc.hasImportStd && msvTarget) { if (auto p = mcpp::toolchain::msvc::find_std_module_source()) { tc.stdModuleSource = *p; diff --git a/src/toolchain/hostflags.cppm b/src/toolchain/hostflags.cppm index 1d6de266..eedb3a64 100644 --- a/src/toolchain/hostflags.cppm +++ b/src/toolchain/hostflags.cppm @@ -79,8 +79,10 @@ struct HostFlagOptions { // and states the stdlib selection explicitly. bool clangStdlibSelect = false; - // Resolved value from platform::macos::deployment_target(); empty = omit. - // Must agree across the std BMI and everything that imports it — clang + // Resolved value from platform::macos::deployment_target(), which the + // caller asks with `targetIsMacos` set for THIS toolchain's target + // (#685) -- not for the host mcpp itself runs on; empty = omit. Must + // agree across the std BMI and everything that imports it — clang // rejects a module built for a different deployment target outright. // // A macOS VERSION, so it is emitted only for a macOS target. An iOS @@ -336,6 +338,12 @@ std::vector host_compile_tokens(const Toolchain& tc, // reads it. if (!tc.crossTargetFlag.empty()) out.push_back(tc.crossTargetFlag); + // THE MSVC TOOLSET AND SDK, SAID TO THE DRIVER (clang on `*-windows-msvc` + // only; empty everywhere else). This producer serves the compile line, the + // std module precompile and the build.mcpp host compile, so all three + // compile against the one toolset prepare chose. + for (auto& t : lm.msvc_driver_tokens(esc)) out.push_back(t); + // AND WHAT A `throw` AND A `thread_local` COMPILE INTO, WHICH IS A // PROPERTY OF THE GRAPH AND NOT OF ANY ONE PACKAGE — see // `graph_runtime_compile_flags` for what and why. @@ -498,19 +506,33 @@ std::vector host_compile_tokens(const Toolchain& tc, // path is exactly the mismatch e2e 181 catches: the std BMI is built for // 14.0 while the TU importing it is not. // - // AND ONLY FOR A macOS TARGET. This asked whether the HOST is macOS, which - // was the same question while macOS was the only Apple target mcpp could - // build for. The iOS rows are built ON a macOS host and FOR another - // platform, and clang refuses the combination outright: + // AND ONLY FOR A macOS TARGET. This asked whether the HOST is macOS, + // which was the same question while macOS was the only Apple target + // mcpp could build for AND the only way to reach it was to build ON one. + // Cross-compiling `aarch64-macos` from Linux (#685) is a macOS TARGET on + // a non-Apple HOST, and the old `mcpp::platform::is_macos` (a + // compile-time constant naming the machine mcpp itself was built for) + // answered false there — so the flag, and the deployment target it + // carries, were silently dropped on every non-Apple host. The + // discriminator is `tc`'s own resolved target, parsed the same way + // `apple_float_macro_words` above already does. + // + // The iOS rows are built ON a macOS host and FOR another platform (or, + // once iOS cross-compiling exists, on any host), and clang refuses the + // combination outright: // // error: invalid argument '-mmacosx-version-min=14.0' not allowed with // 'arm64-apple-ios18.0' // - // so the flag would not merely be useless there, it would stop the build. - // The iOS deployment target travels in the effective triple instead -- - // `arm64-apple-ios18.0` -- which is one place rather than two for the same - // value. - if (mcpp::platform::is_macos && !opt.macosDeploymentTarget.empty() + // so the flag would not merely be useless there, it would stop the + // build. The iOS deployment target travels in the effective triple + // instead -- `arm64-apple-ios18.0` -- which is one place rather than two + // for the same value. `appleSdkRoot` already discriminates that case + // (non-empty only for the iOS rows; see its own comment above), so it + // remains the second half of the gate rather than an `is_ios()` check + // that would say the same thing a second way. + if (auto tt = triple::parse(tc.targetTriple); + tt && tt->os == "macos" && !opt.macosDeploymentTarget.empty() && opt.appleSdkRoot.empty()) out.push_back("-mmacosx-version-min=" + opt.macosDeploymentTarget); @@ -587,6 +609,10 @@ std::vector host_link_tokens(const Toolchain& tc, const auto dm = resolve_clang_driver(tc); const auto lm = resolve_link_model(tc); + // The link half of the same statement: the driver derives the toolset's + // and the SDK's library directories from these, not from the machine. + for (auto& t : lm.msvc_driver_tokens(esc)) out.push_back(t); + const bool bypassCfg = dm.hasCfg && (opt.cfgBypass == HostFlagOptions::CfgBypass::Always || (opt.cfgBypass == HostFlagOptions::CfgBypass::LinuxOnly diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index ed30f1dd..29b77250 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -588,6 +588,16 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg, isDefault ? "*" : "", std::format("msvc {}", inst->display_version()), inst->clPath.string()); + // EVERY INSTALLED TOOLSET, by the version a manifest pins it with. + // `msvc@` (the cl.exe row) and `sysroot = "msvc@"` + // (the clang row) take one of these before they download anything. + auto toolsets = mcpp::toolchain::msvc::describe_system_toolsets( + mcpp::toolchain::msvc::enumerate_vs_instances(), + mcpp::toolchain::msvc::ToolsetNeeds{}); + if (!toolsets.empty()) { + std::println(" installed toolsets (pin one as msvc@):"); + for (auto const& line : toolsets) std::println(" {}", line); + } } else { std::println(""); std::println(" (msvc: not detected — run `mcpp toolchain default msvc` " @@ -1164,6 +1174,31 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, return 0; } + // A PINNED TOOLSET THIS MACHINE HAS NEEDS NO PACKAGE, which is what the + // same spelling means to a build (`msvc@` takes an installed + // toolset first). `xim:` asks for the package and skips this. + if (mcpp::platform::is_windows + && spec->family == mcpp::toolchain::Family::Msvc + && !spec->ecosystemOnly) { + if (auto inst = mcpp::toolchain::msvc::system_installation_matching( + spec->version, mcpp::toolchain::msvc::ToolsetNeeds{})) { + auto wr = mcpp::config::write_default_toolchain(cfg, spec->spec_str()); + if (!wr) { + mcpp::ui::error(wr.error().message); + return 1; + } + if (auto wt = mcpp::config::write_default_target(cfg, ""); !wt) { + mcpp::ui::error(wt.error().message); + return 1; + } + mcpp::ui::status("Default", std::format( + "set to {} (installed: {}; was: {})", spec->spec_str(), + inst->clPath.string(), + cfg.defaultToolchain.empty() ? "" : cfg.defaultToolchain)); + return 0; + } + } + auto pkg = mcpp::toolchain::to_xim_package(*spec); // Partial-version resolution against installed payloads. diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index f3769b23..1e814f40 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -15,8 +15,10 @@ // it — which is what keeps a managed toolset from being a second code path // with its own bugs. // -// Discovery order for the SYSTEM origin is deliberate and is documented at -// find_vs_install_path(): a declared answer outranks a probe. +// Which installed toolset the SYSTEM origin means, and which one a pinned +// `msvc@` finds before it reaches for a payload, is one function, +// `select_system_toolset()`: a declared answer outranks a probe, and the cl.exe +// row and the clang row read the same answer. // // Also used by clang.cppm to find MSVC STL's std.ixx when Clang targets // x86_64-pc-windows-msvc. @@ -132,6 +134,113 @@ std::optional installation_at(const std::filesystem::path& vsR std::string_view toolsVersion, bool identifyVersion = true); +// ─── Toolset selection: one rule for both rows ──────────────────────────── +// +// A toolset is chosen for two different rows. `cl.exe` compiles with it, and +// clang targeting `*-windows-msvc` compiles AGAINST it (its STL, CRT and the +// SDK that follows it). Both rows used to reach an answer on their own: the +// clang driver probed the machine for headers and libraries while this module +// probed it again for `std.ixx`, by a different order, so the two could name +// different toolsets. The selection below is the one answer both rows read. +// +// It is a function of its inputs. The machine enters only through +// `enumerate_vs_instances()` and `msvc_env_snapshot()`, which are the Windows +// half; everything else reads the directories it is handed, so it is tested +// on any host against a synthetic tree. + +// One Visual Studio instance, as the installer reports it. +struct VsInstance { + std::filesystem::path root; // ...\Microsoft Visual Studio\2022\Community + std::string product; // "Visual Studio Community 2022" + std::string installVersion; // "17.14.36301.6"; empty when unknown +}; + +// The environment variables that DECLARE an installation. A developer command +// prompt sets all three; a plain shell sets none. +struct MsvcEnvSnapshot { + std::filesystem::path vcToolsInstallDir; // VCToolsInstallDir + std::filesystem::path vsInstallDir; // VSINSTALLDIR, else VCINSTALLDIR's parent + std::filesystem::path clOnPath; // the first cl.exe on PATH +}; + +// What the build needs from a toolset for it to count as installed. +struct ToolsetNeeds { + bool cl = true; // the cl.exe row; clang does not need it + std::string libArch = "x64"; // lib/ must exist +}; + +struct ToolsetChoice { + std::filesystem::path vsRoot; + std::filesystem::path toolsDir; // /VC/Tools/MSVC/ + std::string version; // "14.44.35207" + std::string product; // for the one line a build prints + std::string via; // the input that decided it + std::vector notes; // candidates skipped, variables ignored +}; + +// Dotted versions compared component by component as numbers, so that +// `14.9` < `14.10`. A missing component counts as zero. +int compare_toolset_versions(std::string_view a, std::string_view b); + +// `request` matches `version` when every component of `request` equals the +// component at the same position: `14.4` matches `14.4.1` and not `14.44`. +bool toolset_version_matches(std::string_view request, std::string_view version); + +// The toolset directory names under `/VC/Tools/MSVC`, highest first. +std::vector toolsets_under(const std::filesystem::path& vsRoot); + +// `/VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt`, trimmed; +// empty when the instance does not have one. +std::string default_toolset_of(const std::filesystem::path& vsRoot); + +// Does `toolsDir` hold what a build needs: `include/`, `lib//`, and +// for the cl.exe row a `cl.exe` under `bin/Host*//`. +bool toolset_complete(const std::filesystem::path& toolsDir, const ToolsetNeeds& needs); + +// Choose a toolset among the machine's installations. +// +// `selector` is `system` or a toolset version, full or partial. +// +// system the first complete candidate of: the toolset VCToolsInstallDir +// names; the default toolset of the instance VSINSTALLDIR names; +// the toolset of the first cl.exe on PATH; the default toolset of +// the instance with the highest installation version. This is the +// order clang's driver and this module used to apply separately, +// merged, so that where the two agreed the answer is unchanged. +// version the highest complete toolset whose version matches, across every +// instance. The environment takes no part, and a variable that +// would have chosen differently is reported in `notes`. +// +// nullopt when no installed toolset qualifies; the caller decides whether that +// is a refusal or a reason to use an ecosystem package. +std::optional +select_system_toolset(const std::vector& instances, + const MsvcEnvSnapshot& env, + std::string_view selector, + const ToolsetNeeds& needs); + +// One line per complete toolset on the machine, for a refusal to list. +std::vector describe_system_toolsets(const std::vector& instances, + const ToolsetNeeds& needs); + +// vswhere's `-format text` output, one instance per `instanceId:` line. Four +// keys are read, so the listing needs no JSON parser and this module keeps the +// imports it had. +std::vector parse_vswhere_text(std::string_view text); + +// The Windows half: every instance vswhere reports (prerelease included), plus +// the one VSINSTALLDIR names, plus the conventional paths when vswhere is +// absent. Empty off Windows. +std::vector enumerate_vs_instances(); +MsvcEnvSnapshot msvc_env_snapshot(); + +// The machine's installation of a pinned toolset, or nullopt. This is what +// makes `msvc@` take an installed toolset before an ecosystem package. +// `notes`, when given, receives what the selection skipped or ignored. +std::optional +system_installation_matching(std::string_view version, const ToolsetNeeds& needs, + std::vector* notes = nullptr); + // Parse a cl.exe banner into (version, arch). Token-based so localized // banners work: first "d.d.d[.d]" run is the version, arch is the arm64/x64/ // x86 token. Pure and cross-platform for unit testing. @@ -314,19 +423,6 @@ namespace { #if defined(_WIN32) -// Run a command and capture stdout (first line, trimmed). -std::string run_capture_line(const std::string& cmd) { - auto r = mcpp::platform::process::capture(cmd); - auto& out = r.output; - // Trim trailing whitespace/newlines - while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ')) - out.pop_back(); - // Take first line only - auto nl = out.find('\n'); - if (nl != std::string::npos) out.resize(nl); - return out; -} - // Strategy 1: VSINSTALLDIR — someone SAID which install to use. // // Set by a developer command prompt, by a CI step that ran vcvarsall, or by @@ -341,26 +437,6 @@ std::optional find_vs_via_vsinstalldir() { return std::nullopt; } -// Strategy 2: vswhere.exe — Microsoft's locator, i.e. a ranked guess. -std::optional find_vs_via_vswhere() { - // vswhere.exe ships with the VS Installer at a well-known path - std::filesystem::path vswhere = - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"; - if (!std::filesystem::exists(vswhere)) return std::nullopt; - - // `-prerelease` or an Insiders instance is invisible here. Without it a - // machine with only an Insiders VS reports "MSVC was not found" while a - // perfectly good cl.exe sits on disk. - auto result = run_capture_line( - "\"" + vswhere.string() + "\" -latest -prerelease -products * " - "-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 " - "-property installationPath 2>nul"); - - if (!result.empty() && std::filesystem::exists(result)) - return std::filesystem::path(result); - return std::nullopt; -} - // Strategy 3: VS*COMNTOOLS — machine-wide leftovers, so they rank BELOW // vswhere. VS150COMNTOOLS lingering from a 2017 install must not outrank a // current one; unlike VSINSTALLDIR nobody set these for this shell. @@ -405,54 +481,38 @@ std::optional find_vs_via_paths() { return std::nullopt; } -// From a VS install path, find the latest MSVC tools version directory. -std::optional find_latest_msvc_tools(const std::filesystem::path& vsRoot) { - auto vcTools = vsRoot / "VC" / "Tools" / "MSVC"; - std::error_code ec; - if (!std::filesystem::exists(vcTools, ec)) return std::nullopt; - - std::filesystem::path latest; - std::string latestVer; - for (auto& entry : std::filesystem::directory_iterator(vcTools, ec)) { - if (!entry.is_directory()) continue; - auto ver = entry.path().filename().string(); - if (ver > latestVer) { - latestVer = ver; - latest = entry.path(); - } - } - return latest.empty() ? std::nullopt : std::optional{latest}; -} - #endif // _WIN32 } // namespace +// The three below answer from the same selection every row uses +// (`select_system_toolset`, selector `system`), so the instance, the toolset +// directory and the `std.ixx` they report are one toolset rather than the +// results of three searches. `cl` is not required: the clang row reads them +// too, and a toolset is complete for it without `cl.exe`. +#if defined(_WIN32) +namespace { +std::optional default_system_choice() { + ToolsetNeeds needs; + needs.cl = false; + return select_system_toolset(enumerate_vs_instances(), msvc_env_snapshot(), + "system", needs); +} +} // namespace +#endif + std::optional find_vs_install_path() { #if defined(_WIN32) - // Declared before probed. vswhere used to run first, and because it - // returns something on almost every developer machine, VSINSTALLDIR was - // effectively unreachable — a build that had exported a complete vcvars - // environment still compiled with whatever vswhere ranked highest - // (measured on xrgui#3: vcvars said 14.52, the build used 14.51, and the - // only way out was to hide vswhere.exe). A guess must not silently - // override an answer. - if (auto p = find_vs_via_vsinstalldir()) return p; - if (auto p = find_vs_via_vswhere()) return p; - if (auto p = find_vs_via_comntools()) return p; - if (auto p = find_vs_via_paths()) return p; + if (auto c = default_system_choice()) return c->vsRoot; #endif return std::nullopt; } std::optional find_msvc_tools_dir() { #if defined(_WIN32) - auto vs = find_vs_install_path(); - if (!vs) return std::nullopt; - return find_latest_msvc_tools(*vs); -#else - return std::nullopt; + if (auto c = default_system_choice()) return c->toolsDir; #endif + return std::nullopt; } std::optional find_std_module_source() { @@ -655,13 +715,382 @@ std::optional installation_at(const std::filesystem::path& vsR return installation_from_tools_dir(vsRoot, tools, identifyVersion); } +// ─── Toolset selection ─────────────────────────────────────────────────── + +namespace { + +// The helpers below each have one caller and live here rather than in the +// exported purview on purpose: an inline helper in the purview is emitted by +// every importer, and that shape has crashed clang 20.1.7 on Windows before +// (see the note beside `toml.cppm`'s `[c-abi-absent]` parser). + +void version_components(std::string_view v, std::vector& out) { + out.clear(); + std::uint64_t n = 0; + bool any = false; + for (char c : v) { + if (c == '.') { out.push_back(n); n = 0; any = false; continue; } + if (c < '0' || c > '9') break; + n = n * 10 + static_cast(c - '0'); + any = true; + } + if (any || out.empty() || (!v.empty() && v.back() == '.')) out.push_back(n); +} + +// `VC/Tools/MSVC/` → the instance root four levels up. +std::filesystem::path vs_root_of_tools(const std::filesystem::path& toolsDir) { + return toolsDir.parent_path().parent_path().parent_path().parent_path(); +} + +// A path from the environment may end in a separator +// (`VCToolsInstallDir=...\14.44.35207\`), which leaves `filename()` empty. +std::filesystem::path without_trailing_separator(std::filesystem::path p) { + if (!p.empty() && p.filename().empty()) p = p.parent_path(); + return p; +} + +std::string product_of(const VsInstance& inst) { + if (!inst.product.empty()) return inst.product; + auto derived = product_from_vs_root(inst.root); + return derived.empty() ? inst.root.string() : "Visual Studio " + derived; +} + +// The choice an instance offers on its own: its default toolset when that is +// complete, else its highest complete one. Empty version when it has none. +void instance_choice(const VsInstance& inst, const ToolsetNeeds& needs, + ToolsetChoice& out) { + out = ToolsetChoice{}; + const auto base = inst.root / "VC" / "Tools" / "MSVC"; + const auto def = default_toolset_of(inst.root); + if (!def.empty() && toolset_complete(base / def, needs)) { + out.version = def; + } else { + for (auto const& v : toolsets_under(inst.root)) { + if (toolset_complete(base / v, needs)) { out.version = v; break; } + } + } + if (out.version.empty()) return; + out.vsRoot = inst.root; + out.toolsDir = base / out.version; + out.product = product_of(inst); +} + +bool same_root(const std::filesystem::path& a, const std::filesystem::path& b) { + std::error_code ec; + if (std::filesystem::equivalent(a, b, ec)) return true; + return without_trailing_separator(a).lexically_normal() + == without_trailing_separator(b).lexically_normal(); +} + +} // namespace + +int compare_toolset_versions(std::string_view a, std::string_view b) { + std::vector x, y; + version_components(a, x); + version_components(b, y); + const auto n = std::max(x.size(), y.size()); + for (std::size_t i = 0; i < n; ++i) { + const std::uint64_t l = i < x.size() ? x[i] : 0; + const std::uint64_t r = i < y.size() ? y[i] : 0; + if (l != r) return l < r ? -1 : 1; + } + return 0; +} + +bool toolset_version_matches(std::string_view request, std::string_view version) { + if (request.empty()) return false; + std::size_t i = 0, j = 0; + while (true) { + auto re = request.find('.', i); + auto ve = version.find('.', j); + auto rc = request.substr(i, re == std::string_view::npos ? std::string_view::npos : re - i); + auto vc = j <= version.size() + ? version.substr(j, ve == std::string_view::npos ? std::string_view::npos : ve - j) + : std::string_view{}; + if (rc != vc) return false; + if (re == std::string_view::npos) return true; + if (ve == std::string_view::npos) return false; + i = re + 1; + j = ve + 1; + } +} + +std::vector toolsets_under(const std::filesystem::path& vsRoot) { + std::vector out; + std::error_code ec; + const auto base = vsRoot / "VC" / "Tools" / "MSVC"; + for (auto& e : std::filesystem::directory_iterator(base, ec)) { + if (e.is_directory(ec)) out.push_back(e.path().filename().string()); + } + std::sort(out.begin(), out.end(), [](const std::string& a, const std::string& b) { + return compare_toolset_versions(a, b) > 0; + }); + return out; +} + +std::string default_toolset_of(const std::filesystem::path& vsRoot) { + std::ifstream in(vsRoot / "VC" / "Auxiliary" / "Build" + / "Microsoft.VCToolsVersion.default.txt"); + std::string line; + if (!in || !std::getline(in, line)) return {}; + while (!line.empty() && (line.back() == '\r' || line.back() == '\n' + || line.back() == ' ' || line.back() == '\t')) + line.pop_back(); + return line; +} + +bool toolset_complete(const std::filesystem::path& toolsDir, const ToolsetNeeds& needs) { + std::error_code ec; + if (!std::filesystem::is_directory(toolsDir / "include", ec)) return false; + if (!std::filesystem::is_directory(toolsDir / "lib" / needs.libArch, ec)) return false; + if (!needs.cl) return true; + for (auto host : {"Hostx64", "Hostarm64", "Hostx86"}) { + if (std::filesystem::exists(toolsDir / "bin" / host / needs.libArch / "cl.exe", ec)) + return true; + } + return false; +} + +std::optional +select_system_toolset(const std::vector& instances, + const MsvcEnvSnapshot& env, + std::string_view selector, + const ToolsetNeeds& needs) { + std::vector notes; + + // The instances in the order "newest installation first"; an instance + // whose installer did not report a version sorts last. + std::vector ranked; + for (auto const& i : instances) ranked.push_back(&i); + std::stable_sort(ranked.begin(), ranked.end(), + [](const VsInstance* a, const VsInstance* b) { + if (a->installVersion.empty() != b->installVersion.empty()) + return b->installVersion.empty(); + return compare_toolset_versions(a->installVersion, b->installVersion) > 0; + }); + + auto product_for_root = [&](const std::filesystem::path& root) { + for (auto const* i : ranked) + if (same_root(i->root, root)) return product_of(*i); + VsInstance bare; + bare.root = root; + return product_of(bare); + }; + + const auto vcTools = without_trailing_separator(env.vcToolsInstallDir); + + if (selector != "system") { + // PINNED. Every instance is a candidate and the environment is not. + ToolsetChoice best; + for (auto const* inst : ranked) { + for (auto const& v : toolsets_under(inst->root)) { + if (!toolset_version_matches(selector, v)) continue; + const auto dir = inst->root / "VC" / "Tools" / "MSVC" / v; + if (!toolset_complete(dir, needs)) { + notes.push_back(std::format( + "{} under {} is incomplete for this build and was skipped", + v, product_of(*inst))); + continue; + } + // Strictly greater: on a tie the newer instance, seen first, stays. + if (best.version.empty() || compare_toolset_versions(v, best.version) > 0) { + best.vsRoot = inst->root; + best.toolsDir = dir; + best.version = v; + best.product = product_of(*inst); + } + } + } + if (best.version.empty()) return std::nullopt; + best.via = "pinned"; + if (!vcTools.empty() && vcTools.filename().string() != best.version) { + notes.push_back(std::format( + "VCToolsInstallDir ({}) is ignored: the toolset is pinned to {}", + vcTools.filename().string(), selector)); + } + best.notes = std::move(notes); + return best; + } + + // SYSTEM. The declarations first, then the installer's newest instance. + if (!vcTools.empty()) { + if (toolset_complete(vcTools, needs)) { + ToolsetChoice c; + c.toolsDir = vcTools; + c.vsRoot = vs_root_of_tools(vcTools); + c.version = vcTools.filename().string(); + c.product = product_for_root(c.vsRoot); + c.via = "VCToolsInstallDir"; + c.notes = std::move(notes); + return c; + } + notes.push_back(std::format( + "VCToolsInstallDir ({}) names an incomplete toolset and was skipped", + vcTools.string())); + } + if (!env.vsInstallDir.empty()) { + VsInstance declared; + declared.root = without_trailing_separator(env.vsInstallDir); + declared.product = product_for_root(declared.root); + ToolsetChoice c; + instance_choice(declared, needs, c); + if (!c.version.empty()) { + c.via = "VSINSTALLDIR"; + c.notes = std::move(notes); + return c; + } + notes.push_back(std::format( + "VSINSTALLDIR ({}) has no complete toolset and was skipped", + declared.root.string())); + } + if (!env.clOnPath.empty()) { + // /bin/Host//cl.exe + const auto tools = env.clOnPath.parent_path().parent_path() + .parent_path().parent_path(); + if (toolset_complete(tools, needs)) { + ToolsetChoice c; + c.toolsDir = tools; + c.vsRoot = vs_root_of_tools(tools); + c.version = tools.filename().string(); + c.product = product_for_root(c.vsRoot); + c.via = "PATH"; + c.notes = std::move(notes); + return c; + } + } + for (auto const* inst : ranked) { + ToolsetChoice c; + instance_choice(*inst, needs, c); + if (c.version.empty()) continue; + c.via = "newest instance"; + c.notes = std::move(notes); + return c; + } + return std::nullopt; +} + +std::vector describe_system_toolsets(const std::vector& instances, + const ToolsetNeeds& needs) { + std::vector out; + for (auto const& inst : instances) { + const auto def = default_toolset_of(inst.root); + for (auto const& v : toolsets_under(inst.root)) { + if (!toolset_complete(inst.root / "VC" / "Tools" / "MSVC" / v, needs)) continue; + out.push_back(std::format("{:<14}{}{}", v, product_of(inst), + v == def ? " (default)" : "")); + } + } + return out; +} + +std::vector parse_vswhere_text(std::string_view text) { + std::vector out; + bool open = false; + while (!text.empty()) { + auto nl = text.find('\n'); + auto line = text.substr(0, nl); + text = nl == std::string_view::npos ? std::string_view{} : text.substr(nl + 1); + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + auto colon = line.find(": "); + if (colon == std::string_view::npos) continue; + auto key = line.substr(0, colon); + auto value = std::string(line.substr(colon + 2)); + if (key == "instanceId") { + out.emplace_back(); + open = true; + } else if (!open) { + continue; + } else if (key == "installationPath") { + out.back().root = std::filesystem::path( + std::u8string(reinterpret_cast(value.c_str()))); + } else if (key == "installationVersion") { + out.back().installVersion = std::move(value); + } else if (key == "displayName") { + out.back().product = std::move(value); + } + } + std::erase_if(out, [](const VsInstance& i) { return i.root.empty(); }); + return out; +} + +std::vector enumerate_vs_instances() { + std::vector out; +#if defined(_WIN32) + auto add = [&](VsInstance inst) { + for (auto const& e : out) + if (same_root(e.root, inst.root)) return; + out.push_back(std::move(inst)); + }; + const std::filesystem::path vswhere = + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"; + bool listed = false; + if (std::filesystem::exists(vswhere)) { + // `-all -prerelease`: every instance, Insiders included, complete or + // not; completeness is judged per toolset below. `-utf8` because an + // installation path may carry characters the console code page cannot. + auto r = mcpp::platform::process::capture( + "\"" + vswhere.string() + "\" -all -prerelease -products * " + "-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 " + "-format text -utf8 2>nul"); + // vswhere ran: an empty list is an answer (no instance has the C++ + // tools), so the conventional paths are not consulted behind it. + if (r.exit_code == 0) { + listed = true; + for (auto& inst : parse_vswhere_text(r.output)) add(std::move(inst)); + } + } + if (auto p = find_vs_via_vsinstalldir()) add(VsInstance{*p, {}, {}}); + if (!listed) { + if (auto p = find_vs_via_comntools()) add(VsInstance{*p, {}, {}}); + if (auto p = find_vs_via_paths()) add(VsInstance{*p, {}, {}}); + } +#endif + return out; +} + +MsvcEnvSnapshot msvc_env_snapshot() { + MsvcEnvSnapshot env; +#if defined(_WIN32) + if (auto* v = std::getenv("VCToolsInstallDir"); v && *v) env.vcToolsInstallDir = v; + if (auto* v = std::getenv("VSINSTALLDIR"); v && *v) { + env.vsInstallDir = v; + } else if (auto* v2 = std::getenv("VCINSTALLDIR"); v2 && *v2) { + env.vsInstallDir = without_trailing_separator(v2).parent_path(); + } + if (auto* path = std::getenv("PATH"); path && *path) { + std::string_view rest = path; + while (!rest.empty()) { + auto semi = rest.find(';'); + auto dir = rest.substr(0, semi); + if (!dir.empty()) { + std::filesystem::path cl = std::filesystem::path(std::string(dir)) / "cl.exe"; + std::error_code ec; + if (std::filesystem::exists(cl, ec)) { env.clOnPath = cl; break; } + } + if (semi == std::string_view::npos) break; + rest.remove_prefix(semi + 1); + } + } +#endif + return env; +} + +std::optional +system_installation_matching(std::string_view version, const ToolsetNeeds& needs, + std::vector* notes) { + auto choice = select_system_toolset(enumerate_vs_instances(), msvc_env_snapshot(), + version, needs); + if (!choice) return std::nullopt; + if (notes) *notes = choice->notes; + return installation_from_tools_dir(choice->vsRoot, choice->toolsDir); +} + std::optional detect_installation() { #if defined(_WIN32) - auto vs = find_vs_install_path(); - if (!vs) return std::nullopt; - auto tools = find_latest_msvc_tools(*vs); - if (!tools) return std::nullopt; - return installation_from_tools_dir(*vs, *tools); + auto choice = select_system_toolset(enumerate_vs_instances(), msvc_env_snapshot(), + "system", ToolsetNeeds{}); + if (!choice) return std::nullopt; + return installation_from_tools_dir(choice->vsRoot, choice->toolsDir); #else return std::nullopt; #endif @@ -1084,14 +1513,16 @@ std::expected enrich_toolchain_from_cl(Toolchain& tc) { .parent_path() // Hostx64 .parent_path() // bin .parent_path(); // + // THE STD MODULE OF THIS TOOLSET, OR NONE. A toolset without + // `modules/std.ixx` used to borrow the one the machine-wide search found, + // which on a machine with two installations compiled one toolset's + // `std.ixx` against another toolset's headers. Absent means no `import std` + // for this toolset, and the caller says so. std::error_code ec; if (auto ixx = toolsDir / "modules" / "std.ixx"; std::filesystem::exists(ixx, ec)) { tc.stdModuleSource = ixx; tc.hasImportStd = true; - } else if (auto found = find_std_module_source()) { - tc.stdModuleSource = *found; - tc.hasImportStd = true; } if (tc.hasImportStd) { // The STL, not the banner. For a real cl installation the two agree by diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 07fa32b0..0beb0aa6 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -95,11 +95,20 @@ struct ToolchainSpec { // is every row but these two, so nothing else's output moves. std::string payloadName; + // `xim:` WAS WRITTEN: the ecosystem package and nothing else. + // + // Every family but msvc has no other origin, so for them the prefix is a + // synonym and the canonical spelling drops it. For msvc it is the one way + // to say "not the toolset this machine may already have": a bare + // `msvc@` takes an installed toolset of that version first. + bool ecosystemOnly = false; + std::string spec_str() const { - return std::format("{}@{}", - payloadName.empty() ? family_name(family) - : std::string_view(payloadName), - version); + auto base = std::format("{}@{}", + payloadName.empty() ? family_name(family) + : std::string_view(payloadName), + version); + return ecosystemOnly && family == Family::Msvc ? "xim:" + base : base; } // "gcc@16.1.0" or "gcc@16.1.0 → x86_64-windows-gnu" — user-facing. @@ -523,6 +532,22 @@ parse_toolchain_spec(std::string compilerArg, if (versionArg.empty()) versionArg = compilerArg.substr(at + 1); compilerArg = compilerArg.substr(0, at); } + // `xim:` IS THE NAMESPACE EVERY PAYLOAD ADDRESS ALREADY CARRIES + // (`to_xim_package` produces `xim:gcc@16.1.0`), so it is the spelling for + // "the ecosystem package". No other namespace names a toolchain, and one + // written by mistake is refused here rather than read as a family name. + bool ecosystemOnly = false; + if (auto colon = compilerArg.find(':'); colon != std::string::npos) { + const auto ns = compilerArg.substr(0, colon); + if (ns != "xim") { + return std::unexpected(std::format( + "'{}:' is not a toolchain namespace; the one namespace a " + "toolchain spelling accepts is `xim:`, which names the " + "ecosystem package (e.g. `xim:msvc@14.44.35207`)", ns)); + } + compilerArg = compilerArg.substr(colon + 1); + ecosystemOnly = true; + } if (compilerArg.empty() && requireCompiler) { return std::unexpected("missing compiler name"); } @@ -539,9 +564,21 @@ parse_toolchain_spec(std::string compilerArg, if (norm->family == "llvm") spec.family = Family::Llvm; else if (norm->family == "msvc") spec.family = Family::Msvc; else spec.family = Family::Gcc; - spec.version = std::move(norm->version); - spec.target = std::move(norm->target); - spec.payloadName = std::move(norm->payload); + spec.version = std::move(norm->version); + spec.target = std::move(norm->target); + spec.payloadName = std::move(norm->payload); + spec.ecosystemOnly = ecosystemOnly; + + // A package and the machine's installation at once is not a spelling of + // anything, and reading it as either would silently drop the other half. + if (ecosystemOnly && spec.version == "system") { + return std::unexpected(std::format( + "'xim:{0}@system' names the ecosystem package and this machine's " + "installation at once.\n" + " {0}@system the machine's installation\n" + " xim:{0}@ an ecosystem package", + family_name(spec.family))); + } // `@system` IS NOT A GENERAL SPELLING, and refusing it here is the point. // diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 3fbf9513..7340f355 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -271,8 +271,16 @@ std::expected derive_std_module( // identical flags also keep the std_build_commands cache key honest). // Std module precompilation only needs compile flags (no linker flags), // so --no-default-config is safe here on all platforms. + // Quoted for the shell that runs this command. POSIX keeps the single + // quotes it always had, byte for byte (the std BMI's cache key includes the + // command). Windows runs it through cmd.exe, which does not read single + // quotes: a path with a space -- the MSVC toolset of the clang row, always + // under `C:\Program Files` -- has to arrive in double quotes. const PathEscape shellEsc = [](const std::filesystem::path& p) { - return std::format("'{}'", p.string()); + if constexpr (mcpp::platform::is_windows) + return mcpp::platform::shell::quote(p.string()); + else + return std::format("'{}'", p.string()); }; // The shared producer (mcpp.toolchain.hostflags) — the same assembly // flags.cppm and the build.mcpp host compile use. This block used to diff --git a/tests/e2e/239_msvc_managed_toolset.sh b/tests/e2e/239_msvc_managed_toolset.sh index a1716021..3beb769e 100755 --- a/tests/e2e/239_msvc_managed_toolset.sh +++ b/tests/e2e/239_msvc_managed_toolset.sh @@ -1,8 +1,14 @@ #!/usr/bin/env bash -# requires: msvc xlings-msvc -# 239_msvc_managed_toolset.sh — `msvc@`: the toolset the manifest +# requires: msvc xlings-msvc python3 +# 239_msvc_managed_toolset.sh — `xim:msvc@`: the package the manifest # names is the one that compiles, regardless of what this machine has. # +# `xim:` IS THE SPELLING FOR "THE PACKAGE". A bare `msvc@` takes an +# installed toolset of that version first (760 covers that half), and this +# runner's Visual Studio may well carry 14.44.35207 -- so the bare spelling +# could legitimately resolve to the machine here and this test would stop +# testing the package. +# # WHY THIS TEST CANNOT BE SATISFIED BY THE MACHINE'S COMPILER, which is the # only reason it is worth running: the runner has its own Visual Studio, and # every assertion below is written so that the system install answering @@ -108,7 +114,7 @@ cd hello_pinned cat >> mcpp.toml <&1) || { echo "FAIL: pinned build: $out"; exit 1; } @@ -135,7 +141,7 @@ out=$("$MCPP" run 2>&1) || { echo "FAIL: pinned run: $out"; exit 1; } # msvc@system: it must resolve to the SYSTEM cl again. Without this, a # "managed works" result is equally consistent with "managed replaced # everything", and the system origin would be quietly gone. -sed -i "s|windows = \"msvc@$TOOLSET\"|windows = \"msvc@system\"|" mcpp.toml +sed -i "s|windows = \"xim:msvc@$TOOLSET\"|windows = \"msvc@system\"|" mcpp.toml out=$("$MCPP" build --verbose 2>&1) || { echo "FAIL: system build: $out"; exit 1; } resolved=$(echo "$out" | grep -iE "Resolved .*msvc" | head -1) [[ -n "$resolved" ]] || { echo "FAIL: no Resolved line (system): $out"; exit 1; } @@ -145,6 +151,35 @@ case "$resolved" in exit 1 ;; esac +# 3b) THE CLANG ROW AGAINST THE SAME PACKAGE. On the MSVC ABI the compiler is +# the toolchain and the toolset is the sysroot, so a clang build names the +# package through `[target.].sysroot`. The record must say the +# package answered, from mcpp's store, at the named version. +cd "$TMP" +"$MCPP" new clang_pinned >/dev/null 2>&1 +cd clang_pinned +cat >> mcpp.toml <&1) || { echo "FAIL: clang build against the package: $out"; exit 1; } +res=$(find target -name resolution.json | head -1) +python3 - "$res" "$TOOLSET" <<'PYEOF' +import json, sys +d = json.load(open(sys.argv[1])) +t = d.get("msvc_toolset") +assert t, "no msvc_toolset recorded: is the default toolchain on this runner clang?" +assert t["origin"] == "managed", t +assert t["version"] == sys.argv[2], t +assert "xim-x-msvc" in t["root"], t +sdk = d.get("windows_sdk", {}) +assert "xim-x-windows-sdk" in sdk.get("root", ""), ("the package's own SDK", sdk) +print("OK: clang row compiled against", t["root"]) +PYEOF +out=$("$MCPP" run 2>&1) || { echo "FAIL: clang run: $out"; exit 1; } +[[ "$out" == *"Hello"* || "$out" == *"hello"* ]] || { echo "FAIL: clang run output: $out"; exit 1; } + # 4) a toolset mcpp installed is removable — the other half of the message # `toolchain remove msvc` prints. cd "$TMP" diff --git a/tests/e2e/241_windows_ucrt_runtime_identity.sh b/tests/e2e/241_windows_ucrt_runtime_identity.sh index ff106ca6..c7e106c5 100755 --- a/tests/e2e/241_windows_ucrt_runtime_identity.sh +++ b/tests/e2e/241_windows_ucrt_runtime_identity.sh @@ -15,12 +15,11 @@ # "the code path ran and produced nothing" — an empty string flows through # every one of those jobs without a complaint. So this asserts the VALUE. # -# WHY IT PINS msvc@system EXPLICITLY. The identity is produced where mcpp -# RESOLVES the SDK itself, which is the native cl.exe path. Windows' default -# toolchain is clang targeting the MSVC ABI, and there clang finds its own SDK -# — mcpp does not know which one, so there is honestly nothing to declare. A -# test that took the default would therefore assert an empty identity and pass -# for the wrong reason. +# WHY IT PINS msvc@system EXPLICITLY. This is the cl.exe row's half. Windows' +# default toolchain is clang targeting the MSVC ABI, and that row now resolves +# its toolset and SDK in mcpp as well (the row's sysroot), so it carries the +# same identity; 760 asserts it there. Pinning keeps this test about the row +# it was written for. set -e TMP=$(mktemp -d) diff --git a/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh new file mode 100755 index 00000000..4e253b8b --- /dev/null +++ b/tests/e2e/746_macos_deployment_target_is_target_keyed_not_host_keyed.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# requires: unix-shell +# 746_macos_deployment_target_is_target_keyed_not_host_keyed.sh — mcpp#685. +# +# `macos_deployment_target` (and MACOSX_DEPLOYMENT_TARGET) used to reach the +# effective triple, the fingerprint and `-mmacosx-version-min` only through +# `platform::macos::deployment_target()`, which read `#if defined(__APPLE__)` +# — the platform mcpp ITSELF was built for. So `mcpp build --target +# aarch64-macos` on a Linux (or Windows) host silently ignored the manifest +# key and the env var: the deployment floor a project stated made no +# difference to the compiler flags, the output directory, or which BMI cache +# entry was reused. This runs the real code path — toolchain resolution, +# triple assembly, ninja-plan generation — end to end, on whatever host runs +# this suite. +# +# DOES NOT `# requires: llvm`. That token is granted by no CI shard in this +# repository (see 641's own header note and +# .agents/*/e2e-requires-llvm-never-runs-on-shards*), so a test that needs an +# LLVM payload probes for it itself and SKIPs cleanly when absent, the same +# pattern the mingw-cross / qemu-arm / android-ndk probes in run_all.sh use. +# +# NO macOS SDK IS NEEDED. `--configure-only` stops after writing +# compile_commands.json / build.ninja, before any header is opened — the +# refusal `toolchain list` reports for `aarch64-macos` on Linux ("needs the +# macOS SDK") is about locating the C library for a REAL compile, not about +# resolving the triple or the flags this test reads. An explicit +# `[target.aarch64-macos] toolchain = "llvm@"` override is the escape +# hatch that reaches this without one (measured). +set -e + +find_llvm() { + local home="${MCPP_HOME:-$HOME/.mcpp}" + for root in "$home/registry/data/xpkgs/xim-x-llvm" "$HOME/.xlings/data/xpkgs/xim-x-llvm"; do + [ -d "$root" ] || continue + # Highest version that actually ships a clang++ — a README-only stub + # (see mcpp#687's 16.1.0 fixture) would otherwise match the glob and + # resolve to a binary that is not there. + for v in $(ls "$root" 2>/dev/null | sort -rV); do + if [ -x "$root/$v/bin/clang++" ]; then + echo "$v" + return 0 + fi + done + done + return 1 +} + +LLVM_VERSION="$(find_llvm || true)" +if [ -z "$LLVM_VERSION" ]; then + echo "SKIP: no xim-x-llvm payload on this host to cross-resolve aarch64-macos with" + exit 0 +fi +echo "using llvm@$LLVM_VERSION" + +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail=0 + +mkdir -p "$t/src" +printf 'int main(){return 0;}\n' > "$t/src/main.cpp" + +manifest() { # deployment_target + cat > "$t/mcpp.toml" <&1) || { echo "FAIL: configure failed:"; echo "$out"; exit 1; } +ok1=1 +echo "$out" | grep -qF -- 'aarch64-macos → arm64-apple-macos11.0' || { + echo "FAIL: resolved triple did not carry the manifest's 11.0:" + echo "$out"; fail=1; ok1=0 +} +cc="$t/compile_commands.json" +[ -f "$cc" ] || { echo "FAIL: --configure-only produced no compile_commands.json"; fail=1; ok1=0; } +grep -qF -- '--target=arm64-apple-macos11.0' "$cc" || { + echo "FAIL: --target=arm64-apple-macos11.0 absent from compile_commands.json"; fail=1; ok1=0; } +grep -qF -- '-mmacosx-version-min=11.0' "$cc" || { + echo "FAIL: -mmacosx-version-min=11.0 absent from compile_commands.json"; fail=1; ok1=0; } +dir11=$(ls -d "$t"/target/aarch64-macos/*/ 2>/dev/null | head -1) +[ -n "$dir11" ] || { echo "FAIL: no target/aarch64-macos// directory written"; fail=1; ok1=0; } +[ "$ok1" = 1 ] && echo " ok manifest 11.0 -> arm64-apple-macos11.0, flags, and an output dir" + +# ── 2. A different manifest value moves the flags AND the fingerprint ─────── +manifest "12.0" +out=$(configure 2>&1) || { echo "FAIL: configure failed:"; echo "$out"; exit 1; } +echo "$out" | grep -qF -- 'aarch64-macos → arm64-apple-macos12.0' || { + echo "FAIL: resolved triple did not move to 12.0:"; echo "$out"; fail=1; } +grep -qF -- '--target=arm64-apple-macos12.0' "$cc" || { + echo "FAIL: --target=arm64-apple-macos12.0 absent after the manifest changed"; fail=1; } +dir12=$(ls -d "$t"/target/aarch64-macos/*/ 2>/dev/null | head -1) +if [ "$dir11" = "$dir12" ]; then + echo "FAIL: output directory unchanged (was $dir11) after macos_deployment_target moved 11.0 -> 12.0" + echo " the fingerprint did not fold the new value — a std.pcm built for 11.0 would be replayed for 12.0" + fail=1 +else + echo " ok manifest 12.0 -> arm64-apple-macos12.0, and a NEW output directory ($dir12 != $dir11)" +fi + +# ── 3. MACOSX_DEPLOYMENT_TARGET overrides the manifest, same as cargo/cc ──── +# manifest() above still writes "12.0"; the env value must win. +out=$(cd "$t" && rm -rf target compile_commands.json \ + && MCPP_NO_AUTO_INSTALL=1 MACOSX_DEPLOYMENT_TARGET=13.0 \ + "$MCPP" build --target aarch64-macos --configure-only 2>&1) \ + || { echo "FAIL: configure failed:"; echo "$out"; exit 1; } +ok3=1 +echo "$out" | grep -qF -- 'aarch64-macos → arm64-apple-macos13.0' || { + echo "FAIL: MACOSX_DEPLOYMENT_TARGET=13.0 did not beat manifest's 12.0:" + echo "$out"; fail=1; ok3=0; } +grep -qF -- '--target=arm64-apple-macos13.0' "$cc" || { + echo "FAIL: --target=arm64-apple-macos13.0 absent with the env override set"; fail=1; ok3=0; } +[ "$ok3" = 1 ] && echo " ok env MACOSX_DEPLOYMENT_TARGET=13.0 beats manifest's 12.0" + +[ "$fail" = 0 ] || exit 1 +echo "OK: macos_deployment_target resolves by TARGET, on a host that is not macOS" diff --git a/tests/e2e/760_msvc_toolset_is_chosen_once.sh b/tests/e2e/760_msvc_toolset_is_chosen_once.sh new file mode 100755 index 00000000..f48c111f --- /dev/null +++ b/tests/e2e/760_msvc_toolset_is_chosen_once.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# requires: msvc python3 +# 760_msvc_toolset_is_chosen_once.sh — on the MSVC ABI one toolset is chosen, +# once, and every consumer reads it. +# +# The clang row used to reach its toolset twice: the driver searched the +# machine for headers and libraries (VCToolsInstallDir, PATH, the newest +# instance's default) while mcpp searched it again for `std.ixx` by a different +# order, and the result reached neither the cache key nor any record. The +# toolset is the row's sysroot now (`[target.].sysroot`, default +# `msvc@system`), resolved by prepare and handed to the driver as +# `-Xmicrosoft-*` words. +# +# Everything here uses the runner's own Visual Studio and downloads nothing; +# the package half of the same rule (`xim:msvc@`) is exercised by +# 239 in ci-windows-msvc-xlings.yml. +# +# The EXPECTED toolset is read independently of the code under test: vswhere +# names the newest instance, and that instance's +# `Microsoft.VCToolsVersion.default.txt` names its default toolset. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" +[[ -x "$VSWHERE" ]] || { echo "SKIP: no vswhere on this runner"; exit 0; } +VSROOT="$("$VSWHERE" -latest -prerelease -products '*' \ + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 \ + -property installationPath | tr -d '\r')" +[[ -n "$VSROOT" ]] || { echo "FAIL: vswhere reports no instance with C++ tools"; exit 1; } +VSROOT_U="$(cygpath -u "$VSROOT")" +EXPECTED="$(tr -d '\r\n ' < "$VSROOT_U/VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt")" +[[ -n "$EXPECTED" ]] || { echo "FAIL: no default toolset in $VSROOT"; exit 1; } +echo "runner: $VSROOT, default toolset $EXPECTED" + +# No developer prompt: the default selection must not depend on one. +unset VCToolsInstallDir VSINSTALLDIR VCINSTALLDIR + +resolution() { find target -name resolution.json | head -1; } + +# ── 1. The clang row, no sysroot declared: the machine's default ────────── +"$MCPP" new chosen > /dev/null +cd chosen +"$MCPP" build > build.log 2>&1 || { cat build.log; exit 1; } +grep -q "sysroot msvc@system → MSVC $EXPECTED" build.log \ + || { echo "FAIL: no line naming the chosen toolset:"; cat build.log; exit 1; } + +python3 - "$(resolution)" "$EXPECTED" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +t = d.get("msvc_toolset") +assert t, "resolution.json records no msvc_toolset for the clang row" +assert t["version"] == sys.argv[2], f"toolset {t['version']} != {sys.argv[2]}" +assert t["origin"] == "system", t +assert t["root"].endswith("/" + sys.argv[2]), t +sdk = d.get("windows_sdk", {}) +assert sdk.get("version"), "no Windows SDK recorded" +rid = d.get("runtime", {}).get("binding", {}).get("runtime_id", "") +# The SDK identity now reaches the clang row's contract too. +assert rid == "ucrt@" + sdk["version"], (rid, sdk) +print("OK:", t["version"], t["origin"], sdk["version"]) +PY + +# The driver is TOLD the toolset, with the same directory the record names. +NINJA="$(find target -name build.ninja | head -1)" +grep -q -- "-Xmicrosoft-visualc-tools-root" "$NINJA" \ + || { echo "FAIL: build.ninja does not pass the toolset to clang"; exit 1; } +grep -q -- "-Xmicrosoft-windows-sdk-version" "$NINJA" \ + || { echo "FAIL: build.ninja does not pass the SDK to clang"; exit 1; } +grep -q "MSVC\\\\$EXPECTED\|MSVC/$EXPECTED" "$NINJA" \ + || { echo "FAIL: build.ninja names a toolset other than $EXPECTED"; exit 1; } + +out=$("$MCPP" run 2>&1) || { echo "FAIL: run: $out"; exit 1; } +[[ "$out" == *"Hello"* || "$out" == *"hello"* ]] || { echo "FAIL: run output: $out"; exit 1; } + +# ── 2. Pinned to an installed toolset; the environment is not consulted ─── +# +# A developer prompt opened with `-vcvars_ver=` exports VCToolsInstallDir. +# The pin must win, and the ignored declaration must be reported. +mkdir -p "$TMP/fake/VC/Tools/MSVC/14.99.0/include" +cat >> mcpp.toml < pinned.log 2>&1 || { cat pinned.log; exit 1; } +grep -q "VCToolsInstallDir (14.99.0) is ignored" pinned.log \ + || { echo "FAIL: the ignored VCToolsInstallDir was not reported:"; cat pinned.log; exit 1; } +grep -q "sysroot msvc@$EXPECTED → MSVC $EXPECTED (system" pinned.log \ + || { echo "FAIL: pinned toolset not taken from the machine:"; cat pinned.log; exit 1; } + +# ── 3. What is not an MSVC toolset is refused where the manifest is read ── +sed -i "s|sysroot = \"msvc@$EXPECTED\"|sysroot = \"xim:glibc@2.39\"|" mcpp.toml +if "$MCPP" build > refused.log 2>&1; then + echo "FAIL: a C library package was accepted as an MSVC sysroot"; exit 1 +fi +grep -q "MSVC toolset" refused.log || { cat refused.log; exit 1; } + +# ── 4. The cl.exe row: a pinned version this machine has is used in place ─ +cd "$TMP" +"$MCPP" new clrow > /dev/null +cd clrow +cat >> mcpp.toml < cl.log 2>&1 || { cat cl.log; exit 1; } +line="$(grep -E "Resolved msvc@$EXPECTED" cl.log | head -1)" +[[ "$line" == *"(installed:"* ]] || { echo "FAIL: cl.exe row did not use the installed toolset: $line"; cat cl.log; exit 1; } +[[ "$line" != *"xim-x-msvc"* ]] || { echo "FAIL: resolved to a package: $line"; exit 1; } + +# ── 5. On the cl.exe row the compiler is its own sysroot ────────────────── +cat >> mcpp.toml <<'EOF' + +[target.x86_64-windows-msvc] +sysroot = "msvc@14.1.0" +EOF +if "$MCPP" build > clmismatch.log 2>&1; then + echo "FAIL: a sysroot naming another toolset was accepted on the cl.exe row"; exit 1 +fi +grep -q "names a different toolset than the compiler" clmismatch.log \ + || { cat clmismatch.log; exit 1; } + +echo "PASS: one toolset per build on the MSVC ABI, pinned or not, recorded and passed to the driver" diff --git a/tests/unit/test_build_profile.cpp b/tests/unit/test_build_profile.cpp index 18c306e2..229c3f83 100644 --- a/tests/unit/test_build_profile.cpp +++ b/tests/unit/test_build_profile.cpp @@ -12,6 +12,7 @@ // 0.00s and left the release artifacts in place. #include +#include import std; import mcpp.build.prepare; @@ -28,6 +29,30 @@ mcpp::manifest::Manifest base() { return m; } +// Save/restore MACOSX_DEPLOYMENT_TARGET, so a value this test sets (or one +// already in the environment) cannot leak into a sibling test in this binary. +class ScopedDeploymentTargetEnv { +public: + explicit ScopedDeploymentTargetEnv(const char* value) { + if (const char* old = std::getenv(kName); old) { had_ = true; old_ = old; } + apply(value); + } + ~ScopedDeploymentTargetEnv() { apply(had_ ? old_.c_str() : nullptr); } + ScopedDeploymentTargetEnv(const ScopedDeploymentTargetEnv&) = delete; + ScopedDeploymentTargetEnv& operator=(const ScopedDeploymentTargetEnv&) = delete; +private: + static constexpr const char* kName = "MACOSX_DEPLOYMENT_TARGET"; + void apply(const char* v) { +#if defined(_WIN32) + ::_putenv_s(kName, v ? v : ""); +#else + if (v) ::setenv(kName, v, 1); else ::unsetenv(kName); +#endif + } + bool had_ = false; + std::string old_; +}; + } // namespace // ── resolve_profile_name: the ONE rule, shared with the fast paths ─────────── @@ -111,6 +136,52 @@ TEST(BuildProfile, CanonicalFlagsStillCoverTheNonProfileKnobs) { EXPECT_NE(mcpp::build::canonical_compile_flags(m), ref); } } +// ── macos_deployment_target: TARGET-keyed, not HOST-keyed (mcpp#685) ──────── +// +// `canonical_compile_flags` used to fold this value in only +// `if constexpr (mcpp::platform::is_macos)` -- the platform mcpp ITSELF was +// built for -- so `mcpp build --target aarch64-macos` on a Linux host kept +// the exact same fingerprint (hence the exact same `target///` +// directory) no matter what `[build] macos_deployment_target` was edited to, +// and a std.pcm built for one deployment target could be replayed for +// another. `targetIsMacos` is now an explicit parameter, answered by the +// caller from the TARGET triple, so these assertions hold on whatever host +// runs this suite -- reverting to the old `if constexpr` would fail +// `TargetIsMacosFoldsDeploymentTargetIntoTheFingerprint` on any non-Apple CI +// runner. + +TEST(BuildProfile, NonMacosTargetIgnoresDeploymentTargetInTheFingerprint) { + ScopedDeploymentTargetEnv noEnv(nullptr); + auto ref = base(); + auto changed = base(); changed.buildConfig.macosDeploymentTarget = "11.0"; + // `targetIsMacos` defaults to false: a caller with no target in hand (or + // one building for something other than macOS) must not see the + // fingerprint move just because the manifest names a macOS floor it will + // never apply. + EXPECT_EQ(mcpp::build::canonical_compile_flags(ref), + mcpp::build::canonical_compile_flags(changed)); + EXPECT_EQ(mcpp::build::canonical_compile_flags(ref, /*targetIsMacos=*/false), + mcpp::build::canonical_compile_flags(changed, /*targetIsMacos=*/false)); +} + +TEST(BuildProfile, TargetIsMacosFoldsDeploymentTargetIntoTheFingerprint) { + ScopedDeploymentTargetEnv noEnv(nullptr); + auto ref = base(); + auto changed = base(); changed.buildConfig.macosDeploymentTarget = "11.0"; + EXPECT_NE(mcpp::build::canonical_compile_flags(ref, /*targetIsMacos=*/true), + mcpp::build::canonical_compile_flags(changed, /*targetIsMacos=*/true)); + + // AND THE ENV OVERRIDE, folded the same way `deployment_target` resolves + // it: with MACOSX_DEPLOYMENT_TARGET set, the manifest value stops + // mattering to the fingerprint too, because it stops mattering to what + // actually gets compiled. + ScopedDeploymentTargetEnv env("12.3"); + auto manifestA = base(); manifestA.buildConfig.macosDeploymentTarget = "11.0"; + auto manifestB = base(); manifestB.buildConfig.macosDeploymentTarget = "13.0"; + EXPECT_EQ(mcpp::build::canonical_compile_flags(manifestA, /*targetIsMacos=*/true), + mcpp::build::canonical_compile_flags(manifestB, /*targetIsMacos=*/true)); +} + // ── cache-mode parsing ────────────────────────────────────────────────────── TEST(BuildProfile, CacheModeParsesTheThreeModes) { diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index d6529059..01ba883b 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -645,3 +645,37 @@ TEST(CacheKey, ACxxLayerProvidersStatedStandardChangesTheKey) { EXPECT_NE(key_for(layer, "c++23"), none); EXPECT_NE(key_for(layer, "c++23"), key_for(layer, "c++26")); } + +// THE MSVC TOOLSET OF THE CLANG ROW IS IN THE KEY. Before it reached the +// command line the driver found the STL by itself and `stdlibVersion` held +// clang's version, so a Visual Studio update that changed the STL left the key +// unchanged. The toolset directory carries its version, so two toolsets are +// two keys. +TEST(CacheKey, TheMsvcToolsetOfTheClangRowChangesTheKey) { + mcpp::toolchain::Toolchain tc; + tc.compiler = mcpp::toolchain::CompilerId::Clang; + tc.version = "20.1.7"; + tc.driverIdent = "clang-20.1.7"; + tc.targetTriple = "x86_64-pc-windows-msvc"; + tc.msvcToolsDir = "C:/VS/VC/Tools/MSVC/14.44.35207"; + tc.windowsSdkRoot = "C:/Kits/10"; + tc.windowsSdkVersion = "10.0.26100.0"; + + mcpp::manifest::Manifest m; + m.package.standard = "c++23"; + const auto a = ck::build_axes(tc, m, "-std=c++23", {}, ""); + bool present = false; + for (auto const& t : a.targetHeaderSet) + present = present || t.find("14.44.35207") != std::string::npos; + EXPECT_TRUE(present) << "the toolset directory is part of the header set"; + + auto tc2 = tc; + tc2.msvcToolsDir = "C:/VS/VC/Tools/MSVC/14.51.36014"; + EXPECT_NE(ck::key_hex(a, pkg()), + ck::key_hex(ck::build_axes(tc2, m, "-std=c++23", {}, ""), pkg())); + + auto tc3 = tc; + tc3.windowsSdkVersion = "10.0.22621.0"; + EXPECT_NE(ck::key_hex(a, pkg()), + ck::key_hex(ck::build_axes(tc3, m, "-std=c++23", {}, ""), pkg())); +} diff --git a/tests/unit/test_doctor_fixincludes.cpp b/tests/unit/test_doctor_fixincludes.cpp new file mode 100644 index 00000000..627c83eb --- /dev/null +++ b/tests/unit/test_doctor_fixincludes.cpp @@ -0,0 +1,135 @@ +#include + +import std; +import mcpp.doctor; + +using mcpp::doctor::find_fixincludes_banners; + +// mcpp#687: GCC's `fixincludes` step writes a frozen copy of the BUILD +// MACHINE's own libc header into `include-fixed`, and mcpp's `-idirafter` +// search order puts it ahead of the glibc a project actually resolves. This +// covers the pure scan `find_fixincludes_banners` — the read side of the +// `mcpp self doctor` check — against a synthetic payload tree, so it needs +// neither a real xim-x-gcc install nor a real doctor run to verify. + +namespace { + +namespace fs = std::filesystem; + +fs::path make_tempdir(std::string_view name) { + auto base = fs::temp_directory_path() + / std::format("{}-{}", name, + std::chrono::steady_clock::now() + .time_since_epoch().count()); + fs::create_directories(base); + return base; +} + +void write_file(const fs::path& p, std::string_view content) { + fs::create_directories(p.parent_path()); + std::ofstream os(p, std::ios::binary); + os << content; +} + +// The real banner's shape, measured from +// xim-x-gcc/{13.3.0,15.1.0}/lib/gcc/x86_64-linux-gnu//include-fixed/pthread.h. +constexpr std::string_view kBannerFixture = + "/* DO NOT EDIT THIS FILE.\n" + "\n" + " It has been auto-edited by fixincludes from:\n" + "\n" + "\t\"/home/xlings/.xlings_data/subos/linux/usr/include/pthread.h\"\n" + "\n" + " This had to be done to correct non-standard usages in the\n" + " original, manufacturer supplied header file. */\n" + "\n" + "#ifndef _PTHREAD_H\n" + "#define _PTHREAD_H 1\n"; + +// A genuine fixincludes README, unedited — no banner, present in every +// version (including 16.1.0, which ships no frozen header at all). +constexpr std::string_view kReadmeFixture = + "This directory contains header files fixed by fixincludes.\n" + "The original files are not modified.\n"; + +} // namespace + +TEST(DoctorFixincludes, ABannerFileIsReportedWithItsSource) { + auto root = make_tempdir("mcpp-fixincludes-banner"); + write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" + / "include-fixed" / "pthread.h", kBannerFixture); + + auto findings = find_fixincludes_banners(root); + ASSERT_EQ(findings.size(), 1u); + EXPECT_EQ(findings[0].file, fs::path("lib") / "gcc" / "x86_64-linux-gnu" + / "13.3.0" / "include-fixed" / "pthread.h"); + EXPECT_EQ(findings[0].source, + "/home/xlings/.xlings_data/subos/linux/usr/include/pthread.h"); + + fs::remove_all(root); +} + +// The exact 16.1.0 shape: an `include-fixed` directory that exists (fixincludes +// always creates it) but holds only its own README, no fixed header at all. +TEST(DoctorFixincludes, AReadmeWithNoBannerIsNotReported) { + auto root = make_tempdir("mcpp-fixincludes-readme-only"); + write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "16.1.0" + / "include-fixed" / "README", kReadmeFixture); + + EXPECT_TRUE(find_fixincludes_banners(root).empty()); + + fs::remove_all(root); +} + +// A fixed header that carries no banner (some fixincludes edits do not add +// one — e.g. a bare wrapped `limits.h`) is not a match either: the check is +// specifically for the auto-edited-FROM-a-source-path shape mcpp#687 is +// about, not for "any file under include-fixed". +TEST(DoctorFixincludes, AFixedHeaderWithoutTheBannerIsNotReported) { + auto root = make_tempdir("mcpp-fixincludes-no-banner"); + write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" + / "include-fixed" / "limits.h", + "/* -*- Mode: C -*- */\n#ifndef _GCC_LIMITS_H_\n"); + + EXPECT_TRUE(find_fixincludes_banners(root).empty()); + + fs::remove_all(root); +} + +// A file that carries the banner text but sits OUTSIDE any `include-fixed` +// directory does not count — the check is scoped to the one directory +// fixincludes actually writes into, not to the whole payload tree. +TEST(DoctorFixincludes, ABannerOutsideIncludeFixedIsNotReported) { + auto root = make_tempdir("mcpp-fixincludes-outside"); + write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" + / "include" / "pthread.h", kBannerFixture); + + EXPECT_TRUE(find_fixincludes_banners(root).empty()); + + fs::remove_all(root); +} + +// Every `include-fixed` directory under the payload root is scanned, not +// only the first one found — a payload that ships more than one fixed +// header (the common case: pthread.h plus others) is reported in full, and +// two DIFFERENT triples under the same payload root (a cross payload's own +// shape) are both covered by one call. +TEST(DoctorFixincludes, MultipleFindingsAreAllReported) { + auto root = make_tempdir("mcpp-fixincludes-multi"); + write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" + / "include-fixed" / "pthread.h", kBannerFixture); + write_file(root / "lib" / "gcc" / "x86_64-linux-gnu" / "13.3.0" + / "include-fixed" / "stdio.h", kBannerFixture); + + auto findings = find_fixincludes_banners(root); + EXPECT_EQ(findings.size(), 2u); + + fs::remove_all(root); +} + +// A payload root that does not exist at all (nothing installed under that +// name/version) is not an error — it is the same as "nothing to report". +TEST(DoctorFixincludes, ANonexistentPayloadRootReportsNothing) { + EXPECT_TRUE(find_fixincludes_banners( + fs::temp_directory_path() / "mcpp-fixincludes-does-not-exist").empty()); +} diff --git a/tests/unit/test_hostflags.cpp b/tests/unit/test_hostflags.cpp index 78577cd7..aa079340 100644 --- a/tests/unit/test_hostflags.cpp +++ b/tests/unit/test_hostflags.cpp @@ -336,16 +336,35 @@ TEST(HostFlags, OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs) { EXPECT_FALSE(names_dir(off, (payload.root / "lib").string())); } -TEST(HostFlags, DeploymentTargetOnlyOnMacos) { - auto tc = tc_for(CompilerId::GCC); +// TARGET-KEYED, NOT HOST-KEYED (mcpp#685). This used to read +// `EXPECT_EQ(found, mcpp::platform::is_macos)` — the flag's presence +// followed the machine RUNNING this test rather than the `Toolchain`'s own +// `targetTriple`, so it could not distinguish "building for macOS" from +// "building on macOS" and stayed green while `--target aarch64-macos` from a +// non-Apple host silently dropped the flag (and the iOS mirror: a macOS host +// building `x86_64-linux-musl` wrongly kept it). Both targets are asked here +// so the assertion holds independent of whichever host runs this suite. +TEST(HostFlags, DeploymentTargetOnlyOnAMacosTarget) { HostFlagOptions opt; opt.macosDeploymentTarget = "14.0"; - auto tokens = mcpp::toolchain::host_compile_tokens( - tc, opt, mcpp::toolchain::no_escape); - bool found = std::ranges::any_of(tokens, [](auto const& t) { + + auto macTc = tc_for(CompilerId::GCC); + macTc.targetTriple = "aarch64-macos"; + auto macTokens = mcpp::toolchain::host_compile_tokens( + macTc, opt, mcpp::toolchain::no_escape); + EXPECT_TRUE(std::ranges::any_of(macTokens, [](auto const& t) { + return t == "-mmacosx-version-min=14.0"; + })) << "a macOS target must carry the resolved deployment target " + "regardless of the host running the build"; + + auto linuxTc = tc_for(CompilerId::GCC); + linuxTc.targetTriple = "x86_64-linux-musl"; + auto linuxTokens = mcpp::toolchain::host_compile_tokens( + linuxTc, opt, mcpp::toolchain::no_escape); + EXPECT_FALSE(std::ranges::any_of(linuxTokens, [](auto const& t) { return t.starts_with("-mmacosx-version-min="); - }); - EXPECT_EQ(found, mcpp::platform::is_macos); + })) << "a non-macOS target must never see a macOS deployment flag, even " + "when built on a macOS host (the mirror defect #685 also fixes)"; } // ── graph_runtime_compile_flags: what a `throw` and a `thread_local` compile @@ -878,3 +897,38 @@ TEST(HostFlags, GccEmitsNeitherIsolationTokenRegardlessOfGraphOrigin) { EXPECT_FALSE(contains(toks, "-nostdlibinc")); EXPECT_FALSE(contains(toks, "-nostdinc++")); } + +// ─── Clang on the MSVC ABI: one toolset for the compile, std module and link +// +// `host_compile_tokens` is read by the compile line, the std module precompile +// and the build.mcpp host compile, so the toolset words appearing here is what +// makes all three compile against the toolset prepare chose. +TEST(HostFlags, ClangOnTheMsvcAbiCompilesAndLinksAgainstTheChosenToolset) { + mcpp::toolchain::Toolchain t; + t.compiler = CompilerId::Clang; + t.targetTriple = "x86_64-pc-windows-msvc"; + t.msvcToolsDir = "C:/VS/VC/Tools/MSVC/14.44.35207"; + t.windowsSdkRoot = "C:/Kits/10"; + t.windowsSdkVersion = "10.0.26100.0"; + HostFlagOptions opt; + auto find_pair = [](const std::vector& v, std::string_view flag, + std::string_view value) { + for (std::size_t i = 0; i + 1 < v.size(); ++i) + if (v[i] == flag && v[i + 1] == value) return true; + return false; + }; + auto compile = mcpp::toolchain::host_compile_tokens(t, opt, mcpp::toolchain::no_escape); + auto link = mcpp::toolchain::host_link_tokens(t, opt, mcpp::toolchain::no_escape); + for (auto const* v : {&compile, &link}) { + EXPECT_TRUE(find_pair(*v, "-Xmicrosoft-visualc-tools-root", + "C:/VS/VC/Tools/MSVC/14.44.35207")); + EXPECT_TRUE(find_pair(*v, "-Xmicrosoft-windows-sdk-root", "C:/Kits/10")); + EXPECT_TRUE(find_pair(*v, "-Xmicrosoft-windows-sdk-version", "10.0.26100.0")); + } + + // Without a recorded toolset nothing is said, which is the behaviour of + // every build before a toolset is bound. + t.msvcToolsDir.clear(); + for (auto const& w : mcpp::toolchain::host_compile_tokens(t, opt, mcpp::toolchain::no_escape)) + EXPECT_EQ(w.find("-Xmicrosoft"), std::string::npos) << w; +} diff --git a/tests/unit/test_linkmodel.cpp b/tests/unit/test_linkmodel.cpp index 76354229..02fcf75e 100644 --- a/tests/unit/test_linkmodel.cpp +++ b/tests/unit/test_linkmodel.cpp @@ -265,3 +265,57 @@ TEST(LinkModel, AnSdkTargetDescribesNoCLibrary) { return p.string(); }).empty()) << target; } } + +// ─── Clang on the MSVC ABI: the toolset and SDK, said to the driver ─────── + +namespace { +tc::Toolchain clang_msvc() { + tc::Toolchain t; + t.compiler = tc::CompilerId::Clang; + t.targetTriple = "x86_64-pc-windows-msvc"; + t.msvcToolsDir = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207"; + t.windowsSdkRoot = "C:/Program Files (x86)/Windows Kits/10"; + t.windowsSdkVersion = "10.0.26100.0"; + return t; +} +} // namespace + +TEST(LinkModel, ClangOnTheMsvcAbiNamesItsToolsetAndSdk) { + auto t = clang_msvc(); + auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); + const std::vector expected = { + "-Xmicrosoft-visualc-tools-root", t.msvcToolsDir.string(), + "-Xmicrosoft-windows-sdk-root", t.windowsSdkRoot.string(), + "-Xmicrosoft-windows-sdk-version", "10.0.26100.0", + }; + EXPECT_EQ(words, expected) << "separate words: the driver rejects the `=` form"; + // Not a C-library mode: the other token sets stay empty. + auto lm = tc::resolve_link_model(t); + EXPECT_EQ(lm.mode, tc::CLibMode::None); + EXPECT_TRUE(lm.compile_tokens(ident).empty()); +} + +TEST(LinkModel, TheToolsetWithoutAnSdkOmitsTheSdkWords) { + auto t = clang_msvc(); + t.windowsSdkRoot.clear(); + t.windowsSdkVersion.clear(); + auto words = tc::resolve_link_model(t).msvc_driver_tokens(ident); + ASSERT_EQ(words.size(), 2u); + EXPECT_EQ(words[0], "-Xmicrosoft-visualc-tools-root"); +} + +TEST(LinkModel, NoOtherRowReceivesMsvcDriverWords) { + // cl.exe carries the toolset in its own path and envOverrides; MinGW is + // not MSVC at all; a row with no toolset recorded says nothing. + auto cl = clang_msvc(); + cl.compiler = tc::CompilerId::MSVC; + EXPECT_TRUE(tc::resolve_link_model(cl).msvc_driver_tokens(ident).empty()); + + auto mingw = clang_msvc(); + mingw.targetTriple = "x86_64-w64-mingw32"; + EXPECT_TRUE(tc::resolve_link_model(mingw).msvc_driver_tokens(ident).empty()); + + auto unbound = clang_msvc(); + unbound.msvcToolsDir.clear(); + EXPECT_TRUE(tc::resolve_link_model(unbound).msvc_driver_tokens(ident).empty()); +} diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 9a9eaaa8..c8c2fe90 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -4273,6 +4273,58 @@ sysroot = "" EXPECT_EQ(w.find("sysroot"), std::string::npos) << w; } +// ── On an MSVC-ABI row the sysroot is an MSVC toolset ───────────────────────── +// +// The compiler is the toolchain (cl.exe or clang) and the toolset -- its STL, +// its CRT and the SDK that follows it -- is what the compiler builds against. +// The row takes the msvc family's own spellings and nothing else. + +namespace { +std::expected +msvc_row(std::string_view triple, std::string_view sysroot) { + return mcpp::manifest::parse_string(std::format(R"( +[package] +name = "x" +version = "0.1.0" +[target.{}] +sysroot = "{}" +)", triple, sysroot)); +} +} // namespace + +TEST(Manifest, MsvcRowSysrootTakesTheMsvcSpellings) { + for (auto triple : {"x86_64-windows-msvc", "x86_64-pc-windows-msvc"}) { + for (auto value : {"msvc@system", "msvc@14.44.35207", "msvc@14.44", + "xim:msvc@14.44.35207"}) { + auto m = msvc_row(triple, value); + ASSERT_TRUE(m.has_value()) << triple << " " << value << ": " + << m.error().format(); + auto it = m->targetOverrides.find(triple); + ASSERT_NE(it, m->targetOverrides.end()); + ASSERT_TRUE(it->second.sysrootDeclared); + EXPECT_EQ(it->second.sysroot, value); + } + } +} + +TEST(Manifest, MsvcRowSysrootRefusesWhatIsNotAnMsvcToolset) { + // A C library package, the zero-libc tier, a package asked for the + // machine's toolset, and a bare family name are all refused by name. + for (auto value : {"xim:glibc@2.39", "", "xim:msvc@system", "msvc", "gcc@16.1.0"}) { + auto m = msvc_row("x86_64-windows-msvc", value); + ASSERT_FALSE(m.has_value()) << "'" << value << "' was accepted"; + EXPECT_NE(m.error().format().find("MSVC toolset"), std::string::npos) + << m.error().format(); + } +} + +TEST(Manifest, TheMsvcSpellingIsNotASysrootElsewhere) { + // On every other row the key keeps its xpkg-reference grammar. + auto m = msvc_row("x86_64-windows-gnu", "msvc@14.44.35207"); + ASSERT_FALSE(m.has_value()); + EXPECT_NE(m.error().format().find("xpkg reference"), std::string::npos); +} + // ── Dependency version requirements are checked where they are written ─────── // // The parser that decides which published version satisfies a requirement diff --git a/tests/unit/test_platform_apple_sdk.cpp b/tests/unit/test_platform_apple_sdk.cpp index f9f97b9c..00d892f7 100644 --- a/tests/unit/test_platform_apple_sdk.cpp +++ b/tests/unit/test_platform_apple_sdk.cpp @@ -1,4 +1,5 @@ #include +#include import std; import mcpp.platform; @@ -6,6 +7,36 @@ import mcpp.platform.macos; namespace mac = mcpp::platform::macos; +namespace { + +// Save/restore MACOSX_DEPLOYMENT_TARGET around a test so its own opinion +// cannot leak into a sibling test in this binary (gtest runs them in one +// process) and so a value set in the CI environment cannot leak into this +// one. +class ScopedDeploymentTargetEnv { +public: + explicit ScopedDeploymentTargetEnv(const char* value) { + if (const char* old = std::getenv(kName); old) { had_ = true; old_ = old; } + apply(value); + } + ~ScopedDeploymentTargetEnv() { apply(had_ ? old_.c_str() : nullptr); } + ScopedDeploymentTargetEnv(const ScopedDeploymentTargetEnv&) = delete; + ScopedDeploymentTargetEnv& operator=(const ScopedDeploymentTargetEnv&) = delete; +private: + static constexpr const char* kName = "MACOSX_DEPLOYMENT_TARGET"; + void apply(const char* v) { +#if defined(_WIN32) + ::_putenv_s(kName, v ? v : ""); +#else + if (v) ::setenv(kName, v, 1); else ::unsetenv(kName); +#endif + } + bool had_ = false; + std::string old_; +}; + +} // namespace + // ─── The three Apple SDKs are a table, not three code paths (C1) ─────────── // // `sdk_path()` located exactly one SDK, because macOS was the only Apple @@ -115,3 +146,56 @@ TEST(AppleSdk, TheDefaultRequestIsTheMacosSdk) { EXPECT_FALSE(mac::sdk_path(mac::sdk_iphoneos).has_value()); } } + +// ─── deployment_target: a TARGET decision, not a HOST one (mcpp#685) ──────── +// +// `deployment_target` used to read `#if defined(__APPLE__)` and answer empty +// on every non-Apple host, no matter what `targetIsMacos` would have said — +// so `mcpp build --target aarch64-macos` from Linux ignored +// `macos_deployment_target` entirely. These run on whatever host is building +// mcpp itself (this repository's CI includes non-Apple runners), which is +// exactly the property under test: nothing here branches on +// `mcpp::platform::is_macos`, and a reintroduced `#if defined(__APPLE__)` +// guard would make `TargetIsMacosEnvBeatsManifestBeatsDefault` fail on any +// non-Apple host. + +TEST(AppleSdk, DeploymentTargetIsEmptyForANonMacosTarget) { + // No env, no manifest value, and no env with a manifest value: a + // non-macOS target gets no floor regardless of what either input says, + // because applying one would be an answer to a question this target + // never asked (see min_platform_version's Apple-only branch). + ScopedDeploymentTargetEnv noEnv(nullptr); + EXPECT_EQ(mac::deployment_target(false, ""), ""); + EXPECT_EQ(mac::deployment_target(false, "11.0"), ""); + ScopedDeploymentTargetEnv env("12.3"); + EXPECT_EQ(mac::deployment_target(false, "11.0"), ""); +} + +TEST(AppleSdk, TargetIsMacosEnvBeatsManifestBeatsDefault) { + { + // Neither input set: the built-in floor. + ScopedDeploymentTargetEnv noEnv(nullptr); + EXPECT_EQ(mac::deployment_target(true, ""), mac::default_deployment_target); + } + { + // Manifest only: the project's stated default. + ScopedDeploymentTargetEnv noEnv(nullptr); + EXPECT_EQ(mac::deployment_target(true, "11.0"), "11.0"); + } + { + // Env only: the per-invocation override, same convention cargo/cc use. + ScopedDeploymentTargetEnv env("12.3"); + EXPECT_EQ(mac::deployment_target(true, ""), "12.3"); + } + { + // Both set: env wins. + ScopedDeploymentTargetEnv env("12.3"); + EXPECT_EQ(mac::deployment_target(true, "11.0"), "12.3"); + } + { + // An env var present but empty is the same as absent (the convention + // every other MACOSX_DEPLOYMENT_TARGET reader in this tree follows). + ScopedDeploymentTargetEnv env(""); + EXPECT_EQ(mac::deployment_target(true, "11.0"), "11.0"); + } +} diff --git a/tests/unit/test_runtime_contract.cpp b/tests/unit/test_runtime_contract.cpp index 48645f2b..768634f5 100644 --- a/tests/unit/test_runtime_contract.cpp +++ b/tests/unit/test_runtime_contract.cpp @@ -442,7 +442,7 @@ TEST(RuntimeIdentity, UcrtIsAFloorDeclarationNotAPrivatePayload) { rt::RuntimeBinding b; b.platform = "windows"; rt::bind_windows_ucrt(b, "10.0.26100.0"); - EXPECT_FALSE(b.libc.has_value()) + EXPECT_TRUE(b.libc.empty()) << "ucrt was projected into the private-libc field"; EXPECT_FALSE(b.loader.has_value()); EXPECT_TRUE(b.libraryDirs.empty()); diff --git a/tests/unit/test_runtime_selection.cpp b/tests/unit/test_runtime_selection.cpp index 31c96a22..1e459d05 100644 --- a/tests/unit/test_runtime_selection.cpp +++ b/tests/unit/test_runtime_selection.cpp @@ -157,9 +157,9 @@ TEST(RuntimeBinding, DefaultAlwaysUsesConfiguredMcppHomeDefault) { EXPECT_EQ(binding->subosDir, expected); EXPECT_EQ(binding->runtimeId, "glibc@2.39"); if constexpr (mcpp::platform::is_linux) - EXPECT_EQ(binding->libc, std::optional("glibc@2.39")); + EXPECT_EQ(binding->libc, "glibc@2.39"); else - EXPECT_FALSE(binding->libc.has_value()); + EXPECT_TRUE(binding->libc.empty()); EXPECT_EQ(binding->provenance, "mcpp_default"); EXPECT_FALSE(binding->contractHash.empty()); ASSERT_EQ(binding->runtimeProviders.size(), 1u); @@ -201,7 +201,7 @@ TEST(RuntimeBinding, PhysicalSubosViewReconcilesAStaleDeclaredGlibcIdentity) { ASSERT_TRUE(binding.has_value()) << binding.error(); EXPECT_EQ(binding->runtimeId, "glibc@2.44"); - EXPECT_EQ(binding->libc, std::optional("glibc@2.44")); + EXPECT_EQ(binding->libc, "glibc@2.44"); ASSERT_EQ(binding->libraryDirs.size(), 1u); EXPECT_EQ(binding->libraryDirs.front(), h.cfg.xlingsHome() / "data" / "xpkgs" @@ -282,10 +282,10 @@ TEST(RuntimeBinding, NamedEnvironmentsHaveDistinctContractsAndRoundTrip) { ASSERT_TRUE(decoded.has_value()) << decoded.error(); EXPECT_EQ(decoded->contractHash, el8->contractHash); if constexpr (mcpp::platform::is_linux) { - ASSERT_TRUE(decoded->hostLibc.has_value()); - EXPECT_EQ(*decoded->hostLibc, "2.43"); + ASSERT_FALSE(decoded->hostLibc.empty()); + EXPECT_EQ(decoded->hostLibc, "2.43"); } else { - EXPECT_FALSE(decoded->hostLibc.has_value()); + EXPECT_TRUE(decoded->hostLibc.empty()); } EXPECT_EQ(decoded->subosDir, el8->subosDir); ASSERT_EQ(decoded->environment.size(), 1u); diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp index 1b7113ae..bfbac137 100644 --- a/tests/unit/test_toolchain_msvc.cpp +++ b/tests/unit/test_toolchain_msvc.cpp @@ -892,3 +892,287 @@ TEST(MsvcSdkOrigin, ASystemToolsetKeepsTheDeclaredSearchChain) { EXPECT_TRUE(choice.note.empty()) << "nothing was ignored, so there is nothing to report: " << choice.note; } + +// ─── Toolset selection: one rule for the cl.exe row and the clang row ───── +// +// `select_system_toolset` reads only the directories it is handed, so these +// run on any host against a synthetic machine: two Visual Studio instances, +// side-by-side toolsets, `Microsoft.VCToolsVersion.default.txt`, and the +// environment a developer prompt exports. + +namespace { + +// One Visual Studio instance on disk: /VC/Tools/MSVC//{include, +// lib/x64, bin/Hostx64/x64/cl.exe} per toolset, and the default-toolset file. +struct FakeInstance { + FakeToolset dir; + msvc::VsInstance inst; + + FakeInstance(std::string_view tag, std::string_view product, + std::string_view installVersion) : dir(tag) { + inst.root = dir.root; + inst.product = std::string(product); + inst.installVersion = std::string(installVersion); + } + // complete=false leaves out lib/x64, the state of a half-installed toolset. + void toolset(std::string_view v, bool complete = true, bool withCl = true) { + auto tools = dir.root / "VC" / "Tools" / "MSVC" / std::string(v); + std::filesystem::create_directories(tools / "include"); + if (complete) std::filesystem::create_directories(tools / "lib" / "x64"); + if (withCl) { + std::filesystem::create_directories(tools / "bin" / "Hostx64" / "x64"); + std::ofstream{tools / "bin" / "Hostx64" / "x64" / "cl.exe"} << "x"; + } + } + void default_is(std::string_view v) { + auto b = dir.root / "VC" / "Auxiliary" / "Build"; + std::filesystem::create_directories(b); + std::ofstream{b / "Microsoft.VCToolsVersion.default.txt"} << v << "\r\n"; + } + std::filesystem::path tools(std::string_view v) const { + return dir.root / "VC" / "Tools" / "MSVC" / std::string(v); + } +}; + +// VS 2022 (older installation) with a side-by-side 14.38, and an Insiders VS +// 18 (newer installation). The shape of a machine that has used Visual Studio +// for a few years. +struct TwoInstances { + FakeInstance vs2022{"sel-2022", "Visual Studio Community 2022", "17.14.36301.6"}; + FakeInstance vs18{"sel-18", "Visual Studio Community 18 Insiders", "18.0.11010.1"}; + TwoInstances() { + vs2022.toolset("14.44.35207"); + vs2022.toolset("14.38.33130"); + vs2022.default_is("14.44.35207"); + vs18.toolset("14.51.36014"); + vs18.default_is("14.51.36014"); + } + std::vector all() const { return {vs2022.inst, vs18.inst}; } +}; + +} // namespace + +TEST(MsvcToolsetVersion, ComparedAsNumbersNotStrings) { + EXPECT_LT(msvc::compare_toolset_versions("14.9", "14.10"), 0); + EXPECT_GT(msvc::compare_toolset_versions("14.44.35207", "14.38.33130"), 0); + EXPECT_EQ(msvc::compare_toolset_versions("14.44", "14.44.0"), 0); + EXPECT_EQ(msvc::compare_toolset_versions("17.14.36301.6", "17.14.36301.6"), 0); +} + +TEST(MsvcToolsetVersion, APrefixMatchesWholeComponents) { + EXPECT_TRUE(msvc::toolset_version_matches("14.44", "14.44.35207")); + EXPECT_TRUE(msvc::toolset_version_matches("14.44.35207", "14.44.35207")); + EXPECT_TRUE(msvc::toolset_version_matches("14", "14.51.36014")); + // `14.4` is the 14.4 line, not a text prefix of 14.44. + EXPECT_FALSE(msvc::toolset_version_matches("14.4", "14.44.35207")); + EXPECT_FALSE(msvc::toolset_version_matches("14.44.35207.1", "14.44.35207")); + EXPECT_FALSE(msvc::toolset_version_matches("", "14.44.35207")); +} + +TEST(MsvcToolsetSelect, APinnedVersionIsFoundInWhicheverInstanceHasIt) { + TwoInstances m; + msvc::ToolsetNeeds needs; + auto a = msvc::select_system_toolset(m.all(), {}, "14.38", needs); + ASSERT_TRUE(a.has_value()); + EXPECT_EQ(a->version, "14.38.33130"); + EXPECT_EQ(a->toolsDir, m.vs2022.tools("14.38.33130")); + EXPECT_EQ(a->via, "pinned"); + + auto b = msvc::select_system_toolset(m.all(), {}, "14.51.36014", needs); + ASSERT_TRUE(b.has_value()); + EXPECT_EQ(b->vsRoot, m.vs18.dir.root); + EXPECT_EQ(b->product, "Visual Studio Community 18 Insiders"); + + // The highest match across every instance, by number. + auto c = msvc::select_system_toolset(m.all(), {}, "14", needs); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.51.36014"); +} + +TEST(MsvcToolsetSelect, APinWithNoInstalledMatchIsNulloptSoThePackageIsUsed) { + TwoInstances m; + EXPECT_FALSE(msvc::select_system_toolset(m.all(), {}, "14.29", + msvc::ToolsetNeeds{}).has_value()); + // `14.4` does not reach 14.44 by text. + EXPECT_FALSE(msvc::select_system_toolset(m.all(), {}, "14.4", + msvc::ToolsetNeeds{}).has_value()); +} + +TEST(MsvcToolsetSelect, APinIgnoresTheEnvironmentAndSaysSo) { + // A developer prompt opened with `-vcvars_ver=14.38` exports this, and a + // pinned manifest must not follow it. + TwoInstances m; + msvc::MsvcEnvSnapshot env; + env.vcToolsInstallDir = m.vs2022.tools("14.38.33130").string() + "/"; + env.vsInstallDir = m.vs2022.dir.root; + auto c = msvc::select_system_toolset(m.all(), env, "14.44.35207", + msvc::ToolsetNeeds{}); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.44.35207"); + bool noted = false; + for (auto const& n : c->notes) + noted = noted || (n.find("VCToolsInstallDir") != std::string::npos + && n.find("14.38.33130") != std::string::npos); + EXPECT_TRUE(noted) << "an ignored declaration has to be reported"; +} + +TEST(MsvcToolsetSelect, AnIncompleteMatchIsSkippedAndReported) { + TwoInstances m; + m.vs18.toolset("14.44.35210", /*complete=*/false); // newer, but no lib/x64 + auto c = msvc::select_system_toolset(m.all(), {}, "14.44", msvc::ToolsetNeeds{}); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.44.35207"); + ASSERT_FALSE(c->notes.empty()); + EXPECT_NE(c->notes.front().find("14.44.35210"), std::string::npos); +} + +TEST(MsvcToolsetSelect, SystemFollowsTheDeclarationsInOrder) { + TwoInstances m; + msvc::ToolsetNeeds needs; + + msvc::MsvcEnvSnapshot byTools; + byTools.vcToolsInstallDir = m.vs2022.tools("14.38.33130"); + auto a = msvc::select_system_toolset(m.all(), byTools, "system", needs); + ASSERT_TRUE(a.has_value()); + EXPECT_EQ(a->version, "14.38.33130"); + EXPECT_EQ(a->via, "VCToolsInstallDir"); + + msvc::MsvcEnvSnapshot byInstance; + byInstance.vsInstallDir = m.vs2022.dir.root; + auto b = msvc::select_system_toolset(m.all(), byInstance, "system", needs); + ASSERT_TRUE(b.has_value()); + EXPECT_EQ(b->version, "14.44.35207") << "the instance's default, not its highest"; + EXPECT_EQ(b->via, "VSINSTALLDIR"); + + msvc::MsvcEnvSnapshot byPath; + byPath.clOnPath = m.vs2022.tools("14.38.33130") / "bin" / "Hostx64" / "x64" / "cl.exe"; + auto c = msvc::select_system_toolset(m.all(), byPath, "system", needs); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.38.33130"); + EXPECT_EQ(c->via, "PATH"); + + auto d = msvc::select_system_toolset(m.all(), {}, "system", needs); + ASSERT_TRUE(d.has_value()); + EXPECT_EQ(d->version, "14.51.36014") << "the newest installation's default"; + EXPECT_EQ(d->via, "newest instance"); +} + +TEST(MsvcToolsetSelect, TheDefaultFileOutranksTheHighestDirectory) { + // A side-by-side toolset newer than the instance's default is what VS's + // own vcvars would NOT pick; neither does this. + FakeInstance vs{"sel-default", "Visual Studio Community 2022", "17.14.1"}; + vs.toolset("14.44.35207"); + vs.toolset("14.50.35717"); + vs.default_is("14.44.35207"); + auto c = msvc::select_system_toolset({vs.inst}, {}, "system", msvc::ToolsetNeeds{}); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.44.35207"); +} + +TEST(MsvcToolsetSelect, TheNewestInstanceWithoutAToolsetIsPassedOver) { + // Visual Studio installed without the C++ workload: newest by installation + // version, and nothing a build can use. + TwoInstances m; + FakeInstance bare{"sel-bare", "Visual Studio Community 2026", "19.0.1.1"}; + auto all = m.all(); + all.insert(all.begin(), bare.inst); + auto c = msvc::select_system_toolset(all, {}, "system", msvc::ToolsetNeeds{}); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.51.36014"); +} + +TEST(MsvcToolsetSelect, TheClangRowDoesNotNeedClExe) { + FakeInstance vs{"sel-nocl", "Visual Studio Build Tools 2022", "17.14.1"}; + vs.toolset("14.44.35207", /*complete=*/true, /*withCl=*/false); + msvc::ToolsetNeeds clRow; + msvc::ToolsetNeeds clangRow; + clangRow.cl = false; + EXPECT_FALSE(msvc::select_system_toolset({vs.inst}, {}, "system", clRow).has_value()); + auto c = msvc::select_system_toolset({vs.inst}, {}, "system", clangRow); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->version, "14.44.35207"); +} + +TEST(MsvcToolsetSelect, TheRefusalListsWhatIsInstalled) { + TwoInstances m; + auto lines = msvc::describe_system_toolsets(m.all(), msvc::ToolsetNeeds{}); + ASSERT_EQ(lines.size(), 3u); + bool defaultMarked = false; + for (auto const& l : lines) + defaultMarked = defaultMarked || (l.find("14.44.35207") != std::string::npos + && l.find("(default)") != std::string::npos); + EXPECT_TRUE(defaultMarked); +} + +// ─── `xim:` names the ecosystem package ────────────────────────────────── + +TEST(ToolchainSpelling, XimMsvcIsThePackageOnly) { + auto spec = parse_toolchain_spec("xim:msvc@14.44.35207"); + ASSERT_TRUE(spec.has_value()) << spec.error(); + EXPECT_EQ(spec->family, Family::Msvc); + EXPECT_EQ(spec->version, "14.44.35207"); + EXPECT_TRUE(spec->ecosystemOnly); + EXPECT_FALSE(is_system_toolchain(*spec)); + // The prefix changes the meaning for msvc, so it survives persistence. + EXPECT_EQ(spec->spec_str(), "xim:msvc@14.44.35207"); + EXPECT_EQ(to_xim_package(*spec).target(), "xim:msvc@14.44.35207"); + + auto bare = parse_toolchain_spec("msvc@14.44.35207"); + ASSERT_TRUE(bare.has_value()); + EXPECT_FALSE(bare->ecosystemOnly); + EXPECT_EQ(bare->spec_str(), "msvc@14.44.35207"); +} + +TEST(ToolchainSpelling, XimIsASynonymWhereThereIsNoSystemOrigin) { + for (auto [prefixed, bare] : {std::pair{"xim:gcc@16.1.0", "gcc@16.1.0"}, + std::pair{"xim:llvm@22.1.8", "llvm@22.1.8"}}) { + auto a = parse_toolchain_spec(prefixed); + auto b = parse_toolchain_spec(bare); + ASSERT_TRUE(a.has_value()) << prefixed; + ASSERT_TRUE(b.has_value()) << bare; + EXPECT_EQ(a->spec_str(), b->spec_str()) << "one canonical spelling"; + EXPECT_EQ(to_xim_package(*a).target(), to_xim_package(*b).target()); + } +} + +TEST(ToolchainSpelling, XimWithSystemIsRefusedNamingBothSpellings) { + auto spec = parse_toolchain_spec("xim:msvc@system"); + ASSERT_FALSE(spec.has_value()); + EXPECT_NE(spec.error().find("msvc@system"), std::string::npos) << spec.error(); + EXPECT_NE(spec.error().find("xim:msvc@"), std::string::npos) << spec.error(); +} + +TEST(ToolchainSpelling, AnotherNamespaceIsRefused) { + auto spec = parse_toolchain_spec("foo:gcc@16.1.0"); + ASSERT_FALSE(spec.has_value()); + EXPECT_NE(spec.error().find("xim:"), std::string::npos) << spec.error(); +} + +// vswhere `-format text`: one instance per `instanceId:` line, CRLF endings, +// `displayName` and not `catalog_productDisplayVersion`, and an instance with +// no installation path dropped. +TEST(MsvcVswhereText, OneInstancePerInstanceId) { + const std::string text = + "instanceId: 1a2b\r\n" + "installationName: VisualStudio/17.14.8+36301.6\r\n" + "installationPath: C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\r\n" + "installationVersion: 17.14.36301.6\r\n" + "displayName: Visual Studio Enterprise 2022\r\n" + "catalog_productDisplayVersion: 17.14.8\r\n" + "properties_nickname: \r\n" + "\r\n" + "instanceId: 3c4d\r\n" + "installationPath: D:\\VS\\Preview\r\n" + "installationVersion: 18.0.11010.1\r\n" + "displayName: Visual Studio Community 2026 Insiders\r\n" + "instanceId: 5e6f\r\n" + "displayName: incomplete\r\n"; + auto v = mcpp::toolchain::msvc::parse_vswhere_text(text); + ASSERT_EQ(v.size(), 2u); + EXPECT_EQ(v[0].root.string(), "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise"); + EXPECT_EQ(v[0].installVersion, "17.14.36301.6"); + EXPECT_EQ(v[0].product, "Visual Studio Enterprise 2022"); + EXPECT_EQ(v[1].root.string(), "D:\\VS\\Preview"); + EXPECT_EQ(v[1].product, "Visual Studio Community 2026 Insiders"); + EXPECT_TRUE(mcpp::toolchain::msvc::parse_vswhere_text("").empty()); +}