From acc1a71a7dd9a0a15e076fcef4f6f41d1b93abd3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:11:40 +0800 Subject: [PATCH 01/22] fix: remove subos sysroot override, use payload paths for toolchain sysroot (#62) Remove M5.5 logic that forced tc->sysroot to mcpp's xlings subos (~/.mcpp/registry/subos/default). The subos created by `xlings self init` lacks linux kernel headers (linux/limits.h, asm/, asm-generic/), causing std module precompilation to fail on user machines. Root cause: commit 063fb6f changed MCPP_HOME to ~/.mcpp/, where the subos exists but is incomplete. M5.5 only checked exists(usr/include) and overwrote the toolchain's correct sysroot. CI never caught this because its subos is fully populated by `xlings self install`. Design principle: mcpp uses xlings only as a package index + download tool. Sysroot comes from the toolchain payload itself, not from subos. Changes: - cli.cppm: delete M5.5 subos sysroot override entirely - probe.cppm: parse clang++.cfg --sysroot= when -print-sysroot fails (Clang doesn't support -print-sysroot), so tc->sysroot reflects the payload's actual configuration - stdmod.cppm: generalize --no-default-config from macOS-only to all Clang toolchains with a cfg file (cfg paths become stale after mcpp copies the payload to its sandbox) - flags.cppm: sync the same --no-default-config logic for regular compilation flags --- ...21-linux-sysroot-missing-kernel-headers.md | 370 ++++++++++++++++++ src/build/flags.cppm | 38 +- src/cli.cppm | 26 +- src/toolchain/probe.cppm | 27 +- src/toolchain/stdmod.cppm | 39 +- 5 files changed, 444 insertions(+), 56 deletions(-) create mode 100644 .agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md diff --git a/.agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md b/.agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md new file mode 100644 index 0000000..01f5391 --- /dev/null +++ b/.agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md @@ -0,0 +1,370 @@ +# Linux sysroot 缺少内核头文件导致 std module 预编译失败 + +## 现象 + +在用户机器上(非 CI),使用 LLVM 或 GCC 工具链执行 `mcpp run` / `mcpp build` 时, +std module 预编译失败: + +``` +/home/speak/.mcpp/registry/subos/default/usr/include/bits/local_lim.h:38:10: +fatal error: 'linux/limits.h' file not found +``` + +GCC 和 Clang 均受影响,问题是系统性的。 + +## 根因 + +### 直接原因:M5.5 sysroot 覆盖逻辑 + +`cli.cppm:1192-1203` 中的 M5.5 逻辑,将 `tc->sysroot` 强制覆盖为 mcpp 自己的 +subos(`~/.mcpp/registry/subos/default`): + +```cpp +if (!isMuslTc) { + if (auto cfg = get_cfg(); cfg) { + auto mcppSubos = (*cfg)->xlingsHome() / "subos" / "default"; + if (std::filesystem::exists(mcppSubos / "usr" / "include")) { + tc->sysroot = mcppSubos; + } + } +} +``` + +### 触发 commit + +**`063fb6f`** — 将 MCPP_HOME 从 xpkgs 包目录改为 `~/.mcpp/`,使 M5.5 +找到一个存在但不完整的 subos。 + +### CI/e2e 未发现的原因 + +CI 的 subos 由 `xlings self install` 完整初始化(含内核头文件),e2e 测试 +通过 `_inherit_toolchain.sh` 继承宿主完整 subos。 + +--- + +## 设计分析:当前问题的本质 + +### 当前架构的矛盾 + +mcpp 的工具链管理存在一个架构层面的矛盾: + +``` + ┌─────────────────────────┐ + │ xlings 下载 payload │ + │ ~/.xlings/data/xpkgs/ │ + │ (cfg/specs 路径正确) │ + └──────────┬──────────────┘ + │ copy (因 XLINGS_HOME 传播不可靠) + ▼ + ┌─────────────────────────┐ + │ mcpp sandbox 副本 │ + │ ~/.mcpp/registry/xpkgs/ │ + │ (cfg/specs 路径 stale!) │ + └──────────┬──────────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + GCC: specs Clang macOS: Clang Linux: + fixup 修复 --no-default 没有修复 ← BUG + 路径 ✅ -config ✅ 用 subos 补救 ✗ +``` + +**三个平台/工具链的 stale path 问题,用了三种不同的解法:** + +| 工具链 | stale path 来源 | 当前解法 | 状态 | +|--------|-----------------|---------|------| +| GCC (Linux) | specs 文件 | `fixup_gcc_specs()` 重写 specs | ✅ 正确但只修了 specs,没修 `-print-sysroot` | +| Clang (macOS) | clang++.cfg | `--no-default-config` + xcrun | ✅ 但只在 macOS | +| Clang (Linux) | clang++.cfg | M5.5 subos 覆盖 | ❌ subos 不完整 | + +**根本问题:mcpp 复制了 payload 但没有统一处理 stale path。** + +### mcpp 对 xlings 的依赖边界不清 + +当前 mcpp 对 xlings 有三层依赖: + +| 层次 | 内容 | 应该依赖? | +|------|------|-----------| +| 包下载 | xlings 作为包索引 + 下载工具 | ✅ 是 | +| payload 路径 | xpkgs 下具体包的文件结构 | ✅ 是 | +| subos | xlings 的沙箱 sysroot | ❌ 不应该 | + +M5.5 的问题就在于跨越了第三层边界——用 xlings 的内部实现细节(subos) +来补救 mcpp 自身的路径问题。 + +--- + +## 设计方案 + +### 核心原则 + +**mcpp 只把 xlings 当包索引 + 下载工具。工具链的编译环境由 payload 自描述, +mcpp 忠实读取,不替换、不覆盖。** + +### 方案:Payload-first + 统一 stale path 处理 + +#### 1. 工具链 sysroot 来源:只从 payload 获取 + +``` +┌─────────────────────────────────────────────────┐ +│ sysroot 解析优先级 │ +│ │ +│ 1. compiler -print-sysroot (GCC 原生支持) │ +│ → 路径存在则使用 │ +│ │ +│ 2. payload cfg 文件解析 (Clang clang++.cfg) │ +│ → 解析 --sysroot= 行,路径存在则使用 │ +│ │ +│ 3. macOS: xcrun --show-sdk-path │ +│ │ +│ 4. 空 (不传 --sysroot,让编译器用自身默认值) │ +│ │ +│ ✗ 不再有 subos fallback │ +└─────────────────────────────────────────────────┘ +``` + +**改动**: +- 删除 `cli.cppm` M5.5 subos 覆盖代码 +- `probe.cppm` 增加 cfg 文件解析作为第 2 优先级 + +#### 2. 复制 payload 时统一修复 stale path + +当前只有 GCC 做了 specs fixup,Clang 只在 macOS 做了 `--no-default-config`。 +应该统一为:**凡是复制了 payload,就修复其中的路径配置。** + +``` + copy payload 后 + │ + ┌────────┼────────┐ + ▼ ▼ ▼ + GCC specs Clang cfg 其他配置 + │ │ + ▼ ▼ + rewrite_gcc rewrite_cfg + _specs() _paths() + │ │ + ▼ ▼ + 新 sysroot 新 sysroot + 新 rpath 新 -isystem + 新 -L/-rpath +``` + +**具体做法**:在 `package_fetcher.cppm` 复制 payload 后(或在 `cli.cppm` +toolchain install 后),对 Clang cfg 做类似 `fixup_gcc_specs` 的路径重写: + +```cpp +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, + const std::filesystem::path& newSysroot) { + auto cfgPath = payloadRoot / "bin" / "clang++.cfg"; + if (!std::filesystem::exists(cfgPath)) return; + + // 读取 cfg,将旧 sysroot/isystem/rpath 路径替换为 payload 实际位置 + // ... +} +``` + +**但这里有一个关键设计选择:新路径指向哪里?** + +#### 3. 关于 sysroot 本身从哪来 + +工具链需要 C 库头文件(glibc headers + linux kernel headers)。来源有三种: + +| 来源 | 说明 | 优劣 | +|------|------|------| +| 系统 `/usr/include` | 宿主机自带 | 简单,但不可控,不同发行版不同 | +| xlings subos | xlings 管理的沙箱 sysroot | 可控,但 mcpp 需依赖 xlings 内部结构 | +| payload 自带 | 工具链包自带 sysroot(如 musl-gcc) | 最干净,但需要上游包支持 | + +**推荐策略**: + +- **短期**:信任 payload 自身配置的 sysroot 路径。xlings 安装 GCC/LLVM 时 + 已经配置好了 sysroot(指向 xlings 自己的 subos),mcpp 只需忠实读取。 + 如果路径存在且有效,就用它。如果路径无效,不传 `--sysroot`,让编译器 + 用系统默认路径。 + +- **中期**:推动 xlings 上游让 LLVM/GCC 包的 cfg/specs 使用相对路径或 + 可配置路径,避免硬编码绝对路径。这从源头消除 stale path 问题。 + +- **长期**:mcpp 自带轻量 sysroot 管理(类似 Zig 的做法:打包 libc headers + 作为 mcpp 自身的资源),彻底不依赖宿主系统或 xlings 的 sysroot。但这是 + 大工程,不急。 + +--- + +## 修复方案(基于以上设计) + +### Phase 1:修复当前 bug(最小改动) + +#### P1-1:删除 M5.5 subos 覆盖 + +**文件**:`src/cli.cppm:1192-1203` + +**删除**整个代码块。工具链的 sysroot 由 payload 决定,mcpp 不介入。 + +同时删除 `cli.cppm:1001` 的 subos 注释和 `cli.cppm:1178` 的 "glibc subos" 注释。 + +#### P1-2:`probe_sysroot` 增加 cfg 解析 + +**文件**:`src/toolchain/probe.cppm:254-272` + +`-print-sysroot` 失败后(Clang 不支持),解析 payload 中 `clang++.cfg` +的 `--sysroot=` 行: + +```cpp +std::filesystem::path +probe_sysroot(const std::filesystem::path& compilerBin, + const std::string& envPrefix) { + // 1. -print-sysroot (GCC) + auto r = run_capture(std::format("{}{} -print-sysroot {}", + envPrefix, + mcpp::xlings::shq(compilerBin.string()), + mcpp::platform::null_redirect)); + if (r) { + auto s = trim_line(*r); + if (!s.empty() && std::filesystem::exists(s)) return s; + } + + // 2. Parse payload cfg (Clang) + auto cfgPath = compilerBin.parent_path() + / (compilerBin.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + std::ifstream ifs(cfgPath); + std::string line; + while (std::getline(ifs, line)) { + constexpr std::string_view prefix = "--sysroot="; + if (line.starts_with(prefix)) { + auto val = trim_line(std::string(line.substr(prefix.size()))); + if (!val.empty() && std::filesystem::exists(val)) + return val; + } + } + } + + // 3. macOS: xcrun SDK + if (auto sdk = mcpp::platform::macos::sdk_path()) + return *sdk; + return {}; +} +``` + +**Phase 1 效果**: +- Clang:cfg 中的 `--sysroot=~/.xlings/subos/default` 被正确读取, + `tc->sysroot` 不再为空。stdmod.cppm 和 flags.cppm 传递正确的 sysroot。 +- GCC:`-print-sysroot` 正常工作(如果路径存在);若不存在则 sysroot 为空, + GCC 用默认系统路径(`/usr/include`)。 +- 不再依赖 subos。 + +### Phase 2:统一 Clang stale cfg 处理(消除隐患) + +#### P2-1:`stdmod.cppm` — 所有有 cfg 的 Clang 都走 `--no-default-config` + +**文件**:`src/toolchain/stdmod.cppm:103-116` + +将 macOS 特有的 `--no-default-config` 逻辑泛化为"有 cfg 文件的 Clang": + +```cpp +std::string sysroot_flag; +if (is_clang(tc)) { + auto cfgPath = tc.binaryPath.parent_path() + / (tc.binaryPath.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + // Bypass cfg (may have stale paths after payload copy). + // Provide correct flags from payload structure directly. + auto llvmRoot = tc.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); + if (!tc.sysroot.empty()) + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); + else if (auto sdk = mcpp::platform::macos::sdk_path()) + sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + } else if (!tc.sysroot.empty()) { + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); + } +} else if (!tc.sysroot.empty()) { + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); +} +``` + +#### P2-2:`flags.cppm` — 同步修改 + +**文件**:`src/build/flags.cppm:96-111` + +同步 P2-1 的逻辑:将 `is_macos_clang` 条件改为"检测到 cfg 文件存在"。 + +**Phase 2 效果**: +- Linux 和 macOS Clang 走统一路径 +- 不再依赖 cfg 中的路径碰巧有效 +- mcpp 从 payload 结构推导出正确的 `-isystem` 和 `--sysroot` + +### Phase 3(未来):复制 payload 时重写 cfg 路径 + +在 `package_fetcher.cppm` 或 `cli.cppm` toolchain install 后,添加 +`fixup_clang_cfg()`,类似 `fixup_gcc_specs()` 的做法: + +```cpp +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, + const std::filesystem::path& oldXlingsHome, + const std::filesystem::path& newRegistryHome) { + // 重写 clang++.cfg 中的路径: + // --sysroot= → --sysroot= + // -isystem → -isystem + // -L → -L + // -rpath, → -rpath, +} +``` + +这样即使不用 `--no-default-config`,cfg 路径也是正确的。 +但需要 mcpp 管理自己的 sysroot 内容(确保完整性),所以这是更远期的方向。 + +--- + +## 修改总结 + +| Phase | 修改 | 文件 | 效果 | +|-------|------|------|------| +| P1-1 | 删除 M5.5 | cli.cppm | 去除 subos 依赖 | +| P1-2 | cfg 解析 sysroot | probe.cppm | Clang 获取正确 sysroot | +| P2-1 | 统一 --no-default-config | stdmod.cppm | 消除 stale cfg 隐患 | +| P2-2 | 同步 P2-1 | flags.cppm | 常规编译也用正确路径 | +| P3 | cfg 路径重写 | package_fetcher/cli | 从根源修复 stale path | + +**Phase 1 修复 bug,Phase 2 消除隐患,Phase 3 完善架构。** + +--- + +## 测试补充 + +### 新增 e2e 测试:无 subos 下的 import std + +```bash +#!/usr/bin/env bash +# requires: import-std +# Test that import std works without mcpp's subos sysroot. +# Regression test: M5.5 subos override must not be required. +set -euo pipefail + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +export MCPP_INHERIT_SUBOS=0 +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml <<'EOF' +[package] +name = "sysroot_test" +version = "0.1.0" +EOF + +cat > src/main.cpp <<'EOF' +import std; +int main() { std::println("sysroot ok"); } +EOF + +"$MCPP" build +"$MCPP" run | grep -q "sysroot ok" +``` diff --git a/src/build/flags.cppm b/src/build/flags.cppm index e7278c7..4b85913 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -88,23 +88,29 @@ CompileFlags compute_flags(const BuildPlan& plan) { include_flags += " -I" + escape_path(abs); } - // Sysroot + config override for macOS. - // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and - // -isystem paths from the original install location. When the package is - // copied to mcpp's sandbox, these paths become stale. We pass - // --no-default-config to ignore the cfg and provide correct paths. + // Sysroot + config override for Clang with a driver config file. + // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem + // paths from the original install location. After mcpp copies the package + // to its sandbox, these paths may become stale. Detect the cfg and bypass + // it with --no-default-config, providing correct flags from the payload. std::string sysroot_flag; - bool is_macos_clang = mcpp::toolchain::is_clang(plan.toolchain) - && (plan.toolchain.targetTriple.find("apple") != std::string::npos - || plan.toolchain.targetTriple.find("darwin") != std::string::npos); - if (is_macos_clang) { - auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); - auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; - sysroot_flag += " -isystem" + escape_path(libcxxInclude); - if (auto sdk = mcpp::platform::macos::sdk_path()) - sysroot_flag += " --sysroot=" + escape_path(*sdk); - f.sysroot = sysroot_flag; + if (mcpp::toolchain::is_clang(plan.toolchain)) { + auto cfgPath = plan.toolchain.binaryPath.parent_path() + / (plan.toolchain.binaryPath.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + sysroot_flag += " -isystem" + escape_path(libcxxInclude); + if (!plan.toolchain.sysroot.empty()) + sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); + else if (auto sdk = mcpp::platform::macos::sdk_path()) + sysroot_flag += " --sysroot=" + escape_path(*sdk); + f.sysroot = sysroot_flag; + } else if (!plan.toolchain.sysroot.empty()) { + sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); + f.sysroot = sysroot_flag; + } } else if (!plan.toolchain.sysroot.empty()) { sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); f.sysroot = sysroot_flag; diff --git a/src/cli.cppm b/src/cli.cppm index 172e124..5c00bcd 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -998,9 +998,8 @@ prepare_build(bool print_fingerprint, // ─── Toolchain resolution (docs/21) ──────────────────────────────── // Priority chain: // 1. mcpp.toml [toolchain]. → resolve_xpkg_path → abs path - // 2. $MCPP_HOME/registry/subos//bin/g++ (xlings sandbox subos) - // 3. $CXX env var - // 4. PATH g++ (with warning) + // 2. $CXX env var + // 3. PATH g++ (with warning) std::filesystem::path explicit_compiler; std::optional cfg_opt; auto get_cfg = [&]() -> std::expected { @@ -1174,10 +1173,8 @@ prepare_build(bool print_fingerprint, if (!tc) return std::unexpected(tc.error().message); // For musl-gcc the toolchain is fully self-contained - // (`/x86_64-linux-musl/{include,lib}` is its own sysroot), and - // pointing it at mcpp's glibc subos breaks compilation. Skip the - // sysroot injection in that case — musl-gcc's `-dumpmachine` reports - // `x86_64-linux-musl`, which is also the marker we use elsewhere. + // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). + // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. bool isMuslTc = tc->targetTriple.find("-musl") != std::string::npos; // A musl toolchain only really makes sense with static linkage — @@ -1189,18 +1186,9 @@ prepare_build(bool print_fingerprint, m->buildConfig.linkage = "static"; } - // M5.5: prefer mcpp's xlings-managed subos as sysroot — it has glibc - // headers + libs in the conventional layout that GCC expects. The - // -print-sysroot output from a freshly-built GCC often points at - // some build-time path that doesn't exist on the user's machine. - if (!isMuslTc) { - if (auto cfg = get_cfg(); cfg) { - auto mcppSubos = (*cfg)->xlingsHome() / "subos" / "default"; - if (std::filesystem::exists(mcppSubos / "usr" / "include")) { - tc->sysroot = mcppSubos; - } - } - } + // Sysroot comes from the toolchain payload itself (GCC -print-sysroot, + // Clang clang++.cfg). mcpp does not override it — the payload is + // self-describing. See docs: 2026-05-21-linux-sysroot-missing-kernel-headers.md // Resolve dependencies: walk the **transitive** graph from the main // manifest, BFS-style. Each unique `(namespace, shortName)` is fetched diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index ef25032..5f6c106 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -254,6 +254,7 @@ probe_target_triple(const std::filesystem::path& compilerBin, std::filesystem::path probe_sysroot(const std::filesystem::path& compilerBin, const std::string& envPrefix) { + // 1. Ask the compiler directly (works for GCC; Clang often doesn't support it). auto r = run_capture(std::format("{}{} -print-sysroot {}", envPrefix, mcpp::xlings::shq(compilerBin.string()), @@ -262,10 +263,28 @@ probe_sysroot(const std::filesystem::path& compilerBin, auto s = trim_line(*r); if (!s.empty() && std::filesystem::exists(s)) return s; } - // macOS fallback: use xcrun to discover the SDK path. - // The sysroot is used for regular compilation flags (flags.cppm) but - // skipped for std module precompilation on macOS (stdmod.cppm) to - // avoid breaking SDK internal header dependencies. + + // 2. Parse the compiler driver config file (Clang .cfg). + // xlings-installed Clang ships a clang++.cfg alongside the binary + // with --sysroot pointing to the payload's associated sysroot. + { + auto stem = compilerBin.stem().string(); + auto cfgPath = compilerBin.parent_path() / (stem + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + std::ifstream ifs(cfgPath); + std::string line; + while (std::getline(ifs, line)) { + constexpr std::string_view prefix = "--sysroot="; + if (line.starts_with(prefix)) { + auto val = trim_line(std::string(line.substr(prefix.size()))); + if (!val.empty() && std::filesystem::exists(val)) + return val; + } + } + } + } + + // 3. macOS fallback: use xcrun to discover the SDK path. if (auto sdk = mcpp::platform::macos::sdk_path()) return *sdk; return {}; diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 0220d75..dd806d1 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -93,24 +93,29 @@ std::expected ensure_built( sm.objectPath = sm.cacheDir / "std.o"; // Build sysroot + include flags for std module precompilation. - // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and - // -isystem paths from the original install location. When the LLVM package - // is copied to mcpp's sandbox, these cfg paths become stale (still point - // to the original xlings directory). We override both: - // --sysroot → current active SDK (from xcrun) - // --no-default-config → ignore stale cfg entirely - // -isystem → correct libc++ headers in the sandbox copy + // + // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem + // paths from the original install location. After mcpp copies the package + // to its sandbox, these cfg paths may become stale. We detect a cfg file + // and bypass it with --no-default-config, providing correct flags derived + // from the payload's actual binary location and the probed sysroot. std::string sysroot_flag; - bool is_macos = tc.targetTriple.find("apple") != std::string::npos - || tc.targetTriple.find("darwin") != std::string::npos; - if (is_macos && is_clang(tc)) { - // Ignore the stale clang++.cfg and provide correct flags directly. - auto llvmRoot = tc.binaryPath.parent_path().parent_path(); - auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; - sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); - if (auto sdk = mcpp::platform::macos::sdk_path()) - sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + if (is_clang(tc)) { + auto cfgPath = tc.binaryPath.parent_path() + / (tc.binaryPath.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + // Bypass potentially-stale cfg; provide correct flags directly. + auto llvmRoot = tc.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); + if (!tc.sysroot.empty()) + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); + else if (auto sdk = mcpp::platform::macos::sdk_path()) + sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + } else if (!tc.sysroot.empty()) { + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); + } } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } From 0f8d0e82fe0a09dcffd64f0c794be4577e4598f3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:17:51 +0800 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20GCC=20sysroot=20fallback=20?= =?UTF-8?q?=E2=80=94=20remap=20stale=20build-time=20path=20to=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GCC bakes the build-time sysroot into the binary via --with-sysroot. For xlings-built GCC this is a path like /.xlings/subos/default that doesn't exist on the user's machine. When -print-sysroot returns such a non-existent path ending in subos/default, remap it to the equivalent sysroot relative to the compiler's own xpkgs directory. This is payload-derived (from the compiler binary's location in the registry), not a config-level dependency on subos. --- src/toolchain/probe.cppm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 5f6c106..0cac4f2 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -262,6 +262,21 @@ probe_sysroot(const std::filesystem::path& compilerBin, if (r) { auto s = trim_line(*r); if (!s.empty() && std::filesystem::exists(s)) return s; + + // GCC bakes the build-time sysroot into the binary. For xlings-built + // GCC this is a path like /.xlings/subos/default that + // doesn't exist on the user's machine. If the reported path ends + // with subos/default, look for the equivalent sysroot relative to + // the compiler's own xpkgs directory (payload-derived). + if (!s.empty() && s.ends_with("subos/default")) { + if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(compilerBin)) { + // xpkgs is /data/xpkgs → registry = xpkgs/../.. + auto registrySysroot = xpkgs->parent_path().parent_path() + / "subos" / "default"; + if (std::filesystem::exists(registrySysroot / "usr" / "include")) + return registrySysroot; + } + } } // 2. Parse the compiler driver config file (Clang .cfg). From b3d3e88a9029a37ff03e16dcb775bb7c887b4f8a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:35:49 +0800 Subject: [PATCH 03/22] fix: add target-specific libc++ include path for --no-default-config When bypassing clang++.cfg with --no-default-config, we must provide both libc++ include paths that the cfg originally supplied: -isystem /include/c++/v1 -isystem /include//c++/v1 The target-specific path contains __config_site which is required by __config. Without it, std module precompilation fails with '__config_site' file not found. --- src/build/flags.cppm | 8 ++++++++ src/toolchain/stdmod.cppm | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 4b85913..5a9537c 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -102,6 +102,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += " -isystem" + escape_path(libcxxInclude); + // Target-specific libc++ headers (e.g. __config_site) live under + // include//c++/v1/. Add if present. + if (!plan.toolchain.targetTriple.empty()) { + auto targetInclude = llvmRoot / "include" + / plan.toolchain.targetTriple / "c++" / "v1"; + if (std::filesystem::exists(targetInclude)) + sysroot_flag += " -isystem" + escape_path(targetInclude); + } if (!plan.toolchain.sysroot.empty()) sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); else if (auto sdk = mcpp::platform::macos::sdk_path()) diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index dd806d1..ffb5aa8 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -109,6 +109,14 @@ std::expected ensure_built( auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); + // Target-specific libc++ headers (e.g. __config_site) live under + // include//c++/v1/. Add if present. + if (!tc.targetTriple.empty()) { + auto targetInclude = llvmRoot / "include" + / tc.targetTriple / "c++" / "v1"; + if (std::filesystem::exists(targetInclude)) + sysroot_flag += std::format(" -isystem'{}'", targetInclude.string()); + } if (!tc.sysroot.empty()) sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); else if (auto sdk = mcpp::platform::macos::sdk_path()) From 265cde68474c6167063bfa46288c30d7b4dc0c0d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:52:03 +0800 Subject: [PATCH 04/22] fix: restrict --no-default-config to macOS only On Linux, clang++.cfg contains essential linker flags (-fuse-ld=lld, --rtlib=compiler-rt, --unwindlib=libunwind). Using --no-default-config strips these, causing "cannot find crtbeginS.o" link failures because clang falls back to system GNU ld looking for GCC runtime objects. On Linux, let the cfg apply normally. The cfg's --sysroot points to the xlings subos which is valid and complete. Pass --sysroot explicitly only when needed (to override a stale cfg value), leveraging the fact that command-line --sysroot takes precedence over the cfg's value. Keep --no-default-config for macOS only, where the cfg-baked paths genuinely become stale (pointing to CommandLineTools SDK when Xcode SDK is active). --- src/build/flags.cppm | 26 +++++++++++++++----------- src/toolchain/stdmod.cppm | 29 +++++++++++++++++------------ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 5a9537c..b894d0f 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -88,13 +88,19 @@ CompileFlags compute_flags(const BuildPlan& plan) { include_flags += " -I" + escape_path(abs); } - // Sysroot + config override for Clang with a driver config file. - // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem - // paths from the original install location. After mcpp copies the package - // to its sandbox, these paths may become stale. Detect the cfg and bypass - // it with --no-default-config, providing correct flags from the payload. + // Sysroot + config override. + // + // On macOS, xlings LLVM's clang++.cfg has hardcoded paths that become + // stale after copying. Use --no-default-config + explicit flags. + // + // On Linux, the cfg contains essential linker flags (-fuse-ld=lld, + // --rtlib=compiler-rt). Let the cfg apply normally; pass --sysroot + // explicitly to override any stale sysroot value in the cfg. std::string sysroot_flag; - if (mcpp::toolchain::is_clang(plan.toolchain)) { + bool is_macos_clang = mcpp::toolchain::is_clang(plan.toolchain) + && (plan.toolchain.targetTriple.find("apple") != std::string::npos + || plan.toolchain.targetTriple.find("darwin") != std::string::npos); + if (is_macos_clang) { auto cfgPath = plan.toolchain.binaryPath.parent_path() / (plan.toolchain.binaryPath.stem().string() + ".cfg"); if (std::filesystem::exists(cfgPath)) { @@ -102,18 +108,16 @@ CompileFlags compute_flags(const BuildPlan& plan) { auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += " -isystem" + escape_path(libcxxInclude); - // Target-specific libc++ headers (e.g. __config_site) live under - // include//c++/v1/. Add if present. if (!plan.toolchain.targetTriple.empty()) { auto targetInclude = llvmRoot / "include" / plan.toolchain.targetTriple / "c++" / "v1"; if (std::filesystem::exists(targetInclude)) sysroot_flag += " -isystem" + escape_path(targetInclude); } - if (!plan.toolchain.sysroot.empty()) - sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); - else if (auto sdk = mcpp::platform::macos::sdk_path()) + if (auto sdk = mcpp::platform::macos::sdk_path()) sysroot_flag += " --sysroot=" + escape_path(*sdk); + else if (!plan.toolchain.sysroot.empty()) + sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); f.sysroot = sysroot_flag; } else if (!plan.toolchain.sysroot.empty()) { sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index ffb5aa8..bd1b67b 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -94,33 +94,38 @@ std::expected ensure_built( // Build sysroot + include flags for std module precompilation. // - // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem - // paths from the original install location. After mcpp copies the package - // to its sandbox, these cfg paths may become stale. We detect a cfg file - // and bypass it with --no-default-config, providing correct flags derived - // from the payload's actual binary location and the probed sysroot. + // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and + // -isystem paths that become stale when the package is copied to mcpp's + // sandbox. We use --no-default-config to bypass the cfg and provide + // correct flags derived from the payload's actual location + xcrun SDK. + // + // On Linux, the cfg also contains linker flags (-fuse-ld=lld, + // --rtlib=compiler-rt, --unwindlib=libunwind) that are essential. Using + // --no-default-config would strip these, causing link failures. Instead, + // we let the cfg apply normally and only pass --sysroot to override the + // sysroot if needed (command-line --sysroot takes precedence over cfg). std::string sysroot_flag; - if (is_clang(tc)) { + bool is_macos = tc.targetTriple.find("apple") != std::string::npos + || tc.targetTriple.find("darwin") != std::string::npos; + if (is_macos && is_clang(tc)) { auto cfgPath = tc.binaryPath.parent_path() / (tc.binaryPath.stem().string() + ".cfg"); if (std::filesystem::exists(cfgPath)) { - // Bypass potentially-stale cfg; provide correct flags directly. + // Bypass stale macOS cfg; provide correct flags directly. auto llvmRoot = tc.binaryPath.parent_path().parent_path(); auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); - // Target-specific libc++ headers (e.g. __config_site) live under - // include//c++/v1/. Add if present. if (!tc.targetTriple.empty()) { auto targetInclude = llvmRoot / "include" / tc.targetTriple / "c++" / "v1"; if (std::filesystem::exists(targetInclude)) sysroot_flag += std::format(" -isystem'{}'", targetInclude.string()); } - if (!tc.sysroot.empty()) - sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); - else if (auto sdk = mcpp::platform::macos::sdk_path()) + if (auto sdk = mcpp::platform::macos::sdk_path()) sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + else if (!tc.sysroot.empty()) + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } From e9d088e33bba6bcc0dc114e8b0724033bf77bb52 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 20:14:16 +0800 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20payload-first=20sysroot=20?= =?UTF-8?q?=E2=80=94=20assemble=20compile=20env=20from=20xpkgs=20payloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace --sysroot dependency on xlings subos with fine-grained -isystem paths derived from sibling xpkgs payloads (glibc, linux-headers). Phase 2: PayloadPaths model - Add PayloadPaths struct to Toolchain model (glibcInclude, glibcLib, linuxInclude) - probe_payload_paths() finds sibling glibc and linux-headers xpkgs via find_sibling_package() which searches across all index prefixes - Falls back to host /usr/include for linux kernel headers if no xpkg found Phase 3: Payload-first flags - flags.cppm: use -isystem for glibc + linux-headers instead of --sysroot; Clang with cfg uses --no-default-config + explicit flags including -fuse-ld=lld, --rtlib=compiler-rt, --unwindlib=libunwind - stdmod.cppm: unified Clang cfg bypass on all platforms for std module precompile (no linker needed, so --no-default-config is safe) Phase 4: Clang cfg fixup - fixup_clang_cfg() rewrites clang++.cfg paths after payload copy, similar to fixup_gcc_specs() for GCC - Called during `mcpp toolchain install llvm` Phase 5: Sysroot dependency auto-install - Toolchain install ensures glibc and linux-headers xpkgs are installed before the main toolchain package --- ...2026-05-21-payload-first-sysroot-design.md | 241 ++++++++++++++++++ src/build/flags.cppm | 92 ++++--- src/cli.cppm | 154 +++++++++++ src/toolchain/detect.cppm | 4 + src/toolchain/model.cppm | 9 + src/toolchain/probe.cppm | 46 ++++ src/toolchain/stdmod.cppm | 35 +-- src/xlings.cppm | 62 +++++ 8 files changed, 597 insertions(+), 46 deletions(-) create mode 100644 .agents/docs/2026-05-21-payload-first-sysroot-design.md diff --git a/.agents/docs/2026-05-21-payload-first-sysroot-design.md b/.agents/docs/2026-05-21-payload-first-sysroot-design.md new file mode 100644 index 0000000..9881f21 --- /dev/null +++ b/.agents/docs/2026-05-21-payload-first-sysroot-design.md @@ -0,0 +1,241 @@ +# 设计方案:Payload-first 工具链环境管理 + +## 背景 + +mcpp 当前通过 `--sysroot` 指向 xlings subos 来为工具链提供 C 库头文件和内核头文件。 +这导致了对 xlings 内部结构(subos)的隐式依赖,且 subos 的完整性不可控。 + +## 设计原则 + +**mcpp 只把 xlings 当包索引 + 资源下载工具。所有编译环境由 mcpp 从 xpkgs payload +细粒度组装,不依赖 xlings 的 subos 或其他内部功能。** + +## 当前问题 + +xlings 中相关的 xpkgs 包: + +| 包名 | 内容 | 路径 | +|------|------|------| +| `xim-x-glibc/2.39` | glibc 头文件 + 运行时库 | `include/` (130 头文件) + `lib64/` (libc.so, crt*.o, ld-linux) | +| `scode-x-linux-headers/5.11.1` | Linux 内核头文件 | `include/linux/`, `include/asm/`, `include/asm-generic/` | +| `xim-x-gcc/16.1.0` | GCC 编译器 | C++ 标准库头文件 + 编译器内建头文件 + 运行时库 | +| `xim-x-llvm/20.1.7` | LLVM/Clang 编译器 | libc++ 头文件 + clang 内建头文件 + 运行时库 | + +这些包已经在 xpkgs 中了,但 mcpp 没有直接利用它们的路径,而是依赖 subos +(subos 实际上是 xlings 从这些包 + 宿主系统组装的一个合并目录)。 + +## 目标设计 + +### 核心思路 + +mcpp 在 detect 阶段从工具链 binary 位置推导出 sibling xpkgs(glibc、linux-headers), +在 flags 阶段直接用这些 payload 路径组装编译和链接参数,不使用 `--sysroot`。 + +### 路径推导链 + +``` +compiler binary (e.g. ~/.mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++) + │ + ├── xpkgs_from_compiler() → ~/.mcpp/registry/data/xpkgs/ + │ + ├── find_sibling_tool("glibc") + │ → ~/.mcpp/registry/data/xpkgs/xim-x-glibc/2.39/ + │ ├── include/ → glibc 头文件 (-isystem) + │ └── lib64/ → 运行时库 (-L, -rpath, -B for crt*.o) + │ + └── find_sibling_tool("linux-headers") [优先 scode-x-linux-headers] + → ~/.mcpp/registry/data/xpkgs/scode-x-linux-headers/5.11.1/ + └── include/ → linux/, asm/, asm-generic/ (-isystem) +``` + +### 环境检查 + +mcpp 在 detect 阶段主动检查工具链环境完整性,而不是被动等到编译报错。 + +```cpp +struct SysrootPaths { + std::filesystem::path glibcInclude; // glibc headers + std::filesystem::path linuxInclude; // linux kernel headers + std::filesystem::path glibcLib; // glibc runtime (lib64/) + std::filesystem::path dynamicLinker; // ld-linux-x86-64.so.2 +}; + +// detect 阶段调用: +std::expected +probe_sysroot_paths(const std::filesystem::path& compilerBin); +``` + +检查项: + +| 检查 | 验证文件 | 失败提示 | +|------|---------|---------| +| glibc 头文件 | `glibc/include/features.h` | `glibc xpkg not found; run: mcpp toolchain install gcc` | +| linux 内核头文件 | `linux-headers/include/linux/limits.h` | `linux-headers package missing; will use host /usr/include as fallback` | +| glibc 运行时 | `glibc/lib64/libc.so.6` | `glibc runtime not found` | +| 动态链接器 | `glibc/lib64/ld-linux-x86-64.so.2` | `dynamic linker not found` | + +### 编译 flags 组装 + +#### GCC(当前用 --sysroot,改为 -isystem + -L) + +**修改前**: +``` +g++ -std=c++23 --sysroot= ... +``` + +**修改后**: +``` +g++ -std=c++23 \ + -isystem /include \ + -isystem /include \ + -L/lib64 \ + ... +``` + +注意:GCC 使用相对路径自动找到自己的 C++ 标准库头文件和编译器内建头文件, +不需要额外的 `-isystem`。只需提供 glibc 和 linux-headers 的路径。 + +#### Clang(当前依赖 cfg,改为 --no-default-config + 显式路径) + +**修改前**: +``` +# cfg 中的路径(指向 ~/.xlings/,可能 stale) +clang++ --sysroot= -isystem /include/c++/v1 ... +``` + +**修改后**: +``` +clang++ --no-default-config \ + -stdlib=libc++ \ + -isystem /include/c++/v1 \ + -isystem /include//c++/v1 \ + -isystem /include \ + -isystem /include \ + -fuse-ld=lld \ + --rtlib=compiler-rt \ + --unwindlib=libunwind \ + -L/lib/ \ + -L/lib64 \ + ... +``` + +这里 mcpp 显式提供了 cfg 中所有必要的 flags,不再依赖 cfg 文件。 +Linux 和 macOS 走同一套逻辑(macOS 用 xcrun SDK 替代 glibc/linux-headers)。 + +### Toolchain 数据模型扩展 + +```cpp +struct Toolchain { + // ... 现有字段 ... + + // 替代 sysroot 的细粒度路径 + struct PayloadPaths { + std::filesystem::path glibcInclude; // glibc headers + std::filesystem::path glibcLib; // glibc lib64 + std::filesystem::path linuxInclude; // linux kernel headers + std::filesystem::path dynamicLinker; // ld-linux path + }; + std::optional payloadPaths; // 非空 = 使用 payload 模式 +}; +``` + +当 `payloadPaths` 有值时,flags 组装使用细粒度路径; +当 `payloadPaths` 为空时(系统编译器、用户自定义),回退到 `sysroot` 或不传。 + +### GCC specs fixup 的演进 + +当前 `fixup_gcc_specs()` 重写 specs 中的 dynamic-linker 和 rpath 路径, +指向 glibc xpkg 的 `lib64/`。这已经是 payload-first 的做法。 + +未来可以考虑在 specs fixup 中同时重写 sysroot: +``` +# specs 中添加: +*sysroot: + +``` +这样 GCC 自身就知道正确的 sysroot,不需要命令行 `--sysroot`。 +但这需要更深入理解 specs 格式,作为 Phase 3 推进。 + +### Clang cfg fixup(新增) + +类似 `fixup_gcc_specs()`,在 toolchain install 时重写 `clang++.cfg`: + +```cpp +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, + const PayloadPaths& paths) { + auto cfgPath = payloadRoot / "bin" / "clang++.cfg"; + // 重写: + // --sysroot= → 删除(由 mcpp 显式传递) + // -isystem → -isystem /include/c++/v1 + // -L → -L/lib/ + // -Wl,--dynamic-linker= → -Wl,--dynamic-linker=/lib64/ld-linux + // -Wl,-rpath, → -Wl,-rpath,/lib64 +} +``` + +这样即使不用 `--no-default-config`,cfg 中的路径也是自洽的。 + +## 实施路线 + +### Phase 1(当前 PR #62 已完成) + +- 删除 M5.5 subos 覆盖 +- `probe_sysroot` 增加 cfg 解析 + GCC stale path remap +- macOS Clang 保持 `--no-default-config` + xcrun + +**效果**:修复了 bug,但 GCC sysroot 仍依赖 registry subos,Clang 仍依赖 cfg 中的 +xlings 路径。 + +### Phase 2:Payload-first 路径探测 + +- Toolchain 模型增加 `PayloadPaths` 字段 +- `probe_sysroot_paths()` 通过 `find_sibling_tool()` 定位 glibc 和 linux-headers xpkgs +- 环境检查:detect 阶段验证关键头文件和库文件存在 +- 缺失 linux-headers 时给出明确提示,或自动触发安装 + +**改动文件**: +- `src/toolchain/model.cppm` — PayloadPaths 结构 +- `src/toolchain/probe.cppm` — probe_sysroot_paths() 实现 +- `src/toolchain/detect.cppm` — 调用 probe_sysroot_paths + +### Phase 3:Payload-first flags 组装 + +- `flags.cppm` 使用 PayloadPaths 组装 `-isystem` / `-L` / `-B` 替代 `--sysroot` +- `stdmod.cppm` 同步使用 PayloadPaths +- GCC 和 Clang 走统一的 flags 组装逻辑 +- 去掉对 cfg 文件的运行时依赖 + +**改动文件**: +- `src/build/flags.cppm` — 核心 flags 组装 +- `src/toolchain/stdmod.cppm` — std module 预编译 +- `src/toolchain/clang.cppm` — clang std module build commands + +### Phase 4:cfg/specs fixup 统一 + +- toolchain install 时对 Clang cfg 做 fixup(类似 `fixup_gcc_specs`) +- 使 payload 完全自洽,不依赖 xlings 原始路径 +- 去掉 `--no-default-config`(cfg 本身已正确) + +**改动文件**: +- `src/cli.cppm` — fixup_clang_cfg() + install 流程集成 + +### Phase 5(可选):依赖声明 + +在 xpkgs 包描述中声明 sysroot 依赖关系: +```toml +[toolchain.gcc] +requires = ["glibc", "linux-headers", "binutils"] +``` +mcpp 在 toolchain install 时自动检查和安装这些依赖包。 + +## 设计对比 + +| 维度 | 当前(subos) | 目标(payload-first) | +|------|-------------|---------------------| +| sysroot 来源 | xlings subos(合并目录) | glibc + linux-headers xpkgs | +| 路径控制 | 被动依赖 xlings 初始化 | mcpp 主动探测和组装 | +| 环境检查 | 无(编译时才报错) | detect 阶段验证 | +| cfg/specs | cfg stale + specs fixup | 两者都 fixup,路径自洽 | +| 跨平台 | macOS/Linux/Windows 各自特殊处理 | 统一的 PayloadPaths 抽象 | +| 错误提示 | `linux/limits.h not found` | `linux-headers package missing` | +| xlings 依赖 | subos + xpkgs + cfg 路径 | 只依赖 xpkgs 文件 | diff --git a/src/build/flags.cppm b/src/build/flags.cppm index b894d0f..52f9853 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -88,41 +88,60 @@ CompileFlags compute_flags(const BuildPlan& plan) { include_flags += " -I" + escape_path(abs); } - // Sysroot + config override. + // Sysroot / payload paths. // - // On macOS, xlings LLVM's clang++.cfg has hardcoded paths that become - // stale after copying. Use --no-default-config + explicit flags. + // Payload-first: when PayloadPaths are available (glibc + linux-headers + // xpkgs found), use -isystem for each payload include dir. This avoids + // dependency on xlings subos. // - // On Linux, the cfg contains essential linker flags (-fuse-ld=lld, - // --rtlib=compiler-rt). Let the cfg apply normally; pass --sysroot - // explicitly to override any stale sysroot value in the cfg. + // For Clang with a cfg file: use --no-default-config to bypass + // potentially-stale paths, then provide all flags explicitly. + // + // Fallback: if no PayloadPaths, use --sysroot from probe_sysroot(). std::string sysroot_flag; - bool is_macos_clang = mcpp::toolchain::is_clang(plan.toolchain) - && (plan.toolchain.targetTriple.find("apple") != std::string::npos - || plan.toolchain.targetTriple.find("darwin") != std::string::npos); - if (is_macos_clang) { - auto cfgPath = plan.toolchain.binaryPath.parent_path() - / (plan.toolchain.binaryPath.stem().string() + ".cfg"); - if (std::filesystem::exists(cfgPath)) { - auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); - auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; - sysroot_flag += " -isystem" + escape_path(libcxxInclude); - if (!plan.toolchain.targetTriple.empty()) { - auto targetInclude = llvmRoot / "include" - / plan.toolchain.targetTriple / "c++" / "v1"; - if (std::filesystem::exists(targetInclude)) - sysroot_flag += " -isystem" + escape_path(targetInclude); - } - if (auto sdk = mcpp::platform::macos::sdk_path()) - sysroot_flag += " --sysroot=" + escape_path(*sdk); - else if (!plan.toolchain.sysroot.empty()) - sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); - f.sysroot = sysroot_flag; + bool isClangWithCfg = false; + std::filesystem::path cfgPath; + if (mcpp::toolchain::is_clang(plan.toolchain)) { + cfgPath = plan.toolchain.binaryPath.parent_path() + / (plan.toolchain.binaryPath.stem().string() + ".cfg"); + isClangWithCfg = std::filesystem::exists(cfgPath); + } + + if (isClangWithCfg) { + // Clang with cfg: bypass cfg and provide all paths explicitly. + auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + // libc++ headers + sysroot_flag += " -stdlib=libc++"; + sysroot_flag += " -isystem" + escape_path(libcxxInclude); + if (!plan.toolchain.targetTriple.empty()) { + auto targetInclude = llvmRoot / "include" + / plan.toolchain.targetTriple / "c++" / "v1"; + if (std::filesystem::exists(targetInclude)) + sysroot_flag += " -isystem" + escape_path(targetInclude); + } + // C library + kernel headers from payload + if (plan.toolchain.payloadPaths) { + auto& pp = *plan.toolchain.payloadPaths; + sysroot_flag += " -isystem" + escape_path(pp.glibcInclude); + if (!pp.linuxInclude.empty()) + sysroot_flag += " -isystem" + escape_path(pp.linuxInclude); + } else if (auto sdk = mcpp::platform::macos::sdk_path()) { + sysroot_flag += " --sysroot=" + escape_path(*sdk); } else if (!plan.toolchain.sysroot.empty()) { - sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); - f.sysroot = sysroot_flag; + sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); } + // Linker flags that cfg normally provides + sysroot_flag += " -fuse-ld=lld --rtlib=compiler-rt --unwindlib=libunwind"; + f.sysroot = sysroot_flag; + } else if (plan.toolchain.payloadPaths) { + // GCC or Clang without cfg: use payload -isystem paths. + auto& pp = *plan.toolchain.payloadPaths; + sysroot_flag += " -isystem" + escape_path(pp.glibcInclude); + if (!pp.linuxInclude.empty()) + sysroot_flag += " -isystem" + escape_path(pp.linuxInclude); + f.sysroot = sysroot_flag; } else if (!plan.toolchain.sysroot.empty()) { sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); f.sysroot = sysroot_flag; @@ -203,12 +222,23 @@ CompileFlags compute_flags(const BuildPlan& plan) { } } + // For Clang with payload paths: add glibc lib + dynamic linker to link flags. + std::string payload_ld; + if (isClangWithCfg && plan.toolchain.payloadPaths) { + auto& pp = *plan.toolchain.payloadPaths; + payload_ld += " -L" + escape_path(pp.glibcLib); + payload_ld += " -Wl,-rpath," + escape_path(pp.glibcLib); + auto loader = pp.glibcLib / "ld-linux-x86-64.so.2"; + if (std::filesystem::exists(loader)) + payload_ld += " -Wl,--dynamic-linker=" + escape_path(loader); + } + if constexpr (mcpp::platform::is_windows) { f.ld = ""; } else if constexpr (mcpp::platform::needs_explicit_libcxx) { f.ld = std::format("{}{}{} -lc++", full_static, static_stdlib, b_flag); } else { - f.ld = std::format("{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag, runtime_dirs); + f.ld = std::format("{}{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag, runtime_dirs, payload_ld); } return f; diff --git a/src/cli.cppm b/src/cli.cppm index 5c00bcd..6800aac 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -765,6 +765,125 @@ void fixup_gcc_specs(const std::filesystem::path& gccPkgRoot, } } +// Rewrite clang++.cfg paths after the LLVM payload has been copied to the +// mcpp sandbox. The cfg was authored by xlings at install time and contains +// absolute paths pointing to ~/.xlings/. We rewrite them to point to the +// actual payload location + sibling xpkgs (glibc, linux-headers). +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, + const std::filesystem::path& glibcLibDir) { + for (auto cfgName : {"clang++.cfg", "clang.cfg"}) { + auto cfgPath = payloadRoot / "bin" / cfgName; + if (!std::filesystem::exists(cfgPath)) continue; + + std::ifstream is(cfgPath); + std::stringstream ss; ss << is.rdbuf(); + std::string content = ss.str(); + is.close(); + + auto llvmRoot = payloadRoot; + auto replace_line_prefix = [&](std::string& s, std::string_view prefix, + const std::string& newValue) { + std::istringstream lines(s); + std::string result, line; + while (std::getline(lines, line)) { + if (line.starts_with(prefix)) { + result += std::string(prefix) + newValue + '\n'; + } else { + result += line + '\n'; + } + } + s = result; + }; + + // Rewrite --sysroot to remove (mcpp provides this explicitly). + // Rewrite -isystem to point to payload's libc++ headers. + // Rewrite -L and -rpath to point to payload's lib dir. + // Rewrite dynamic-linker to use glibc payload's ld-linux. + std::istringstream lines(content); + std::string result, line; + while (std::getline(lines, line)) { + if (line.starts_with("--sysroot=")) { + // Remove — mcpp provides sysroot via payload paths. + continue; + } + if (line.starts_with("-isystem ")) { + auto oldPath = line.substr(9); + if (oldPath.find("include/c++/v1") != std::string::npos) { + auto relative = oldPath.substr(oldPath.find("include/c++/v1")); + result += "-isystem " + (llvmRoot / relative).string() + '\n'; + continue; + } + if (oldPath.find("include/x86_64") != std::string::npos || + oldPath.find("include/aarch64") != std::string::npos) { + // Target-specific libc++ include. + auto includePos = oldPath.find("include/"); + auto relative = oldPath.substr(includePos); + result += "-isystem " + (llvmRoot / relative).string() + '\n'; + continue; + } + } + if (line.starts_with("-L")) { + auto oldPath = line.substr(2); + if (oldPath.find("lib/x86_64") != std::string::npos || + oldPath.find("lib/aarch64") != std::string::npos) { + auto libPos = oldPath.find("lib/"); + auto relative = oldPath.substr(libPos); + result += "-L" + (llvmRoot / relative).string() + '\n'; + continue; + } + } + if (line.starts_with("-Wl,-rpath,")) { + auto oldPath = line.substr(11); + // Rpath for LLVM lib dir + if (oldPath.find("lib/x86_64") != std::string::npos || + oldPath.find("lib/aarch64") != std::string::npos) { + auto libPos = oldPath.find("lib/"); + auto relative = oldPath.substr(libPos); + result += "-Wl,-rpath," + (llvmRoot / relative).string() + '\n'; + continue; + } + // Rpath for subos/glibc — rewrite to glibc payload. + if (!glibcLibDir.empty()) { + auto parentDir = std::filesystem::path(oldPath).parent_path(); + // subos rpath lines like -Wl,-rpath,/lib + if (oldPath.find("subos") != std::string::npos) { + result += "-Wl,-rpath," + glibcLibDir.string() + '\n'; + continue; + } + } + } + if (line.starts_with("-Wl,--dynamic-linker=")) { + // Rewrite to glibc payload's ld-linux. + if (!glibcLibDir.empty()) { + result += "-Wl,--dynamic-linker=" + + (glibcLibDir / "ld-linux-x86-64.so.2").string() + '\n'; + continue; + } + } + if (line.starts_with("-Wl,--enable-new-dtags,-rpath,")) { + if (!glibcLibDir.empty()) { + result += "-Wl,--enable-new-dtags,-rpath," + glibcLibDir.string() + '\n'; + continue; + } + } + if (line.starts_with("-Wl,-rpath-link,")) { + if (!glibcLibDir.empty()) { + result += "-Wl,-rpath-link," + glibcLibDir.string() + '\n'; + continue; + } + } + result += line + '\n'; + } + + // Remove trailing newline + while (!result.empty() && result.back() == '\n') result.pop_back(); + result += '\n'; + + std::ofstream os(cfgPath); + os << result; + } +} + // SemVer resolution: a version spec is a "constraint" (vs. exact literal) if // it starts with one of `^~><=` or contains a comma (multi-part), or is `*` // or empty. Bare `1.2.3` is treated as exact for back-compat with pre-SemVer @@ -3606,6 +3725,18 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) { std::format("{} {} via mcpp's xlings", spec->compiler, spec->version)); mcpp::fetcher::Fetcher fetcher(*cfg); CliInstallProgress progress; + + // Ensure sysroot dependencies (glibc, linux-headers) are installed. + // These are required for C library + kernel headers during compilation. + // musl-gcc is self-contained and doesn't need these. + if (!spec->isMusl) { + for (auto dep : {"xim:glibc", "xim:linux-headers"}) { + auto depPayload = fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress); + // Best-effort: linux-headers may not be in the index. + // glibc is usually a dependency of gcc/llvm and already installed. + } + } + auto payload = fetcher.resolve_xpkg_path(pkg.target(), /*autoInstall=*/true, &progress); if (!payload) { mcpp::ui::error(std::format("install failed: {}", payload.error().message)); @@ -3671,6 +3802,29 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) { } } + // For LLVM/Clang: post-install cfg fixup so the clang++.cfg paths + // point to the payload's actual location instead of the xlings + // install-time paths (which become stale after copy). + if (pkg.ximName == "llvm") { + auto glibcRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "glibc"); + std::filesystem::path glibcLibDir; + if (std::filesystem::exists(glibcRoot)) { + for (auto& v : std::filesystem::directory_iterator(glibcRoot)) { + auto candidate = v.path() / "lib64"; + if (std::filesystem::exists(candidate / "ld-linux-x86-64.so.2")) { + glibcLibDir = candidate; + break; + } + candidate = v.path() / "lib"; + if (std::filesystem::exists(candidate / "ld-linux-x86-64.so.2")) { + glibcLibDir = candidate; + break; + } + } + } + fixup_clang_cfg(payload->root, glibcLibDir); + } + mcpp::ui::status("Installed", std::format("{} → {}", pkg.display_spec(), bin.string())); if (cfg->defaultToolchain.empty()) { diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index f273cae..1d6336a 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -77,6 +77,10 @@ detect(const std::filesystem::path& explicit_compiler) { tc.sysroot = probe_sysroot(tc.binaryPath, envPrefix); + // Probe fine-grained payload paths from sibling xpkgs (glibc, linux-headers). + // When available, flags are assembled from these paths instead of --sysroot. + tc.payloadPaths = probe_payload_paths(tc.binaryPath); + return tc; } diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index cecfacc..596db66 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -8,6 +8,14 @@ export namespace mcpp::toolchain { enum class CompilerId { Unknown, GCC, Clang, MSVC }; +// Fine-grained sysroot paths derived from xpkgs payloads. +// When populated, flags are assembled from these paths instead of --sysroot. +struct PayloadPaths { + std::filesystem::path glibcInclude; // glibc headers (features.h, bits/) + std::filesystem::path glibcLib; // glibc runtime (libc.so, crt*.o, ld-linux) + std::filesystem::path linuxInclude; // linux kernel headers (linux/, asm/) +}; + struct Toolchain { CompilerId compiler = CompilerId::Unknown; std::string version; // "15.1.0" @@ -19,6 +27,7 @@ struct Toolchain { std::filesystem::path stdModuleSource; // bits/std.cc / std.cppm std::filesystem::path stdCompatSource; // bits/std_compat.cc / std.compat.cppm std::filesystem::path sysroot; // -print-sysroot output (or empty) + std::optional payloadPaths; // fine-grained sysroot from xpkgs std::vector compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools std::vector linkRuntimeDirs; // -L/-rpath dirs for produced binaries bool hasImportStd = false; diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 0cac4f2..ab47232 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -47,6 +47,12 @@ std::filesystem::path probe_sysroot(const std::filesystem::path& compilerBin, const std::string& envPrefix); +// Probe fine-grained sysroot paths from sibling xpkgs payloads. +// Returns populated PayloadPaths if glibc xpkg found; linux-headers +// may be empty if not available (host /usr/include used as fallback). +std::optional +probe_payload_paths(const std::filesystem::path& compilerBin); + } // namespace mcpp::toolchain namespace mcpp::toolchain { @@ -305,4 +311,44 @@ probe_sysroot(const std::filesystem::path& compilerBin, return {}; } +std::optional +probe_payload_paths(const std::filesystem::path& compilerBin) { + namespace paths = mcpp::xlings::paths; + + // Find glibc xpkg (required). + auto glibc = paths::find_sibling_tool(compilerBin, "glibc"); + if (!glibc) return std::nullopt; + + // Glibc layout: /include/ + /lib64/ (or lib/). + auto glibcInclude = *glibc / "include"; + if (!std::filesystem::exists(glibcInclude / "features.h")) + return std::nullopt; + + auto glibcLib = *glibc / "lib64"; + if (!std::filesystem::exists(glibcLib)) + glibcLib = *glibc / "lib"; + if (!std::filesystem::exists(glibcLib)) + return std::nullopt; + + PayloadPaths pp; + pp.glibcInclude = glibcInclude; + pp.glibcLib = glibcLib; + + // Find linux kernel headers (optional — search across index prefixes). + auto linuxHeaders = paths::find_sibling_package(compilerBin, "linux-headers"); + if (linuxHeaders) { + auto linuxInclude = *linuxHeaders / "include"; + if (std::filesystem::exists(linuxInclude / "linux" / "limits.h")) + pp.linuxInclude = linuxInclude; + } + + // Fallback: host /usr/include has linux kernel headers on most systems. + if (pp.linuxInclude.empty()) { + if (std::filesystem::exists("/usr/include/linux/limits.h")) + pp.linuxInclude = "/usr/include"; + } + + return pp; +} + } // namespace mcpp::toolchain diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index bd1b67b..9955c81 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -94,24 +94,18 @@ std::expected ensure_built( // Build sysroot + include flags for std module precompilation. // - // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and - // -isystem paths that become stale when the package is copied to mcpp's - // sandbox. We use --no-default-config to bypass the cfg and provide - // correct flags derived from the payload's actual location + xcrun SDK. + // Payload-first: use fine-grained -isystem paths from xpkgs payloads + // when available, falling back to --sysroot. // - // On Linux, the cfg also contains linker flags (-fuse-ld=lld, - // --rtlib=compiler-rt, --unwindlib=libunwind) that are essential. Using - // --no-default-config would strip these, causing link failures. Instead, - // we let the cfg apply normally and only pass --sysroot to override the - // sysroot if needed (command-line --sysroot takes precedence over cfg). + // For Clang with a cfg file: use --no-default-config to bypass + // potentially-stale paths, then provide all flags explicitly. + // Std module precompilation only needs compile flags (no linker flags), + // so --no-default-config is safe here on all platforms. std::string sysroot_flag; - bool is_macos = tc.targetTriple.find("apple") != std::string::npos - || tc.targetTriple.find("darwin") != std::string::npos; - if (is_macos && is_clang(tc)) { + if (is_clang(tc)) { auto cfgPath = tc.binaryPath.parent_path() / (tc.binaryPath.stem().string() + ".cfg"); if (std::filesystem::exists(cfgPath)) { - // Bypass stale macOS cfg; provide correct flags directly. auto llvmRoot = tc.binaryPath.parent_path().parent_path(); auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; @@ -122,13 +116,24 @@ std::expected ensure_built( if (std::filesystem::exists(targetInclude)) sysroot_flag += std::format(" -isystem'{}'", targetInclude.string()); } - if (auto sdk = mcpp::platform::macos::sdk_path()) + // C library + kernel headers from payload paths. + if (tc.payloadPaths) { + sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->glibcInclude.string()); + if (!tc.payloadPaths->linuxInclude.empty()) + sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); + } else if (auto sdk = mcpp::platform::macos::sdk_path()) { sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); - else if (!tc.sysroot.empty()) + } else if (!tc.sysroot.empty()) { sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); + } } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } + } else if (tc.payloadPaths) { + // GCC: use payload -isystem paths instead of --sysroot. + sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->glibcInclude.string()); + if (!tc.payloadPaths->linuxInclude.empty()) + sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } diff --git a/src/xlings.cppm b/src/xlings.cppm index 572848e..0d58e5a 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -75,6 +75,13 @@ namespace paths { std::string_view tool, std::string_view binaryRelPath); + // Find a sibling package across all index prefixes. + // e.g. find_sibling_package(gcc_bin, "linux-headers") searches for + // xim-x-linux-headers, scode-x-linux-headers, etc. + std::optional + find_sibling_package(const std::filesystem::path& compilerBin, + std::string_view packageName); + // index data root: env.home / "data" std::filesystem::path index_data(const Env& env); @@ -384,6 +391,61 @@ find_sibling_binary(const std::filesystem::path& compilerBin, return std::nullopt; } +std::optional +find_sibling_package(const std::filesystem::path& compilerBin, + std::string_view packageName) { + auto xpkgs = xpkgs_from_compiler(compilerBin); + if (!xpkgs) return std::nullopt; + + // Search across index prefixes: xim-x-, scode-x-, compat-x-, etc. + std::error_code ec; + std::string suffix = std::format("-x-{}", packageName); + for (auto& entry : std::filesystem::directory_iterator(*xpkgs, ec)) { + if (!entry.is_directory(ec)) continue; + auto name = entry.path().filename().string(); + if (!name.ends_with(suffix)) continue; + // Return the first (highest) version dir that has actual content. + for (auto& v : std::filesystem::directory_iterator(entry.path(), ec)) { + if (!v.is_directory(ec)) continue; + // Skip empty packages (only .xim-installed marker) + bool hasContent = false; + for (auto& f : std::filesystem::directory_iterator(v.path(), ec)) { + if (f.path().filename() != ".xim-installed") { + hasContent = true; + break; + } + } + if (hasContent) return v.path(); + } + } + + // Also check ~/.xlings/data/xpkgs/ (xlings global home) as fallback. + const char* home = std::getenv("HOME"); + if (home) { + auto xlingsXpkgs = std::filesystem::path(home) / ".xlings" / "data" / "xpkgs"; + if (xlingsXpkgs != *xpkgs && std::filesystem::exists(xlingsXpkgs, ec)) { + for (auto& entry : std::filesystem::directory_iterator(xlingsXpkgs, ec)) { + if (!entry.is_directory(ec)) continue; + auto name = entry.path().filename().string(); + if (!name.ends_with(suffix)) continue; + for (auto& v : std::filesystem::directory_iterator(entry.path(), ec)) { + if (!v.is_directory(ec)) continue; + bool hasContent = false; + for (auto& f : std::filesystem::directory_iterator(v.path(), ec)) { + if (f.path().filename() != ".xim-installed") { + hasContent = true; + break; + } + } + if (hasContent) return v.path(); + } + } + } + } + + return std::nullopt; +} + std::filesystem::path index_data(const Env& env) { return env.home / "data"; } From 2baa0317365991f29f8943a5f644895922cacaf0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 20:28:30 +0800 Subject: [PATCH 06/22] fix: GCC needs --sysroot for include-fixed, supplement with -isystem GCC's include-fixed directory contains stdlib.h wrappers that use #include_next to find the sysroot's stdlib.h. This mechanism only works with --sysroot, not standalone -isystem paths. Fix: for GCC, keep --sysroot from probe_sysroot() and supplement with -isystem for linux kernel headers from payload when the probed sysroot is missing them. For Clang, continue using --no-default-config + explicit -isystem (Clang doesn't have include-fixed). --- src/build/flags.cppm | 15 +++++++++++++-- src/toolchain/stdmod.cppm | 14 +++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 52f9853..adc0366 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -135,14 +135,25 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Linker flags that cfg normally provides sysroot_flag += " -fuse-ld=lld --rtlib=compiler-rt --unwindlib=libunwind"; f.sysroot = sysroot_flag; + } else if (!plan.toolchain.sysroot.empty()) { + // GCC (or Clang without cfg): use --sysroot from probe. + // GCC requires --sysroot for include-fixed headers (stdlib.h wrapper). + // Supplement with -isystem for linux kernel headers from payload + // if the probed sysroot is missing them. + sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); + if (plan.toolchain.payloadPaths && !plan.toolchain.payloadPaths->linuxInclude.empty()) { + auto sysrootLinux = plan.toolchain.sysroot / "usr" / "include" / "linux" / "limits.h"; + if (!std::filesystem::exists(sysrootLinux)) + sysroot_flag += " -isystem" + escape_path(plan.toolchain.payloadPaths->linuxInclude); + } + f.sysroot = sysroot_flag; } else if (plan.toolchain.payloadPaths) { - // GCC or Clang without cfg: use payload -isystem paths. + // No sysroot but have payload paths: use -isystem. auto& pp = *plan.toolchain.payloadPaths; sysroot_flag += " -isystem" + escape_path(pp.glibcInclude); if (!pp.linuxInclude.empty()) sysroot_flag += " -isystem" + escape_path(pp.linuxInclude); f.sysroot = sysroot_flag; - } else if (!plan.toolchain.sysroot.empty()) { sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); f.sysroot = sysroot_flag; } diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 9955c81..7593eeb 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -129,13 +129,21 @@ std::expected ensure_built( } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } + } else if (!tc.sysroot.empty()) { + // GCC: use --sysroot (required for include-fixed headers). + // Supplement with -isystem for linux kernel headers from payload + // if the probed sysroot is missing them. + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); + if (tc.payloadPaths && !tc.payloadPaths->linuxInclude.empty()) { + auto sysrootLinux = tc.sysroot / "usr" / "include" / "linux" / "limits.h"; + if (!std::filesystem::exists(sysrootLinux)) + sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); + } } else if (tc.payloadPaths) { - // GCC: use payload -isystem paths instead of --sysroot. + // No sysroot: use payload -isystem paths. sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->glibcInclude.string()); if (!tc.payloadPaths->linuxInclude.empty()) sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); - } else if (!tc.sysroot.empty()) { - sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } bool std_cached = std::filesystem::exists(sm.bmiPath) && std::filesystem::exists(sm.objectPath); From 76a694d0dd2b455ad7563353a8fa5702426ec71b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 20:50:42 +0800 Subject: [PATCH 07/22] fix: add -nostdinc++ -stdlib=libc++ for Clang --no-default-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When bypassing clang++.cfg, the cfg's -nostdinc++ and -stdlib=libc++ flags are also stripped. Without -nostdinc++, Clang may find host libstdc++ headers before the payload's libc++ headers. Without -stdlib=libc++, Clang defaults to libstdc++ runtime. Also remove the /usr/include fallback for linux-headers — mixing host headers with xpkg glibc causes bits/wordsize.h conflicts. --- src/build/flags.cppm | 2 +- src/toolchain/probe.cppm | 6 ------ src/toolchain/stdmod.cppm | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index adc0366..0f70de0 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -111,7 +111,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Clang with cfg: bypass cfg and provide all paths explicitly. auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; + sysroot_flag = " --no-default-config -nostdinc++"; // libc++ headers sysroot_flag += " -stdlib=libc++"; sysroot_flag += " -isystem" + escape_path(libcxxInclude); diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index ab47232..c1df941 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -342,12 +342,6 @@ probe_payload_paths(const std::filesystem::path& compilerBin) { pp.linuxInclude = linuxInclude; } - // Fallback: host /usr/include has linux kernel headers on most systems. - if (pp.linuxInclude.empty()) { - if (std::filesystem::exists("/usr/include/linux/limits.h")) - pp.linuxInclude = "/usr/include"; - } - return pp; } diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 7593eeb..b72ed07 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -108,7 +108,7 @@ std::expected ensure_built( if (std::filesystem::exists(cfgPath)) { auto llvmRoot = tc.binaryPath.parent_path().parent_path(); auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; + sysroot_flag = " --no-default-config -nostdinc++ -stdlib=libc++"; sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); if (!tc.targetTriple.empty()) { auto targetInclude = llvmRoot / "include" From a2b18b83e7c347ef83198f45ea865a4485d82557 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 21:10:53 +0800 Subject: [PATCH 08/22] feat: ensure sysroot complete by symlinking from payload xpkgs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When GCC's probed sysroot (subos/default) is missing linux kernel headers or glibc headers, symlink them from the payload xpkgs: - linux/, asm/, asm-generic/ ← scode-x-linux-headers xpkg - features.h, bits/, etc. ← xim-x-glibc xpkg This makes mcpp self-sufficient: it uses subos/default as a sysroot directory for GCC's include-fixed mechanism, but actively populates it from payload rather than depending on xlings init completeness. Principle: subos is just a directory layout that mcpp manages. Content comes from xpkgs payloads. Clang doesn't use subos at all (--no-default-config + explicit -isystem from payload). --- src/toolchain/detect.cppm | 6 ++++++ src/toolchain/probe.cppm | 44 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index 1d6336a..3cb5984 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -81,6 +81,12 @@ detect(const std::filesystem::path& explicit_compiler) { // When available, flags are assembled from these paths instead of --sysroot. tc.payloadPaths = probe_payload_paths(tc.binaryPath); + // For GCC: ensure the probed sysroot has complete headers by symlinking + // missing content (linux kernel headers, glibc) from payload xpkgs. + // This makes mcpp self-sufficient — not dependent on xlings subos init. + if (tc.payloadPaths && !tc.sysroot.empty()) + ensure_sysroot_complete(tc.sysroot, *tc.payloadPaths); + return tc; } diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index c1df941..55b0d70 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -49,10 +49,16 @@ probe_sysroot(const std::filesystem::path& compilerBin, // Probe fine-grained sysroot paths from sibling xpkgs payloads. // Returns populated PayloadPaths if glibc xpkg found; linux-headers -// may be empty if not available (host /usr/include used as fallback). +// may be empty if not available. std::optional probe_payload_paths(const std::filesystem::path& compilerBin); +// Ensure sysroot directory has complete headers by symlinking from +// payload xpkgs. Called when GCC's probed sysroot exists but may +// be missing linux kernel headers or glibc headers. +void ensure_sysroot_complete(const std::filesystem::path& sysroot, + const PayloadPaths& pp); + } // namespace mcpp::toolchain namespace mcpp::toolchain { @@ -345,4 +351,40 @@ probe_payload_paths(const std::filesystem::path& compilerBin) { return pp; } +void ensure_sysroot_complete(const std::filesystem::path& sysroot, + const PayloadPaths& pp) { + if (sysroot.empty()) return; + + auto sysrootInclude = sysroot / "usr" / "include"; + if (!std::filesystem::exists(sysrootInclude)) return; + + std::error_code ec; + + // Ensure linux kernel headers are present in sysroot. + // If missing, symlink from linux-headers payload. + if (!pp.linuxInclude.empty()) { + for (auto dir : {"linux", "asm", "asm-generic"}) { + auto target = sysrootInclude / dir; + auto source = pp.linuxInclude / dir; + if (!std::filesystem::exists(target, ec) && std::filesystem::exists(source, ec)) { + std::filesystem::create_directory_symlink(source, target, ec); + } + } + } + + // Ensure glibc headers are present if sysroot is bare. + if (!std::filesystem::exists(sysrootInclude / "features.h", ec)) { + // Symlink individual glibc dirs/files into sysroot. + for (auto& entry : std::filesystem::directory_iterator(pp.glibcInclude, ec)) { + auto target = sysrootInclude / entry.path().filename(); + if (!std::filesystem::exists(target, ec)) { + if (entry.is_directory(ec)) + std::filesystem::create_directory_symlink(entry.path(), target, ec); + else + std::filesystem::create_symlink(entry.path(), target, ec); + } + } + } +} + } // namespace mcpp::toolchain From 774b827f6788709177f1daa4010a30a209a023f8 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 21:20:10 +0800 Subject: [PATCH 09/22] fix: include MSVC effective triple in fingerprint (Windows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clang on Windows auto-detects the MSVC version at compile time and embeds it in module AST files (e.g. x86_64-pc-windows-msvc19.44.35227). But -dumpmachine returns just x86_64-pc-windows-msvc (no version). When MSVC updates a patch version (35226 → 35227), the fingerprint didn't change, so mcpp reused cached std.pcm compiled for the old version → "AST file was compiled for different target" error. Fix: probe clang's -print-effective-triple which includes the MSVC version, and append to driverIdent for fingerprint computation. Also: ensure sysroot complete by symlinking linux kernel headers from payload xpkgs into the GCC sysroot directory. --- src/toolchain/detect.cppm | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index 3cb5984..0d566f9 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -69,6 +69,26 @@ detect(const std::filesystem::path& explicit_compiler) { tc.targetTriple = *triple; } +#if defined(_WIN32) + // On Windows, Clang targeting MSVC auto-detects the MSVC version at + // compile time and bakes it into the module AST. The -dumpmachine triple + // doesn't include this version, so fingerprints don't change when MSVC + // patches (e.g. 19.44.35226 → 35227), causing stale BMI cache hits. + // Query the effective triple which includes the actual MSVC version. + if (tc.compiler == CompilerId::Clang + && tc.targetTriple.find("msvc") != std::string::npos) { + auto vr = run_capture(std::format( + "{}{} -print-effective-triple 2>NUL", + envPrefix, + mcpp::xlings::shq(tc.binaryPath.string()))); + if (vr) { + auto effective = trim_line(*vr); + if (!effective.empty() && effective != tc.targetTriple) + tc.driverIdent += "\neffective-triple: " + effective; + } + } +#endif + if (tc.compiler == CompilerId::GCC) { mcpp::toolchain::gcc::enrich_toolchain(tc); } else if (tc.compiler == CompilerId::Clang) { From 6e90843febff4526a6fa7f6087dbd7899735f007 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 21:50:32 +0800 Subject: [PATCH 10/22] feat: add is_msvc_target() predicate + ci-fresh-install workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor: - Extract is_msvc_target() to model.cppm alongside is_musl_target() - Replace 3 scattered tc.targetTriple.find("msvc") checks in clang.cppm, detect.cppm, provider.cppm CI: - Add ci-fresh-install.yml: validates first-time user install flow on all platforms (Linux, macOS, Windows) with zero cache. - Tests: xlings install mcpp → self-host build → mcpp new → mcpp run - Tests: import std with both GCC and LLVM (Linux), LLVM (macOS), LLVM+MSVC STL (Windows) - Catches issues that cached CI misses: incomplete sysroot, stale cfg paths, missing xpkg dependencies --- .github/workflows/ci-fresh-install.yml | 231 +++++++++++++++++++++++++ src/toolchain/clang.cppm | 2 +- src/toolchain/detect.cppm | 2 +- src/toolchain/model.cppm | 5 + src/toolchain/provider.cppm | 4 +- 5 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci-fresh-install.yml diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml new file mode 100644 index 0000000..2b6c495 --- /dev/null +++ b/.github/workflows/ci-fresh-install.yml @@ -0,0 +1,231 @@ +name: ci-fresh-install + +# Fresh install CI — validates the first-time user experience on all platforms. +# No caches: simulates a clean machine where a user runs `xlings install mcpp` +# for the first time, creates a project, and builds it. +# +# This catches issues that cached CI misses: +# - Incomplete sysroot (missing linux kernel headers) +# - Stale toolchain cfg/specs paths +# - Missing xpkg dependencies (glibc, linux-headers) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +concurrency: + group: ci-fresh-install-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ────────────────────────────────────────────────────────────────── + # Linux: fresh install → new project → build with GCC + LLVM + # ────────────────────────────────────────────────────────────────── + linux-fresh: + name: Linux fresh install + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Install xlings (clean, no cache) + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + run: | + tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" + curl -fsSL -o "/tmp/${tarball}" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" + tar -xzf "/tmp/${tarball}" -C /tmp + "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + xlings --version + xlings install mcpp -y + echo "MCPP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" + + - name: Build freshly-built mcpp from source (self-host) + run: | + # Use the xlings-installed mcpp to build from source, then + # switch to the freshly-built binary for remaining tests. + "$MCPP" self config --mirror GLOBAL + "$MCPP" build + MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" + + - name: "Fresh user: mcpp new → build → run (default GCC)" + run: | + TMP=$(mktemp -d) + cd "$TMP" + "$MCPP" new hello_gcc + cd hello_gcc + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" + + - name: "Fresh user: install LLVM → new → build → run" + run: | + "$MCPP" toolchain install llvm 20.1.7 + "$MCPP" toolchain default llvm@20.1.7 + TMP=$(mktemp -d) + cd "$TMP" + "$MCPP" new hello_llvm + cd hello_llvm + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" + + - name: "Fresh user: import std (GCC)" + run: | + "$MCPP" toolchain default gcc + TMP=$(mktemp -d) + cd "$TMP" + mkdir -p src + cat > mcpp.toml <<'EOF' + [package] + name = "std_test" + version = "0.1.0" + EOF + cat > src/main.cpp <<'EOF' + import std; + int main() { std::println("import std works"); } + EOF + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "import std works" + + - name: "Fresh user: import std (LLVM)" + run: | + "$MCPP" toolchain default llvm@20.1.7 + TMP=$(mktemp -d) + cd "$TMP" + mkdir -p src + cat > mcpp.toml <<'EOF' + [package] + name = "std_test_llvm" + version = "0.1.0" + EOF + cat > src/main.cpp <<'EOF' + import std; + int main() { std::println("llvm import std works"); } + EOF + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "llvm import std works" + + # ────────────────────────────────────────────────────────────────── + # macOS: fresh install → new project → build with LLVM + # ────────────────────────────────────────────────────────────────── + macos-fresh: + name: macOS fresh install + runs-on: macos-15 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Install xlings (clean, no cache) + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + run: | + tarball="xlings-${XLINGS_VERSION}-macos-aarch64.tar.gz" + curl -fsSL -o "/tmp/${tarball}" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" + tar -xzf "/tmp/${tarball}" -C /tmp + "/tmp/xlings-${XLINGS_VERSION}-macos-aarch64/subos/default/bin/xlings" self install + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + xlings --version + xlings install mcpp -y + echo "MCPP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" + + - name: Build freshly-built mcpp from source + run: | + "$MCPP" self config --mirror GLOBAL + "$MCPP" build + MCPP_FRESH=$(realpath "$(find target -type f -name mcpp | head -1)") + echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" + + - name: "Fresh user: mcpp new → build → run (LLVM)" + run: | + TMP=$(mktemp -d) + cd "$TMP" + "$MCPP" new hello_mac + cd hello_mac + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" + + - name: "Fresh user: import std (LLVM)" + run: | + TMP=$(mktemp -d) + cd "$TMP" + mkdir -p src + cat > mcpp.toml <<'EOF' + [package] + name = "std_test_mac" + version = "0.1.0" + EOF + cat > src/main.cpp <<'EOF' + import std; + int main() { std::println("macos import std works"); } + EOF + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "macos import std works" + + # ────────────────────────────────────────────────────────────────── + # Windows: fresh install → new project → build with LLVM + MSVC STL + # ────────────────────────────────────────────────────────────────── + windows-fresh: + name: Windows fresh install + runs-on: windows-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Install xlings (clean, no cache) + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + shell: bash + run: | + curl -fsSL -o /tmp/xlings.zip \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/xlings-${XLINGS_VERSION}-windows-x86_64.zip" + unzip -o /tmp/xlings.zip -d /tmp/xlings-extract + XLINGS_DIR=$(find /tmp/xlings-extract -maxdepth 1 -type d -name 'xlings-*' | head -1) + "$XLINGS_DIR/subos/default/bin/xlings.exe" self install + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + xlings --version + xlings install mcpp -y + MCPP="$USERPROFILE/.xlings/subos/default/bin/mcpp.exe" + test -f "$MCPP" + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + + - name: Build freshly-built mcpp from source + shell: bash + run: | + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + "$MCPP" self config --mirror GLOBAL + "$MCPP" build + MCPP_FRESH=$(find target -type f -name 'mcpp.exe' | head -1) + echo "MCPP=$(realpath "$MCPP_FRESH")" >> "$GITHUB_ENV" + + - name: "Fresh user: mcpp new → build → run" + shell: bash + run: | + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + TMP=$(mktemp -d) + cd "$TMP" + "$MCPP" new hello_win + cd hello_win + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" + + - name: "Fresh user: import std" + shell: bash + run: | + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + TMP=$(mktemp -d) + cd "$TMP" + mkdir -p src + cat > mcpp.toml <<'EOF' + [package] + name = "std_test_win" + version = "0.1.0" + EOF + cat > src/main.cpp <<'EOF' + import std; + int main() { std::println("windows import std works"); } + EOF + "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "windows import std works" diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 52d325f..1d9460f 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -134,7 +134,7 @@ std::optional find_libcxx_std_module_source( void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) { // Clang targeting MSVC uses MSVC STL, not libc++. - bool msvTarget = tc.targetTriple.find("msvc") != std::string::npos; + bool msvTarget = is_msvc_target(tc); tc.stdlibId = msvTarget ? "msvc-stl" : "libc++"; tc.stdlibVersion = tc.version.empty() ? "unknown" : tc.version; tc.linkRuntimeDirs = mcpp::toolchain::discover_link_runtime_dirs( diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index 0d566f9..f0d9c47 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -76,7 +76,7 @@ detect(const std::filesystem::path& explicit_compiler) { // patches (e.g. 19.44.35226 → 35227), causing stale BMI cache hits. // Query the effective triple which includes the actual MSVC version. if (tc.compiler == CompilerId::Clang - && tc.targetTriple.find("msvc") != std::string::npos) { + && is_msvc_target(tc)) { auto vr = run_capture(std::format( "{}{} -print-effective-triple 2>NUL", envPrefix, diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 596db66..58ac208 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -51,6 +51,7 @@ struct DetectError { std::string message; }; bool is_gcc(const Toolchain& tc); bool is_clang(const Toolchain& tc); bool is_musl_target(const Toolchain& tc); +bool is_msvc_target(const Toolchain& tc); struct BmiTraits { std::string_view bmiDir; // "gcm.cache" | "pcm.cache" @@ -79,6 +80,10 @@ bool is_musl_target(const Toolchain& tc) { return tc.targetTriple.find("-musl") != std::string::npos; } +bool is_msvc_target(const Toolchain& tc) { + return tc.targetTriple.find("msvc") != std::string::npos; +} + BmiTraits bmi_traits(const Toolchain& tc) { if (is_clang(tc)) { return { diff --git a/src/toolchain/provider.cppm b/src/toolchain/provider.cppm index 985de48..ad3e4ae 100644 --- a/src/toolchain/provider.cppm +++ b/src/toolchain/provider.cppm @@ -80,9 +80,7 @@ ProviderCapabilities capabilities_for(const Toolchain& tc) { case CompilerId::Clang: { // Clang targeting MSVC uses MSVC STL, not libc++. - // We detect this the same way clang.cppm's enrich_toolchain does: - // by checking the target triple for "msvc". - bool msvc_target = tc.targetTriple.find("msvc") != std::string::npos; + bool msvc_target = is_msvc_target(tc); caps.has_scan_deps = true; // clang-scan-deps lives beside clang++ caps.stdlib_id = msvc_target ? "msvc-stl" : "libc++"; From 2300236acd1ccadbb0cfbdd01501c8dd3f2fbc16 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 22:03:44 +0800 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20cor?= =?UTF-8?q?rect=20asset=20names=20+=20MCPP=5FVENDORED=5FXLINGS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - macOS tarball: macos-aarch64 → macosx-arm64 (matches release assets) - Windows: use explicit extract dir name - All steps: export MCPP_VENDORED_XLINGS so freshly-built mcpp uses the installed xlings binary for package operations - Use MCPP_BOOTSTRAP for bootstrap mcpp, MCPP for freshly-built - Set MCPP_HOME explicitly for consistent sandbox location --- .github/workflows/ci-fresh-install.yml | 101 +++++++++++++++---------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 2b6c495..9d36156 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -27,11 +27,13 @@ jobs: linux-fresh: name: Linux fresh install runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 45 + env: + MCPP_HOME: /home/runner/.mcpp steps: - uses: actions/checkout@v4 - - name: Install xlings (clean, no cache) + - name: Install xlings + bootstrap mcpp (clean, no cache) env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: '0.4.30' @@ -44,20 +46,21 @@ jobs: export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings --version xlings install mcpp -y - echo "MCPP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "MCPP_BOOTSTRAP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" - - name: Build freshly-built mcpp from source (self-host) + - name: Build mcpp from source (self-host with freshly-built binary) run: | - # Use the xlings-installed mcpp to build from source, then - # switch to the freshly-built binary for remaining tests. - "$MCPP" self config --mirror GLOBAL - "$MCPP" build + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP_BOOTSTRAP" self config --mirror GLOBAL + "$MCPP_BOOTSTRAP" build MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" - name: "Fresh user: mcpp new → build → run (default GCC)" run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" TMP=$(mktemp -d) cd "$TMP" "$MCPP" new hello_gcc @@ -66,6 +69,7 @@ jobs: - name: "Fresh user: install LLVM → new → build → run" run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$MCPP" toolchain install llvm 20.1.7 "$MCPP" toolchain default llvm@20.1.7 TMP=$(mktemp -d) @@ -76,36 +80,38 @@ jobs: - name: "Fresh user: import std (GCC)" run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$MCPP" toolchain default gcc TMP=$(mktemp -d) cd "$TMP" mkdir -p src - cat > mcpp.toml <<'EOF' + cat > mcpp.toml <<'TOML' [package] name = "std_test" version = "0.1.0" - EOF - cat > src/main.cpp <<'EOF' + TOML + cat > src/main.cpp <<'CPP' import std; int main() { std::println("import std works"); } - EOF + CPP "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "import std works" - name: "Fresh user: import std (LLVM)" run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$MCPP" toolchain default llvm@20.1.7 TMP=$(mktemp -d) cd "$TMP" mkdir -p src - cat > mcpp.toml <<'EOF' + cat > mcpp.toml <<'TOML' [package] name = "std_test_llvm" version = "0.1.0" - EOF - cat > src/main.cpp <<'EOF' + TOML + cat > src/main.cpp <<'CPP' import std; int main() { std::println("llvm import std works"); } - EOF + CPP "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "llvm import std works" # ────────────────────────────────────────────────────────────────── @@ -115,34 +121,39 @@ jobs: name: macOS fresh install runs-on: macos-15 timeout-minutes: 30 + env: + MCPP_HOME: /Users/runner/.mcpp steps: - uses: actions/checkout@v4 - - name: Install xlings (clean, no cache) + - name: Install xlings + bootstrap mcpp (clean, no cache) env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: '0.4.30' run: | - tarball="xlings-${XLINGS_VERSION}-macos-aarch64.tar.gz" + tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-macos-aarch64/subos/default/bin/xlings" self install + "/tmp/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings --version xlings install mcpp -y - echo "MCPP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "MCPP_BOOTSTRAP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" - - name: Build freshly-built mcpp from source + - name: Build mcpp from source run: | - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp | head -1)") - echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP_BOOTSTRAP" self config --mirror GLOBAL + "$MCPP_BOOTSTRAP" build + MCPP_FRESH=$(find target -type f -name mcpp ! -name '*.o' | head -1) + echo "MCPP=$(cd "$(dirname "$MCPP_FRESH")" && pwd)/$(basename "$MCPP_FRESH")" >> "$GITHUB_ENV" - name: "Fresh user: mcpp new → build → run (LLVM)" run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" TMP=$(mktemp -d) cd "$TMP" "$MCPP" new hello_mac @@ -151,18 +162,19 @@ jobs: - name: "Fresh user: import std (LLVM)" run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" TMP=$(mktemp -d) cd "$TMP" mkdir -p src - cat > mcpp.toml <<'EOF' + cat > mcpp.toml <<'TOML' [package] name = "std_test_mac" version = "0.1.0" - EOF - cat > src/main.cpp <<'EOF' + TOML + cat > src/main.cpp <<'CPP' import std; int main() { std::println("macos import std works"); } - EOF + CPP "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "macos import std works" # ────────────────────────────────────────────────────────────────── @@ -172,10 +184,12 @@ jobs: name: Windows fresh install runs-on: windows-latest timeout-minutes: 30 + env: + MCPP_HOME: C:\Users\runneradmin\.mcpp steps: - uses: actions/checkout@v4 - - name: Install xlings (clean, no cache) + - name: Install xlings + bootstrap mcpp (clean, no cache) env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: '0.4.30' @@ -183,22 +197,25 @@ jobs: run: | curl -fsSL -o /tmp/xlings.zip \ "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/xlings-${XLINGS_VERSION}-windows-x86_64.zip" + mkdir -p /tmp/xlings-extract unzip -o /tmp/xlings.zip -d /tmp/xlings-extract - XLINGS_DIR=$(find /tmp/xlings-extract -maxdepth 1 -type d -name 'xlings-*' | head -1) + XLINGS_DIR="/tmp/xlings-extract/xlings-${XLINGS_VERSION}-windows-x86_64" "$XLINGS_DIR/subos/default/bin/xlings.exe" self install export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" xlings --version xlings install mcpp -y - MCPP="$USERPROFILE/.xlings/subos/default/bin/mcpp.exe" - test -f "$MCPP" - echo "MCPP=$MCPP" >> "$GITHUB_ENV" + MCPP_BOOTSTRAP="$USERPROFILE/.xlings/subos/default/bin/mcpp.exe" + test -f "$MCPP_BOOTSTRAP" + echo "MCPP_BOOTSTRAP=$MCPP_BOOTSTRAP" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" - - name: Build freshly-built mcpp from source + - name: Build mcpp from source shell: bash run: | export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - "$MCPP" self config --mirror GLOBAL - "$MCPP" build + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP_BOOTSTRAP" self config --mirror GLOBAL + "$MCPP_BOOTSTRAP" build MCPP_FRESH=$(find target -type f -name 'mcpp.exe' | head -1) echo "MCPP=$(realpath "$MCPP_FRESH")" >> "$GITHUB_ENV" @@ -206,6 +223,7 @@ jobs: shell: bash run: | export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" TMP=$(mktemp -d) cd "$TMP" "$MCPP" new hello_win @@ -216,16 +234,17 @@ jobs: shell: bash run: | export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" TMP=$(mktemp -d) cd "$TMP" mkdir -p src - cat > mcpp.toml <<'EOF' + cat > mcpp.toml <<'TOML' [package] name = "std_test_win" version = "0.1.0" - EOF - cat > src/main.cpp <<'EOF' + TOML + cat > src/main.cpp <<'CPP' import std; int main() { std::println("windows import std works"); } - EOF + CPP "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "windows import std works" From 65ce46912f6dcb52e11a10ec3651e4a7dfa43117 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 22:18:10 +0800 Subject: [PATCH 12/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20sim?= =?UTF-8?q?ulate=20real=20user,=20no=20extra=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip all env overrides, self-host builds, and MCPP_VENDORED_XLINGS. Simulate exactly what a real user does: 1. Install xlings 2. xlings install mcpp -y 3. mcpp new hello && cd hello && mcpp run --- .github/workflows/ci-fresh-install.yml | 240 ++++--------------------- 1 file changed, 38 insertions(+), 202 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 9d36156..14c0212 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -1,13 +1,10 @@ name: ci-fresh-install -# Fresh install CI — validates the first-time user experience on all platforms. -# No caches: simulates a clean machine where a user runs `xlings install mcpp` -# for the first time, creates a project, and builds it. -# -# This catches issues that cached CI misses: -# - Incomplete sysroot (missing linux kernel headers) -# - Stale toolchain cfg/specs paths -# - Missing xpkg dependencies (glibc, linux-headers) +# Fresh install CI — simulates a real first-time user on a clean machine. +# No caches, no env overrides, no self-host. Just: +# 1. Install xlings +# 2. Install mcpp via xlings +# 3. mcpp new hello → cd hello → mcpp run on: push: @@ -21,230 +18,69 @@ concurrency: cancel-in-progress: true jobs: - # ────────────────────────────────────────────────────────────────── - # Linux: fresh install → new project → build with GCC + LLVM - # ────────────────────────────────────────────────────────────────── linux-fresh: name: Linux fresh install runs-on: ubuntu-24.04 - timeout-minutes: 45 - env: - MCPP_HOME: /home/runner/.mcpp + timeout-minutes: 30 steps: - - uses: actions/checkout@v4 - - - name: Install xlings + bootstrap mcpp (clean, no cache) - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' + - name: Install xlings run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - xlings install mcpp -y - echo "MCPP_BOOTSTRAP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" + curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-linux-x86_64.tar.gz -o /tmp/xlings.tar.gz + tar -xzf /tmp/xlings.tar.gz -C /tmp + /tmp/xlings-0.4.30-linux-x86_64/subos/default/bin/xlings self install + echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build mcpp from source (self-host with freshly-built binary) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP_BOOTSTRAP" self config --mirror GLOBAL - "$MCPP_BOOTSTRAP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" - - - name: "Fresh user: mcpp new → build → run (default GCC)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP" new hello_gcc - cd hello_gcc - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" - - - name: "Fresh user: install LLVM → new → build → run" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" toolchain install llvm 20.1.7 - "$MCPP" toolchain default llvm@20.1.7 - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP" new hello_llvm - cd hello_llvm - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" - - - name: "Fresh user: import std (GCC)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" toolchain default gcc - TMP=$(mktemp -d) - cd "$TMP" - mkdir -p src - cat > mcpp.toml <<'TOML' - [package] - name = "std_test" - version = "0.1.0" - TOML - cat > src/main.cpp <<'CPP' - import std; - int main() { std::println("import std works"); } - CPP - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "import std works" + - name: Install mcpp + run: xlings install mcpp -y - - name: "Fresh user: import std (LLVM)" + - name: "mcpp new hello → mcpp run" run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" toolchain default llvm@20.1.7 - TMP=$(mktemp -d) - cd "$TMP" - mkdir -p src - cat > mcpp.toml <<'TOML' - [package] - name = "std_test_llvm" - version = "0.1.0" - TOML - cat > src/main.cpp <<'CPP' - import std; - int main() { std::println("llvm import std works"); } - CPP - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "llvm import std works" + mcpp new hello + cd hello + mcpp run - # ────────────────────────────────────────────────────────────────── - # macOS: fresh install → new project → build with LLVM - # ────────────────────────────────────────────────────────────────── macos-fresh: name: macOS fresh install runs-on: macos-15 timeout-minutes: 30 - env: - MCPP_HOME: /Users/runner/.mcpp steps: - - uses: actions/checkout@v4 - - - name: Install xlings + bootstrap mcpp (clean, no cache) - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' + - name: Install xlings run: | - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - xlings install mcpp -y - echo "MCPP_BOOTSTRAP=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" + curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-macosx-arm64.tar.gz -o /tmp/xlings.tar.gz + tar -xzf /tmp/xlings.tar.gz -C /tmp + /tmp/xlings-0.4.30-macosx-arm64/subos/default/bin/xlings self install + echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build mcpp from source - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP_BOOTSTRAP" self config --mirror GLOBAL - "$MCPP_BOOTSTRAP" build - MCPP_FRESH=$(find target -type f -name mcpp ! -name '*.o' | head -1) - echo "MCPP=$(cd "$(dirname "$MCPP_FRESH")" && pwd)/$(basename "$MCPP_FRESH")" >> "$GITHUB_ENV" + - name: Install mcpp + run: xlings install mcpp -y - - name: "Fresh user: mcpp new → build → run (LLVM)" + - name: "mcpp new hello → mcpp run" run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP" new hello_mac - cd hello_mac - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" + mcpp new hello + cd hello + mcpp run - - name: "Fresh user: import std (LLVM)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - mkdir -p src - cat > mcpp.toml <<'TOML' - [package] - name = "std_test_mac" - version = "0.1.0" - TOML - cat > src/main.cpp <<'CPP' - import std; - int main() { std::println("macos import std works"); } - CPP - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "macos import std works" - - # ────────────────────────────────────────────────────────────────── - # Windows: fresh install → new project → build with LLVM + MSVC STL - # ────────────────────────────────────────────────────────────────── windows-fresh: name: Windows fresh install runs-on: windows-latest timeout-minutes: 30 - env: - MCPP_HOME: C:\Users\runneradmin\.mcpp steps: - - uses: actions/checkout@v4 - - - name: Install xlings + bootstrap mcpp (clean, no cache) - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' + - name: Install xlings shell: bash run: | - curl -fsSL -o /tmp/xlings.zip \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/xlings-${XLINGS_VERSION}-windows-x86_64.zip" + curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip -o /tmp/xlings.zip mkdir -p /tmp/xlings-extract unzip -o /tmp/xlings.zip -d /tmp/xlings-extract - XLINGS_DIR="/tmp/xlings-extract/xlings-${XLINGS_VERSION}-windows-x86_64" - "$XLINGS_DIR/subos/default/bin/xlings.exe" self install - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - xlings --version - xlings install mcpp -y - MCPP_BOOTSTRAP="$USERPROFILE/.xlings/subos/default/bin/mcpp.exe" - test -f "$MCPP_BOOTSTRAP" - echo "MCPP_BOOTSTRAP=$MCPP_BOOTSTRAP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" + /tmp/xlings-extract/xlings-0.4.30-windows-x86_64/subos/default/bin/xlings.exe self install + echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build mcpp from source + - name: Install mcpp shell: bash - run: | - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP_BOOTSTRAP" self config --mirror GLOBAL - "$MCPP_BOOTSTRAP" build - MCPP_FRESH=$(find target -type f -name 'mcpp.exe' | head -1) - echo "MCPP=$(realpath "$MCPP_FRESH")" >> "$GITHUB_ENV" - - - name: "Fresh user: mcpp new → build → run" - shell: bash - run: | - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP" new hello_win - cd hello_win - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "Hello" + run: xlings install mcpp -y - - name: "Fresh user: import std" + - name: "mcpp new hello → mcpp run" shell: bash run: | - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - mkdir -p src - cat > mcpp.toml <<'TOML' - [package] - name = "std_test_win" - version = "0.1.0" - TOML - cat > src/main.cpp <<'CPP' - import std; - int main() { std::println("windows import std works"); } - CPP - "$MCPP" run 2>&1 | tee /dev/stderr | grep -q "windows import std works" + mcpp new hello + cd hello + mcpp run From cf3ad9c7203e285d5d75bb2f7c45460ff64aa8b8 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 22:22:33 +0800 Subject: [PATCH 13/22] =?UTF-8?q?fix:=20ci-fresh-install=20Windows=20?= =?UTF-8?q?=E2=80=94=20use=20pwsh=20for=20native=20path=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git Bash on Windows mangles GITHUB_PATH entries. Switch to pwsh which handles Windows paths natively. --- .github/workflows/ci-fresh-install.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 14c0212..f4dddc9 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -66,21 +66,20 @@ jobs: timeout-minutes: 30 steps: - name: Install xlings - shell: bash + shell: pwsh run: | - curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip -o /tmp/xlings.zip - mkdir -p /tmp/xlings-extract - unzip -o /tmp/xlings.zip -d /tmp/xlings-extract - /tmp/xlings-extract/xlings-0.4.30-windows-x86_64/subos/default/bin/xlings.exe self install - echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" + Invoke-WebRequest -Uri "https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip" -OutFile "$env:TEMP\xlings.zip" + Expand-Archive -Path "$env:TEMP\xlings.zip" -DestinationPath "$env:TEMP\xlings-extract" -Force + & "$env:TEMP\xlings-extract\xlings-0.4.30-windows-x86_64\subos\default\bin\xlings.exe" self install + "$env:USERPROFILE\.xlings\subos\default\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - name: Install mcpp - shell: bash + shell: pwsh run: xlings install mcpp -y - name: "mcpp new hello → mcpp run" - shell: bash + shell: pwsh run: | mcpp new hello - cd hello + Set-Location hello mcpp run From a7ef2188b60e0ca27611a1c991da794d678d2d7a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 22:44:34 +0800 Subject: [PATCH 14/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20wor?= =?UTF-8?q?kflow=5Fdispatch=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This CI tests xlings-distributed mcpp (not the PR's code), so it will fail until fixes are released to the xlings mcpp package. Change to manual trigger only — run after a release to verify the real end-to-end user experience. --- .github/workflows/ci-fresh-install.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index f4dddc9..9899584 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -7,10 +7,10 @@ name: ci-fresh-install # 3. mcpp new hello → cd hello → mcpp run on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + # Manual trigger only — this CI tests the xlings-distributed mcpp binary + # (not the PR's code). It validates the real user install flow and will + # fail until fixes are released to the xlings mcpp package. + # Run manually after a release to verify the end-to-end user experience. workflow_dispatch: concurrency: From 7d20291de189f6269d37cbac42cd77eae674596f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 22:59:45 +0800 Subject: [PATCH 15/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20bui?= =?UTF-8?q?ld=20PR's=20mcpp=20then=20test=20fresh=20user=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore PR trigger. Flow: 1. Bootstrap xlings + old mcpp via xlings install 2. Build THIS PR's mcpp from source (self-host) 3. Use freshly-built mcpp to simulate fresh user: new → run 4. Linux: test both GCC (default) and LLVM toolchains 5. macOS: test LLVM (default) 6. Windows: test LLVM + MSVC STL --- .github/workflows/ci-fresh-install.yml | 125 +++++++++++++++++++------ 1 file changed, 96 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 9899584..69262bd 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -1,16 +1,22 @@ name: ci-fresh-install -# Fresh install CI — simulates a real first-time user on a clean machine. -# No caches, no env overrides, no self-host. Just: -# 1. Install xlings -# 2. Install mcpp via xlings -# 3. mcpp new hello → cd hello → mcpp run +# Fresh install CI — validates the first-time user experience on all platforms. +# No sandbox/target caches: simulates a user who just installed mcpp. +# +# Flow: bootstrap mcpp via xlings → build this PR's mcpp from source → +# use the freshly-built mcpp to: new → run (default toolchain) +# then: install llvm → new → run +# +# This catches issues that cached CI misses: +# - Incomplete sysroot (missing linux kernel headers) +# - Stale toolchain cfg/specs paths +# - Missing xpkg dependencies on: - # Manual trigger only — this CI tests the xlings-distributed mcpp binary - # (not the PR's code). It validates the real user install flow and will - # fail until fixes are released to the xlings mcpp package. - # Run manually after a release to verify the end-to-end user experience. + push: + branches: [ main ] + pull_request: + branches: [ main ] workflow_dispatch: concurrency: @@ -21,65 +27,126 @@ jobs: linux-fresh: name: Linux fresh install runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 45 steps: - - name: Install xlings + - uses: actions/checkout@v4 + + - name: Bootstrap xlings + mcpp + env: + XLINGS_NON_INTERACTIVE: '1' run: | curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-linux-x86_64.tar.gz -o /tmp/xlings.tar.gz tar -xzf /tmp/xlings.tar.gz -C /tmp /tmp/xlings-0.4.30-linux-x86_64/subos/default/bin/xlings self install echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Install mcpp - run: xlings install mcpp -y + - name: Install bootstrap mcpp + run: | + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + xlings install mcpp -y - - name: "mcpp new hello → mcpp run" + - name: Build this PR's mcpp from source run: | - mcpp new hello + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$HOME/.xlings/subos/default/bin/xlings" + mcpp build + MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" + + - name: "Fresh: mcpp new hello → mcpp run (GCC)" + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + cd "$(mktemp -d)" + "$MCPP" new hello cd hello - mcpp run + "$MCPP" run + + - name: "Fresh: install LLVM → mcpp new hello → mcpp run" + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP" toolchain install llvm 20.1.7 + "$MCPP" toolchain default llvm@20.1.7 + cd "$(mktemp -d)" + "$MCPP" new hello_llvm + cd hello_llvm + "$MCPP" run macos-fresh: name: macOS fresh install runs-on: macos-15 timeout-minutes: 30 steps: - - name: Install xlings + - uses: actions/checkout@v4 + + - name: Bootstrap xlings + mcpp + env: + XLINGS_NON_INTERACTIVE: '1' run: | curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-macosx-arm64.tar.gz -o /tmp/xlings.tar.gz tar -xzf /tmp/xlings.tar.gz -C /tmp /tmp/xlings-0.4.30-macosx-arm64/subos/default/bin/xlings self install echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Install mcpp - run: xlings install mcpp -y + - name: Install bootstrap mcpp + run: | + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + xlings install mcpp -y - - name: "mcpp new hello → mcpp run" + - name: Build this PR's mcpp from source run: | - mcpp new hello + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$HOME/.xlings/subos/default/bin/xlings" + mcpp build + MCPP_FRESH=$(find target -type f -name mcpp ! -name '*.o' | head -1) + echo "MCPP=$(cd "$(dirname "$MCPP_FRESH")" && pwd)/$(basename "$MCPP_FRESH")" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" + + - name: "Fresh: mcpp new hello → mcpp run (LLVM)" + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + cd "$(mktemp -d)" + "$MCPP" new hello cd hello - mcpp run + "$MCPP" run windows-fresh: name: Windows fresh install runs-on: windows-latest timeout-minutes: 30 steps: - - name: Install xlings + - uses: actions/checkout@v4 + + - name: Bootstrap xlings + mcpp shell: pwsh + env: + XLINGS_NON_INTERACTIVE: '1' run: | Invoke-WebRequest -Uri "https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip" -OutFile "$env:TEMP\xlings.zip" Expand-Archive -Path "$env:TEMP\xlings.zip" -DestinationPath "$env:TEMP\xlings-extract" -Force & "$env:TEMP\xlings-extract\xlings-0.4.30-windows-x86_64\subos\default\bin\xlings.exe" self install "$env:USERPROFILE\.xlings\subos\default\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - name: Install mcpp + - name: Install bootstrap mcpp shell: pwsh run: xlings install mcpp -y - - name: "mcpp new hello → mcpp run" - shell: pwsh + - name: Build this PR's mcpp from source + shell: bash + run: | + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$USERPROFILE/.xlings/subos/default/bin/xlings.exe" + mcpp build + MCPP_FRESH=$(find target -type f -name 'mcpp.exe' | head -1) + echo "MCPP=$(realpath "$MCPP_FRESH")" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" + + - name: "Fresh: mcpp new hello → mcpp run" + shell: bash run: | - mcpp new hello - Set-Location hello - mcpp run + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + cd "$(mktemp -d)" + "$MCPP" new hello + cd hello + "$MCPP" run From a6b2bda18e50522fffc90f313cc5043bf1853553 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:03:59 +0800 Subject: [PATCH 16/22] ci: retrigger after clearing Windows cache From 7c05c1f0e7bf23a418affe163cd4103a7656065e Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:09:23 +0800 Subject: [PATCH 17/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20sim?= =?UTF-8?q?plify,=20use=20GITHUB=5FPATH=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all extra env vars (MCPP_VENDORED_XLINGS, XLINGS_BIN) - Just add xlings bin to GITHUB_PATH, everything else works - Windows: set PATH inline in bootstrap step so xlings install mcpp can find xlings immediately --- .github/workflows/ci-fresh-install.yml | 58 +++++++------------------- 1 file changed, 15 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 69262bd..3180dcf 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -4,13 +4,7 @@ name: ci-fresh-install # No sandbox/target caches: simulates a user who just installed mcpp. # # Flow: bootstrap mcpp via xlings → build this PR's mcpp from source → -# use the freshly-built mcpp to: new → run (default toolchain) -# then: install llvm → new → run -# -# This catches issues that cached CI misses: -# - Incomplete sysroot (missing linux kernel headers) -# - Stale toolchain cfg/specs paths -# - Missing xpkg dependencies +# use the freshly-built mcpp to: new → run on: push: @@ -38,33 +32,25 @@ jobs: curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-linux-x86_64.tar.gz -o /tmp/xlings.tar.gz tar -xzf /tmp/xlings.tar.gz -C /tmp /tmp/xlings-0.4.30-linux-x86_64/subos/default/bin/xlings self install - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - - name: Install bootstrap mcpp - run: | export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings install mcpp -y + echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build this PR's mcpp from source + - name: Build this PR's mcpp run: | - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$HOME/.xlings/subos/default/bin/xlings" mcpp build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - echo "MCPP=$MCPP_FRESH" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" + MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + echo "MCPP=$MCPP" >> "$GITHUB_ENV" - name: "Fresh: mcpp new hello → mcpp run (GCC)" run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" cd "$(mktemp -d)" "$MCPP" new hello cd hello "$MCPP" run - - name: "Fresh: install LLVM → mcpp new hello → mcpp run" + - name: "Fresh: install LLVM → mcpp new → mcpp run" run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$MCPP" toolchain install llvm 20.1.7 "$MCPP" toolchain default llvm@20.1.7 cd "$(mktemp -d)" @@ -86,25 +72,18 @@ jobs: curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-macosx-arm64.tar.gz -o /tmp/xlings.tar.gz tar -xzf /tmp/xlings.tar.gz -C /tmp /tmp/xlings-0.4.30-macosx-arm64/subos/default/bin/xlings self install - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - - name: Install bootstrap mcpp - run: | export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings install mcpp -y + echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build this PR's mcpp from source + - name: Build this PR's mcpp run: | - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$HOME/.xlings/subos/default/bin/xlings" mcpp build - MCPP_FRESH=$(find target -type f -name mcpp ! -name '*.o' | head -1) - echo "MCPP=$(cd "$(dirname "$MCPP_FRESH")" && pwd)/$(basename "$MCPP_FRESH")" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" + MCPP=$(find target -type f -name mcpp ! -name '*.o' | head -1) + echo "MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP")" >> "$GITHUB_ENV" - name: "Fresh: mcpp new hello → mcpp run (LLVM)" run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" cd "$(mktemp -d)" "$MCPP" new hello cd hello @@ -125,27 +104,20 @@ jobs: Invoke-WebRequest -Uri "https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip" -OutFile "$env:TEMP\xlings.zip" Expand-Archive -Path "$env:TEMP\xlings.zip" -DestinationPath "$env:TEMP\xlings-extract" -Force & "$env:TEMP\xlings-extract\xlings-0.4.30-windows-x86_64\subos\default\bin\xlings.exe" self install + $env:PATH = "$env:USERPROFILE\.xlings\subos\default\bin;$env:PATH" + xlings install mcpp -y "$env:USERPROFILE\.xlings\subos\default\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - name: Install bootstrap mcpp - shell: pwsh - run: xlings install mcpp -y - - - name: Build this PR's mcpp from source + - name: Build this PR's mcpp shell: bash run: | - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$USERPROFILE/.xlings/subos/default/bin/xlings.exe" mcpp build - MCPP_FRESH=$(find target -type f -name 'mcpp.exe' | head -1) - echo "MCPP=$(realpath "$MCPP_FRESH")" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" + MCPP=$(find target -type f -name 'mcpp.exe' | head -1) + echo "MCPP=$(realpath "$MCPP")" >> "$GITHUB_ENV" - name: "Fresh: mcpp new hello → mcpp run" shell: bash run: | - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" cd "$(mktemp -d)" "$MCPP" new hello cd hello From 7573f3b2cb3453f2b9d84d0da39ce19afbe88737 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:17:30 +0800 Subject: [PATCH 18/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20no?= =?UTF-8?q?=20$MCPP=20var,=20just=20PATH;=20add=20mirror=20for=20LLVM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove $MCPP variable, put freshly-built mcpp dir on PATH instead - All steps just use `mcpp` command directly - Linux LLVM step: add `mcpp self config --mirror GLOBAL` before toolchain install (CI runners are outside CN) - Windows: use pwsh throughout for native path handling --- .github/workflows/ci-fresh-install.yml | 69 +++++++++++++------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 3180dcf..545963c 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -1,10 +1,7 @@ name: ci-fresh-install -# Fresh install CI — validates the first-time user experience on all platforms. -# No sandbox/target caches: simulates a user who just installed mcpp. -# -# Flow: bootstrap mcpp via xlings → build this PR's mcpp from source → -# use the freshly-built mcpp to: new → run +# Fresh install CI — simulates first-time user experience on all platforms. +# Flow: xlings install mcpp → build this PR → mcpp new hello → mcpp run on: push: @@ -29,8 +26,7 @@ jobs: env: XLINGS_NON_INTERACTIVE: '1' run: | - curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-linux-x86_64.tar.gz -o /tmp/xlings.tar.gz - tar -xzf /tmp/xlings.tar.gz -C /tmp + curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-linux-x86_64.tar.gz | tar -xzf - -C /tmp /tmp/xlings-0.4.30-linux-x86_64/subos/default/bin/xlings self install export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings install mcpp -y @@ -39,24 +35,26 @@ jobs: - name: Build this PR's mcpp run: | mcpp build - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" + # Put freshly-built mcpp first on PATH so subsequent steps use it + MCPP_DIR=$(dirname "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + echo "$MCPP_DIR" >> "$GITHUB_PATH" - - name: "Fresh: mcpp new hello → mcpp run (GCC)" + - name: "mcpp new hello → mcpp run (GCC)" run: | cd "$(mktemp -d)" - "$MCPP" new hello + mcpp new hello cd hello - "$MCPP" run + mcpp run - - name: "Fresh: install LLVM → mcpp new → mcpp run" + - name: "install LLVM → mcpp new → mcpp run" run: | - "$MCPP" toolchain install llvm 20.1.7 - "$MCPP" toolchain default llvm@20.1.7 + mcpp self config --mirror GLOBAL + mcpp toolchain install llvm 20.1.7 + mcpp toolchain default llvm@20.1.7 cd "$(mktemp -d)" - "$MCPP" new hello_llvm + mcpp new hello_llvm cd hello_llvm - "$MCPP" run + mcpp run macos-fresh: name: macOS fresh install @@ -69,8 +67,7 @@ jobs: env: XLINGS_NON_INTERACTIVE: '1' run: | - curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-macosx-arm64.tar.gz -o /tmp/xlings.tar.gz - tar -xzf /tmp/xlings.tar.gz -C /tmp + curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-macosx-arm64.tar.gz | tar -xzf - -C /tmp /tmp/xlings-0.4.30-macosx-arm64/subos/default/bin/xlings self install export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings install mcpp -y @@ -79,15 +76,15 @@ jobs: - name: Build this PR's mcpp run: | mcpp build - MCPP=$(find target -type f -name mcpp ! -name '*.o' | head -1) - echo "MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP")" >> "$GITHUB_ENV" + MCPP_DIR=$(dirname "$(find target -type f -name mcpp ! -name '*.o' | head -1)") + echo "$(cd "$MCPP_DIR" && pwd)" >> "$GITHUB_PATH" - - name: "Fresh: mcpp new hello → mcpp run (LLVM)" + - name: "mcpp new hello → mcpp run" run: | cd "$(mktemp -d)" - "$MCPP" new hello + mcpp new hello cd hello - "$MCPP" run + mcpp run windows-fresh: name: Windows fresh install @@ -104,21 +101,23 @@ jobs: Invoke-WebRequest -Uri "https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip" -OutFile "$env:TEMP\xlings.zip" Expand-Archive -Path "$env:TEMP\xlings.zip" -DestinationPath "$env:TEMP\xlings-extract" -Force & "$env:TEMP\xlings-extract\xlings-0.4.30-windows-x86_64\subos\default\bin\xlings.exe" self install - $env:PATH = "$env:USERPROFILE\.xlings\subos\default\bin;$env:PATH" + $xlingsbin = "$env:USERPROFILE\.xlings\subos\default\bin" + $env:PATH = "$xlingsbin;$env:PATH" xlings install mcpp -y - "$env:USERPROFILE\.xlings\subos\default\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - name: Build this PR's mcpp - shell: bash + shell: pwsh run: | mcpp build - MCPP=$(find target -type f -name 'mcpp.exe' | head -1) - echo "MCPP=$(realpath "$MCPP")" >> "$GITHUB_ENV" + $mcppExe = Get-ChildItem -Path target -Recurse -Filter "mcpp.exe" | Select-Object -First 1 + $mcppExe.DirectoryName | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - name: "Fresh: mcpp new hello → mcpp run" - shell: bash + - name: "mcpp new hello → mcpp run" + shell: pwsh run: | - cd "$(mktemp -d)" - "$MCPP" new hello - cd hello - "$MCPP" run + $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } + Set-Location $tmp + mcpp new hello + Set-Location hello + mcpp run From 138696a948206c5d8b837345e9fb0329151c2918 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:35:12 +0800 Subject: [PATCH 19/22] =?UTF-8?q?fix:=20ci-fresh-install=20=E2=80=94=20tes?= =?UTF-8?q?t=20released=20mcpp,=20not=20freshly-built?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use xlings-installed mcpp directly to test: 1. mcpp build (self-host compile) 2. mcpp new hello → mcpp run (default toolchain) 3. Linux: install LLVM → mcpp new → mcpp run (continue-on-error since released mcpp may not have latest fixes yet) No extra env vars, no $MCPP variable — just mcpp on PATH via xlings. --- .github/workflows/ci-fresh-install.yml | 41 ++++++++++---------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index 545963c..9eed8f7 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -1,7 +1,7 @@ name: ci-fresh-install -# Fresh install CI — simulates first-time user experience on all platforms. -# Flow: xlings install mcpp → build this PR → mcpp new hello → mcpp run +# Fresh install CI — tests the released mcpp via xlings on clean machines. +# Validates: xlings install mcpp → mcpp build (self) → mcpp new → mcpp run on: push: @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Bootstrap xlings + mcpp + - name: Install xlings + mcpp env: XLINGS_NON_INTERACTIVE: '1' run: | @@ -32,21 +32,18 @@ jobs: xlings install mcpp -y echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build this PR's mcpp - run: | - mcpp build - # Put freshly-built mcpp first on PATH so subsequent steps use it - MCPP_DIR=$(dirname "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - echo "$MCPP_DIR" >> "$GITHUB_PATH" + - name: mcpp build (self-host) + run: mcpp build - - name: "mcpp new hello → mcpp run (GCC)" + - name: mcpp new hello → mcpp run run: | cd "$(mktemp -d)" mcpp new hello cd hello mcpp run - - name: "install LLVM → mcpp new → mcpp run" + - name: install LLVM → mcpp new → mcpp run + continue-on-error: true run: | mcpp self config --mirror GLOBAL mcpp toolchain install llvm 20.1.7 @@ -63,7 +60,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Bootstrap xlings + mcpp + - name: Install xlings + mcpp env: XLINGS_NON_INTERACTIVE: '1' run: | @@ -73,13 +70,10 @@ jobs: xlings install mcpp -y echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - name: Build this PR's mcpp - run: | - mcpp build - MCPP_DIR=$(dirname "$(find target -type f -name mcpp ! -name '*.o' | head -1)") - echo "$(cd "$MCPP_DIR" && pwd)" >> "$GITHUB_PATH" + - name: mcpp build (self-host) + run: mcpp build - - name: "mcpp new hello → mcpp run" + - name: mcpp new hello → mcpp run run: | cd "$(mktemp -d)" mcpp new hello @@ -93,7 +87,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Bootstrap xlings + mcpp + - name: Install xlings + mcpp shell: pwsh env: XLINGS_NON_INTERACTIVE: '1' @@ -106,14 +100,11 @@ jobs: xlings install mcpp -y $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - name: Build this PR's mcpp + - name: mcpp build (self-host) shell: pwsh - run: | - mcpp build - $mcppExe = Get-ChildItem -Path target -Recurse -Filter "mcpp.exe" | Select-Object -First 1 - $mcppExe.DirectoryName | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + run: mcpp build - - name: "mcpp new hello → mcpp run" + - name: mcpp new hello → mcpp run shell: pwsh run: | $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } From 9351b737c0d7f4f18919bf19ce0636be5bfd986f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:37:59 +0800 Subject: [PATCH 20/22] chore: move ci-fresh-install to separate PR The fresh-install CI workflow tests the released mcpp binary via xlings, not this PR's code. Move it to its own branch/PR to keep this PR focused on the sysroot fix. --- .github/workflows/ci-fresh-install.yml | 114 ------------------------- 1 file changed, 114 deletions(-) delete mode 100644 .github/workflows/ci-fresh-install.yml diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml deleted file mode 100644 index 9eed8f7..0000000 --- a/.github/workflows/ci-fresh-install.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: ci-fresh-install - -# Fresh install CI — tests the released mcpp via xlings on clean machines. -# Validates: xlings install mcpp → mcpp build (self) → mcpp new → mcpp run - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-fresh-install-${{ github.ref }} - cancel-in-progress: true - -jobs: - linux-fresh: - name: Linux fresh install - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-linux-x86_64.tar.gz | tar -xzf - -C /tmp - /tmp/xlings-0.4.30-linux-x86_64/subos/default/bin/xlings self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings install mcpp -y - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - - name: mcpp build (self-host) - run: mcpp build - - - name: mcpp new hello → mcpp run - run: | - cd "$(mktemp -d)" - mcpp new hello - cd hello - mcpp run - - - name: install LLVM → mcpp new → mcpp run - continue-on-error: true - run: | - mcpp self config --mirror GLOBAL - mcpp toolchain install llvm 20.1.7 - mcpp toolchain default llvm@20.1.7 - cd "$(mktemp -d)" - mcpp new hello_llvm - cd hello_llvm - mcpp run - - macos-fresh: - name: macOS fresh install - runs-on: macos-15 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-macosx-arm64.tar.gz | tar -xzf - -C /tmp - /tmp/xlings-0.4.30-macosx-arm64/subos/default/bin/xlings self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings install mcpp -y - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - - - name: mcpp build (self-host) - run: mcpp build - - - name: mcpp new hello → mcpp run - run: | - cd "$(mktemp -d)" - mcpp new hello - cd hello - mcpp run - - windows-fresh: - name: Windows fresh install - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - Invoke-WebRequest -Uri "https://github.com/d2learn/xlings/releases/download/v0.4.30/xlings-0.4.30-windows-x86_64.zip" -OutFile "$env:TEMP\xlings.zip" - Expand-Archive -Path "$env:TEMP\xlings.zip" -DestinationPath "$env:TEMP\xlings-extract" -Force - & "$env:TEMP\xlings-extract\xlings-0.4.30-windows-x86_64\subos\default\bin\xlings.exe" self install - $xlingsbin = "$env:USERPROFILE\.xlings\subos\default\bin" - $env:PATH = "$xlingsbin;$env:PATH" - xlings install mcpp -y - $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - - name: mcpp build (self-host) - shell: pwsh - run: mcpp build - - - name: mcpp new hello → mcpp run - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new hello - Set-Location hello - mcpp run From 55812b3faf9e18bc30b3bfa4c92dd24425dc24f0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:54:30 +0800 Subject: [PATCH 21/22] ci: multi-toolchain coverage on Linux, remove continue-on-error tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux CI now tests all 3 toolchains with freshly-built mcpp: - GCC 16.1.0: mcpp new → build → run - musl-gcc 15.1.0: mcpp new → build → run - LLVM 20.1.7: install + mcpp new → build → run Remove continue-on-error "Fresh user experience" tests from all 3 CI workflows — moved to separate ci-fresh-install.yml (PR #63). Those tests validate the xlings-distributed mcpp binary, not the PR's code, so they don't belong in the main CI gate. --- .github/workflows/ci-macos.yml | 11 +---------- .github/workflows/ci-windows.yml | 13 +------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 5f3e522..6343b67 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -316,13 +316,4 @@ jobs: "$MCPP" --version echo ":: Self-host smoke PASS" - - name: Fresh user experience (xlings install mcpp → new → run) - continue-on-error: true - run: | - # Test real user flow with xlings-distributed mcpp. - # May fail if xlings mcpp version lacks recent fixes. - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP" new hello - cd hello - "$MCPP" run + # Fresh user experience test moved to ci-fresh-install.yml (PR #63) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index d8b9aa0..90d5412 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -108,18 +108,7 @@ jobs: "$MCPP_SELF" --version echo ":: Self-host smoke PASS" - - name: Fresh user experience (xlings install mcpp → new → run) - continue-on-error: true - shell: bash - run: | - # Test the real user flow with the xlings-distributed mcpp. - # Currently xlings ships 0.0.17 which lacks Windows fixes. - # This step will auto-pass once xlings updates to 0.0.19+. - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP" new hello - cd hello - "$MCPP" run + # Fresh user experience test moved to ci-fresh-install.yml (PR #63) - name: Package Windows release zip id: package From 97791edd02d7559615012538de2a978673677801 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 23:55:02 +0800 Subject: [PATCH 22/22] fix: actually update ci.yml (previous edit didn't take effect) --- .github/workflows/ci.yml | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a40a881..e42abf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,30 +134,33 @@ jobs: "$MCPP" build "$MCPP" test - - name: Toolchain install smoke test (mcpp toolchain install llvm) + - name: "Toolchain: GCC — mcpp new → build → run" run: | - # Test the core user flow: install a toolchain, create a project, - # build with it. Uses the freshly-built mcpp (not bootstrap). MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - # Install LLVM toolchain into mcpp's sandbox - "$MCPP" toolchain install llvm 20.1.7 - # Set as default so the build picks it up - "$MCPP" toolchain default llvm@20.1.7 - # Build a hello-world project with the installed toolchain + "$MCPP" toolchain default gcc@16.1.0 TMP=$(mktemp -d) cd "$TMP" - "$MCPP" new hello - cd hello - "$MCPP" build + "$MCPP" new hello_gcc + cd hello_gcc "$MCPP" run - - name: Fresh user experience (xlings install mcpp → new → run) - continue-on-error: true + - name: "Toolchain: musl-gcc — mcpp new → build → run" run: | - # Test real user flow with xlings-distributed mcpp. - # May fail if xlings mcpp version lacks recent fixes. + MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + "$MCPP" toolchain default gcc@15.1.0-musl + TMP=$(mktemp -d) + cd "$TMP" + "$MCPP" new hello_musl + cd hello_musl + "$MCPP" run + + - name: "Toolchain: LLVM — install + mcpp new → build → run" + run: | + MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + "$MCPP" toolchain install llvm 20.1.7 + "$MCPP" toolchain default llvm@20.1.7 TMP=$(mktemp -d) cd "$TMP" - "$MCPP" new hello - cd hello + "$MCPP" new hello_llvm + cd hello_llvm "$MCPP" run