From c66099b660cd15fd428cbb46f20fc93f25ac62b5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 17:19:42 +0800 Subject: [PATCH 1/9] =?UTF-8?q?rules-spirv:=20=E4=B8=A4=E4=B8=AA=E5=8F=AA?= =?UTF-8?q?=E5=B7=AE=E7=9B=AE=E5=BD=95=E7=9A=84=E7=9D=80=E8=89=B2=E5=99=A8?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E5=88=B0=E5=90=8C=E4=B8=80=E4=B8=AA=E8=BE=93?= =?UTF-8?q?=E5=87=BA,=E7=8E=B0=E5=9C=A8=E8=A2=AB=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=8B=92=E7=BB=9D=E5=B9=B6=E7=82=B9=E5=90=8D=E4=B8=A4=E8=80=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 输出名是着色器的 stem 加阶段,所以 `ui/text.vert` 与 `world/text.vert` 都产出 `text_vert.h`、都声明 `text_vert_spv`。 实测:在这条检查之前 ninja 会抓到重复输出 —— **所以它从来不是静默的**。但那条消息 点的是生成文件而不是两个着色器,以图加载失败的形式出现而不是这条规则的拒绝,而且 没有出路。每个阶段只有一个着色器的工程永远碰不到它,这是它活下来的原因;而按用途 分目录的图形工程是第一个会有两个的。 把目录并进名字不是修法:两个头文件只要进到同一个翻译单元,符号照样撞。 --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ rules/spirv.cppm | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cca1bd3..e76cb9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,49 @@ jobs: grep -q '^magic=07230203' run-glslc.log rm -rf shaderc.tar.gz shaderc-2026.3 + # TWO SHADERS THAT DIFFER ONLY BY DIRECTORY. + # + # The output name is the stem and the stage, so `a/x.vert` and `b/x.vert` + # both produce `x_vert.h` declaring `x_vert_spv`. Before this rule checked + # for it, ninja caught the duplicate output -- so it was never silent, but + # the message named the generated file and neither shader, arrived as a + # graph-loading failure rather than as this rule's refusal, and stated no + # way out. A project with one shader per stage never meets it, which is + # how it survived; a graphics project organising shaders by purpose is the + # first to have two. + - name: two shaders with one stem are refused, naming both + working-directory: tests/spirv-consumer + run: | + set -e + mkdir -p shaders/dup + cp shaders/scale.comp shaders/dup/scale.comp + # The fixture globs `shaders/*.comp`; widen it for this step only. + cp mcpp.toml mcpp.toml.bak + sed -i 's#shaders/\*.comp#shaders/**/*.comp#' mcpp.toml + rm -rf target + if "$MCPP" build > dup.log 2>&1; then + echo "FAIL: two shaders mapping to one output were accepted" + exit 1 + fi + for needle in 'two shaders map to one output' 'shaders/scale.comp' \ + 'shaders/dup/scale.comp' 'scale_comp_spv' 'rename one of them'; do + grep -q -- "$needle" dup.log || { + echo "FAIL: the refusal does not mention '$needle'" + tail -20 dup.log + exit 1 + } + done + echo "ok: refused, naming both shaders, the symbol and the way out" + # …and the fixture still builds once the duplicate is gone, so this + # step cannot leave a project that refuses everything. + mv mcpp.toml.bak mcpp.toml + rm -rf shaders/dup target + "$MCPP" build > restored.log 2>&1 || { + echo "FAIL: the fixture no longer builds after the duplicate was removed" + tail -20 restored.log; exit 1; } + rm -f dup.log restored.log + echo "ok: and it builds again with one shader per stem" + # A tool, not a rule: the header is written while the build program runs, # so there is no action to schedule. The second build is the measurement # that matters -- editing the data file must reach the binary, which is diff --git a/rules/spirv.cppm b/rules/spirv.cppm index 78b3b3b..da306c5 100644 --- a/rules/spirv.cppm +++ b/rules/spirv.cppm @@ -441,6 +441,49 @@ inline bool compile(std::span shaders, options opt = {}) { std::error_code ec; std::filesystem::create_directories(gen, ec); + // TWO SHADERS THAT DIFFER ONLY BY DIRECTORY PRODUCE ONE HEADER AND ONE + // SYMBOL, AND THAT HAS TO BE REFUSED HERE. + // + // The output name is the stem and the stage, as this rule documents, so + // `shaders/ui/text.vert` and `shaders/world/text.vert` both resolve to + // `text_vert.h` declaring `text_vert_spv`. Disambiguating by directory is + // not the fix: the SYMBOL would still collide the moment both headers + // reached one translation unit, and the naming rule is what consumers write + // `#include` lines against. + // + // Measured before this check existed: ninja caught it -- `multiple rules + // generate .../text_vert.h` -- so it was never silent. What it did not do + // is name the two SHADERS, say which rule produced them, or state the way + // out; and it arrives as a graph-loading failure rather than as this rule's + // refusal. A project with one shader per stage never meets it, which is why + // it survived: a graphics project organising shaders by purpose is the + // first to have two. + { + std::map seen; // output stem -> first source + for (auto const& src : shaders) { + const std::filesystem::path p(src); + const auto stage = stage_of(p.extension().string()); + if (stage.empty()) continue; // reported below, per source + const auto key = p.stem().string() + "_" + std::string(stage); + auto [it, fresh] = seen.try_emplace(key, src); + if (!fresh) { + std::println(std::cerr, + "mcpp.rules.spirv: two shaders map to one output.\n" + " {}\n" + " {}\n" + " both produce `{}.h` declaring `{}`, because the name is the " + "shader's stem\n" + " and its stage -- the directory is not part of it, and could not " + "be: two\n" + " headers reaching one translation unit would still collide on the " + "symbol.\n" + " fix: rename one of them, or compile only one.", + it->second, src, key, symbol_of(p.stem().string(), stage)); + return false; + } + } + } + for (auto const& src : shaders) { const std::filesystem::path p(src); const auto stage = stage_of(p.extension().string()); From 1de4f7bfbf1825acce9f22348cac7bab9d9a3068 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 17:32:48 +0800 Subject: [PATCH 2/9] =?UTF-8?q?0.2.5:=20rules-spirv=20=E5=9C=A8=E4=B8=89?= =?UTF-8?q?=E4=B8=AA=E5=B9=B3=E5=8F=B0=E4=B8=8A=E5=90=84=E8=87=AA=E5=B8=A6?= =?UTF-8?q?=E5=AF=B9=E7=9A=84=E9=82=A3=E4=B8=AA=E7=BC=96=E8=AF=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xim:shaderc` 补齐 macOS arm64 与 Windows x86_64 之后,这条规则可以在三个平台上 都自带它驱动的编译器。而它带的不是同一个: [target.'cfg(all(accelerator = "vulkan", linux))'.feature-xlings.rules-spirv] "xim:glslang" = ">=15.1.0" [target.'cfg(all(accelerator = "vulkan", macos))'.feature-xlings.rules-spirv] "xim:shaderc" = ">=2026.3" [target.'cfg(all(accelerator = "vulkan", windows))'.feature-xlings.rules-spirv] "xim:shaderc" = ">=2026.3" 这条规则自 0.2.0 起同时驱动两个参考编译器,并在 glslc 那条路线上自己写出 C 声明, 所以「哪一个在场」不是消费者看得见的差别。**跨平台一致性由规则的选择能力提供,不由 把同一个编译器发三遍提供。** Linux 保持 glslang,既有构建的读数一个字都不变。 新增的 CI job 在 macOS 与 Windows 上跑同一个 spirv 夹具,并施加与 Linux 同一条 来源判据:载荷必须来自**图**那一遍,不是夹具自己声明的。只跑 spirv 一个夹具 —— CUDA/HIP/SYCL 的载荷上游只有 Linux,把每个夹具都摆到每个平台上会因为不是缺陷的 理由变红。 `MCPP_HOME` 从 workflow 级挪到 job 级:那是一条 Linux 路径,留在上面会在另外两个 runner 上悄悄地错 —— 而且是「看起来还能跑」的那种错。 --- .github/workflows/ci.yml | 99 +++++++++++++++++++++++++++++++++++++++- mcpp.toml | 27 +++++++++-- 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e76cb9b..005a8dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,18 @@ env: # saved and restored `~/.mcpp` -- a directory mcpp never touched. Every run # was therefore the cold case, which is what the 90-minute budget below was # sized for. mcpp's own ci-linux.yml pins it for this reason and says so. - MCPP_HOME: /home/runner/.mcpp + # + # Set per JOB rather than here: the path differs by platform, and a + # workflow-level `/home/runner/.mcpp` would silently be wrong on the other + # two runners -- wrong in the direction that still appears to work, which is + # the kind this repository has already paid for once. jobs: consumers: name: consumers (linux x86_64) runs-on: ubuntu-24.04 + env: + MCPP_HOME: /home/runner/.mcpp # 90 rather than 60: the SYCL fixture pulls three payloads this job did not # need before -- dpcpp (578 MB installed), gcc (265 MB) and cuda-nvcc # (319 MB) -- and a cold cache downloads all of them before the first @@ -324,3 +330,94 @@ jobs: fi done [ "$fail" -eq 0 ] + + # ── THE SAME RULE ON THE OTHER TWO PLATFORMS ──────────────────────────────── + # + # `rules-spirv` is the only member whose payload this ecosystem publishes for + # all three, so it is the only one that can be checked on all three. What this + # job proves is the claim the per-platform declaration makes: a project writes + # the rule edge and nothing else, and the compiler that arrives is whichever + # one that platform has -- glslang on Linux, glslc here. + # + # ONLY THE SPIR-V FIXTURE. The CUDA, HIP and SYCL payloads are Linux-only + # upstream, so a matrix that ran every fixture everywhere would be red for a + # reason that is not a defect. + spirv-cross-platform: + name: rules-spirv (${{ matrix.name }}) + runs-on: ${{ matrix.runs-on }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - name: macos arm64 + runs-on: macos-14 + asset: macosx-arm64.tar.gz + dir-suffix: macosx-arm64 + - name: windows x86_64 + runs-on: windows-2022 + asset: windows-x86_64.zip + dir-suffix: windows-x86_64 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + + - name: Pin MCPP_HOME under this runner's home + run: echo "MCPP_HOME=$HOME/.mcpp" >> "$GITHUB_ENV" + + - name: Cache the mcpp sandbox + uses: actions/cache@v4 + with: + path: ~/.mcpp + key: mcpp-sandbox-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}-${{ hashFiles('tests/spirv-consumer/mcpp.toml') }} + restore-keys: | + mcpp-sandbox-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_VERSION }}- + + - name: Fetch the released mcpp + run: | + set -e + curl -L -fsS --retry 3 --retry-all-errors -o mcpp.pkg \ + "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/mcpp-${MCPP_VERSION}-${{ matrix.asset }}" + case "${{ matrix.asset }}" in + *.zip) unzip -q mcpp.pkg ;; + *) tar -xzf mcpp.pkg ;; + esac + dir="mcpp-${MCPP_VERSION}-${{ matrix.dir-suffix }}" + MCPP="$PWD/$dir/bin/mcpp" + "$MCPP" --version + # The bundled xlings, named explicitly because MCPP_HOME is pinned + # away from the tarball. See the Linux job for the measurement. + export MCPP_VENDORED_XLINGS="$PWD/$dir/registry/bin/xlings" + "$MCPP" self config --mirror GLOBAL + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + + - name: rules-spirv through a consumer + working-directory: tests/spirv-consumer + run: | + set -e + "$MCPP" build + "$MCPP" run | tee run.log + grep -q '^magic=07230203' run.log + + # The same provenance criterion the Linux job applies: the payload has to + # come from the GRAPH, not from the fixture. Without it, a fixture that + # quietly regained an `[xlings.workspace]` would keep this green while the + # claim stopped being true. + - name: the rule declared its own compiler + working-directory: tests/spirv-consumer + run: | + set -e + if grep -qE '^\[(target\..*\.)?xlings\.workspace\]' mcpp.toml; then + echo "FAIL: the fixture declares payloads itself"; exit 1 + fi + HOME_DIR=$("$MCPP" self env | awk -F'= *' '/^MCPP_HOME/{print $2; exit}') + rm -rf "$HOME_DIR/provisioned" target + "$MCPP" build > prov.log 2>&1 + grep -q 'entries declared by dependencies' prov.log || { + echo "FAIL: no payload came from the graph" + grep -i provisioning prov.log || echo "(no provisioning line at all)" + exit 1; } + echo "ok: $(grep -m1 'entries declared by dependencies' prov.log)" diff --git a/mcpp.toml b/mcpp.toml index 5ad0d38..9a4419a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.2.4" +version = "0.2.5" description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature" license = "Apache-2.0" authors = ["mcpp-community"] @@ -115,12 +115,29 @@ tools-embed = { sources = ["tools/embed.cppm"] } [target.'cfg(all(accelerator = "sycl", accelerator = "cuda"))'.feature-xlings.rules-sycl] "xim:cuda-nvcc" = "12.9.86" -# glslang alone: it is the rule's first choice, and `xim:shaderc` is the -# fallback a project names when it wants glslc instead. Declaring both would -# install both and use one. -[target.'cfg(accelerator = "vulkan")'.feature-xlings.rules-spirv] +# ONE COMPILER PER PLATFORM, AND NOT THE SAME ONE EVERYWHERE. +# +# This rule drives both reference compilers and writes the C declaration itself +# on the glslc route, so which one is present is not a difference a consumer +# sees. That is what makes cross-platform parity affordable: it is provided by +# the rule's ability to CHOOSE, not by publishing one compiler three times. +# +# Linux keeps `xim:glslang`, so nothing about an existing Linux build changes. +# macOS and Windows take `xim:shaderc`, which is the compiler this ecosystem +# publishes for them -- glslang has no upstream binary for either, and building +# it three times buys nothing glslc does not already give. +# +# Declaring both on one platform would install both and use one, so each block +# names exactly the compiler that platform will run. +[target.'cfg(all(accelerator = "vulkan", linux))'.feature-xlings.rules-spirv] "xim:glslang" = ">=15.1.0" +[target.'cfg(all(accelerator = "vulkan", macos))'.feature-xlings.rules-spirv] +"xim:shaderc" = ">=2026.3" + +[target.'cfg(all(accelerator = "vulkan", windows))'.feature-xlings.rules-spirv] +"xim:shaderc" = ">=2026.3" + # 8.5.0 is a real floor rather than a preference: the mixed-mode object -- the # one carrying both the device binary and a host-callable launcher, which is # what lets it join an ordinary link -- and the 38 SoC simulators are what this From 416b667c7c63a783d3c4074041f9da2402b56cff Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 17:39:11 +0800 Subject: [PATCH 3/9] =?UTF-8?q?rules-spirv:=20=E5=9C=A8=20Windows=20?= =?UTF-8?q?=E4=B8=8A=E6=89=BE=E5=BE=97=E5=88=B0=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处宿主假设:载荷里的编译器按 `bin/glslc` 查(Windows 上是 `glslc.exe`), 而 PATH 按 ':' 切(Windows 上是 ';')。于是刚加的 Windows job 会以「找不到着色器 编译器」失败,而载荷就在那里。 `program_in()` 在每个宿主上都同时试裸名与 `.exe`,代价是一次 stat;换来的是一个 用另一种约定重打包的载荷会被找到而不是被静默错过。后缀与分隔符按**宿主**决定 —— 构建程序跑在做构建的那台机器上,交叉构建到 Windows 时要找的仍然是 `glslc`。 `classify` 用的是 stem,`glslc.exe` 本来就分类正确;版本串里那个冒号与平台无关。 --- rules/spirv.cppm | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/rules/spirv.cppm b/rules/spirv.cppm index da306c5..90de39c 100644 --- a/rules/spirv.cppm +++ b/rules/spirv.cppm @@ -173,17 +173,43 @@ inline flavour classify(const std::string& path) { return flavour::none; } +// HOW THIS HOST SPELLS A PROGRAM, decided where the build program is compiled. +// +// The build program runs on the machine doing the building, so these are +// properties of the HOST and not of the target being compiled for -- a cross +// build from Linux to Windows still looks for `glslc`, because that is the +// binary about to be executed. +#if defined(_WIN32) +inline constexpr std::string_view kExeSuffix = ".exe"; +inline constexpr char kPathSep = ';'; +#else +inline constexpr std::string_view kExeSuffix = ""; +inline constexpr char kPathSep = ':'; +#endif + +// The first of `/` and `/.exe` that exists. Both are +// tried on every host rather than only the one whose suffix matches: a payload +// repacked with the other convention is then found instead of silently missed, +// and the cost is one `stat`. +inline std::string program_in(const std::filesystem::path& dir, std::string_view name) { + std::string bare(name); + for (auto const& n : { bare, bare + std::string(kExeSuffix) }) { + auto p = (dir / n).string(); + if (is_file(p)) return p; + } + return {}; +} + inline std::string first_on_path(const char* exe) { const char* path = std::getenv("PATH"); if (!path || !*path) return {}; std::string_view sv(path); for (std::size_t i = 0; i <= sv.size();) { - auto sep = sv.find(':', i); + auto sep = sv.find(kPathSep, i); auto dir = sv.substr(i, sep == std::string_view::npos ? sv.size() - i : sep - i); i = sep == std::string_view::npos ? sv.size() + 1 : sep + 1; if (dir.empty()) continue; - auto p = (std::filesystem::path(dir) / exe).string(); - if (is_file(p)) return p; + if (auto p = program_in(std::filesystem::path(dir), exe); !p.empty()) return p; } return {}; } @@ -213,10 +239,10 @@ inline compiler find_compiler(const options& opt) { if (const char* dir = mcpp::xpkg_dir("glslang"); dir && *dir) for (const char* exe : {"glslangValidator", "glslang"}) - if (auto p = (std::filesystem::path(dir) / "bin" / exe).string(); is_file(p)) + if (auto p = program_in(std::filesystem::path(dir) / "bin", exe); !p.empty()) return { p, flavour::glslang }; if (const char* dir = mcpp::xpkg_dir("shaderc"); dir && *dir) - if (auto p = (std::filesystem::path(dir) / "bin" / "glslc").string(); is_file(p)) + if (auto p = program_in(std::filesystem::path(dir) / "bin", "glslc"); !p.empty()) return { p, flavour::glslc }; for (const char* exe : {"glslangValidator", "glslang"}) From 90a7bfeaaca3835878fa82cfed42136c8e3e1073 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 18:09:50 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat(rules):=20cuda=20=E4=B8=8E=20sycl=20?= =?UTF-8?q?=E5=9C=A8=20Windows=20=E4=B8=8A=E7=9A=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=8E=E5=88=A4=E6=96=AD,=E4=BB=A5=E5=8F=8A=E4=B8=80?= =?UTF-8?q?=E4=B8=AA"=E6=AF=8F=E6=9D=A1=E8=A7=84=E5=88=99=E9=83=BD?= =?UTF-8?q?=E4=B8=BA=E6=9C=AC=E5=AE=BF=E4=B8=BB=E7=BC=96=E8=AF=91=E8=BF=87?= =?UTF-8?q?"=E7=9A=84=E5=A4=B9=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xim-pkgindex 26f682fc 之后,CUDA 的四个组件与 dpcpp 在 Windows 上装得上了。 规则这一侧还是按 Linux 写的:程序名不带 `.exe`,导入库不在 `lib/x64`,而 SYCL 那条把宿主 C/C++ 库挡在外面的三个载荷在那个平台上既不存在也不需要。 ## rules-cuda `nvcc()`、clang++、g++、ptxas/fatbinary 的存在性检查都按宿主取后缀;`lib_dirs()` 把 `/lib/x64` 一并搜索(不存在的目录不贡献任何东西,按宿主分表反而多一处要同步)。 **Windows 上默认走 clang 路线,与项目的编译器无关。** 两条路线的区别是谁编译设备 单元的宿主那一半:nvcc 路线交给 `-ccbin` 命名的编译器,而在这个平台上那只能是 MSVC 的 `cl.exe` —— 找到它要问机器的 Visual Studio 安装,正是这个生态在消除的那 种宿主依赖。clang 路线不驱动第二个编译器,它自己就会定位 MSVC 的头与库,和它编译 任何一个普通翻译单元时一样。显式要 nvcc 路线的项目会在 Windows 上被拒绝,并说明 这一点,而不是拿两个没有意义的答案拼出一条命令行。 `-fPIC` 只在非 Windows 上加:在那个平台上它命名的是无条件成立的性质,每个设备 单元会多一条 unused argument 警告。 ## rules-sycl `clang++.exe`;`--gcc-install-dir`、`-isystem `、`-isystem `、 `-fPIC` 与 `-l:libstdc++.so.6` 都只在 Linux 上出现 —— 它们存在的理由是"两个 C++ 运行时不能共处一个进程"和"dpcpp 的 clang 不是 mcpp 解析的那个 clang,没有被配置 过这个生态的 glibc",而 Windows 上只有一个 C++ 运行时(MSVC 的),两边用的是同 一个。要求那三个载荷会让 Windows 的构建因为三个本生态不为它发布、而那个平台的 编译器也不需要的包而失败 —— 一个补救措施不存在的错误。 `accel` 里点名 NVIDIA 架构的构建在 Windows 上被拒绝并说明:上游不为 Windows 构建 CUDA 与 HIP 插件,已发布的资产也确实只带 Level Zero 与 OpenCL 两个适配器。 mcpp.toml 里那条 `sycl + cuda` 的声明因此加上 `linux` —— 供给发生在规则之前, 一个在几个 GB 下载之后才到达的拒绝是更坏的拒绝。 ## tests/all-rules-compile 上面每一条改动都写在 `#if defined(_WIN32)` 里,而本仓库此前没有任何东西在 Windows 上编译过 cuda.cppm 或 sycl.cppm。 其余每个 consumer 都端到端地跑一条规则,因此都需要那条规则的载荷,而载荷只为一个、 两个或三个平台发布 —— **于是一条规则里为某个宿主写的那半段,恰好是那个宿主从不 编译的那半段**。语法错了也能一直绿,直到第一个在 Windows 上构建的人从一个他只想 用的包里拿到一个编译错误。 这个夹具不点名任何 accelerator。每条规则的 `compile()` 在那个状态下立刻返回,所以 一个字节都不下载、也不需要第二个编译器;它断言的是六个模块都为**本宿主**编译过。 足够便宜,矩阵里每个平台都跑。 实测:往 `rules/cuda.cppm` 末尾加一条 `static_assert(false)`,这个夹具当场报 `host module 'mcpp.rules.cuda' compile failed`。 `spirv-cross-platform` 因此更名为 `rules-cross-platform`。 --- .github/workflows/ci.yml | 33 +++++++- mcpp.toml | 9 ++- rules/cuda.cppm | 84 +++++++++++++++++--- rules/sycl.cppm | 112 ++++++++++++++++++++++----- tests/all-rules-compile/build.mcpp | 31 ++++++++ tests/all-rules-compile/mcpp.toml | 51 ++++++++++++ tests/all-rules-compile/src/main.cpp | 9 +++ 7 files changed, 296 insertions(+), 33 deletions(-) create mode 100644 tests/all-rules-compile/build.mcpp create mode 100644 tests/all-rules-compile/mcpp.toml create mode 100644 tests/all-rules-compile/src/main.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 005a8dd..77cd1d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,6 +193,21 @@ jobs: "$MCPP" run --no-accel | tee run.log grep -q '^12 24 36 48' run.log + # EVERY RULE COMPILED FOR THIS HOST, which is a different claim from any + # step above. Each of those exercises one rule and needs that rule's + # payload, so a rule is only ever compiled on the platforms its payload + # exists for -- and a rule's host-dependent halves are exactly the code + # those platforms do not compile. This fixture names no accelerator, so + # every rule returns before looking for a payload and nothing is + # downloaded; what it asserts is that all six modules compile here. + - name: every rule module compiles for this host + working-directory: tests/all-rules-compile + run: | + set -e + "$MCPP" build + "$MCPP" run | tee run.log + grep -q '^all-rules-compile ok' run.log + # TWO RULES IN ONE BUILD PROGRAM, which every step above has exactly # one of. With one, a rule can take the whole of `device_sources()` and # be right by accident; with two, that list holds a `.cu` AND a `.comp`, @@ -342,8 +357,8 @@ jobs: # ONLY THE SPIR-V FIXTURE. The CUDA, HIP and SYCL payloads are Linux-only # upstream, so a matrix that ran every fixture everywhere would be red for a # reason that is not a defect. - spirv-cross-platform: - name: rules-spirv (${{ matrix.name }}) + rules-cross-platform: + name: rules (${{ matrix.name }}) runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 strategy: @@ -406,6 +421,20 @@ jobs: # come from the GRAPH, not from the fixture. Without it, a fixture that # quietly regained an `[xlings.workspace]` would keep this green while the # claim stopped being true. + # THE STEP THAT MAKES THIS JOB WORTH ITS NAME. Above it, one rule is + # exercised because one rule's compiler is published for these hosts. + # Below it, EVERY rule is compiled for them -- including the ones whose + # payload reaches a device that macOS and Windows do not both have, and + # whose host-dependent code was therefore written without ever being + # compiled on the host it was written for. + - name: every rule module compiles for this host + working-directory: tests/all-rules-compile + run: | + set -e + "$MCPP" build + "$MCPP" run | tee run.log + grep -q '^all-rules-compile ok' run.log + - name: the rule declared its own compiler working-directory: tests/spirv-consumer run: | diff --git a/mcpp.toml b/mcpp.toml index 9a4419a..60c5cba 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -112,7 +112,14 @@ tools-embed = { sources = ["tools/embed.cppm"] } # SET, so this is a conjunction of two membership tests rather than a # contradiction -- and without it every SYCL build would download a CUDA # toolkit it may have no target for. -[target.'cfg(all(accelerator = "sycl", accelerator = "cuda"))'.feature-xlings.rules-sycl] +# +# `linux` is part of the predicate for a reason that is measured rather than +# stylistic: upstream does not build the CUDA plugin for Windows, and the +# published Windows asset carries Level Zero and OpenCL adapters only. The rule +# refuses that combination on that host, and a refusal that arrives after a +# multi-gigabyte download is a worse refusal -- provisioning runs before the +# rule does. +[target.'cfg(all(accelerator = "sycl", accelerator = "cuda", linux))'.feature-xlings.rules-sycl] "xim:cuda-nvcc" = "12.9.86" # ONE COMPILER PER PLATFORM, AND NOT THE SAME ONE EVERYWHERE. diff --git a/rules/cuda.cppm b/rules/cuda.cppm index 4fc7318..207fa80 100644 --- a/rules/cuda.cppm +++ b/rules/cuda.cppm @@ -47,6 +47,23 @@ import mcpp; export namespace mcpp::rules::cuda { +// A PROGRAM'S NAME CARRIES ITS HOST'S SUFFIX, AND THE PAYLOAD LAYOUT DOES TOO. +// +// The NVIDIA redistributables are published for Windows as well as Linux and +// carry the same tree under different names: `bin/nvcc.exe` rather than +// `bin/nvcc`, and import libraries under `lib/x64` rather than shared objects +// under `lib` or `lib64`. Every one of the paths this rule builds is derived +// from those two facts, so both are stated once here rather than at each site +// -- a `std::filesystem::exists` against the wrong spelling does not fail, it +// answers false, and this rule reads several such answers as decisions. +#if defined(_WIN32) +inline constexpr bool kWindows = true; +inline constexpr const char* kExe = ".exe"; +#else +inline constexpr bool kWindows = false; +inline constexpr const char* kExe = ""; +#endif + enum class route { automatic, clang, nvcc }; struct options { @@ -177,7 +194,7 @@ struct toolkit { // 12.x toolkits ship as the separate `cuda-cccl` package, and which the // host's /usr/include had supplied in the same way. std::string curand_root, cccl_root; - std::string nvcc() const { return nvcc_root + "/bin/nvcc"; } + std::string nvcc() const { return nvcc_root + "/bin/nvcc" + kExe; } std::string host_config() const { for (auto const* r : { &crt_root, &nvcc_root, &cudart_root }) { if (r->empty()) continue; @@ -203,8 +220,11 @@ struct toolkit { } std::vector lib_dirs() const { std::vector out; + // `/lib/x64` is the Windows layout; it is searched on every host + // because a directory that does not exist contributes nothing, and a + // list keyed on the host would be a second place to keep in step. for (auto const* r : { &cudart_root, &nvcc_root }) - for (auto const* sub : { "/lib", "/lib64" }) + for (auto const* sub : { "/lib", "/lib64", "/lib/x64" }) if (!r->empty() && std::filesystem::is_directory(*r + sub)) out.push_back(*r + sub); return out; @@ -451,9 +471,25 @@ struct edge { std::vector command, inputs, outputs; }; +// ON WINDOWS THE DEFAULT IS THE CLANG ROUTE WHATEVER THE PROJECT'S COMPILER IS. +// +// The two routes differ in who compiles the host half of a device unit. On the +// nvcc route that is a compiler nvcc drives through `-ccbin`, and on Windows +// the only one it accepts is MSVC's `cl.exe` -- whose directory is found +// through `vswhere` and the installed Visual Studio instance, neither of which +// mcpp resolves or states to a build program. The rule would have to search +// the host for it, which is the one thing a rule of this ecosystem does not do. +// +// The clang route has no such gap: it drives no second compiler, and clang +// locates the MSVC headers and libraries it needs for the host half itself, +// the same way it does for every ordinary C++ translation unit on this host. inline route decide(route asked) { if (asked != route::automatic) return asked; +#if defined(_WIN32) + return route::clang; +#else return std::string_view(mcpp::compiler()) == "clang" ? route::clang : route::nvcc; +#endif } inline std::vector plan(std::span sources, options opt = {}) { @@ -483,9 +519,18 @@ inline std::vector plan(std::span sources, options opt std::string driver_cc; // the compiler that runs the device unit std::vector front; // the command up to the input file if (r == route::clang) { - driver_cc = tcdir + "/bin/clang++"; + driver_cc = tcdir + "/bin/clang++" + kExe; if (!std::filesystem::exists(driver_cc)) { - std::println(std::cerr, "mcpp.rules.cuda: the clang route needs the toolchain's clang++ at {}", driver_cc); + std::println(std::cerr, + "mcpp.rules.cuda: the clang route needs the toolchain's clang++ at {}.\n" + " Name an LLVM toolchain for this project:\n" + " [toolchain]\n" + " default = \"llvm@22.1.8\"{}", + driver_cc, + kWindows ? "\n On Windows this route is the only one the rule takes: nvcc " + "drives a host\n compiler named by -ccbin, and locating MSVC's " + "cl.exe is not something\n this rule does." + : ""); return out; } // Refused here rather than at clang's include error: the header it @@ -506,7 +551,7 @@ inline std::vector plan(std::span sources, options opt " (found cccl: '{}', curand: '{}')", tk->cccl_root, tk->curand_root); return out; } - front = { driver_cc, "-x", "cuda", "-std=c++17", "-O2", "-fPIC", + front = { driver_cc, "-x", "cuda", "-std=c++17", "-O2", "--cuda-path=" + tk->nvcc_root, "-Wno-unknown-cuda-version", // NVIDIA'S HEADER REFUSES libc++, AND THE REFUSAL IS // ABOUT nvcc RATHER THAN ABOUT THIS COMPILER. @@ -527,15 +572,36 @@ inline std::vector plan(std::span sources, options opt // on this route: nvcc's host pass really does break against // libc++, and nothing here weakens that. "-D_ALLOW_UNSUPPORTED_LIBCPP" }; + // Position-independent code is the default on Windows and naming it + // is an unused-argument warning on every device unit. + if constexpr (!kWindows) front.insert(front.begin() + 5, "-fPIC"); for (auto const& inc : tk->include_dirs()) front.push_back("-I" + inc); for (auto const& a : tg.archs) front.push_back("--cuda-gpu-arch=" + a); // clang checks ptxas and fatbinary itself; say so before it does. for (auto const* tool : { "ptxas", "fatbinary" }) - if (!std::filesystem::exists(tk->nvcc_root + "/bin/" + tool)) + if (!std::filesystem::exists(tk->nvcc_root + "/bin/" + tool + kExe)) mcpp::warning(std::format("the toolkit payload has no {}; clang invokes it " "after generating PTX", tool).c_str()); std::println("mcpp.rules.cuda: clang route -- {} (toolkit {})", driver_cc, tk->nvcc_root); } else { + // ASKED FOR EXPLICITLY, BECAUSE `decide` NEVER CHOOSES IT HERE. + // Everything below reads a GCC bound out of the toolkit's header and + // hands nvcc a `g++` to drive. On Windows there is no such compiler in + // the toolchain and the bound in `host_config.h` is stated in + // `_MSC_VER`, so the branch would build a command line out of two + // answers that mean nothing and nvcc would report the third. + if constexpr (kWindows) { + std::println(std::cerr, + "mcpp.rules.cuda: the nvcc route was asked for, and on Windows this rule does " + "not take it.\n" + " nvcc compiles the host half through a compiler named by -ccbin, which on " + "this host\n is MSVC's cl.exe; finding it means asking the machine about its " + "Visual Studio\n installation, which is the kind of host dependence this " + "ecosystem removes.\n" + " Leave the route unset: the clang route is the default here and needs no " + "second compiler."); + return out; + } // nvcc drives the toolchain's own compiler, and refuses one newer than // the bound its header states. Read the bound; if exceeded, pass the // escape hatch and say so -- an unexplained flag is worse than a note. @@ -580,13 +646,13 @@ inline std::vector plan(std::span sources, options opt // otherwise a gcc payload the project declared for this purpose, and // otherwise a refusal that says which declaration to add. const auto b = read_bounds(tk->host_config()); - const std::string tcGcc = tcdir + "/bin/g++"; + const std::string tcGcc = tcdir + "/bin/g++" + kExe; const int tcMajor = compiler_major(tcGcc); if (b.gcc == 0 || tcMajor <= b.gcc) { driver_cc = tcGcc; } else if (auto payload = xpkg("gcc"); !payload.empty() - && compiler_major(payload + "/bin/g++") <= b.gcc) { - driver_cc = payload + "/bin/g++"; + && compiler_major(payload + "/bin/g++" + kExe) <= b.gcc) { + driver_cc = payload + "/bin/g++" + kExe; mcpp::warning(std::format( "nvcc {} states gcc <= {} in {}; the toolchain's gcc {} exceeds it, so the " "device unit is compiled with the declared xim:gcc payload ({}). The clang " diff --git a/rules/sycl.cppm b/rules/sycl.cppm index 422a32a..4190768 100644 --- a/rules/sycl.cppm +++ b/rules/sycl.cppm @@ -97,6 +97,30 @@ struct options { // The device is spelled the way every other rule in this ecosystem spells it, // so `sm_89` does not acquire a second spelling because the source file says // `.sycl` instead of `.cu`. +// THE TWO PUBLISHED SYCL TOOLCHAINS DIFFER IN MORE THAN A FILE SUFFIX. +// +// Upstream publishes `sycl_linux.tar.gz` and `sycl_windows.tar.gz` from one +// tag, and the compiler is the same compiler. What differs is everything +// around it: the host half of a SYCL unit compiles against libstdc++ and glibc +// on Linux and against MSVC's standard library on Windows, so the three +// payloads that exist to keep the host's copies out of the search list +// (`xim:gcc`, `xim:glibc`, `xim:linux-headers`) have no counterpart there -- +// clang finds the MSVC installation itself, the same way it does for every +// ordinary translation unit on that host. `-fPIC` likewise names a property +// that is unconditional on Windows. +// +// And the device coverage differs: upstream states that the HIP and CUDA +// plugins are not built for Windows, and the asset agrees -- it carries +// Level Zero and OpenCL adapters and no others. An ahead-of-time NVIDIA +// build is therefore refused there rather than attempted. +#if defined(_WIN32) +inline constexpr bool kWindows = true; +inline constexpr const char* kExe = ".exe"; +#else +inline constexpr bool kWindows = false; +inline constexpr const char* kExe = ""; +#endif + struct target { bool sycl = false; std::vector cuda_archs; // {"sm_89"} @@ -174,13 +198,29 @@ inline std::string gcc_install_dir(const std::string& gcc_root) { // `dpcpp --version` states the release and the intel/llvm revision it was // built from. Stated as a fact so a build log answers "which SYCL compiler" // without anyone reproducing the build. +inline FILE* open_pipe(const std::string& cmd) { +#if defined(_WIN32) + return ::_popen(cmd.c_str(), "r"); +#else + return ::popen(cmd.c_str(), "r"); +#endif +} +inline void close_pipe(FILE* p) { +#if defined(_WIN32) + ::_pclose(p); +#else + ::pclose(p); +#endif +} + inline std::string compiler_version(const std::string& exe) { - FILE* p = ::popen(("\"" + exe + "\" --version 2>/dev/null").c_str(), "r"); + FILE* p = open_pipe("\"" + exe + "\" --version 2>" + + (kWindows ? std::string("NUL") : std::string("/dev/null"))); if (!p) return {}; std::string text; char buf[512]; while (std::fgets(buf, sizeof buf, p)) text += buf; - ::pclose(p); + close_pipe(p); for (auto line : split(text, '\n')) { auto at = line.find("DPC++ compiler "); if (at == std::string_view::npos) continue; @@ -241,6 +281,18 @@ inline std::vector plan(std::span sources, options opt } const auto tg = parse_target(mcpp::accel()); + if constexpr (kWindows) { + if (!tg.cuda_archs.empty()) { + std::println(std::cerr, + "mcpp.rules.sycl: [build] accel names an NVIDIA target and this host's SYCL\n" + " compiler cannot reach it. Upstream states that the CUDA and HIP plugins are\n" + " not built for Windows, and the published asset agrees: its Unified Runtime\n" + " adapters are Level Zero and OpenCL, and no others.\n" + " Available on this host: accel = \"sycl\" -- SPIR-V, consumed by whichever\n" + " Level Zero or OpenCL device the runtime finds."); + return out; + } + } if (!tg.amd_archs.empty() && tg.cuda_archs.empty()) { std::println(std::cerr, "mcpp.rules.sycl: [build] accel names AMD architectures and this ecosystem\n" @@ -282,6 +334,11 @@ inline std::vector plan(std::span sources, options opt // does not need it. Asking for both would tell someone who has already // solved this to solve it again. if (dpcpp.empty() && opt.compiler.empty()) missing += " \"xim:dpcpp\" = \"7.1.0\"\n"; + // THE THREE BELOW ARE THE HOST C AND C++ LIBRARIES, AND ONLY LINUX HAS + // THIS PROBLEM. Requiring them on Windows would refuse a build over three + // packages that this ecosystem does not publish for it and that the + // compiler there does not need -- an error whose remedy does not exist. + if constexpr (!kWindows) { if (gcc.empty()) missing += " \"xim:gcc\" = \"15.1.0\"\n"; // UNPINNED ON PURPOSE, and this is the one detail that makes the // declaration portable. The C library version is the RUNTIME BINDING's @@ -294,6 +351,7 @@ inline std::vector plan(std::span sources, options opt // truthfully say about a library it does not select. if (glibc.empty()) missing += " \"xim:glibc\" = \"\"\n"; if (uapi.empty()) missing += " \"xim:linux-headers\" = \"\"\n"; + } if (!tg.cuda_archs.empty() && cuda.empty()) missing += " \"xim:cuda-nvcc\" = \"12.9.86\"\n"; if (!missing.empty()) { @@ -315,7 +373,7 @@ inline std::vector plan(std::span sources, options opt } auto exe = opt.compiler; - if (exe.empty()) exe = dpcpp + "/bin/clang++"; + if (exe.empty()) exe = dpcpp + "/bin/clang++" + kExe; if (!is_file(exe)) { std::println(std::cerr, "mcpp.rules.sycl: {} is not a file. The dpcpp payload publishes its SYCL\n" @@ -324,12 +382,15 @@ inline std::vector plan(std::span sources, options opt } if (auto v = compiler_version(exe); !v.empty()) mcpp::fact("dpcpp", v.c_str()); - const auto gid = gcc_install_dir(gcc); - if (gid.empty()) { - std::println(std::cerr, - "mcpp.rules.sycl: the xim:gcc payload at {} has no lib/gcc//\n" - " directory, which is what --gcc-install-dir names.", gcc); - return out; + // Empty on Windows, where the flag it feeds is not passed at all. + const auto gid = kWindows ? std::string{} : gcc_install_dir(gcc); + if constexpr (!kWindows) { + if (gid.empty()) { + std::println(std::cerr, + "mcpp.rules.sycl: the xim:gcc payload at {} has no lib/gcc//\n" + " directory, which is what --gcc-install-dir names.", gcc); + return out; + } } // The target selection, once, shared by the compile and the device link: @@ -344,23 +405,28 @@ inline std::vector plan(std::span sources, options opt } } - std::vector front{ exe, "-fsycl", "-std=c++17", "-O2", "-fPIC", - "--gcc-install-dir=" + gid }; - // The C library, ahead of whatever the compiler would have found. This is - // the shape mcpp uses for its own translation units, and it puts the - // ecosystem's glibc at the front of the search list; `/usr/include` stays - // last, as a fallback for C headers no payload provides, which is what the - // engine does too. - front.push_back("-isystem" + glibc + "/include"); - front.push_back("-isystem" + uapi + "/include"); + std::vector front{ exe, "-fsycl", "-std=c++17", "-O2" }; + if constexpr (!kWindows) { + front.push_back("-fPIC"); + front.push_back("--gcc-install-dir=" + gid); + // The C library, ahead of whatever the compiler would have found. This + // is the shape mcpp uses for its own translation units, and it puts + // the ecosystem's glibc at the front of the search list; + // `/usr/include` stays last, as a fallback for C headers no payload + // provides, which is what the engine does too. + front.push_back("-isystem" + glibc + "/include"); + front.push_back("-isystem" + uapi + "/include"); + } front.insert(front.end(), targeting.begin(), targeting.end()); // The link line gets its directories from here, not from the manifest: the // rule resolved the payload, so the rule names where its libraries are. mcpp::link_search((dpcpp + "/lib").c_str()); mcpp::link_lib("sycl"); - // See the file header for why this is not `-lstdc++`. - mcpp::link_lib(":libstdc++.so.6"); + // See the file header for why this is not `-lstdc++`. The reason is a + // Linux one: two C++ runtimes cannot share a process, and on Windows there + // is one -- MSVC's, which both this payload and mcpp's own compiler use. + if constexpr (!kWindows) mcpp::link_lib(":libstdc++.so.6"); // SPIR-V IS NOT A DEVICE, AND A BUILD THAT NAMES NO DEVICE SHOULD BE TOLD. // @@ -423,7 +489,11 @@ inline std::vector plan(std::span sources, options opt d.id = "sycl:device-link"; d.role = "object"; d.description = "dpcpp -fsycl-link (device images -> registration)"; - d.command = { exe, "-fsycl", "-fPIC", "--gcc-install-dir=" + gid }; + d.command = { exe, "-fsycl" }; + if constexpr (!kWindows) { + d.command.push_back("-fPIC"); + d.command.push_back("--gcc-install-dir=" + gid); + } d.command.insert(d.command.end(), targeting.begin(), targeting.end()); d.command.push_back("-fsycl-link"); for (auto const& o : objects) d.command.push_back(o); diff --git a/tests/all-rules-compile/build.mcpp b/tests/all-rules-compile/build.mcpp new file mode 100644 index 0000000..f265633 --- /dev/null +++ b/tests/all-rules-compile/build.mcpp @@ -0,0 +1,31 @@ +// Imports every rule and calls it. The call matters as much as the import: a +// module can be well-formed and still hold a template or an `if constexpr` +// branch that is only instantiated at a call site, and a rule that refuses to +// build the host it is being compiled for should say so here rather than in +// someone's project. +import std; +import mcpp; +import mcpp.rules.ascendc; +import mcpp.rules.cuda; +import mcpp.rules.hip; +import mcpp.rules.spirv; +import mcpp.rules.sycl; +import mcpp.tools.embed; + +int main() { + // No accelerator is named, so each of these returns true without looking + // for a payload. What is asserted is that the module compiled and the + // entry point resolved on this host. + bool ok = mcpp::rules::ascendc::compile() + && mcpp::rules::cuda::compile() + && mcpp::rules::hip::compile() + && mcpp::rules::spirv::compile() + && mcpp::rules::sycl::compile(); + // `tools::embed` has no accelerator gate, so it is asked the one question + // that writes nothing: where it WOULD put a header. `embed::file` is not + // called, because a fixture whose point is a compilation should not also + // produce output. + ok = ok && !mcpp::tools::embed::header_path("probe.bin").empty(); + std::println("all-rules-compile: every rule module compiled for this host"); + return ok ? 0 : 1; +} diff --git a/tests/all-rules-compile/mcpp.toml b/tests/all-rules-compile/mcpp.toml new file mode 100644 index 0000000..4c3f3e4 --- /dev/null +++ b/tests/all-rules-compile/mcpp.toml @@ -0,0 +1,51 @@ +# The fixture that compiles every rule in this collection, on whatever host is +# running the build. +# +# WHY A FIXTURE WHOSE ONLY PRODUCT IS A COMPILATION. +# +# Each of the other consumers exercises one rule end to end, and each of them +# needs that rule's payload: a shader compiler, a CUDA toolkit, a SYCL +# compiler. Those payloads are published for one, two or three platforms, so +# the consumers that need them can only run where they exist -- and the halves +# of a rule that are written for a host are exactly the halves that host never +# compiles. +# +# A rule's host-dependent code is `#if defined(_WIN32)`, a `.exe` suffix, a +# directory named `lib/x64`, a listing command. None of it is compiled by a +# Linux build, so a Windows branch can be syntactically wrong for as long as +# nobody builds on Windows -- and the first person to do so gets a compile +# error out of a package they only wanted to use. +# +# This fixture names no accelerator. Every rule's `compile()` returns +# immediately in that state, so no payload is installed and no second compiler +# is needed; what happens is that all six modules are compiled as host modules +# for this host, which is the assertion. It is cheap enough to run on every +# platform in the matrix. +[package] +name = "all-rules-compile" +namespace = "example" +version = "0.1.0" +description = "Compiles every rule module in this collection for the host running the build" + +[language] +standard = "c++23" +modules = true +import_std = true + +# Every feature this package publishes. A rule left out of this list is a rule +# whose host-dependent code is compiled on one platform only. +[build-dependencies.mcpp] +plugins = { path = "../..", features = [ + "rules-ascendc", "rules-cuda", "rules-hip", "rules-spirv", "rules-sycl", + "tools-embed", +], host-module = true } + +# NO `accel`, and that is the whole design: with none, every rule returns +# before it looks for a payload, so this fixture downloads nothing on any host. + +[build] +sources = ["src/*.cpp"] + +[targets.all-rules-compile] +kind = "bin" +main = "src/main.cpp" diff --git a/tests/all-rules-compile/src/main.cpp b/tests/all-rules-compile/src/main.cpp new file mode 100644 index 0000000..84b5a8c --- /dev/null +++ b/tests/all-rules-compile/src/main.cpp @@ -0,0 +1,9 @@ +#include + +// The program exists so the fixture has something to link. What is being +// tested happened before it: the build program compiled every rule module in +// the collection for this host. +int main() { + std::puts("all-rules-compile ok"); + return 0; +} From e874c6743ec4258b2148909427be2c7080a64155 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 18:19:35 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E8=A7=84=E5=88=99=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=94=A8=20std::println=20=E2=80=94=E2=80=94=20macOS=2014=20?= =?UTF-8?q?=E4=B8=8A=E5=AE=83=E7=BC=96=E5=BE=97=E8=BF=87=E9=93=BE=E4=B8=8D?= =?UTF-8?q?=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `rules (macos arm64)` 与 `rules (windows x86_64)` 是这条分支新加的两个 job, 第一次跑就红,而两条红各自指向一个真实的缺口。 ## macOS 14:std::println 不是 header-only ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) ld64.lld: error: undefined symbol: std::__1::__get_ostream_file(...) `std::print`/`std::println` 的两个重载都要到 libc++ **dylib** 里取符号,而这两个 符号是在 macOS 14 不带的那一版里加进去的。构建程序的链接在那台机器上把 `-lc++` 解析到系统那份,于是规则**编译通过、链接失败**,报的两个符号既不说是哪一次调用要 的,也不说原因。 `std::format` 是 header-only。三十七处消息改成先 format 再 stream,六个模块各留 一段说明为什么。 ⭐ **为什么此前没人看见**:mcpp 自己的 macOS CI 跑在 `macos-15` 上,那一版系统 libc++ 有这两个符号;而唯一在 build.mcpp 里用 `std::println` 的 e2e(110)带 `# requires: gcc`,macOS 上直接跳过。这个矩阵钉的是 `macos-14` —— 两个受支持 版本里更低的那个 —— 所以它看见了。钉子保留。 ## Windows:MCPP_VENDORED_XLINGS 指向一个不存在的文件 `registry/bin/xlings` 在那个平台上是 `xlings.exe`。报出来的是 error: xlings binary not found 后面跟着三条补救建议,没有一条是实际适用的那条。改成按宿主选后缀,并在导出前 断言那个文件确实存在 —— 一个指向不存在文件的变量不该以"没配置"的形态报出来。 --- .github/workflows/ci.yml | 10 ++++++- rules/ascendc.cppm | 37 ++++++++++++++++------- rules/cuda.cppm | 65 +++++++++++++++++++++++----------------- rules/hip.cppm | 46 ++++++++++++++++++---------- rules/spirv.cppm | 41 +++++++++++++++++-------- rules/sycl.cppm | 51 +++++++++++++++++++------------ tools/embed.cppm | 32 +++++++++++++++----- 7 files changed, 188 insertions(+), 94 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77cd1d2..53807a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -404,7 +404,15 @@ jobs: "$MCPP" --version # The bundled xlings, named explicitly because MCPP_HOME is pinned # away from the tarball. See the Linux job for the measurement. - export MCPP_VENDORED_XLINGS="$PWD/$dir/registry/bin/xlings" + # + # The suffix is the host's: the Windows distribution ships + # `registry/bin/xlings.exe`, and a variable pointing at a file that + # does not exist produces `error: xlings binary not found` naming + # three remedies, none of which is the one that applies. + XL="$PWD/$dir/registry/bin/xlings" + [ -f "$XL" ] || XL="$XL.exe" + test -f "$XL" || { echo "no vendored xlings at $XL"; exit 1; } + export MCPP_VENDORED_XLINGS="$XL" "$MCPP" self config --mirror GLOBAL echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" diff --git a/rules/ascendc.cppm b/rules/ascendc.cppm index 43bef15..b7325d4 100644 --- a/rules/ascendc.cppm +++ b/rules/ascendc.cppm @@ -59,6 +59,25 @@ export module mcpp.rules.ascendc; import std; import mcpp; + +// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. +// +// `std::print` and `std::println` are not header-only. Both of their overloads +// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout +// form and `__get_ostream_file(ostream&)` for the stream form -- and those +// symbols were added to that library in a version macOS 14 does not ship. A +// build program's link resolves `-lc++` to the system copy there, so a rule +// that printed with `std::println` compiled and then failed to link: +// +// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) +// +// naming neither the call that needed it nor the reason. Measured on +// macos-14; macos-15 has the symbol, which is why nothing saw this until a +// rule was first compiled on the older of the two supported releases. +// +// `std::format` is header-only and has no such dependency, so every message in +// this file is formatted and then streamed. + export namespace mcpp::rules::ascendc { // ─── What the engine said ────────────────────────────────────────────────── @@ -209,8 +228,7 @@ inline std::optional find_toolkit() { toolkit t; const auto pkg = xpkg("cann-toolkit"); if (pkg.empty()) { - std::println(std::cerr, - "mcpp.rules.ascendc: the CANN toolkit is not installed.\n" + std::cerr << std::format("mcpp.rules.ascendc: the CANN toolkit is not installed.\n" " This rule DECLARES it, so a project normally writes nothing. Check, in " "order:\n" " mcpp older than 2026.9.6.6; `features = [\"rules-ascendc\"]` missing from " @@ -220,7 +238,7 @@ inline std::optional find_toolkit() { " [target.'cfg(accelerator = \"ascend\")'.xlings.workspace]\n" " \"xim:cann-toolkit\" = \"8.5.0\"\n" " It carries both halves this rule needs: the device compiler and,\n" - " for a machine with no NPU, the per-SoC simulators."); + " for a machine with no NPU, the per-SoC simulators.") << '\n'; return std::nullopt; } t.root = pkg + "/cann"; @@ -229,10 +247,9 @@ inline std::optional find_toolkit() { if (std::filesystem::is_directory(candidate)) { t.arch_root = candidate; break; } } if (t.arch_root.empty() || !std::filesystem::exists(t.bisheng())) { - std::println(std::cerr, - "mcpp.rules.ascendc: `xim:cann-toolkit` is installed at '{}' but has no\n" + std::cerr << std::format("mcpp.rules.ascendc: `xim:cann-toolkit` is installed at '{}' but has no\n" " device compiler under -linux/ccec_compiler/bin/bisheng.\n" - " The install is incomplete; reinstall the package.", pkg); + " The install is incomplete; reinstall the package.", pkg) << '\n'; return std::nullopt; } return t; @@ -258,8 +275,7 @@ inline std::vector plan(std::span sources, options opt std::vector out; const std::string root = mcpp::manifest_dir(); if (root.empty()) { - std::println(std::cerr, - "mcpp.rules.ascendc: no mcpp build context -- this runs from build.mcpp"); + std::cerr << std::format("mcpp.rules.ascendc: no mcpp build context -- this runs from build.mcpp") << '\n'; return out; } const auto tg = parse_target(mcpp::accel()); @@ -267,12 +283,11 @@ inline std::vector plan(std::span sources, options opt // The same refusal `mcpp.rules.cuda` makes, for the same reason: a // device build that names no device is refused HERE rather than at run // time, where it is a kernel that does not exist for the part present. - std::println(std::cerr, - "mcpp.rules.ascendc: [build] accel names no Da Vinci architecture " + std::cerr << std::format("mcpp.rules.ascendc: [build] accel names no Da Vinci architecture " "(accel = \"{}\").\n" " Write e.g. accel = \"ascend8.5+{{dav-c220}}\" -- the set a build\n" " compiles for is a decision, and the machine's own hardware is a poor\n" - " default for it.", mcpp::accel()); + " default for it.", mcpp::accel()) << '\n'; return out; } auto tk = find_toolkit(); diff --git a/rules/cuda.cppm b/rules/cuda.cppm index 207fa80..27c4c44 100644 --- a/rules/cuda.cppm +++ b/rules/cuda.cppm @@ -45,6 +45,25 @@ export module mcpp.rules.cuda; import std; import mcpp; + +// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. +// +// `std::print` and `std::println` are not header-only. Both of their overloads +// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout +// form and `__get_ostream_file(ostream&)` for the stream form -- and those +// symbols were added to that library in a version macOS 14 does not ship. A +// build program's link resolves `-lc++` to the system copy there, so a rule +// that printed with `std::println` compiled and then failed to link: +// +// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) +// +// naming neither the call that needed it nor the reason. Measured on +// macos-14; macos-15 has the symbol, which is why nothing saw this until a +// rule was first compiled on the older of the two supported releases. +// +// `std::format` is header-only and has no such dependency, so every message in +// this file is formatted and then streamed. + export namespace mcpp::rules::cuda { // A PROGRAM'S NAME CARRIES ITS HOST'S SUFFIX, AND THE PAYLOAD LAYOUT DOES TOO. @@ -245,8 +264,7 @@ inline std::optional find_toolkit() { t.cccl_root = xpkg("cuda-cccl"); t.driver_dir = xpkg("libcuda-host-link"); if (t.nvcc_root.empty() || t.cudart_root.empty()) { - std::println(std::cerr, - "mcpp.rules.cuda: the toolkit is not installed.\n" + std::cerr << std::format("mcpp.rules.cuda: the toolkit is not installed.\n" " This rule DECLARES it, so a project normally writes nothing. Three " "things stop\n" " that from reaching the build, in the order worth checking:\n" @@ -260,7 +278,7 @@ inline std::optional find_toolkit() { " [target.'cfg(accelerator = \"cuda\")'.xlings.workspace]\n" " \"xim:cuda-nvcc\" = \"12.9.86\"\n" " \"xim:cuda-cudart\" = \"12.9.79\"\n" - " (found nvcc: '{}', cudart: '{}')", t.nvcc_root, t.cudart_root); + " (found nvcc: '{}', cudart: '{}')", t.nvcc_root, t.cudart_root) << '\n'; return std::nullopt; } return t; @@ -496,18 +514,17 @@ inline std::vector plan(std::span sources, options opt std::vector out; const std::string root = mcpp::manifest_dir(); if (root.empty()) { - std::println(std::cerr, "mcpp.rules.cuda: no mcpp build context -- this runs from build.mcpp"); + std::cerr << std::format("mcpp.rules.cuda: no mcpp build context -- this runs from build.mcpp") << '\n'; return out; } const auto tg = parse_target(mcpp::accel()); if (!tg.present || tg.archs.empty()) { // C19: a device build that names no device is refused HERE, not at // run time as `no kernel image is available for execution`. - std::println(std::cerr, - "mcpp.rules.cuda: [build] accel names no CUDA architecture (accel = \"{}\").\n" + std::cerr << std::format("mcpp.rules.cuda: [build] accel names no CUDA architecture (accel = \"{}\").\n" " Write e.g. accel = \"cuda12.9+{{sm_89}} ptx>=89\" -- the set a build compiles\n" " for is a decision, and the machine's own hardware is a poor default for it.", - mcpp::accel()); + mcpp::accel()) << '\n'; return out; } auto tk = find_toolkit(); @@ -521,8 +538,7 @@ inline std::vector plan(std::span sources, options opt if (r == route::clang) { driver_cc = tcdir + "/bin/clang++" + kExe; if (!std::filesystem::exists(driver_cc)) { - std::println(std::cerr, - "mcpp.rules.cuda: the clang route needs the toolchain's clang++ at {}.\n" + std::cerr << std::format("mcpp.rules.cuda: the clang route needs the toolchain's clang++ at {}.\n" " Name an LLVM toolchain for this project:\n" " [toolchain]\n" " default = \"llvm@22.1.8\"{}", @@ -530,7 +546,7 @@ inline std::vector plan(std::span sources, options opt kWindows ? "\n On Windows this route is the only one the rule takes: nvcc " "drives a host\n compiler named by -ccbin, and locating MSVC's " "cl.exe is not something\n this rule does." - : ""); + : "") << '\n'; return out; } // Refused here rather than at clang's include error: the header it @@ -540,15 +556,14 @@ inline std::vector plan(std::span sources, options opt const bool curand_ok = !tk->curand_root.empty() && std::filesystem::exists(tk->curand_root + "/include/curand_mtgp32_kernel.h"); if (!curand_ok || !tk->has_cccl()) { - std::println(std::cerr, - "mcpp.rules.cuda: the clang route needs cuRAND's headers, which clang's CUDA " + std::cerr << std::format("mcpp.rules.cuda: the clang route needs cuRAND's headers, which clang's CUDA " "wrapper includes unconditionally, and CCCL's, which they include in turn.\n" " This rule declares both; see the note above for why they may not have " "arrived.\n" " To pin a different line, name it in your own project and it wins:\n" " \"xim:cuda-cccl\" = \"12.9.27\" (the 12.9 line; 13.x pairs with 13.x)\n" " \"xim:libcurand\" = \"10.3.10.19\" (the 12.9 line; 10.4.x pairs with 13.x)\n" - " (found cccl: '{}', curand: '{}')", tk->cccl_root, tk->curand_root); + " (found cccl: '{}', curand: '{}')", tk->cccl_root, tk->curand_root) << '\n'; return out; } front = { driver_cc, "-x", "cuda", "-std=c++17", "-O2", @@ -582,7 +597,7 @@ inline std::vector plan(std::span sources, options opt if (!std::filesystem::exists(tk->nvcc_root + "/bin/" + tool + kExe)) mcpp::warning(std::format("the toolkit payload has no {}; clang invokes it " "after generating PTX", tool).c_str()); - std::println("mcpp.rules.cuda: clang route -- {} (toolkit {})", driver_cc, tk->nvcc_root); + std::cout << std::format("mcpp.rules.cuda: clang route -- {} (toolkit {})", driver_cc, tk->nvcc_root) << '\n'; } else { // ASKED FOR EXPLICITLY, BECAUSE `decide` NEVER CHOOSES IT HERE. // Everything below reads a GCC bound out of the toolkit's header and @@ -591,15 +606,14 @@ inline std::vector plan(std::span sources, options opt // `_MSC_VER`, so the branch would build a command line out of two // answers that mean nothing and nvcc would report the third. if constexpr (kWindows) { - std::println(std::cerr, - "mcpp.rules.cuda: the nvcc route was asked for, and on Windows this rule does " + std::cerr << std::format("mcpp.rules.cuda: the nvcc route was asked for, and on Windows this rule does " "not take it.\n" " nvcc compiles the host half through a compiler named by -ccbin, which on " "this host\n is MSVC's cl.exe; finding it means asking the machine about its " "Visual Studio\n installation, which is the kind of host dependence this " "ecosystem removes.\n" " Leave the route unset: the clang route is the default here and needs no " - "second compiler."); + "second compiler.") << '\n'; return out; } // nvcc drives the toolchain's own compiler, and refuses one newer than @@ -612,10 +626,9 @@ inline std::vector plan(std::span sources, options opt // LLVM toolchain's clang uses. The pairing that works is nvcc with // a GCC toolchain; with an LLVM toolchain the clang route is the // one to take, and it is the default. - std::println(std::cerr, - "mcpp.rules.cuda: the nvcc route needs a GCC host compiler; this project's " + std::cerr << std::format("mcpp.rules.cuda: the nvcc route needs a GCC host compiler; this project's " "toolchain is LLVM, whose clang uses libc++ and nvcc refuses it. Use the clang " - "route (the default for an LLVM toolchain) or set [toolchain] to a gcc payload."); + "route (the default for an LLVM toolchain) or set [toolchain] to a gcc payload.") << '\n'; return out; } // The other pairing this route cannot have: an old toolkit and a C @@ -624,8 +637,7 @@ inline std::vector plan(std::span sources, options opt // decision. if (major_of(tg.version) < 13 && libc_declares_c23_pi_math(mcpp::toolchain_sysroot())) { - std::println(std::cerr, - "mcpp.rules.cuda: toolkit {} redeclares the C23 functions cospi, sinpi and " + std::cerr << std::format("mcpp.rules.cuda: toolkit {} redeclares the C23 functions cospi, sinpi and " "rsqrt for the host without `noexcept`, and the C library this build compiles " "against declares them with it; nvcc's front end refuses the pair.\n" " Name a 13.x toolkit, whose headers leave them to the C library:\n" @@ -634,7 +646,7 @@ inline std::vector plan(std::span sources, options opt " \"xim:cuda-crt\" = \"13.3.33\"\n" " \"xim:cuda-cudart\" = \"13.3.29\"\n" " or take the clang route, which does not include that header at all.", - tg.version); + tg.version) << '\n'; return out; } // The host compiler nvcc drives, chosen within the bound the toolkit @@ -659,14 +671,13 @@ inline std::vector plan(std::span sources, options opt "route has no such bound.", tg.version, b.gcc, tk->host_config(), tcMajor, driver_cc).c_str()); } else { - std::println(std::cerr, - "mcpp.rules.cuda: nvcc {} accepts gcc <= {} ({}), and this project's " + std::cerr << std::format("mcpp.rules.cuda: nvcc {} accepts gcc <= {} ({}), and this project's " "toolchain is gcc {}.\n" " Declare a gcc payload within the bound and the rule drives that one:\n" " [xlings.workspace]\n" " \"xim:gcc\" = \"13.3.0\"\n" " or take the clang route with [toolchain] default = \"llvm@22.1.8\".", - tg.version, b.gcc, tk->host_config(), tcMajor); + tg.version, b.gcc, tk->host_config(), tcMajor) << '\n'; return out; } front = { tk->nvcc(), "-ccbin", driver_cc, "-std=c++17", "-O2", @@ -700,7 +711,7 @@ inline std::vector plan(std::span sources, options opt "nvcc cannot reach its own back-end: it invokes '{}' by name and that name " "does not resolve on the search path it states. On the 13.x line install " "xim:libnvvm beside xim:cuda-nvcc.", *missing).c_str()); - std::println("mcpp.rules.cuda: nvcc route -- {} with -ccbin {}", tk->nvcc(), driver_cc); + std::cout << std::format("mcpp.rules.cuda: nvcc route -- {} with -ccbin {}", tk->nvcc(), driver_cc) << '\n'; } // The link line gets its directories from here, not from the manifest: the diff --git a/rules/hip.cppm b/rules/hip.cppm index f4388ac..c7cb79a 100644 --- a/rules/hip.cppm +++ b/rules/hip.cppm @@ -38,6 +38,25 @@ export module mcpp.rules.hip; import std; import mcpp; + +// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. +// +// `std::print` and `std::println` are not header-only. Both of their overloads +// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout +// form and `__get_ostream_file(ostream&)` for the stream form -- and those +// symbols were added to that library in a version macOS 14 does not ship. A +// build program's link resolves `-lc++` to the system copy there, so a rule +// that printed with `std::println` compiled and then failed to link: +// +// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) +// +// naming neither the call that needed it nor the reason. Measured on +// macos-14; macos-15 has the symbol, which is why nothing saw this until a +// rule was first compiled on the older of the two supported releases. +// +// `std::format` is header-only and has no such dependency, so every message in +// this file is formatted and then streamed. + export namespace mcpp::rules::hip { // The two implementations, named. `automatic` reads the accelerator axis. @@ -222,8 +241,7 @@ inline std::vector plan(std::span sources, options opt std::vector out; const std::string root = mcpp::manifest_dir(); if (root.empty()) { - std::println(std::cerr, - "mcpp.rules.hip: no mcpp build context -- this runs from build.mcpp"); + std::cerr << std::format("mcpp.rules.hip: no mcpp build context -- this runs from build.mcpp") << '\n'; return out; } @@ -238,27 +256,25 @@ inline std::vector plan(std::span sources, options opt // runtime and device library, and this ecosystem publishes neither // yet; compiling for it would produce an object nothing on this // machine can link or run. - std::println(std::cerr, - "mcpp.rules.hip: [build] accel names AMD architectures ({}) and no ROCm\n" + std::cerr << std::format("mcpp.rules.hip: [build] accel names AMD architectures ({}) and no ROCm\n" " payload is published in this ecosystem yet, so nothing could link or run\n" " the result. The NVIDIA platform is available today:\n" " accel = \"hip, cuda12.9+{{sm_89}}\"\n" " which reaches the device through the CUDA runtime, with HIP as the API.", - tg.amd_archs.empty() ? std::string("none") : tg.amd_archs.front()); + tg.amd_archs.empty() ? std::string("none") : tg.amd_archs.front()) << '\n'; return out; } if (tg.cuda_archs.empty()) { // A device build that names no device is refused here, not at run time // as `no kernel image is available for execution`. - std::println(std::cerr, - "mcpp.rules.hip: [build] accel names no device architecture (accel = \"{}\").\n" + std::cerr << std::format("mcpp.rules.hip: [build] accel names no device architecture (accel = \"{}\").\n" " On the NVIDIA platform HIP compiles through the CUDA back end, and the\n" " device is spelled the way every other rule in this ecosystem spells it:\n" " accel = \"hip, cuda12.9+{{sm_89}}\"\n" " The set a build compiles for is a decision; the machine's own hardware is\n" " a poor default for it.", - mcpp::accel()); + mcpp::accel()) << '\n'; return out; } @@ -289,8 +305,7 @@ inline std::vector plan(std::span sources, options opt if (n.root->empty()) missing += std::format(" \"xim:{}\" = \"{}\"\n", n.pkg, n.version); if (!missing.empty()) { - std::println(std::cerr, - "mcpp.rules.hip: the HIP island needs payloads that are not installed.\n" + std::cerr << std::format("mcpp.rules.hip: the HIP island needs payloads that are not installed.\n" " This rule DECLARES them, so a project normally writes nothing. Check, in " "order:\n" " mcpp older than 2026.9.6.6; `features = [\"rules-hip\"]` missing from the\n" @@ -298,7 +313,7 @@ inline std::vector plan(std::span sources, options opt " To pin different versions, name them in your own project and they win:\n\n" " [target.'cfg(accelerator = \"hip\")'.xlings.workspace]\n{}\n" " They are PAYLOADS: the version is the project's choice, not the machine's.", - missing); + missing) << '\n'; return out; } @@ -315,10 +330,9 @@ inline std::vector plan(std::span sources, options opt const std::string tcdir = mcpp::toolchain_dir(); const std::string cc = tcdir + "/bin/clang++"; if (tcdir.empty() || !std::filesystem::exists(cc)) { - std::println(std::cerr, - "mcpp.rules.hip: the NVIDIA platform compiles through clang, and this " + std::cerr << std::format("mcpp.rules.hip: the NVIDIA platform compiles through clang, and this " "project's\n toolchain has no clang++ at {}.\n" - " Select an LLVM toolchain: [toolchain] default = \"llvm@22.1.8\"", cc); + " Select an LLVM toolchain: [toolchain] default = \"llvm@22.1.8\"", cc) << '\n'; return out; } @@ -382,9 +396,9 @@ inline std::vector plan(std::span sources, options opt // rule resolved the payload, so the rule names where its libraries are. for (auto const& d : tk.lib_dirs()) mcpp::link_search(d.c_str()); - std::println("mcpp.rules.hip: NVIDIA platform -- HIP {} over CUDA {}, {} for {}", + std::cout << std::format("mcpp.rules.hip: NVIDIA platform -- HIP {} over CUDA {}, {} for {}", hip_version(tk.hip_root), tg.cuda_version.empty() ? "?" : tg.cuda_version, - std::filesystem::path(cc).filename().string(), tg.cuda_archs.front()); + std::filesystem::path(cc).filename().string(), tg.cuda_archs.front()) << '\n'; for (auto const& src : sources) { const auto stem = std::filesystem::path(src).stem().string(); diff --git a/rules/spirv.cppm b/rules/spirv.cppm index 90de39c..773eaaa 100644 --- a/rules/spirv.cppm +++ b/rules/spirv.cppm @@ -58,6 +58,25 @@ export module mcpp.rules.spirv; import std; import mcpp; + +// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. +// +// `std::print` and `std::println` are not header-only. Both of their overloads +// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout +// form and `__get_ostream_file(ostream&)` for the stream form -- and those +// symbols were added to that library in a version macOS 14 does not ship. A +// build program's link resolves `-lc++` to the system copy there, so a rule +// that printed with `std::println` compiled and then failed to link: +// +// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) +// +// naming neither the call that needed it nor the reason. Measured on +// macos-14; macos-15 has the symbol, which is why nothing saw this until a +// rule was first compiled on the older of the two supported releases. +// +// `std::format` is header-only and has no such dependency, so every message in +// this file is formatted and then streamed. + export namespace mcpp::rules::spirv { struct options { @@ -226,10 +245,9 @@ inline compiler find_compiler(const options& opt) { // Named but unrecognised: taking it as glslang would pass glslang's // flags to something that is not glslang, and the error would name // a flag rather than this decision. - std::println(stderr, - "mcpp.rules.spirv: options::compiler names '{}', which is neither glslang\n" + std::cerr << std::format("mcpp.rules.spirv: options::compiler names '{}', which is neither glslang\n" " nor glslc by program name, and the two share almost no flags. Rename the\n" - " program or point at the real one.", opt.compiler); + " program or point at the real one.", opt.compiler) << '\n'; return { .reported = true }; } return { opt.compiler, k }; @@ -407,7 +425,7 @@ inline bool wrap_glslc_output(const std::string& header, const std::string& inc, const std::string& sym) { std::ofstream out{header, std::ios::trunc}; if (!out) { - std::println(stderr, "mcpp.rules.spirv: cannot write {}", header); + std::cerr << std::format("mcpp.rules.spirv: cannot write {}", header) << '\n'; return false; } out << "// Generated by mcpp.rules.spirv. glslc emits an initialiser list;\n" @@ -426,8 +444,7 @@ inline bool compile(std::span shaders, options opt = {}) { const auto cc = find_compiler(opt); if (!cc) { if (cc.reported) return false; - std::println(stderr, - "mcpp.rules.spirv: no shader compiler found.\n" + std::cerr << std::format("mcpp.rules.spirv: no shader compiler found.\n" " This rule DECLARES glslang, so a project normally writes nothing. Check, in " "order:\n" " mcpp older than 2026.9.6.6; `features = [\"rules-spirv\"]` missing from the\n" @@ -438,7 +455,7 @@ inline bool compile(std::span shaders, options opt = {}) { " \"xim:glslang\" = \"15.1.0\" # glslangValidator\n" " \"xim:shaderc\" = \"2026.3\" # glslc\n" "or name it: MCPP_GLSLANG=/path/to/glslangValidator, MCPP_GLSLC=/path/to/glslc,\n" - "or set options::compiler."); + "or set options::compiler.") << '\n'; return false; } // The fact is keyed on the flavour, not on a shared name: which of the two @@ -493,8 +510,7 @@ inline bool compile(std::span shaders, options opt = {}) { const auto key = p.stem().string() + "_" + std::string(stage); auto [it, fresh] = seen.try_emplace(key, src); if (!fresh) { - std::println(std::cerr, - "mcpp.rules.spirv: two shaders map to one output.\n" + std::cerr << std::format("mcpp.rules.spirv: two shaders map to one output.\n" " {}\n" " {}\n" " both produce `{}.h` declaring `{}`, because the name is the " @@ -504,7 +520,7 @@ inline bool compile(std::span shaders, options opt = {}) { " headers reaching one translation unit would still collide on the " "symbol.\n" " fix: rename one of them, or compile only one.", - it->second, src, key, symbol_of(p.stem().string(), stage)); + it->second, src, key, symbol_of(p.stem().string(), stage)) << '\n'; return false; } } @@ -514,10 +530,9 @@ inline bool compile(std::span shaders, options opt = {}) { const std::filesystem::path p(src); const auto stage = stage_of(p.extension().string()); if (stage.empty()) { - std::println(stderr, - "mcpp.rules.spirv: {} has no shader stage. Both compilers derive the stage " + std::cerr << std::format("mcpp.rules.spirv: {} has no shader stage. Both compilers derive the stage " "from the extension; rename it to one of .comp .vert .frag .geom .tesc " - ".tese .mesh .task .rgen .rint .rahit .rchit .rmiss .rcall", src); + ".tese .mesh .task .rgen .rint .rahit .rchit .rmiss .rcall", src) << '\n'; return false; } const auto sym = symbol_of(p.stem().string(), stage); diff --git a/rules/sycl.cppm b/rules/sycl.cppm index 4190768..cd204d3 100644 --- a/rules/sycl.cppm +++ b/rules/sycl.cppm @@ -73,6 +73,25 @@ export module mcpp.rules.sycl; import std; import mcpp; + +// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. +// +// `std::print` and `std::println` are not header-only. Both of their overloads +// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout +// form and `__get_ostream_file(ostream&)` for the stream form -- and those +// symbols were added to that library in a version macOS 14 does not ship. A +// build program's link resolves `-lc++` to the system copy there, so a rule +// that printed with `std::println` compiled and then failed to link: +// +// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) +// +// naming neither the call that needed it nor the reason. Measured on +// macos-14; macos-15 has the symbol, which is why nothing saw this until a +// rule was first compiled on the older of the two supported releases. +// +// `std::format` is header-only and has no such dependency, so every message in +// this file is formatted and then streamed. + export namespace mcpp::rules::sycl { struct options { @@ -275,30 +294,27 @@ inline std::vector plan(std::span sources, options opt std::vector out; const std::string root = mcpp::manifest_dir(); if (root.empty()) { - std::println(std::cerr, - "mcpp.rules.sycl: no mcpp build context -- this runs from build.mcpp"); + std::cerr << std::format("mcpp.rules.sycl: no mcpp build context -- this runs from build.mcpp") << '\n'; return out; } const auto tg = parse_target(mcpp::accel()); if constexpr (kWindows) { if (!tg.cuda_archs.empty()) { - std::println(std::cerr, - "mcpp.rules.sycl: [build] accel names an NVIDIA target and this host's SYCL\n" + std::cerr << std::format("mcpp.rules.sycl: [build] accel names an NVIDIA target and this host's SYCL\n" " compiler cannot reach it. Upstream states that the CUDA and HIP plugins are\n" " not built for Windows, and the published asset agrees: its Unified Runtime\n" " adapters are Level Zero and OpenCL, and no others.\n" " Available on this host: accel = \"sycl\" -- SPIR-V, consumed by whichever\n" - " Level Zero or OpenCL device the runtime finds."); + " Level Zero or OpenCL device the runtime finds.") << '\n'; return out; } } if (!tg.amd_archs.empty() && tg.cuda_archs.empty()) { - std::println(std::cerr, - "mcpp.rules.sycl: [build] accel names AMD architectures and this ecosystem\n" + std::cerr << std::format("mcpp.rules.sycl: [build] accel names AMD architectures and this ecosystem\n" " publishes no ROCm payload yet, so nothing could link or run the result.\n" " Available today: accel = \"sycl\" (SPIR-V, any device the runtime finds)\n" - " or accel = \"sycl, cuda12.9+{{sm_89}}\" (ahead of time for NVIDIA)."); + " or accel = \"sycl, cuda12.9+{{sm_89}}\" (ahead of time for NVIDIA).") << '\n'; return out; } @@ -355,8 +371,7 @@ inline std::vector plan(std::span sources, options opt if (!tg.cuda_archs.empty() && cuda.empty()) missing += " \"xim:cuda-nvcc\" = \"12.9.86\"\n"; if (!missing.empty()) { - std::println(std::cerr, - "mcpp.rules.sycl: the SYCL island needs payloads that are not installed.\n" + std::cerr << std::format("mcpp.rules.sycl: the SYCL island needs payloads that are not installed.\n" " This rule DECLARES them, so a project normally writes nothing. Check, in " "order:\n" " mcpp older than 2026.9.6.6; `features = [\"rules-sycl\"]` missing from the\n" @@ -368,16 +383,15 @@ inline std::vector plan(std::span sources, options opt " library underneath it. Without them dpcpp's clang reads the HOST's headers,\n" " which is measurable in its include search list and invisible on its command\n" " line.", - missing); + missing) << '\n'; return out; } auto exe = opt.compiler; if (exe.empty()) exe = dpcpp + "/bin/clang++" + kExe; if (!is_file(exe)) { - std::println(std::cerr, - "mcpp.rules.sycl: {} is not a file. The dpcpp payload publishes its SYCL\n" - " compiler under clang's own name; set options::compiler to name another.", exe); + std::cerr << std::format("mcpp.rules.sycl: {} is not a file. The dpcpp payload publishes its SYCL\n" + " compiler under clang's own name; set options::compiler to name another.", exe) << '\n'; return out; } if (auto v = compiler_version(exe); !v.empty()) mcpp::fact("dpcpp", v.c_str()); @@ -386,9 +400,8 @@ inline std::vector plan(std::span sources, options opt const auto gid = kWindows ? std::string{} : gcc_install_dir(gcc); if constexpr (!kWindows) { if (gid.empty()) { - std::println(std::cerr, - "mcpp.rules.sycl: the xim:gcc payload at {} has no lib/gcc//\n" - " directory, which is what --gcc-install-dir names.", gcc); + std::cerr << std::format("mcpp.rules.sycl: the xim:gcc payload at {} has no lib/gcc//\n" + " directory, which is what --gcc-install-dir names.", gcc) << '\n'; return out; } } @@ -452,11 +465,11 @@ inline std::vector plan(std::span sources, options opt "scheduler, where the program cannot catch it. Name the device to " "compile ahead of time: accel = \"sycl, cuda12.9+{sm_89}\"."); - std::println("mcpp.rules.sycl: {} -- {} for {}", + std::cout << std::format("mcpp.rules.sycl: {} -- {} for {}", tg.cuda_archs.empty() ? "SPIR-V, compiled by the runtime" : "ahead of time, NVIDIA back end", std::filesystem::path(exe).filename().string(), - tg.cuda_archs.empty() ? std::string("any device") : tg.cuda_archs.front()); + tg.cuda_archs.empty() ? std::string("any device") : tg.cuda_archs.front()) << '\n'; std::vector objects; for (auto const& src : sources) { diff --git a/tools/embed.cppm b/tools/embed.cppm index dad7fb2..6a1dd82 100644 --- a/tools/embed.cppm +++ b/tools/embed.cppm @@ -32,6 +32,25 @@ export module mcpp.tools.embed; import std; import mcpp; + +// WHY NOTHING HERE USES `std::println`, AND WHY THAT IS NOT A STYLE CHOICE. +// +// `std::print` and `std::println` are not header-only. Both of their overloads +// reach into the libc++ DYLIB -- `__is_posix_terminal(FILE*)` for the stdout +// form and `__get_ostream_file(ostream&)` for the stream form -- and those +// symbols were added to that library in a version macOS 14 does not ship. A +// build program's link resolves `-lc++` to the system copy there, so a rule +// that printed with `std::println` compiled and then failed to link: +// +// ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*) +// +// naming neither the call that needed it nor the reason. Measured on +// macos-14; macos-15 has the symbol, which is why nothing saw this until a +// rule was first compiled on the older of the two supported releases. +// +// `std::format` is header-only and has no such dependency, so every message in +// this file is formatted and then streamed. + export namespace mcpp::tools::embed { // The element the array is made of. A byte array is the general answer; a @@ -116,15 +135,14 @@ inline bool file(const std::filesystem::path& input, options opt = {}) { std::ifstream in(absolute, std::ios::binary); if (!in) { - std::println(stderr, "mcpp.tools.embed: cannot read {}", absolute.string()); + std::cerr << std::format("mcpp.tools.embed: cannot read {}", absolute.string()) << '\n'; return false; } std::string bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); if (opt.elem == element::word32 && bytes.size() % 4 != 0) { - std::println(stderr, - "mcpp.tools.embed: {} is {} bytes, which is not a multiple of 4, and " - "element::word32 was asked for", absolute.string(), bytes.size()); + std::cerr << std::format("mcpp.tools.embed: {} is {} bytes, which is not a multiple of 4, and " + "element::word32 was asked for", absolute.string(), bytes.size()) << '\n'; return false; } @@ -171,7 +189,7 @@ inline bool file(const std::filesystem::path& input, options opt = {}) { if (!opt.name_space.empty()) text += "\n} // namespace " + opt.name_space + "\n"; if (!write_if_different(out, text)) { - std::println(stderr, "mcpp.tools.embed: cannot write {}", out.string()); + std::cerr << std::format("mcpp.tools.embed: cannot write {}", out.string()) << '\n'; return false; } @@ -188,8 +206,8 @@ inline bool file(const std::filesystem::path& input, options opt = {}) { // to the first input only. inline bool files(std::span inputs, options opt = {}) { if (!opt.identifier.empty()) { - std::println(stderr, "mcpp.tools.embed: options::identifier names one " - "symbol and files() writes several; call file() per input"); + std::cerr << std::format("mcpp.tools.embed: options::identifier names one " + "symbol and files() writes several; call file() per input") << '\n'; return false; } for (auto const& one : inputs) From 04147cff1d28db7cfc89aa8b9d7c42aa525617a7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 18:23:57 +0800 Subject: [PATCH 6/9] =?UTF-8?q?ci:=20Windows=20=E7=9A=84=20POSIX=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A6=81=E8=BD=AC=E6=88=90=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6,macOS=20=E7=9F=A9=E9=98=B5=E6=9A=82=E9=92=89?= =?UTF-8?q?=20macos-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两条红都不是规则的问题。 Windows:这个 job 跑在 Git Bash 下,$PWD 是 `/d/a/...`,而 mcpp 是原生 Windows 程序,把 MCPP_VENDORED_XLINGS 当 Windows 路径读。后缀与路径形状两个差异报出来 是同一句 `xlings binary not found` 加同样三条建议,没有一条是「你给的路径是另一 种语法」。改成 cygpath -m。 macOS:macos-14 上构建程序链接失败于 `std::__1::__is_posix_terminal` —— 它由 `import std` 自己引到,规则改掉 std::println 并不足够。引擎那一半是 host_link_tokens 在 macOS 上提前返回、从不加载荷的运行时目录。矩阵暂钉 macos-15(与 mcpp 自己的 macOS CI 一致),等带修复的发布出来再移回 macos-14 —— 那是 README 声明的下限。 --- .github/workflows/ci.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53807a6..ecbf6c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -365,8 +365,28 @@ jobs: fail-fast: false matrix: include: + # macos-15, matching mcpp's own macOS CI, and the reason is a + # measured limitation of the older release rather than a preference. + # + # On macos-14 a build program fails to LINK, with + # + # ld64.lld: error: undefined symbol: + # std::__1::__is_posix_terminal(__sFILE*) + # + # referenced from `std::__1::__print::__is_terminal`. That symbol + # lives in the libc++ DYLIB and was added in a version macOS 14 does + # not ship, and the build program's link resolves `-lc++` to the + # system copy rather than to the LLVM payload's. It is reached + # through `import std` itself, so no rule package can avoid it -- the + # rules stopped using `std::println` for the same reason and it was + # not enough. + # + # Recorded rather than worked around here: the engine-side half is + # `host_link_tokens` returning before it adds the payload's runtime + # directories on macOS. Once a release carries that, this moves back + # to macos-14, which is the floor mcpp's README states. - name: macos arm64 - runs-on: macos-14 + runs-on: macos-15 asset: macosx-arm64.tar.gz dir-suffix: macosx-arm64 - name: windows x86_64 @@ -409,9 +429,17 @@ jobs: # `registry/bin/xlings.exe`, and a variable pointing at a file that # does not exist produces `error: xlings binary not found` naming # three remedies, none of which is the one that applies. + # Two host differences, and the second one is the interesting one. + # The suffix: the Windows distribution ships `xlings.exe`. The PATH + # SHAPE: this job runs under Git Bash, whose `$PWD` is a POSIX path + # (`/d/a/...`), while `mcpp` is a native Windows program and reads + # this variable as a Windows path. Both failures report the same + # `error: xlings binary not found` and offer the same three remedies, + # none of which is "the path you gave is in the other syntax". XL="$PWD/$dir/registry/bin/xlings" [ -f "$XL" ] || XL="$XL.exe" test -f "$XL" || { echo "no vendored xlings at $XL"; exit 1; } + command -v cygpath >/dev/null && XL=$(cygpath -m "$XL") export MCPP_VENDORED_XLINGS="$XL" "$MCPP" self config --mirror GLOBAL echo "MCPP=$MCPP" >> "$GITHUB_ENV" From 9d226af5553f28c8c51c5f60d1fb753d2219baa6 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 18:29:48 +0800 Subject: [PATCH 7/9] =?UTF-8?q?ci:=20=E4=B8=A4=E6=9D=A1=E8=B7=A8=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=9A=84=E8=B7=AF=E5=BE=84=E4=B8=8E=E7=AE=A1=E9=81=93?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows:MCPP_HOME 也要用宿主的路径语法。Git Bash 下 $HOME 是 `/c/Users/runneradmin`,而 mcpp 把这个变量交给 xlings —— 一个原生 Windows 程序,它答 `The filename, directory name, or volume label syntax is incorrect.` 然后退 1,而报出来的那一层说的是包名不是路径。 macOS:`mcpp self env | awk '...; exit'` 会在第一次匹配后关掉管道读端,mcpp 随后死于 `internal: unhandled exception: failed to write formatted output` (exit 70)。改成先写文件再读。两个 job 写成同一形状,免得再分叉。 这一轮 macOS 的读数:`rules-spirv through a consumer` 与 `every rule module compiles for this host` 都过了 —— 六个规则模块(含各自的 Windows/macOS 分支)在 macOS arm64 上编译通过,上一条提交换掉 std::println 的 修复成立。 --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecbf6c5..583dd8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,8 +257,14 @@ jobs: # rehearsal passed because a DEV build is disqualified from # self-contained mode and does use ~/.mcpp, so the rehearsal and CI # were clearing different directories. - HOME_DIR=$("$MCPP" self env | awk -F'= *' '/^MCPP_HOME/{print $2; exit}') - [ -n "$HOME_DIR" ] || { echo "FAIL: could not read MCPP_HOME"; exit 1; } + # Through a FILE rather than a pipe: on macOS this same expression + # killed mcpp with `internal: unhandled exception: failed to write + # formatted output`, because `awk ... exit` closes the read end at + # the first match. Written the same way on every platform so the two + # jobs cannot drift. + "$MCPP" self env > mcpp-env.txt + HOME_DIR=$(awk -F'= *' '/^MCPP_HOME/{print $2; exit}' mcpp-env.txt) + [ -n "$HOME_DIR" ] || { echo "FAIL: could not read MCPP_HOME"; cat mcpp-env.txt; exit 1; } echo "clearing provisioning stamps under $HOME_DIR" rm -rf "$HOME_DIR/provisioned" for d in tests/cuda-consumer tests/hip-consumer \ @@ -400,7 +406,17 @@ jobs: - uses: actions/checkout@v4 - name: Pin MCPP_HOME under this runner's home - run: echo "MCPP_HOME=$HOME/.mcpp" >> "$GITHUB_ENV" + run: | + set -e + # The value has to be in the HOST's path syntax, not Git Bash's. + # `$HOME` there is `/c/Users/runneradmin`, and mcpp hands this + # variable to xlings, which is a native Windows program: it answered + # `The filename, directory name, or volume label syntax is + # incorrect.` and exited 1, from inside a provisioning step whose own + # message named a package rather than a path. + H="$HOME/.mcpp" + command -v cygpath >/dev/null && H=$(cygpath -m "$H") + echo "MCPP_HOME=$H" >> "$GITHUB_ENV" - name: Cache the mcpp sandbox uses: actions/cache@v4 @@ -478,7 +494,13 @@ jobs: if grep -qE '^\[(target\..*\.)?xlings\.workspace\]' mcpp.toml; then echo "FAIL: the fixture declares payloads itself"; exit 1 fi - HOME_DIR=$("$MCPP" self env | awk -F'= *' '/^MCPP_HOME/{print $2; exit}') + # `mcpp self env > file`, not `mcpp self env | awk`. Measured on + # macos-15: through a pipe whose reader exits at the first match, + # mcpp died with `internal: unhandled exception: failed to write + # formatted output` and exit 70. A file has no reader to disappear. + "$MCPP" self env > env.txt + HOME_DIR=$(awk -F'= *' '/^MCPP_HOME/{print $2; exit}' env.txt) + test -n "$HOME_DIR" || { echo "FAIL: mcpp self env named no MCPP_HOME"; cat env.txt; exit 1; } rm -rf "$HOME_DIR/provisioned" target "$MCPP" build > prov.log 2>&1 grep -q 'entries declared by dependencies' prov.log || { From b1d0ffc560f9a93b0868bd2a8220f9428269c66a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 18:39:45 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20Windows/macOS=20=E7=9A=84=20shaderc?= =?UTF-8?q?=20=E6=94=B9=E6=88=90=E7=B2=BE=E7=A1=AE=E7=89=88=E6=9C=AC=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20cmd.exe=20=E6=8A=8A=20`>`=20=E8=AF=BB?= =?UTF-8?q?=E6=88=90=E9=87=8D=E5=AE=9A=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit windows-2022 上,声明 `xim:shaderc@>=2026.3` 的构建死在 Provisioning [xlings.workspace] entries declared by dependencies (xim:shaderc@>=2026.3) The filename, directory name, or volume label syntax is incorrect. error: provisioning ... failed: xlings exited 1 那句话是 cmd.exe 说的,说的是一个用不了的重定向目标。mcpp 把供给请求作为一个 JSON 参数放在 shell 命令行上;Windows 上解析它的是 cmd.exe,而 JSON 用的是 MSVCRT 的 `\"` 转义 —— cmd 不认这个转义,每个 `"` 都在切换它的引用状态,数到版本约束里 那个 `>` 的时候状态恰好是「未引用」,于是 `>` 成了重定向。 **在这之前没有任何一条能在 Windows 上生效的声明带过 `>`**,所以整个 `>=` 形态在 那个平台上从没被走到过。 精确版本不是伪装的绕过:mcpp 把裸版本读成**选择**,项目写一个不同的仍然赢并被报告。 macOS 取同一个值,让用这个编译器的两个平台一致。引擎侧的转义修好并发布之后,两处都 回到 `>=2026.3`。 --- mcpp.toml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 60c5cba..d53832c 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -139,11 +139,34 @@ tools-embed = { sources = ["tools/embed.cppm"] } [target.'cfg(all(accelerator = "vulkan", linux))'.feature-xlings.rules-spirv] "xim:glslang" = ">=15.1.0" +# AN EXACT VERSION HERE, WHERE LINUX HAS A FLOOR, AND THE REASON IS THE +# ENGINE'S COMMAND LINE RATHER THAN THE PACKAGE. +# +# mcpp hands its provisioning request to xlings as a JSON argument on a shell +# command line. On Windows that line is parsed by cmd.exe, whose quoting rules +# are not the ones the JSON is escaped for: the `\"` sequences leave cmd's +# quote state OFF by the time it reaches a `>` in a version constraint, so the +# `>` is read as a REDIRECTION. Measured on windows-2022 with mcpp 2026.9.6.6: +# +# Provisioning [xlings.workspace] entries declared by dependencies +# (xim:shaderc@>=2026.3) +# The filename, directory name, or volume label syntax is incorrect. +# error: ... xlings exited 1 +# +# -- cmd's own message about an unusable redirection target, arriving as a +# packaging failure. No declaration reachable on Windows had carried a `>` +# before, which is why the whole `>=` shape had never been exercised there. +# +# An exact version is a legitimate declaration and not a workaround in +# disguise: mcpp reads it as a CHOICE, so a project that wants a different one +# still wins and the override is reported. macOS takes the same value so the +# two platforms that use this compiler agree. Both revert to `>=2026.3` once a +# released engine escapes the argument for cmd. [target.'cfg(all(accelerator = "vulkan", macos))'.feature-xlings.rules-spirv] -"xim:shaderc" = ">=2026.3" +"xim:shaderc" = "2026.3" [target.'cfg(all(accelerator = "vulkan", windows))'.feature-xlings.rules-spirv] -"xim:shaderc" = ">=2026.3" +"xim:shaderc" = "2026.3" # 8.5.0 is a real floor rather than a preference: the mixed-mode object -- the # one carrying both the device binary and a host-callable launcher, which is From c8abf43a806337343a1f364838510a76a75b217c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 7 Sep 2026 18:47:40 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(spirv):=20popen=20=E6=98=AF=20POSIX=20?= =?UTF-8?q?=E7=9A=84,Windows=20=E6=8B=BC=E4=BD=9C=20=5Fpopen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit windows-2022 上 `mcpp.rules.spirv` 的宿主模块编译失败: rules/spirv.cppm:277:17: error: no member named 'popen' in the global namespace rules/spirv.cppm:282:7: error: no type named 'pclose' in the global namespace 同一形状此前在 sycl 里已经处理过。空设备也一样(`/dev/null` / `NUL`)。两者都命名 成常量,让调用点在每个宿主上读起来一样 —— 另一种写法是每个调用点套一个 `#if`,而 被忘掉的那一个恰好是没人编译的那一个。 hip 那条 lane 的 clang++ 也补上后缀:它今天只到得了 Linux(NVIDIA 平台的头文件包 只为它发布),所以这段 Windows 拼写没有任何东西在走。仍然写下来,因为错误的路径会 把自己报成「工具链缺 clang」。 CI:跨平台 job 里把「每条规则都为本宿主编译过」挪到端到端那一步之前。它更便宜、答案 更有信息量;放在后面时,它的失败以别人的构建错误形态到达 —— 一个与它无关的 consumer 报 `no member named 'popen'`。 --- .github/workflows/ci.yml | 30 ++++++++++++++++-------------- rules/hip.cppm | 10 ++++++++++ rules/spirv.cppm | 32 +++++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 583dd8f..9454261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -461,6 +461,22 @@ jobs: echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + # FIRST, BECAUSE IT IS THE CHEAPER QUESTION AND THE MORE INFORMATIVE + # ANSWER. Every other step here drives one rule end to end and needs + # that rule's payload; this one names no accelerator, downloads nothing, + # and asks only whether all six modules COMPILE for this host -- which + # is the half of a rule that a Linux-only CI never sees. Run after the + # end-to-end step, its failure arrived as somebody else's build error: + # `no member named 'popen' in the global namespace`, reported against a + # consumer that had nothing to do with it. + - name: every rule module compiles for this host + working-directory: tests/all-rules-compile + run: | + set -e + "$MCPP" build + "$MCPP" run | tee run.log + grep -q '^all-rules-compile ok' run.log + - name: rules-spirv through a consumer working-directory: tests/spirv-consumer run: | @@ -473,20 +489,6 @@ jobs: # come from the GRAPH, not from the fixture. Without it, a fixture that # quietly regained an `[xlings.workspace]` would keep this green while the # claim stopped being true. - # THE STEP THAT MAKES THIS JOB WORTH ITS NAME. Above it, one rule is - # exercised because one rule's compiler is published for these hosts. - # Below it, EVERY rule is compiled for them -- including the ones whose - # payload reaches a device that macOS and Windows do not both have, and - # whose host-dependent code was therefore written without ever being - # compiled on the host it was written for. - - name: every rule module compiles for this host - working-directory: tests/all-rules-compile - run: | - set -e - "$MCPP" build - "$MCPP" run | tee run.log - grep -q '^all-rules-compile ok' run.log - - name: the rule declared its own compiler working-directory: tests/spirv-consumer run: | diff --git a/rules/hip.cppm b/rules/hip.cppm index c7cb79a..eb5a8dd 100644 --- a/rules/hip.cppm +++ b/rules/hip.cppm @@ -328,7 +328,17 @@ inline std::vector plan(std::span sources, options opt // everywhere else. `mcpp.rules.cuda` takes the same path for the same // reason. const std::string tcdir = mcpp::toolchain_dir(); + // The suffix is the host's. This lane reaches only Linux today -- the + // NVIDIA-platform header package is published for it alone -- so the + // Windows spelling is not exercised by anything. It is written anyway, + // because the alternative is a path that is wrong on a host this rule + // will one day be asked about, and a wrong path reports itself as a + // missing toolchain. +#if defined(_WIN32) + const std::string cc = tcdir + "/bin/clang++.exe"; +#else const std::string cc = tcdir + "/bin/clang++"; +#endif if (tcdir.empty() || !std::filesystem::exists(cc)) { std::cerr << std::format("mcpp.rules.hip: the NVIDIA platform compiles through clang, and this " "project's\n toolchain has no clang++ at {}.\n" diff --git a/rules/spirv.cppm b/rules/spirv.cppm index 773eaaa..865884f 100644 --- a/rules/spirv.cppm +++ b/rules/spirv.cppm @@ -273,13 +273,38 @@ inline compiler find_compiler(const options& opt) { // magic, the second is the release. The release is what a floor compares, and // stating it as a fact is what makes a build log answer "which compiler // produced this SPIR-V" without anyone having to reproduce the build. +// `popen` is POSIX and Windows spells it `_popen`; the null device differs +// too. Both are named here so the call sites below read the same on every +// host -- the alternative is a `#if` around each one, and the one that gets +// forgotten is the one nobody compiles. +inline FILE* open_pipe(const std::string& cmd) { +#if defined(_WIN32) + return ::_popen(cmd.c_str(), "r"); +#else + return ::popen(cmd.c_str(), "r"); +#endif +} +inline void close_pipe(FILE* p) { +#if defined(_WIN32) + ::_pclose(p); +#else + ::pclose(p); +#endif +} +inline constexpr const char* kNullDevice = +#if defined(_WIN32) + "NUL"; +#else + "/dev/null"; +#endif + inline std::string run_and_capture(const std::string& cmd) { - FILE* p = ::popen(cmd.c_str(), "r"); + FILE* p = open_pipe(cmd); if (!p) return {}; std::string text; char buf[512]; while (std::fgets(buf, sizeof buf, p)) text += buf; - ::pclose(p); + close_pipe(p); return text; } @@ -301,7 +326,8 @@ inline bool has_optimizer(const std::string& exe) { } inline std::string compiler_version(const compiler& cc) { - const std::string text = run_and_capture("\"" + cc.path + "\" --version 2>/dev/null"); + const std::string text = run_and_capture("\"" + cc.path + "\" --version 2>" + + std::string(kNullDevice)); // glslc: `shaderc v2026.3 2fbab05...` on the first line. glslang: // `Glslang Version: 11:15.1.0`, whose first field is the SPIR-V generator // magic and whose second is the release.