From d3c5fe6289fb12b30b545992c49ba5eceffcde82 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 3 Jun 2026 23:17:39 +0800 Subject: [PATCH 01/19] Bake dependency runtime library_dirs into binary RUNPATH; default Linux toolchain to glibc R2: flags.cppm emitted -Wl,-rpath only from toolchain.linkRuntimeDirs, dropping dependency packages' [runtime] library_dirs (already collected into plan.runtimeLibraryDirs by plan.cppm). So host-GL passthrough dirs (e.g. compat.glx-runtime) never reached the binary RUNPATH and dlopen()'d libGL/libGLX failed at run time (GLX: Failed to load GLX). Iterate plan.runtimeLibraryDirs (superset) instead. R1: cli.cppm first-run default on Linux was gcc-musl-static, which cannot link the glibc world (X11/GL/system libs). Default to platform-native glibc gcc; musl-static stays opt-in via --target x86_64-linux-musl (Cargo-style). --- src/build/flags.cppm | 8 +++++++- src/cli.cppm | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 235ee37..94a0d40 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -256,7 +256,13 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string static_stdlib = (f.staticStdlib && !isClang && !mcpp::platform::is_windows) ? " -static-libstdc++" : ""; std::string runtime_dirs; if constexpr (mcpp::platform::supports_rpath) { - for (auto& dir : plan.toolchain.linkRuntimeDirs) { + // Bake ALL resolved runtime library dirs into the binary's RUNPATH — + // not just the toolchain's. plan.runtimeLibraryDirs is the union of + // dependency packages' [runtime] library_dirs (e.g. compat.glx-runtime's + // host-GL passthrough dir) plus the toolchain/payload dirs. Using only + // toolchain.linkRuntimeDirs here dropped dependency runtime dirs, so + // dlopen()'d host libs (libGL/libGLX) were unreachable at run time. + for (auto& dir : plan.runtimeLibraryDirs) { runtime_dirs += " -L" + escape_path(dir); runtime_dirs += " -Wl,-rpath," + escape_path(dir); } diff --git a/src/cli.cppm b/src/cli.cppm index d7200a9..6f96680 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1386,9 +1386,13 @@ prepare_build(bool print_fingerprint, // // macOS: LLVM/Clang — Apple doesn't ship GCC; upstream LLVM with // bundled libc++ is the self-contained choice. - // Linux: musl-gcc — produces portable static binaries. + // Linux: glibc gcc — the platform-native ABI. A musl-static default + // cannot link the glibc world (X11/GL/system libs), so it + // breaks GUI/native packages out of the box. musl-static stays + // opt-in via `mcpp build --target x86_64-linux-musl` for users + // who explicitly want portable static binaries. std::string defaultSpec = (mcpp::platform::is_macos || mcpp::platform::is_windows) - ? "llvm@20.1.7" : "gcc@15.1.0-musl"; + ? "llvm@20.1.7" : "gcc@16.1.0"; auto defaultParsed = mcpp::toolchain::parse_toolchain_spec(defaultSpec); auto defaultPkg = mcpp::toolchain::to_xim_package(*defaultParsed); @@ -1398,7 +1402,7 @@ prepare_build(bool print_fingerprint, defaultSpec)); } else { mcpp::ui::info("First run", - std::format("no toolchain configured — installing {} (musl, static) as default", + std::format("no toolchain configured — installing {} (glibc, native ABI) as default", defaultSpec)); } From af91e99a2a3db2e2e3180ca1e63b19429ef6d9a3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 3 Jun 2026 23:17:39 +0800 Subject: [PATCH 02/19] docs: runtime closure (rpath) + toolchain default design --- ...-runtime-closure-and-toolchain-defaults.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .agents/docs/2026-06-03-runtime-closure-and-toolchain-defaults.md diff --git a/.agents/docs/2026-06-03-runtime-closure-and-toolchain-defaults.md b/.agents/docs/2026-06-03-runtime-closure-and-toolchain-defaults.md new file mode 100644 index 0000000..4b09592 --- /dev/null +++ b/.agents/docs/2026-06-03-runtime-closure-and-toolchain-defaults.md @@ -0,0 +1,83 @@ +# mcpp core: runtime closure (rpath) + toolchain defaults + +> 2026-06-03 · part of the mcpp ecosystem打通 plan +> Master plan: /home/speak/workspace/github/agentdocs/2026-06-03-mcpp-ecosystem-architecture-plan.md + +This change fixes two general, long-term issues that block "native + GUI" +packages (e.g. the imgui module package) from working out of the box. Neither +is a special-case for any one package. + +## R2 — dependency `[runtime] library_dirs` were dropped from binary RUNPATH + +### Symptom +A fresh consumer that depends (transitively) on `compat.glfw` builds fine but +`mcpp run` fails at window creation: `GLX: Failed to load GLX`. + +### Root cause (confirmed in source) +- `compat.glx-runtime` (pulled in by `compat.glfw` on Linux) symlinks the host + GLVND/GL/GLX libraries into its install dir and declares + `[runtime] library_dirs = { mcpp_generated/glx_runtime/lib }`. +- `src/build/plan.cppm` (~L220) already collects every dependency package's + `runtime.library_dirs` into `plan.runtimeLibraryDirs` (resolved to absolute). +- BUT `src/build/flags.cppm` (~L258) built the produced binary's RUNPATH by + iterating only `plan.toolchain.linkRuntimeDirs` — i.e. the toolchain's own + runtime dirs. The dependency runtime dirs in `plan.runtimeLibraryDirs` were + never emitted as `-Wl,-rpath`. So the host-GL passthrough dir was not on the + binary's RUNPATH, and the dlopen()'d `libGL.so.1` / `libGLX.so.0` were + unreachable at run time. + +The dependency dirs were correctly used for the *build/process* environment but +not baked into the *binary* — so anything reached via dlopen (GL/GLX, and any +plugin-style runtime lib) failed. + +### Fix +`src/build/flags.cppm`: iterate `plan.runtimeLibraryDirs` (the union of +dependency runtime dirs + toolchain + payload) instead of +`plan.toolchain.linkRuntimeDirs` when emitting `-L`/`-Wl,-rpath`. This is a +superset, so toolchain dirs are still covered; it additionally bakes each +dependency's declared runtime dir into RUNPATH. + +This is the correct general behavior: any package that declares +`[runtime] library_dirs` is promising "binaries that use me need these dirs at +run time"; the producer binary must carry them as RUNPATH. + +## R1 — fresh-machine bootstrap default toolchain was musl-static on Linux + +### Symptom +On a clean machine, "First run no toolchain configured" auto-installs +`gcc@15.1.0-musl` (musl, static). Building any package that links the glibc +world (X11/GL/system libs) then fails, e.g. `libXdmcp` `arc4random_buf` +implicit-declaration under musl. + +### Root cause +`src/cli.cppm` (~L1390) hard-coded the Linux first-run default to +`gcc@15.1.0-musl`. + +### Fix +Default Linux first-run toolchain to the platform-native glibc gcc +(`gcc@16.1.0`). musl-static remains fully available but **opt-in** via +`mcpp build --target x86_64-linux-musl` (which the project already supports via +`[target.x86_64-linux-musl]`). This mirrors Cargo/Rust: default triple is +`-gnu` (glibc), `-musl` is an explicit target for portable static binaries. +musl-static is a poor *default* because it cannot link the glibc/native world. + +## Why these are long-term/industrial, not workarounds +- R2 makes the existing two-plane design actually work: the *host plane* + (drivers/GLVND, provided by `compat.glx-runtime`, never vendored) is bound to + the binary via RUNPATH, which is the standard ELF mechanism. No package code + changes; no env hacks. +- R1 aligns the default with the platform-native ABI, the same principle Cargo + uses. Static/musl stays a first-class explicit option. + +## Test plan (acceptance, via imgui-m, no special-casing) +1. Self-build mcpp with these changes. +2. Fresh consumer: `mcpp new app && mcpp add imgui` then: + - `mcpp build` → uses glibc gcc by default (R1), no musl error. + - `readelf -d ` → RUNPATH contains the `compat.glx-runtime` lib dir (R2). + - `mcpp run` → window opens, ImGui renders, no `GLX: Failed to load GLX`. +3. `mcpp test` headless still passes on all platforms. + +## Follow-up (separate, tracked in master plan) +- Declarative `abi` capability on native packages so the resolver *derives* the + ABI-correct toolchain instead of relying on a good default (defense in depth). +- Capability→provider resolution for `opengl.glx.driver` (glvnd/cocoa/win32). From 6e79919bf0fdf9bf1faa94e0d24cb3569647ae98 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:03:34 +0800 Subject: [PATCH 03/19] feat: mcpp new --template gui (imgui.app starter) + package-template design Adds a builtin gui scaffold to `mcpp new` (Tier-0 imgui.app window starter, deps imgui 0.0.2, no toolchain pin). Verified: new --template gui -> build -> window renders. Also documents the long-term package-based template model (mcpp new name --template pkg@ver:tmpl, libraries ship templates/) as TODO. --- .agents/docs/2026-06-03-package-templates.md | 91 ++++++++++++++++++++ src/cli.cppm | 34 +++++++- 2 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 .agents/docs/2026-06-03-package-templates.md diff --git a/.agents/docs/2026-06-03-package-templates.md b/.agents/docs/2026-06-03-package-templates.md new file mode 100644 index 0000000..dc3e307 --- /dev/null +++ b/.agents/docs/2026-06-03-package-templates.md @@ -0,0 +1,91 @@ +# mcpp 模板系统(package-based templates)— 设计 + TODO + +> 2026-06-03 · 状态:builtin 模板已实现;**package 模板为设计/TODO(本轮未实现)** +> 关联:agentdocs/2026-06-03-capability-architecture-rfc.md §9 + +## 目标 + +`mcpp new` 的模板不应只有内置几种,而应**复用"库模型":一个库同时携带实现 + 示例 + +可实例化模板**。让库作者把"上手骨架"和库一起分发,消费者一条命令拉起。 + +``` +mcpp new myapp # builtin: bin(默认) +mcpp new myapp --template gui # builtin: imgui.app 窗口骨架(已实现) +mcpp new myapp --template imgui@0.0.2:window # package 模板(本设计) +mcpp new myapp --template imgui:window # 省略版本 = 最新 +``` + +## 两层模型 + +### 1) builtin 模板(已实现) +- `--template bin|gui`,硬编码在 `src/cli.cppm cmd_new`。 +- 用途:无网络/零依赖即可起步;`gui` 给出 imgui.app Tier-0 骨架。 +- 这是 fallback,也是 package 模板的"标准库"等价物。 + +### 2) package 模板(设计 / TODO) +语法:`--template [@]:`。 + +**库侧目录约定**(库仓库里新增 `templates/`): +``` +imgui-m/ +├── src/ # 库实现 +├── examples/ # 可运行示例 +└── templates/ + └── window/ # 模板名 = 目录名 + ├── template.toml # 模板元数据(见下) + ├── mcpp.toml.in # 带占位符的清单 + └── src/main.cpp.in # 带占位符的源码 +``` + +**template.toml**: +```toml +[template] +name = "window" +description = "Minimal imgui.app window app" +# 占位符 → 取值来源 +[template.vars] +PROJECT = "{{name}}" # mcpp new 的 name +IMGUI_VER = "{{self.version}}" # 该模板所属包的版本(自动) +# 生成后提示 +post_message = "Edit src/main.cpp, then `mcpp run`." +``` + +**占位符渲染**:`{{name}}`、`{{self.version}}`、`{{self.name}}` 等;`.in` 后缀文件渲染后去掉 `.in`;非 `.in` 文件原样拷贝。 + +### 解析与执行流程(core) +1. 解析 `--template` 值: + - 不含 `:` → builtin(`bin`/`gui`)。 + - 含 `:` → `pkg[@ver]:tmpl`。 +2. 经现有 fetcher/index 解析并下载该 `pkg@ver`(复用 `mcpp.pm` / `fetcher.cppm`)。 +3. 读取包内 `templates//template.toml`;若缺失 → 报错并列出该包可用模板(`templates/*/`)。 +4. 渲染:对模板目录递归拷贝,`.in` 文件做占位符替换,写入新项目目录。 +5. 若模板 mcpp.toml 未声明对该库的依赖,自动注入 `[dependencies] = ""`(让模板默认依赖它所属的库)。 +6. 打印 `template.post_message`。 + +### 代码定位(实现时) +- `src/cli.cppm cmd_new`:解析 `--template`,分流 builtin vs package。 +- 新增 `src/scaffold/template.cppm`:模板下载 + 渲染引擎(占位符、`.in` 处理)。 +- 复用:`src/fetcher.cppm` / `mcpp.pm.*`(下载包)、`src/manifest.cppm`(注入依赖)。 +- index:无需改 schema(模板随源码 tarball 分发,已在 `templates/`)。 + +### 发现/列举 +- `mcpp new --list-templates [@ver]`:下载并列出 `templates/*/` 及其 description。 +- `mcpp new --template :`(空模板名)→ 同上列举提示。 + +## 为什么这样设计(契合架构不变量) +- I5 复杂度下沉:模板由库作者写一次,消费者一条命令继承。 +- I1/I4:`--template gui` builtin 保零配置;package 模板可被 `--list-templates` 解释。 +- 与 capability 模型正交:模板只是"起点物料",不改变解析/能力体系。 + +## TODO(实现顺序) +- [ ] T1 模板字符串解析 `pkg@ver:tmpl`(+ builtin 分流)。 +- [ ] T2 `template.cppm` 渲染引擎(`.in` + `{{var}}`)。 +- [ ] T3 接 fetcher 下载模板包 + 读取 `templates//`。 +- [ ] T4 自动注入依赖 + post_message。 +- [ ] T5 `--list-templates`。 +- [ ] T6 imgui-m 仓增 `templates/window/`、`templates/headless/` 作为首批样例。 +- [ ] T7 文档 + `mcpp new --help` 更新。 + +## 现状(本轮已落地) +- builtin `--template bin|gui` 已实现并验证(`mcpp new x --template gui` → imgui.app 窗口骨架 → 直接出窗口)。 +- package 模板:本文件为设计与 TODO,留待后续实现。 diff --git a/src/cli.cppm b/src/cli.cppm index 6f96680..9c21262 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1035,6 +1035,16 @@ int cmd_new(const mcpplibs::cmdline::ParsedArgs& parsed) { return 2; } + // `--template` selects the project skeleton: "bin" (default) or "gui" + // (an imgui.app starter — Tier-0 zero-boilerplate window). + std::string tmpl = "bin"; + if (auto t = parsed.value("template")) tmpl = *t; + if (tmpl != "bin" && tmpl != "gui") { + std::println(stderr, "error: unknown --template '{}' (expected: bin | gui)", tmpl); + return 2; + } + const bool gui = (tmpl == "gui"); + std::filesystem::path root = std::filesystem::current_path() / name; if (std::filesystem::exists(root)) { std::println(stderr, "error: '{}' already exists", root.string()); @@ -1051,10 +1061,28 @@ int cmd_new(const mcpplibs::cmdline::ParsedArgs& parsed) { { std::ofstream os(root / "mcpp.toml"); os << mcpp::manifest::default_template(name); + if (gui) { + // The GUI template depends on the imgui module package. It does not + // pin a toolchain — mcpp resolves the environment/default toolchain + // and the GL runtime is closed by the ecosystem (compat.glx-runtime). + os << "\n[dependencies]\nimgui = \"0.0.2\"\n"; + } } // src/main.cpp — template with PROJECT placeholder, replaced with `name`. { - std::string body = R"(// PROJECT — generated by `mcpp new` + std::string body = gui ? R"GUI(// PROJECT — generated by `mcpp new --template gui` +// Tier-0 zero-boilerplate window via the imgui.app facade. No #include. +import imgui.core; +import imgui.app; + +int main() { + return ImGui::App::run([] { + ImGui::Begin("PROJECT"); + ImGui::TextUnformatted("Hello from mcpp + imgui (imgui.app facade)"); + ImGui::End(); + }); +} +)GUI" : R"(// PROJECT — generated by `mcpp new` import std; int main(int argc, char* argv[]) { @@ -1094,7 +1122,7 @@ int main() { os << "target/\n"; } - std::println("Created package '{}' at {}", name, root.string()); + std::println("Created {} package '{}' at {}", gui ? "gui" : "bin", name, root.string()); std::println("Next: cd {} && mcpp build && mcpp run (or `mcpp test`)", name); return 0; } @@ -5367,6 +5395,8 @@ int run(int argc, char** argv) { .subcommand(cl::App("new") .description("Create a new mcpp package skeleton") .arg(cl::Arg("name").help("Package directory name").required()) + .option(cl::Option("template").short_name('t').takes_value().value_name("KIND") + .help("Project template: bin (default) | gui (imgui.app window starter)")) .action(wrap_rc(cmd_new))) .subcommand(cl::App("build") .description("Build the current package") From 074bcdd7c79d95cefb6042ffc53d4a0fd4a264b4 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:12:39 +0800 Subject: [PATCH 04/19] feat: mcpp why / resolve --explain + capability-level doctor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcpp why [toolchain|runtime|deps] (alias: resolve --explain) explains the resolved toolchain (incl. abi), the runtime library dirs baked into RUNPATH (surfacing the compat.glx-runtime host-GL closure), and locked deps — so defaults are inspectable, not magic (I4). self doctor: add a runtime-capabilities section probing x11/wayland display and the host GLVND opengl.glx.driver (libGLX + vendor), reporting the provider — capability-level, not just env health. --- src/cli.cppm | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/src/cli.cppm b/src/cli.cppm index 9c21262..b6a0332 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -4277,6 +4277,45 @@ int cmd_doctor(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { ok(std::format("BMI cache size = {}", human_bytes(sz))); } + mcpp::ui::status("Checking", "runtime capabilities"); + { +#if defined(__APPLE__) || defined(_WIN32) + ok("host GL/windowing provided by platform framework"); +#else + if (const char* d = std::getenv("DISPLAY"); d && *d) + ok(std::format("x11.display: ok ($DISPLAY={})", d)); + else if (const char* w = std::getenv("WAYLAND_DISPLAY"); w && *w) + ok(std::format("wayland.display: ok ($WAYLAND_DISPLAY={})", w)); + else + warn("display: none — windowed apps need $DISPLAY or $WAYLAND_DISPLAY"); + + const char* gldirs[] = {"/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu", + "/usr/lib64", "/usr/lib"}; + auto find_lib = [&](std::string_view prefix) -> std::string { + for (auto* dir : gldirs) { + std::error_code ec; + if (!std::filesystem::exists(dir, ec)) continue; + for (auto& e : std::filesystem::directory_iterator(dir, ec)) { + auto fn = e.path().filename().string(); + if (fn.rfind(prefix, 0) == 0) return e.path().string(); + } + } + return {}; + }; + auto glx = find_lib("libGLX.so"); + auto gl = find_lib("libGL.so"); + auto vnd = find_lib("libGLX_nvidia.so"); + if (vnd.empty()) vnd = find_lib("libGLX_mesa.so"); + if (!glx.empty() && !gl.empty()) { + ok(std::format("opengl.glx.driver: ok (provider compat.glx-runtime; {}, {})", + std::filesystem::path(glx).filename().string(), + vnd.empty() ? "no GLVND vendor" : std::filesystem::path(vnd).filename().string())); + } else { + warn("opengl.glx.driver: host libGL/libGLX not found — `mcpp run` of GL apps may fail"); + } +#endif + } + std::println(""); if (errors) std::println("Doctor result: {} errors, {} warnings", errors, warns); else if (warns) std::println("Doctor result: {} warnings", warns); @@ -4284,6 +4323,67 @@ int cmd_doctor(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { return errors ? 2 : (warns ? 1 : 0); } +// `mcpp why [topic]` / `mcpp resolve --explain` — explain how the toolchain, +// runtime closure, and dependencies were resolved (I4: defaults are not magic). +int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { + std::string topic = parsed.positional(0); + const bool all = topic.empty() || topic == "all"; + + auto ctx = prepare_build(/*print_fingerprint=*/false); + if (!ctx) { std::println(stderr, "error: {}", ctx.error()); return 2; } + auto& tc = ctx->tc; + auto& plan = ctx->plan; + + auto abi_of = [](const mcpp::toolchain::Toolchain& t) -> std::string { + if (t.targetTriple.find("musl") != std::string::npos) return "musl"; + if (t.stdlibId == "libc++") return "libc++"; + if (t.compiler == mcpp::toolchain::CompilerId::MSVC) return "msvc"; + return "glibc"; + }; + + if (all || topic == "toolchain") { + std::println("toolchain: {}", tc.label()); + std::println(" abi={} stdlib={} triple={}", abi_of(tc), tc.stdlibId, tc.targetTriple); + std::println(" reason: [toolchain] in mcpp.toml if set, else platform-native default"); + } + if (all || topic == "runtime") { + std::println("runtime library dirs (baked into binary RUNPATH):"); + if (plan.runtimeLibraryDirs.empty()) std::println(" (none)"); + for (auto& d : plan.runtimeLibraryDirs) { + auto s = d.string(); + std::string note; + if (s.find("glx_runtime") != std::string::npos) + note = " <- host GL/GLX runtime (compat.glx-runtime)"; + else if (s.find("glibc") != std::string::npos) note = " <- glibc"; + else if (s.find("xim-x-gcc") != std::string::npos + || s.find("xim-x-llvm") != std::string::npos) note = " <- toolchain"; + std::println(" - {}{}", s, note); + } + } + if (all || topic == "deps") { + std::println("dependencies (mcpp.lock):"); + std::ifstream in(ctx->projectRoot / "mcpp.lock"); + if (!in) { + std::println(" (no mcpp.lock — run `mcpp build` or `mcpp update`)"); + } else { + std::string line, cur; + auto quoted = [](const std::string& l) -> std::string { + auto a = l.find('"'); if (a == std::string::npos) return {}; + auto b = l.find('"', a + 1); if (b == std::string::npos) return {}; + return l.substr(a + 1, b - a - 1); + }; + while (std::getline(in, line)) { + if (line.find("[package.\"") != std::string::npos) cur = quoted(line); + else if (!cur.empty() && line.find("version") != std::string::npos) { + std::println(" - {} {}", cur, quoted(line)); + cur.clear(); + } + } + } + } + return 0; +} + // ─── M4 #4: mcpp cache list / prune / clean / info ────────────────────── struct CacheEntry { std::filesystem::path dir; @@ -5426,6 +5526,14 @@ int run(int argc, char** argv) { .description("Remove target/ (and optionally the global BMI cache)") .option(cl::Option("bmi-cache").help("Also wipe the global BMI cache")) .action(wrap_rc(cmd_clean))) + .subcommand(cl::App("why") + .description("Explain how the toolchain / runtime / deps were resolved") + .arg(cl::Arg("topic").help("toolchain | runtime | deps (default: all)")) + .action(wrap_rc(cmd_why))) + .subcommand(cl::App("resolve") + .description("Re-resolve the build plan and explain it") + .option(cl::Option("explain").help("Print resolved toolchain / runtime / deps")) + .action(wrap_rc(cmd_why))) .subcommand(cl::App("add") .description("Add a dependency to mcpp.toml") .arg(cl::Arg("pkg").help("Package spec, e.g. foo@1.0.0").required()) @@ -5640,11 +5748,11 @@ int run(int argc, char** argv) { { std::string_view first = argv[1]; if (!first.starts_with('-')) { - static constexpr std::array known = { + static constexpr std::array known = { "new", "build", "run", "test", "clean", "add", "remove", "update", "search", "publish", "pack", "emit", "toolchain", "cache", "index", "self", "explain", - "version", "dyndep", + "version", "dyndep", "why", "resolve", }; bool ok = false; for (auto k : known) if (k == first) { ok = true; break; } From 0d537f6882cb5caa7e028473c5673edf53d8cc69 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:17:33 +0800 Subject: [PATCH 05/19] refactor: capability/provider-driven doctor (no platform #ifdef) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggregate dependency providers' [runtime] dlopen_libs/capabilities into the BuildPlan (with capability->provider mapping). doctor now reports each required host capability + its provider and verifies each provider-declared dlopen soname against the resolved runtime library_dirs — fully data-driven. Removes the previous #ifdef __APPLE__/_WIN32 + hardcoded /usr/lib paths: platform knowledge belongs in provider packages, not in mcpp core. why also surfaces capability->provider. --- src/build/plan.cppm | 15 ++++++++++ src/cli.cppm | 72 +++++++++++++++++++++++++-------------------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 5d7ba04..8e405dc 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -56,6 +56,12 @@ struct BuildPlan { std::vector compileUnits; // topologically sorted std::vector linkUnits; std::vector runtimeLibraryDirs; + // Aggregated host-runtime requirements from dependency packages' + // [runtime] metadata. Capability/provider-driven — no platform special-casing + // in mcpp: providers (e.g. compat.glx-runtime) declare these per platform. + std::vector runtimeDlopenLibs; // union of deps' dlopen sonames + std::vector runtimeCapabilities; // union of host capabilities + std::vector> runtimeProviders; // (capability, provider pkg) }; // Build a BuildPlan from already-validated inputs. @@ -222,6 +228,15 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, append_unique_path(plan.runtimeLibraryDirs, dir.is_absolute() ? dir : package.root / dir); } + for (auto const& lib : package.manifest.runtimeConfig.dlopenLibs) { + if (std::ranges::find(plan.runtimeDlopenLibs, lib) == plan.runtimeDlopenLibs.end()) + plan.runtimeDlopenLibs.push_back(lib); + } + for (auto const& cap : package.manifest.runtimeConfig.capabilities) { + if (std::ranges::find(plan.runtimeCapabilities, cap) == plan.runtimeCapabilities.end()) + plan.runtimeCapabilities.push_back(cap); + plan.runtimeProviders.emplace_back(cap, package.manifest.package.name); + } } // The same private runtime directories embedded as executable RUNPATH are // also needed in the process environment for libraries reached only via diff --git a/src/cli.cppm b/src/cli.cppm index b6a0332..8d7ecd0 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -4279,41 +4279,41 @@ int cmd_doctor(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { mcpp::ui::status("Checking", "runtime capabilities"); { -#if defined(__APPLE__) || defined(_WIN32) - ok("host GL/windowing provided by platform framework"); -#else - if (const char* d = std::getenv("DISPLAY"); d && *d) - ok(std::format("x11.display: ok ($DISPLAY={})", d)); - else if (const char* w = std::getenv("WAYLAND_DISPLAY"); w && *w) - ok(std::format("wayland.display: ok ($WAYLAND_DISPLAY={})", w)); - else - warn("display: none — windowed apps need $DISPLAY or $WAYLAND_DISPLAY"); - - const char* gldirs[] = {"/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu", - "/usr/lib64", "/usr/lib"}; - auto find_lib = [&](std::string_view prefix) -> std::string { - for (auto* dir : gldirs) { - std::error_code ec; - if (!std::filesystem::exists(dir, ec)) continue; - for (auto& e : std::filesystem::directory_iterator(dir, ec)) { - auto fn = e.path().filename().string(); - if (fn.rfind(prefix, 0) == 0) return e.path().string(); + // Capability/provider-driven — no platform special-casing in mcpp. + // Required capabilities and the sonames to probe come entirely from the + // dependency graph's provider packages (e.g. compat.glx-runtime); the + // search dirs are the resolved runtime library_dirs. The same code path + // works on every platform — providers carry the platform knowledge. + auto pctx = prepare_build(/*print_fingerprint=*/false); + if (!pctx) { + ok("(run inside a package to check its runtime capabilities)"); + } else if (pctx->plan.runtimeCapabilities.empty()) { + ok("no host runtime capabilities required"); + } else { + auto& plan = pctx->plan; + for (auto& cap : plan.runtimeCapabilities) { + std::string provider; + for (auto& [c, p] : plan.runtimeProviders) + if (c == cap) { provider = p; break; } + ok(std::format("{}: required (provider {})", + cap, provider.empty() ? "?" : provider)); + } + auto resolves = [&](std::string_view soname) { + for (auto& dir : plan.runtimeLibraryDirs) { + std::error_code ec; + if (!std::filesystem::exists(dir, ec)) continue; + for (auto& e : std::filesystem::directory_iterator(dir, ec)) { + auto fn = e.path().filename().string(); + if (fn == soname || fn.rfind(soname, 0) == 0) return true; + } } + return false; + }; + for (auto& lib : plan.runtimeDlopenLibs) { + if (resolves(lib)) ok(std::format("dlopen {}: resolvable on RUNPATH", lib)); + else warn(std::format("dlopen {}: not found on resolved runtime dirs", lib)); } - return {}; - }; - auto glx = find_lib("libGLX.so"); - auto gl = find_lib("libGL.so"); - auto vnd = find_lib("libGLX_nvidia.so"); - if (vnd.empty()) vnd = find_lib("libGLX_mesa.so"); - if (!glx.empty() && !gl.empty()) { - ok(std::format("opengl.glx.driver: ok (provider compat.glx-runtime; {}, {})", - std::filesystem::path(glx).filename().string(), - vnd.empty() ? "no GLVND vendor" : std::filesystem::path(vnd).filename().string())); - } else { - warn("opengl.glx.driver: host libGL/libGLX not found — `mcpp run` of GL apps may fail"); } -#endif } std::println(""); @@ -4359,6 +4359,14 @@ int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { || s.find("xim-x-llvm") != std::string::npos) note = " <- toolchain"; std::println(" - {}{}", s, note); } + if (!plan.runtimeCapabilities.empty()) { + std::println("runtime capabilities (provider):"); + for (auto& cap : plan.runtimeCapabilities) { + std::string prov; + for (auto& [c, p] : plan.runtimeProviders) if (c == cap) { prov = p; break; } + std::println(" - {} -> {}", cap, prov.empty() ? "?" : prov); + } + } } if (all || topic == "deps") { std::println("dependencies (mcpp.lock):"); From 2eb076cba30d5254857356ad5c07deb5dfbca180 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:22:18 +0800 Subject: [PATCH 06/19] feat: capability-driven ABI enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dependency may declare an abi: capability (e.g. compat.glfw -> abi:glibc). prepare_build now verifies the resolved toolchain's ABI satisfies every such requirement and fails fast with an actionable message on mismatch — turning a cryptic deep musl build error (libXdmcp arc4random_buf) into an upfront capability error. Enforces/diagnoses; abi-driven reselection (toolchain is resolved before the dep graph) is a resolution-ordering follow-up. --- src/cli.cppm | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/cli.cppm b/src/cli.cppm index 8d7ecd0..d7dca0f 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -3131,6 +3131,32 @@ prepare_build(bool print_fingerprint, } } + // Capability-driven ABI enforcement: if any dependency declares an + // `abi:` capability, the resolved toolchain must satisfy it. (Toolchain + // is resolved before the dep graph, so this enforces/diagnoses rather than + // reselects — abi-driven reselection is a resolution-ordering follow-up.) + { + const std::string tcAbi = + ctx.tc.targetTriple.find("musl") != std::string::npos ? "musl" + : ctx.tc.stdlibId == "libc++" ? "libc++" + : ctx.tc.compiler == mcpp::toolchain::CompilerId::MSVC ? "msvc" + : "glibc"; + for (auto& cap : ctx.plan.runtimeCapabilities) { + if (cap.rfind("abi:", 0) != 0) continue; + std::string need = cap.substr(4); + if (need == tcAbi) continue; + std::string provider; + for (auto& [c, p] : ctx.plan.runtimeProviders) + if (c == cap) { provider = p; break; } + return std::unexpected(std::format( + "ABI mismatch: dependency '{}' requires abi={} but the resolved " + "toolchain '{}' is abi={}.\n" + " fix: `mcpp toolchain default <{}-compatible>` " + "(e.g. gcc@16.1.0 for glibc), or set [toolchain] in mcpp.toml.", + provider.empty() ? "?" : provider, need, ctx.tc.label(), tcAbi, need)); + } + } + return ctx; } From 388217d50c39070a6ba5dda97eda393e578f71a3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:24:08 +0800 Subject: [PATCH 07/19] feat: per-build resolution.json manifest artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Writes a machine-readable resolution manifest (toolchain/abi, runtime closure library_dirs, dlopen_libs, capability->provider) next to build outputs — the serialized capability->plan, same data as `mcpp why`, usable by CI/tooling. --- src/cli.cppm | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/cli.cppm b/src/cli.cppm index d7dca0f..c024650 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -3157,6 +3157,49 @@ prepare_build(bool print_fingerprint, } } + // Per-build resolution manifest artifact: a machine-readable record of the + // resolved plan (toolchain/abi, runtime closure, capabilities+providers, + // deps) written next to the build outputs. Same data as `mcpp why`; usable + // by CI/tooling. (capability -> plan, serialized.) + { + const std::string tcAbi = + ctx.tc.targetTriple.find("musl") != std::string::npos ? "musl" + : ctx.tc.stdlibId == "libc++" ? "libc++" + : ctx.tc.compiler == mcpp::toolchain::CompilerId::MSVC ? "msvc" + : "glibc"; + auto jstr = [](std::string_view s) { + std::string o = "\""; + for (char c : s) { if (c == '\\' || c == '"') o += '\\'; o += c; } + return o + "\""; + }; + std::error_code ec; + std::filesystem::create_directories(ctx.plan.outputDir, ec); + std::ofstream js(ctx.plan.outputDir / "resolution.json"); + if (js) { + js << "{\n"; + js << " \"toolchain\": {\"spec\": " << jstr(ctx.tc.label()) + << ", \"abi\": " << jstr(tcAbi) + << ", \"triple\": " << jstr(ctx.tc.targetTriple) + << ", \"stdlib\": " << jstr(ctx.tc.stdlibId) << "},\n"; + js << " \"runtime\": {\n"; + js << " \"library_dirs\": ["; + for (std::size_t i = 0; i < ctx.plan.runtimeLibraryDirs.size(); ++i) + js << (i ? ", " : "") << jstr(ctx.plan.runtimeLibraryDirs[i].string()); + js << "],\n"; + js << " \"dlopen_libs\": ["; + for (std::size_t i = 0; i < ctx.plan.runtimeDlopenLibs.size(); ++i) + js << (i ? ", " : "") << jstr(ctx.plan.runtimeDlopenLibs[i]); + js << "],\n"; + js << " \"capabilities\": ["; + for (std::size_t i = 0; i < ctx.plan.runtimeProviders.size(); ++i) { + auto& [cap, prov] = ctx.plan.runtimeProviders[i]; + js << (i ? ", " : "") << "{\"capability\": " << jstr(cap) + << ", \"provider\": " << jstr(prov) << "}"; + } + js << "]\n }\n}\n"; + } + } + return ctx; } From b17adb9788e20636d20fb56380304be87226bfdc Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:26:56 +0800 Subject: [PATCH 08/19] refactor: resolution.json via mcpp.libs.json (nlohmann), not string concat Use the existing json module for safe serialization/escaping instead of hand-built JSON. --- src/cli.cppm | 45 +++++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/src/cli.cppm b/src/cli.cppm index c024650..cd235f1 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -15,6 +15,7 @@ module; export module mcpp.cli; import std; +import mcpp.libs.json; import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; @@ -3167,37 +3168,25 @@ prepare_build(bool print_fingerprint, : ctx.tc.stdlibId == "libc++" ? "libc++" : ctx.tc.compiler == mcpp::toolchain::CompilerId::MSVC ? "msvc" : "glibc"; - auto jstr = [](std::string_view s) { - std::string o = "\""; - for (char c : s) { if (c == '\\' || c == '"') o += '\\'; o += c; } - return o + "\""; + nlohmann::json j; + j["toolchain"] = { + {"spec", ctx.tc.label()}, {"abi", tcAbi}, + {"triple", ctx.tc.targetTriple}, {"stdlib", ctx.tc.stdlibId}, + }; + nlohmann::json dirs = nlohmann::json::array(); + for (auto& d : ctx.plan.runtimeLibraryDirs) dirs.push_back(d.string()); + nlohmann::json caps = nlohmann::json::array(); + for (auto& [cap, prov] : ctx.plan.runtimeProviders) + caps.push_back({{"capability", cap}, {"provider", prov}}); + j["runtime"] = { + {"library_dirs", dirs}, + {"dlopen_libs", ctx.plan.runtimeDlopenLibs}, + {"capabilities", caps}, }; std::error_code ec; std::filesystem::create_directories(ctx.plan.outputDir, ec); - std::ofstream js(ctx.plan.outputDir / "resolution.json"); - if (js) { - js << "{\n"; - js << " \"toolchain\": {\"spec\": " << jstr(ctx.tc.label()) - << ", \"abi\": " << jstr(tcAbi) - << ", \"triple\": " << jstr(ctx.tc.targetTriple) - << ", \"stdlib\": " << jstr(ctx.tc.stdlibId) << "},\n"; - js << " \"runtime\": {\n"; - js << " \"library_dirs\": ["; - for (std::size_t i = 0; i < ctx.plan.runtimeLibraryDirs.size(); ++i) - js << (i ? ", " : "") << jstr(ctx.plan.runtimeLibraryDirs[i].string()); - js << "],\n"; - js << " \"dlopen_libs\": ["; - for (std::size_t i = 0; i < ctx.plan.runtimeDlopenLibs.size(); ++i) - js << (i ? ", " : "") << jstr(ctx.plan.runtimeDlopenLibs[i]); - js << "],\n"; - js << " \"capabilities\": ["; - for (std::size_t i = 0; i < ctx.plan.runtimeProviders.size(); ++i) { - auto& [cap, prov] = ctx.plan.runtimeProviders[i]; - js << (i ? ", " : "") << "{\"capability\": " << jstr(cap) - << ", \"provider\": " << jstr(prov) << "}"; - } - js << "]\n }\n}\n"; - } + if (std::ofstream js(ctx.plan.outputDir / "resolution.json"); js) + js << j.dump(2) << "\n"; } return ctx; From eade49ed765309c406d69b9051383fead24f44be Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:43:58 +0800 Subject: [PATCH 09/19] feat: build profiles ([profile.] + --profile) Adds bundled build settings: built-in release (-O2) / dev (-O0 -g) / dist (-O3 -flto -s), overridable/extendable via [profile.] (opt/debug/lto/ strip) in mcpp.toml. --profile selects; flags.cppm applies opt/debug/lto to compile and lto/strip to link. Verified: release/dev/dist emit distinct flags. --- src/build/flags.cppm | 22 ++++++++++++++++------ src/cli.cppm | 19 +++++++++++++++++++ src/manifest.cppm | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 94a0d40..863a868 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -198,8 +198,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { // AR binary f.arBinary = mcpp::toolchain::archive_tool(plan.toolchain); - // Opt level (musl ICE workaround) - std::string opt_flag = isMuslTc ? " -Og" : " -O2"; + // Opt level + debug come from the resolved build profile + // ([profile.] → buildConfig). musl keeps -Og as an ICE workaround + // unless the profile pins -O0. + auto& prof = plan.manifest.buildConfig; + std::string opt_flag = isMuslTc && prof.optLevel != "0" + ? " -Og" : (" -O" + prof.optLevel); + if (prof.debug) opt_flag += " -g"; + if (prof.lto) opt_flag += " -flto"; // User link flags std::string user_ldflags; @@ -279,13 +285,17 @@ CompileFlags compute_flags(const BuildPlan& plan) { payload_ld += " -Wl,--dynamic-linker=" + escape_path(loader); } + std::string link_extra; + if (prof.lto) link_extra += " -flto"; + if (prof.strip) link_extra += " -s"; + if constexpr (mcpp::platform::is_windows) { - f.ld = user_ldflags; + f.ld = user_ldflags + link_extra; } else if constexpr (mcpp::platform::needs_explicit_libcxx) { - f.ld = std::format("{}{}{} -lc++{}", full_static, static_stdlib, b_flag, user_ldflags); + f.ld = std::format("{}{}{} -lc++{}{}", full_static, static_stdlib, b_flag, user_ldflags, link_extra); } else { - f.ld = std::format("{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag, - runtime_dirs, payload_ld, user_ldflags); + f.ld = std::format("{}{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag, + runtime_dirs, payload_ld, user_ldflags, link_extra); } return f; diff --git a/src/cli.cppm b/src/cli.cppm index cd235f1..f0f09ce 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1156,6 +1156,7 @@ struct BuildOverrides { std::string target_triple; // empty = host triple, fall through to [toolchain] bool force_static = false; // --static (or implied by musl target) std::string package_filter; // -p : only build this workspace member + std::string profile; // --profile (default "release") }; // `prepare_build` builds the BuildContext for any verb that compiles. @@ -1312,6 +1313,21 @@ prepare_build(bool print_fingerprint, // 1. project mcpp.toml [toolchain]. or .default // 2. global ~/.mcpp/config.toml [toolchain].default // 3. hard error (no system fallback) + // Resolve the build profile: --profile (default "release") → built-in + // defaults, overlaid by any [profile.] from the manifest → buildConfig. + { + std::string pname = overrides.profile.empty() ? "release" : overrides.profile; + mcpp::manifest::Profile pr; + if (pname == "dev" || pname == "debug") { pr.optLevel = "0"; pr.debug = true; } + else if (pname == "dist") { pr.optLevel = "3"; pr.lto = true; pr.strip = true; } + else { pr.optLevel = "2"; } // release + if (auto it = m->profiles.find(pname); it != m->profiles.end()) pr = it->second; + m->buildConfig.optLevel = pr.optLevel; + m->buildConfig.debug = pr.debug; + m->buildConfig.lto = pr.lto; + m->buildConfig.strip = pr.strip; + } + auto tcSpec = m->toolchain.for_platform(kCurrentPlatform); if (!tcSpec.has_value()) { auto cfg = get_cfg(); @@ -3530,6 +3546,7 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { BuildOverrides ov; if (auto t = parsed.value("target")) ov.target_triple = *t; if (auto p = parsed.value("package")) ov.package_filter = *p; + if (auto pr = parsed.value("profile")) ov.profile = *pr; ov.force_static = parsed.is_flag_set("static"); // P0: try fast-path if inputs haven't changed. @@ -5576,6 +5593,8 @@ int run(int argc, char** argv) { "Force static linking (-static). On Linux, prefer pairing with --target -linux-musl")) .option(cl::Option("package").short_name('p').takes_value().value_name("NAME") .help("Build only the named workspace member")) + .option(cl::Option("profile").takes_value().value_name("NAME") + .help("Build profile: release (default) | dev | dist | <[profile.*] name>")) .action(wrap_rc(cmd_build))) .subcommand(cl::App("run") .description("Build + run a binary target (after `--`, args are passed to it)") diff --git a/src/manifest.cppm b/src/manifest.cppm index 720ec67..ec53556 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -103,6 +103,11 @@ struct BuildConfig { std::vector cxxflags; std::vector ldflags; std::string cStandard; + // Resolved build-profile knobs (from [profile.] + built-in defaults). + std::string optLevel = "2"; // -O level + bool debug = false; // -g + bool lto = false; // -flto + bool strip = false; // link -s }; // `[runtime]` — requirements needed when launching built binaries. @@ -175,6 +180,14 @@ struct WorkspaceConfig { bool present = false; }; +// [profile.] — bundled build settings (opt level, debug, lto, strip). +struct Profile { + std::string optLevel = "2"; + bool debug = false; + bool lto = false; + bool strip = false; +}; + struct Manifest { std::filesystem::path sourcePath; // mcpp.toml's filesystem path @@ -191,6 +204,7 @@ struct Manifest { Toolchain toolchain; // optional; empty == fallback BuildConfig buildConfig; RuntimeConfig runtimeConfig; + std::map profiles; // [profile.] // [target.] tables — empty if user didn't declare any. std::map targetOverrides; @@ -469,6 +483,24 @@ std::expected parse_string(std::string_view content, } // [targets.*] — M5.0: now optional. If absent, defer to auto-inference (in load()). + // [profile.] — bundled build settings. + if (auto* profile_table = doc->get_table("profile"); + profile_table && !profile_table->empty()) { + for (auto& [pname, pval] : *profile_table) { + if (!pval.is_table()) continue; + auto& tt = pval.as_table(); + Profile pr; + if (auto it = tt.find("opt"); it != tt.end()) { + if (it->second.is_string()) pr.optLevel = it->second.as_string(); + else if (it->second.is_int()) pr.optLevel = std::to_string(it->second.as_int()); + } + if (auto it = tt.find("debug"); it != tt.end() && it->second.is_bool()) pr.debug = it->second.as_bool(); + if (auto it = tt.find("lto"); it != tt.end() && it->second.is_bool()) pr.lto = it->second.as_bool(); + if (auto it = tt.find("strip"); it != tt.end() && it->second.is_bool()) pr.strip = it->second.as_bool(); + m.profiles[pname] = pr; + } + } + auto* targets_table = doc->get_table("targets"); if (targets_table && !targets_table->empty()) { for (auto& [tname, tval] : *targets_table) { From 1b4c117a01d4cd35920a5c66d6deb349c6e6bf63 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:50:08 +0800 Subject: [PATCH 10/19] fix: rpath only dependency runtime dirs; update first-run default test R2 precision: baking ALL of plan.runtimeLibraryDirs into -L/-rpath pulled the glibc payload dir into musl/static links (undefined _DYNAMIC; broke e2e 28/30). Split out plan.depRuntimeLibraryDirs (dependency [runtime] library_dirs only, e.g. compat.glx-runtime) and emit toolchain.linkRuntimeDirs + dep dirs, as before plus the dep closure. e2e 29 updated for the intentional glibc first-run default (musl-static is opt-in via --target). --- src/build/flags.cppm | 17 ++++++++++------- src/build/plan.cppm | 10 ++++++++-- tests/e2e/29_toolchain_partial_versions.sh | 19 ++++++++++--------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 863a868..94811c4 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -262,13 +262,16 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string static_stdlib = (f.staticStdlib && !isClang && !mcpp::platform::is_windows) ? " -static-libstdc++" : ""; std::string runtime_dirs; if constexpr (mcpp::platform::supports_rpath) { - // Bake ALL resolved runtime library dirs into the binary's RUNPATH — - // not just the toolchain's. plan.runtimeLibraryDirs is the union of - // dependency packages' [runtime] library_dirs (e.g. compat.glx-runtime's - // host-GL passthrough dir) plus the toolchain/payload dirs. Using only - // toolchain.linkRuntimeDirs here dropped dependency runtime dirs, so - // dlopen()'d host libs (libGL/libGLX) were unreachable at run time. - for (auto& dir : plan.runtimeLibraryDirs) { + // Toolchain runtime dirs (glibc/gcc) as before... + for (auto& dir : plan.toolchain.linkRuntimeDirs) { + runtime_dirs += " -L" + escape_path(dir); + runtime_dirs += " -Wl,-rpath," + escape_path(dir); + } + // ...plus dependency packages' [runtime] library_dirs (e.g. + // compat.glx-runtime's host-GL passthrough), so dlopen()'d host libs + // (libGL/libGLX) are reachable at run time. Only the dep dirs — NOT the + // glibc payload dir — so static/musl links stay clean. + for (auto& dir : plan.depRuntimeLibraryDirs) { runtime_dirs += " -L" + escape_path(dir); runtime_dirs += " -Wl,-rpath," + escape_path(dir); } diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 8e405dc..9ff8d93 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -56,6 +56,11 @@ struct BuildPlan { std::vector compileUnits; // topologically sorted std::vector linkUnits; std::vector runtimeLibraryDirs; + // ONLY the dependency packages' [runtime] library_dirs (not toolchain/ + // payload dirs). These are the dirs that must be baked into the produced + // binary's RUNPATH (e.g. compat.glx-runtime). Kept separate so static/musl + // links don't pull the glibc payload dir. + std::vector depRuntimeLibraryDirs; // Aggregated host-runtime requirements from dependency packages' // [runtime] metadata. Capability/provider-driven — no platform special-casing // in mcpp: providers (e.g. compat.glx-runtime) declare these per platform. @@ -225,8 +230,9 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, for (auto const& package : packages) { for (auto const& dir : package.manifest.runtimeConfig.libraryDirs) { - append_unique_path(plan.runtimeLibraryDirs, - dir.is_absolute() ? dir : package.root / dir); + auto abs = dir.is_absolute() ? dir : package.root / dir; + append_unique_path(plan.runtimeLibraryDirs, abs); + append_unique_path(plan.depRuntimeLibraryDirs, abs); } for (auto const& lib : package.manifest.runtimeConfig.dlopenLibs) { if (std::ranges::find(plan.runtimeDlopenLibs, lib) == plan.runtimeDlopenLibs.end()) diff --git a/tests/e2e/29_toolchain_partial_versions.sh b/tests/e2e/29_toolchain_partial_versions.sh index 4e84853..cf87ab1 100755 --- a/tests/e2e/29_toolchain_partial_versions.sh +++ b/tests/e2e/29_toolchain_partial_versions.sh @@ -52,9 +52,9 @@ grep -q 'gcc@16.1.0' "$TMP/def2.log" || { # ─── Section 2: first-run auto-install ────────────────────────────────── # Brand-new MCPP_HOME with no config/default state, brand-new package with no # [toolchain] declared — `mcpp build` should auto-install the canonical -# default (musl-gcc 15.1 for portable static binaries) + use it. We still -# inherit payloads so CI does not download the same large archives into a -# throw-away home. +# default (platform-native glibc gcc — musl-static is opt-in via --target) + +# use it. We still inherit payloads so CI does not download the same large +# archives into a throw-away home. export MCPP_HOME="$TMP/h2" inherit_payloads_only configure_e2e_mirror @@ -75,20 +75,21 @@ fi # Must show the friendly first-run banner AND the build must succeed. grep -q 'First run' "$TMP/firstrun.log" || { cat "$TMP/firstrun.log"; echo "missing First-run banner"; exit 1; } -grep -q 'gcc@15.1.0-musl' "$TMP/firstrun.log" || { - cat "$TMP/firstrun.log"; echo "first run didn't pick gcc@15.1.0-musl as default"; exit 1; } +grep -q 'gcc@16.1.0' "$TMP/firstrun.log" || { + cat "$TMP/firstrun.log"; echo "first run didn't pick glibc gcc@16.1.0 as default"; exit 1; } grep -q 'Finished' "$TMP/firstrun.log" || { cat "$TMP/firstrun.log"; echo "build did not finish"; exit 1; } -# Built binary must exist, run, AND be statically linked (because the -# default toolchain is musl, mcpp infers `linkage = static` automatically). +# Built binary must exist and run. The default toolchain is now platform-native +# glibc gcc (musl-static is opt-in via --target), so it is dynamically linked. binary=$(find target -name hello -type f | head -1) [[ -n "$binary" && -x "$binary" ]] || { echo "no hello binary produced"; exit 1; } -file "$binary" | grep -q 'statically linked' || { +file "$binary" | grep -q 'statically linked' && { file "$binary" - echo "first-run build is not statically linked; musl default not propagated" + echo "first-run default unexpectedly produced a static binary (glibc default expected)" exit 1 } +"$binary" >/dev/null 2>&1 || { echo "first-run binary did not run"; exit 1; } # Second build should be silent on toolchain — no re-install banner. "$MCPP" build > "$TMP/secondrun.log" 2>&1 || { From acf2d5c99ae1d42ebee688bcceb928453803f756 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 00:57:17 +0800 Subject: [PATCH 11/19] feat: Cargo-style features ([features] + --features + dep features=[...]) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [features] declares feature -> implied-features (with a 'default' set); long-form dep specs' features=[...] is now stored (was accepted-but-ignored) and requests features of that dependency. Activation = default ∪ requested, expanded transitively; each active feature becomes -DMCPP_FEATURE_ on that package's compile flags. Root activation via --features a,b. Verified: default set, --features, and implication closure all observable via #ifdef. Transitive dep->dep feature propagation is a follow-up. --- src/cli.cppm | 66 ++++++++++++++++++++++++++++++++++++++++++++ src/manifest.cppm | 20 ++++++++++++++ src/pm/dep_spec.cppm | 1 + 3 files changed, 87 insertions(+) diff --git a/src/cli.cppm b/src/cli.cppm index f0f09ce..827dbc2 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1157,6 +1157,7 @@ struct BuildOverrides { bool force_static = false; // --static (or implied by musl target) std::string package_filter; // -p : only build this workspace member std::string profile; // --profile (default "release") + std::string features; // --features a,b,c (root package activation) }; // `prepare_build` builds the BuildContext for any verb that compiles. @@ -2885,6 +2886,68 @@ prepare_build(bool print_fingerprint, computeUsageRequirements(); + // ─── Feature activation (Cargo-style, additive) ──────────────────── + // activated(pkg) = pkg.[features].default ∪ features requested for it + // (root: --features; deps: the root dep spec's `features = [...]`). + // Implied features expand transitively. Each active feature becomes + // -DMCPP_FEATURE_ on that package's compile flags. + // (Transitive dep→dep feature requests are not yet propagated.) + { + auto sanitize = [](std::string f) { + for (auto& c : f) + c = std::isalnum(static_cast(c)) + ? static_cast(std::toupper(static_cast(c))) : '_'; + return f; + }; + auto activate = [](const mcpp::manifest::Manifest& pm, + const std::vector& requested) { + std::vector act, q; + if (auto it = pm.featuresMap.find("default"); it != pm.featuresMap.end()) + q.insert(q.end(), it->second.begin(), it->second.end()); + q.insert(q.end(), requested.begin(), requested.end()); + std::set seen; + while (!q.empty()) { + auto f = q.back(); q.pop_back(); + if (f == "default" || !seen.insert(f).second) continue; + act.push_back(f); + if (auto it = pm.featuresMap.find(f); it != pm.featuresMap.end()) + q.insert(q.end(), it->second.begin(), it->second.end()); + } + return act; + }; + auto apply = [&](mcpp::modgraph::PackageRoot& pkg, + const std::vector& requested) { + for (auto& f : activate(pkg.manifest, requested)) { + auto def = "-DMCPP_FEATURE_" + sanitize(f); + pkg.manifest.buildConfig.cflags.push_back(def); + pkg.manifest.buildConfig.cxxflags.push_back(def); + pkg.privateBuild.cflags.push_back(def); + pkg.privateBuild.cxxflags.push_back(def); + } + }; + if (!packages.empty()) { + std::vector rootReq; + for (std::size_t p = 0; p < overrides.features.size();) { + auto c = overrides.features.find_first_of(", ", p); + auto tok = overrides.features.substr( + p, c == std::string::npos ? std::string::npos : c - p); + if (!tok.empty()) rootReq.push_back(tok); + if (c == std::string::npos) break; + p = c + 1; + } + apply(packages[0], rootReq); + } + for (std::size_t i = 1; i < packages.size(); ++i) { + auto& pname = packages[i].manifest.package.name; + std::vector req; + for (auto& [dname, dspec] : m->dependencies) { + if (dname == pname || dspec.shortName == pname) { req = dspec.features; break; } + } + if (!req.empty() || packages[i].manifest.featuresMap.contains("default")) + apply(packages[i], req); + } + } + // Modgraph: regex scanner by default; opt-in to compiler-driven P1689 // scanner via env var MCPP_SCANNER=p1689 (see docs/27). auto scan = [&] { @@ -3547,6 +3610,7 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) { if (auto t = parsed.value("target")) ov.target_triple = *t; if (auto p = parsed.value("package")) ov.package_filter = *p; if (auto pr = parsed.value("profile")) ov.profile = *pr; + if (auto fs = parsed.value("features")) ov.features = *fs; ov.force_static = parsed.is_flag_set("static"); // P0: try fast-path if inputs haven't changed. @@ -5595,6 +5659,8 @@ int run(int argc, char** argv) { .help("Build only the named workspace member")) .option(cl::Option("profile").takes_value().value_name("NAME") .help("Build profile: release (default) | dev | dist | <[profile.*] name>")) + .option(cl::Option("features").takes_value().value_name("LIST") + .help("Activate root-package features (comma-separated)")) .action(wrap_rc(cmd_build))) .subcommand(cl::App("run") .description("Build + run a binary target (after `--`, args are passed to it)") diff --git a/src/manifest.cppm b/src/manifest.cppm index ec53556..f2ba116 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -205,6 +205,8 @@ struct Manifest { BuildConfig buildConfig; RuntimeConfig runtimeConfig; std::map profiles; // [profile.] + // [features] — feature name → implied features ("default" = default set). + std::map> featuresMap; // [target.] tables — empty if user didn't declare any. std::map targetOverrides; @@ -501,6 +503,20 @@ std::expected parse_string(std::string_view content, } } + // [features] — feature name → implied features. "default" lists the + // default-active set. + if (auto* features_table = doc->get_table("features"); + features_table && !features_table->empty()) { + for (auto& [fname, fval] : *features_table) { + std::vector implied; + if (fval.is_array()) { + for (auto& v : fval.as_array()) + if (v.is_string()) implied.push_back(v.as_string()); + } + m.featuresMap[fname] = std::move(implied); + } + } + auto* targets_table = doc->get_table("targets"); if (targets_table && !targets_table->empty()) { for (auto& [tname, tval] : *targets_table) { @@ -600,6 +616,10 @@ std::expected parse_string(std::string_view content, section, fqName))); } } + if (auto it = sub.find("features"); it != sub.end() && it->second.is_array()) { + for (auto& fv : it->second.as_array()) + if (fv.is_string()) spec.features.push_back(fv.as_string()); + } if (auto it = sub.find("rev"); it != sub.end() && it->second.is_string()) { spec.gitRev = it->second.as_string(); spec.gitRefKind = "rev"; diff --git a/src/pm/dep_spec.cppm b/src/pm/dep_spec.cppm index 173d7c9..1f38b16 100644 --- a/src/pm/dep_spec.cppm +++ b/src/pm/dep_spec.cppm @@ -39,6 +39,7 @@ struct DependencySpec { std::string gitRev; // commit / tag / branch (any one) std::string gitRefKind; // "rev" / "tag" / "branch" (for clarity) std::string visibility = "public"; // public / private / interface + std::vector features; // requested feature set (long-form dep spec) std::vector candidates; // ordered lookup candidates bool inheritWorkspace = false; // .workspace = true From 9c61fcc943cca5692eab4e162d80ea3e14041de0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 01:02:10 +0800 Subject: [PATCH 12/19] feat: backend= dep knob, [runtime.] provider= override, [package] platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dep spec backend = "" — sugar for requesting the dependency's backend- feature (library backend selection knob; verified the dep compiles with -DMCPP_FEATURE_BACKEND_). - [runtime.] provider = "" — explicit provider selection: prefers the named provider for matching capabilities (surfaced by why/ doctor/resolution.json), warns when the provider isn't in the graph. - [package] platforms = [...] — declared platform support, surfaced by mcpp why as the CI matrix hint. --- src/cli.cppm | 27 +++++++++++++++++++++++++++ src/manifest.cppm | 22 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/cli.cppm b/src/cli.cppm index 827dbc2..4f3de23 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -3211,6 +3211,25 @@ prepare_build(bool print_fingerprint, } } + // Apply [runtime.] provider = "" overrides: prefer the + // named provider for matching capabilities (capability name prefix match). + // Warn if the named provider isn't in the dependency graph. + for (auto& [capKey, prov] : ctx.manifest.runtimeConfig.providerOverrides) { + bool found = false; + std::stable_partition(ctx.plan.runtimeProviders.begin(), + ctx.plan.runtimeProviders.end(), + [&](const std::pair& pr) { + bool match = pr.first.rfind(capKey, 0) == 0 && pr.second == prov; + found = found || match; + return match; + }); + if (!found) { + std::println(stderr, + "warning: [runtime.{}] provider = \"{}\" — no such provider in the " + "dependency graph for that capability", capKey, prov); + } + } + // Capability-driven ABI enforcement: if any dependency declares an // `abi:` capability, the resolved toolchain must satisfy it. (Toolchain // is resolved before the dep graph, so this enforces/diagnoses rather than @@ -4484,6 +4503,14 @@ int cmd_why(const mcpplibs::cmdline::ParsedArgs& parsed) { std::println("toolchain: {}", tc.label()); std::println(" abi={} stdlib={} triple={}", abi_of(tc), tc.stdlibId, tc.targetTriple); std::println(" reason: [toolchain] in mcpp.toml if set, else platform-native default"); + if (!ctx->manifest.package.platforms.empty()) { + std::string ps; + for (auto& p : ctx->manifest.package.platforms) { + if (!ps.empty()) ps += ", "; + ps += p; + } + std::println(" declared platforms: {} (CI matrix hint)", ps); + } } if (all || topic == "runtime") { std::println("runtime library dirs (baked into binary RUNPATH):"); diff --git a/src/manifest.cppm b/src/manifest.cppm index f2ba116..07b5ab1 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -37,6 +37,7 @@ struct Package { std::string license; std::vector authors; std::string repo; + std::vector platforms; // declared supported platforms (CI matrix hint) }; struct Language { @@ -115,6 +116,9 @@ struct RuntimeConfig { std::vector libraryDirs; // relative to package root std::vector dlopenLibs; // runtime-loaded sonames std::vector capabilities; // host/system capabilities + // [runtime.] provider = "" — explicit provider selection + // (the three-tier knob: default/auto → explicit override). + std::map providerOverrides; }; // `[target.]` — per-target overrides. @@ -437,6 +441,7 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_string("package.license")) m.package.license = *v; if (auto v = doc->get_string("package.repo")) m.package.repo = *v; if (auto v = doc->get_string_array("package.authors")) m.package.authors = *v; + if (auto v = doc->get_string_array("package.platforms")) m.package.platforms = *v; // [package].standard (M5.0 new home) if (auto v = doc->get_string("package.standard")) m.package.standard = *v; @@ -588,7 +593,8 @@ std::expected parse_string(std::string_view content, auto is_dep_spec_key = [](std::string_view k) { return k == "path" || k == "version" || k == "git" || k == "rev" || k == "tag" || k == "branch" - || k == "features" || k == "workspace" || k == "visibility"; + || k == "features" || k == "workspace" || k == "visibility" + || k == "backend"; }; auto looks_like_inline_dep_spec = [&](const t::Table& sub) { if (sub.empty()) return false; @@ -620,6 +626,11 @@ std::expected parse_string(std::string_view content, for (auto& fv : it->second.as_array()) if (fv.is_string()) spec.features.push_back(fv.as_string()); } + // `backend = ""` — sugar for requesting the dependency's + // `backend-` feature (library-level backend selection knob). + if (auto it = sub.find("backend"); it != sub.end() && it->second.is_string()) { + spec.features.push_back("backend-" + it->second.as_string()); + } if (auto it = sub.find("rev"); it != sub.end() && it->second.is_string()) { spec.gitRev = it->second.as_string(); spec.gitRefKind = "rev"; @@ -877,6 +888,15 @@ std::expected parse_string(std::string_view content, m.runtimeConfig.dlopenLibs = *v; if (auto v = doc->get_string_array("runtime.capabilities")) m.runtimeConfig.capabilities = *v; + // [runtime.] provider = "" — explicit provider override. + if (auto* rt = doc->get_table("runtime"); rt && !rt->empty()) { + for (auto& [rk, rv] : *rt) { + if (!rv.is_table()) continue; // flat keys handled above + auto& tt = rv.as_table(); + if (auto it = tt.find("provider"); it != tt.end() && it->second.is_string()) + m.runtimeConfig.providerOverrides[rk] = it->second.as_string(); + } + } // [lib] — library root convention (cargo-style). if (auto v = doc->get_string("lib.path")) { From 4dbd0744c3271b02f287c15dd63ce45e3409db25 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 01:46:15 +0800 Subject: [PATCH 13/19] fix: first-run default toolchain installs sysroot deps (glibc, linux-headers) The glibc first-run default (R1) needs the sysroot payloads exactly like `mcpp toolchain install` provides; the old musl-static default was self-contained, which masked this. Fixes fresh-home e2e 29/31 in CI (std module precompile: stdlib.h not found). --- src/cli.cppm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cli.cppm b/src/cli.cppm index 4f3de23..2c9e1f6 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1457,6 +1457,14 @@ prepare_build(bool print_fingerprint, mcpp::fetcher::Fetcher fetcher(**cfg); CliInstallProgress progress; + // The glibc default toolchain needs the sysroot payloads (C library + + // kernel headers), exactly like `mcpp toolchain install` provides. + // The old musl-static default was self-contained, which masked this. + if constexpr (!mcpp::platform::is_macos && !mcpp::platform::is_windows) { + for (auto dep : {"xim:glibc", "xim:linux-headers"}) { + (void)fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress); + } + } auto payload = fetcher.resolve_xpkg_path(defaultPkg.target(), /*autoInstall=*/true, &progress); if (!payload) { From db3b47e4c164c4893d117e71b58a091c727b3256 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 02:22:28 +0800 Subject: [PATCH 14/19] fix: first-run gcc default runs the same post-install fixup pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract gcc_post_install_fixup (patchelf PT_INTERP/RUNPATH for gcc/binutils + linker-specs wiring against sandbox glibc) out of `toolchain install` and run it from the first-run auto-install too. A fresh-sandbox glibc default previously skipped the fixup and could not find the C library (stdlib.h: No such file or directory — e2e 29/31 on CI). One shared pipeline, no duplicate. --- src/cli.cppm | 98 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/src/cli.cppm b/src/cli.cppm index 2c9e1f6..9fc4c1b 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1018,6 +1018,54 @@ void fixup_clang_cfg(const std::filesystem::path& payloadRoot, } } +// Post-install fixup for a freshly-installed GNU gcc payload: patchelf +// PT_INTERP/RUNPATH for gcc/binutils binaries + linker-specs wiring against +// the sandbox glibc. ONE pipeline shared by `mcpp toolchain install` and the +// first-run auto-install (the latter previously skipped this, leaving a +// fresh-sandbox glibc gcc unable to find the C library: stdlib.h not found). +void gcc_post_install_fixup(const mcpp::config::GlobalConfig& cfg, + const std::filesystem::path& payloadRoot) { + auto xlEnv = mcpp::config::make_xlings_env(cfg); + 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; + } + } + } + auto gccLibDir = payloadRoot / "lib64"; + auto patchelfBin = mcpp::xlings::paths::xim_tool(xlEnv, "patchelf", + mcpp::xlings::pinned::kPatchelfVersion) / "bin" / "patchelf"; + + if (!glibcLibDir.empty() && std::filesystem::exists(gccLibDir) + && std::filesystem::exists(patchelfBin)) + { + auto loader = glibcLibDir / "ld-linux-x86-64.so.2"; + auto rpath = std::format("{}:{}", + glibcLibDir.string(), gccLibDir.string()); + + mcpp::log::verbose("toolchain", std::format( + "gcc fixup: patchelf_walk rpath='{}'", rpath)); + auto binutilsRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "binutils"); + if (std::filesystem::exists(binutilsRoot)) { + for (auto& v : std::filesystem::directory_iterator(binutilsRoot)) + patchelf_walk(v.path(), loader, rpath, patchelfBin); + } + patchelf_walk(payloadRoot, loader, rpath, patchelfBin); + + mcpp::log::verbose("toolchain", "gcc fixup: fixup_gcc_specs"); + fixup_gcc_specs(payloadRoot, glibcLibDir, gccLibDir); + } else { + mcpp::ui::warning( + "could not locate sandbox glibc/gcc/patchelf paths; " + "gcc-built binaries may have unresolved PT_INTERP/RUNPATH"); + } +} + // 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 @@ -1482,6 +1530,14 @@ prepare_build(bool print_fingerprint, defaultPkg.target(), payload->binDir.string())); } + // The freshly-installed glibc gcc needs the SAME post-install fixup + // (patchelf + specs wiring against the sandbox glibc) that + // `mcpp toolchain install` performs — without it a fresh sandbox + // cannot find the C library (stdlib.h: No such file or directory). + if (defaultPkg.needsGccPostInstallFixup) { + gcc_post_install_fixup(**cfg, payload->root); + } + // Persist the default so we don't ask again next time. if (auto wr = mcpp::config::write_default_toolchain(**cfg, defaultSpec); wr) { (*cfg)->defaultToolchain = defaultSpec; @@ -4913,47 +4969,7 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) { // `/x86_64-linux-musl/{include,lib}` and doesn't link against // xim:glibc, so this fixup is both unnecessary and harmful for it. if (pkg.needsGccPostInstallFixup) { - 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; - } - } - } - auto gccLibDir = payload->root / "lib64"; - auto patchelfBin = mcpp::xlings::paths::xim_tool(xlEnv, "patchelf", - mcpp::xlings::pinned::kPatchelfVersion) / "bin" / "patchelf"; - - if (!glibcLibDir.empty() && std::filesystem::exists(gccLibDir) - && std::filesystem::exists(patchelfBin)) - { - auto loader = glibcLibDir / "ld-linux-x86-64.so.2"; - auto rpath = std::format("{}:{}", - glibcLibDir.string(), gccLibDir.string()); - - // (1) patchelf walk: rewrite PT_INTERP + RUNPATH for binutils - // and gcc xpkgs so they're self-contained in sandbox. - mcpp::log::verbose("toolchain", std::format( - "gcc fixup: patchelf_walk rpath='{}'", rpath)); - auto binutilsRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "binutils"); - if (std::filesystem::exists(binutilsRoot)) { - for (auto& v : std::filesystem::directory_iterator(binutilsRoot)) - patchelf_walk(v.path(), loader, rpath, patchelfBin); - } - patchelf_walk(payload->root, loader, rpath, patchelfBin); - - // (2) specs fixup. - mcpp::log::verbose("toolchain", "gcc fixup: fixup_gcc_specs"); - fixup_gcc_specs(payload->root, glibcLibDir, gccLibDir); - } else { - mcpp::ui::warning( - "could not locate sandbox glibc/gcc/patchelf paths; " - "gcc-built binaries may have unresolved PT_INTERP/RUNPATH"); - } + gcc_post_install_fixup(*cfg, payload->root); } // For LLVM/Clang: post-install fixup so the shared libraries and From 81d84cff5f73042ab2514b1cbe8b661b93bde9dd Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 02:27:59 +0800 Subject: [PATCH 15/19] =?UTF-8?q?fix:=20ownership=20guard=20=E2=80=94=20ne?= =?UTF-8?q?ver=20fixup=20payloads=20inherited=20from=20another=20home?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gcc_post_install_fixup now skips payloads whose canonical path resolves outside this MCPP_HOME's registry: inherited (symlinked) payloads belong to their owner home, whose fixup is already valid; patching through the symlink rewrote the canonical binaries against ephemeral paths and bricked the owner's toolchain. Verified: fresh-home e2e (26/28/29/31) pass and the owner toolchain stays intact. --- src/cli.cppm | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/cli.cppm b/src/cli.cppm index 9fc4c1b..38012c3 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1025,6 +1025,22 @@ void fixup_clang_cfg(const std::filesystem::path& payloadRoot, // fresh-sandbox glibc gcc unable to find the C library: stdlib.h not found). void gcc_post_install_fixup(const mcpp::config::GlobalConfig& cfg, const std::filesystem::path& payloadRoot) { + // Ownership guard: payloads inherited via symlink from another MCPP_HOME + // are not ours to patch — their owner already ran the fixup, and patching + // through the symlink would rewrite the canonical files against OUR + // (possibly ephemeral) paths, bricking the owner's toolchain. + { + std::error_code ec; + auto canonicalRoot = std::filesystem::weakly_canonical(payloadRoot, ec); + auto homeRegistry = std::filesystem::weakly_canonical(cfg.registryDir, ec); + if (!ec && !canonicalRoot.string().starts_with(homeRegistry.string())) { + mcpp::log::verbose("toolchain", std::format( + "skip gcc fixup: payload '{}' resolves outside this home ('{}') — " + "inherited payload, owner is responsible for its fixup", + payloadRoot.string(), canonicalRoot.string())); + return; + } + } auto xlEnv = mcpp::config::make_xlings_env(cfg); auto glibcRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "glibc"); std::filesystem::path glibcLibDir; From c7c8ff8203c8bf9fb0be2306bc1d316c4044e888 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 02:50:49 +0800 Subject: [PATCH 16/19] fix: payload probe falls back to the active home registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit probe_payload_paths found glibc/linux-headers only as compiler SIBLINGS. With an inherited (symlinked) compiler the binary resolves into its owner home, so sysroot payloads freshly installed into the ACTIVE home (first-run default path) were invisible -> std module precompile failed with stdlib.h not found (CI fresh-home e2e 29/31). Add paths::active_home_xpkgs()/find_home_tool() and consult them after sibling search: discovery = compiler siblings ∪ active home registry. --- src/toolchain/probe.cppm | 10 ++++++++-- src/xlings.cppm | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 6f2d483..f5d0b9e 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -303,8 +303,12 @@ std::optional probe_payload_paths(const std::filesystem::path& compilerBin) { namespace paths = mcpp::xlings::paths; - // Find glibc xpkg (required). + // Find glibc xpkg (required). Compiler siblings first; fall back to the + // ACTIVE home registry — an inherited/symlinked compiler resolves into + // its owner home, while the active home may own (or have just installed) + // the sysroot payloads. auto glibc = paths::find_sibling_tool(compilerBin, "glibc"); + if (!glibc) glibc = paths::find_home_tool("glibc"); if (!glibc) return std::nullopt; // Glibc layout: /include/ + /lib64/ (or lib/). @@ -322,8 +326,10 @@ probe_payload_paths(const std::filesystem::path& compilerBin) { pp.glibcInclude = glibcInclude; pp.glibcLib = glibcLib; - // Find linux kernel headers (optional — search across index prefixes). + // Find linux kernel headers (optional — search across index prefixes, + // then the active home registry). auto linuxHeaders = paths::find_sibling_package(compilerBin, "linux-headers"); + if (!linuxHeaders) linuxHeaders = paths::find_home_tool("linux-headers"); if (linuxHeaders) { auto linuxInclude = *linuxHeaders / "include"; if (std::filesystem::exists(linuxInclude / "linux" / "limits.h")) diff --git a/src/xlings.cppm b/src/xlings.cppm index 419f4cb..e48db8f 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -82,6 +82,15 @@ namespace paths { find_sibling_package(const std::filesystem::path& compilerBin, std::string_view packageName); + // xpkgs root of the ACTIVE mcpp home ($MCPP_HOME or ~/.mcpp). Payload + // discovery consults this in addition to compiler siblings: an + // inherited/symlinked compiler resolves into its owner home, while the + // active home may own (or have just installed) the sysroot payloads. + std::optional active_home_xpkgs(); + + // Like find_sibling_tool, but anchored at the active home's xpkgs. + std::optional find_home_tool(std::string_view tool); + // index data root: env.home / "data" std::filesystem::path index_data(const Env& env); @@ -519,6 +528,35 @@ find_sibling_tool(const std::filesystem::path& compilerBin, return std::nullopt; } +std::optional active_home_xpkgs() { + std::filesystem::path home; + if (const char* h = std::getenv("MCPP_HOME"); h && *h) { + home = h; + } else if (const char* u = std::getenv("HOME"); u && *u) { + home = std::filesystem::path(u) / ".mcpp"; + } else { + return std::nullopt; + } + auto xpkgs = home / "registry" / "data" / "xpkgs"; + std::error_code ec; + if (!std::filesystem::exists(xpkgs, ec)) return std::nullopt; + return xpkgs; +} + +std::optional find_home_tool(std::string_view tool) { + auto xpkgs = active_home_xpkgs(); + if (!xpkgs) return std::nullopt; + + auto root = *xpkgs / std::format("xim-x-{}", tool); + std::error_code ec; + if (!std::filesystem::exists(root, ec)) return std::nullopt; + + for (auto& v : std::filesystem::directory_iterator(root, ec)) { + if (v.is_directory(ec)) return v.path(); + } + return std::nullopt; +} + std::optional find_sibling_binary(const std::filesystem::path& compilerBin, std::string_view tool, From 4541b0528916cb36b9d67a3453b71d872ec08fba Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 03:14:52 +0800 Subject: [PATCH 17/19] fix: validate probed sysroot carries C headers; diagnostic std-precompile error A probed/remapped sysroot that exists but lacks stdlib.h (e.g. a partially-bootstrapped sandbox subos in a fresh MCPP_HOME) silently shadowed the payload -isystem fallback in both stdmod and flags, failing deep in the std module build. probe_sysroot now only returns a sysroot that actually contains the C headers (glibc usr/include or musl include layout) so all consumers uniformly fall through to payload paths. std-precompile failures now include the full compile command for actionable diagnosis. --- src/toolchain/probe.cppm | 23 ++++++++++++++++++++--- src/toolchain/stdmod.cppm | 5 ++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index f5d0b9e..ad2f17d 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -271,6 +271,15 @@ probe_target_triple(const std::filesystem::path& compilerBin, std::filesystem::path probe_sysroot(const std::filesystem::path& compilerBin, const std::string& envPrefix) { + // A sysroot is only usable if it actually carries the C library headers. + // A merely-existing directory (e.g. a partially-bootstrapped sandbox + // subos) would silently shadow the payload -isystem fallback and produce + // "stdlib.h: No such file or directory" deep inside the std module build. + auto usable = [](const std::filesystem::path& root) { + return std::filesystem::exists(root / "usr" / "include" / "stdlib.h") // glibc layout + || std::filesystem::exists(root / "include" / "stdlib.h"); // musl layout + }; + // 1. Ask the compiler directly (works for GCC; Clang often doesn't support it). auto r = run_capture(std::format("{}{} -print-sysroot {}", envPrefix, @@ -278,13 +287,21 @@ probe_sysroot(const std::filesystem::path& compilerBin, mcpp::platform::null_redirect)); if (r) { auto s = trim_line(*r); - if (!s.empty() && std::filesystem::exists(s)) return s; + if (!s.empty() && std::filesystem::exists(s)) { + if (usable(s)) return s; + mcpp::log::debug("probe", std::format( + "sysroot '{}' exists but lacks usr/include/stdlib.h — ignoring", 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. Remap via fallback module. - if (auto remapped = mcpp::fallback::remap_xlings_baked_sysroot(s, compilerBin)) - return *remapped; + if (auto remapped = mcpp::fallback::remap_xlings_baked_sysroot(s, compilerBin)) { + if (usable(*remapped)) return *remapped; + mcpp::log::debug("probe", std::format( + "remapped sysroot '{}' lacks usr/include/stdlib.h — ignoring", + remapped->string())); + } } // 2. Parse the compiler driver config file (Clang .cfg). diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 3487f00..8915e0f 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -61,8 +61,11 @@ namespace { std::expected run_capture_command(const std::string& cmd) { auto r = mcpp::platform::process::capture(cmd); if (r.exit_code != 0) { + // Include the command: its --sysroot/-isystem flags are the first + // thing needed to diagnose header-resolution failures. return std::unexpected(StdModError{ - std::format("std module precompile failed (rc={}):\n{}", r.exit_code, r.output)}); + std::format("std module precompile failed (rc={}):\n{}\ncommand: {}", + r.exit_code, r.output, cmd)}); } return r.output; } From 52c19ef42433f2182ca465a0fc6c39c7c3c67bba Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 03:32:58 +0800 Subject: [PATCH 18/19] fix: payload C headers via -idirafter for GCC (include_next reachability) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ground truth from CI (command now in the error): -isystem'd payload glibc include was present yet #include_next still failed. GCC's libstdc++ wraps libc headers with #include_next, which only searches directories AFTER the current header's dir — gcc's built-in dirs are last, so -isystem (inserted before them) is unreachable. Use -idirafter (appended to the very end) for GCC payload headers in both the std-module precompile and per-TU flags; clang keeps -isystem. Verified with a faithful fresh-home repro (no subos => invalid sysroot): first-run installs glibc default and builds clean. --- src/build/flags.cppm | 13 ++++++++++--- src/toolchain/stdmod.cppm | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 94811c4..c46327b 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -173,11 +173,18 @@ CompileFlags compute_flags(const BuildPlan& plan) { } f.sysroot = link_toolchain_flags; } else if (plan.toolchain.payloadPaths) { - // No sysroot but have payload paths: use -isystem. + // No usable sysroot: wire the C library headers from the payload. + // For GCC use -idirafter (appended after the built-in dirs) so that + // libstdc++'s #include_next wrappers can reach them; -isystem would + // place them BEFORE the built-ins, invisible to #include_next. auto& pp = *plan.toolchain.payloadPaths; - compile_toolchain_flags += " -isystem" + escape_path(pp.glibcInclude); + const bool clangTc = mcpp::toolchain::is_clang(plan.toolchain); + auto inc_flag = [&](const std::filesystem::path& p) { + return (clangTc ? " -isystem" : " -idirafter") + escape_path(p); + }; + compile_toolchain_flags += inc_flag(pp.glibcInclude); if (!pp.linuxInclude.empty()) - compile_toolchain_flags += " -isystem" + escape_path(pp.linuxInclude); + compile_toolchain_flags += inc_flag(pp.linuxInclude); } // Binutils -B flag diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 8915e0f..eb1edbb 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -236,10 +236,21 @@ std::expected ensure_built( sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); } } else if (tc.payloadPaths) { - // No sysroot: use payload -isystem paths. - sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->glibcInclude.string()); + // No usable sysroot: wire the C library headers from the payload. + // GCC's libstdc++ wraps libc headers via #include_next, which only + // searches directories AFTER the one the current header came from — + // and gcc's built-in dirs are LAST in the search order, so an + // -isystem payload dir (inserted before the built-ins) is unreachable + // from #include_next. -idirafter appends the payload to the very end, + // exactly where #include_next looks. + const bool clang = is_clang(tc); + auto add_inc = [&](const std::filesystem::path& p) { + if (clang) sysroot_flag += std::format(" -isystem'{}'", p.string()); + else sysroot_flag += std::format(" -idirafter'{}'", p.string()); + }; + add_inc(tc.payloadPaths->glibcInclude); if (!tc.payloadPaths->linuxInclude.empty()) - sysroot_flag += std::format(" -isystem'{}'", tc.payloadPaths->linuxInclude.string()); + add_inc(tc.payloadPaths->linuxInclude); } std::vector stdCommands; From 12ab866a6a434a80c53d1148eefde4b8eef65dde Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 03:51:22 +0800 Subject: [PATCH 19/19] fix: payload branch wires link-time C runtime (-B/-L glibc lib) With the sysroot cleared (validation) the linker lost the implicit crt1.o/ crti.o and -lm/-lc locations (CI: 'cannot find crt1.o'). The payload fallback now passes -B/-L at link, completing the no-sysroot path end-to-end (compile headers via -idirafter + link runtime via -B/-L). Verified: fresh-home first-run builds AND runs. --- src/build/flags.cppm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index c46327b..234955c 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -185,6 +185,13 @@ CompileFlags compute_flags(const BuildPlan& plan) { compile_toolchain_flags += inc_flag(pp.glibcInclude); if (!pp.linuxInclude.empty()) compile_toolchain_flags += inc_flag(pp.linuxInclude); + // Link-time C runtime: a usable --sysroot would have provided the + // startup objects and core libs implicitly. Without one, point the + // driver at the glibc payload lib dir: -B for crt1.o/crti.o discovery, + // -L for -lm/-lc resolution. + link_toolchain_flags += " -B" + escape_path(pp.glibcLib); + link_toolchain_flags += " -L" + escape_path(pp.glibcLib); + f.sysroot = link_toolchain_flags; } // Binutils -B flag