From ee3e909132eec6482e3c078b4d35020fed56e215 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 10 Jun 2026 09:09:25 +0800 Subject: [PATCH 1/6] docs(agents): cli modularization architecture + implementation plan --- .agents/docs/2026-06-10-cli-modularization.md | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .agents/docs/2026-06-10-cli-modularization.md diff --git a/.agents/docs/2026-06-10-cli-modularization.md b/.agents/docs/2026-06-10-cli-modularization.md new file mode 100644 index 0000000..ec2ec28 --- /dev/null +++ b/.agents/docs/2026-06-10-cli-modularization.md @@ -0,0 +1,134 @@ +# CLI Modularization — Architecture & Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Reduce `src/cli.cppm` (6192 lines) to a thin command-dispatch layer (≤ 500 lines) by moving every implementation concern into focused C++20 modules, with zero behavior change. + +**Architecture:** Continue the PR-R4/PR-R5 extraction series (`2026-05-08-pm-subsystem-architecture.md`). `cli.cppm` keeps only the canonical usage screen and the `cmdline::App` dispatcher; commands move into `src/cli/cmd_*.cppm` modules grouped by CLI surface; cross-cutting CLI plumbing moves into `mcpp.cli.common` / `mcpp.cli.install_ui`; the build-orchestration core (`prepare_build`) becomes `mcpp.cli.build`; toolchain payload post-install fixups move into the toolchain domain as `mcpp.toolchain.post_install`. Every move is a strict zero-behavior-change relocation (same statement order, same messages, same exit codes), exactly like the PR-R5 precedent. + +**Tech Stack:** C++23 named modules (GCC 16 self-host), mcpp convention-mode source globbing (`src/**/*.cppm` — no build-file edits needed), existing `mcpp test` unit suite + `tests/e2e/run_all.sh`. + +--- + +## 1. Current state (analysis) + +`src/cli.cppm` mixes five unrelated responsibilities in one 6192-line module: + +| Lines (pre-refactor) | Content | Problem | +|---|---|---| +| 75–181 | usage screen, project/workspace root discovery | shared helpers trapped in CLI module (pm.commands keeps a private copy of `find_manifest_root` for this reason) | +| 183–517 | toolchain version matching, xlings NDJSON install-progress UI | UI plumbing interleaved with command logic | +| 519–1038 | fingerprint flag canonicalization, gcc/clang payload post-install fixups (patchelf, specs, cfg rewrite) | toolchain-domain logic living in the CLI layer | +| 1051–3561 | `cmd_new` + templates, `BuildContext` + `prepare_build` (≈ 2 240 lines: workspace → toolchain → dependency resolution → features → modgraph → fingerprint → plan → lockfile) | the build pipeline is unreachable for unit testing and unreviewable as a diff target | +| 3563–5789 | build cache/fast-path + 25 `cmd_*` entry points | every command edit rebuilds/reviews the whole module | +| 5793–6192 | `run()` dispatcher | the only part that belongs here | + +Consequences: slowest incremental rebuild unit in the repo, high merge-conflict surface, no module-level ownership boundaries, and `mcpp.cli` exports nothing reusable (`pm.commands` duplicates helpers to avoid a circular import). + +## 2. Target architecture + +``` +src/main.cpp ──▶ mcpp.cli (dispatcher only: usage + cmdline::App + run()) + │ + ├─ mcpp.cli.cmd_build build / run / test / clean / dyndep + │ └─ mcpp.cli.build BuildContext, BuildOverrides, prepare_build + ├─ mcpp.cli.cmd_new new + package templates + ├─ mcpp.cli.cmd_registry search / index * + ├─ mcpp.cli.cmd_cache cache list|info|prune|clean + ├─ mcpp.cli.cmd_toolchain toolchain install|list|default|remove + │ └─ mcpp.toolchain.post_install patchelf/specs/cfg fixups + ├─ mcpp.cli.cmd_publish publish / pack / emit xpkg + ├─ mcpp.cli.cmd_self self * / doctor / why / env / explain + └─ mcpp.pm.commands add / remove / update (PR-R5, unchanged) + +shared CLI plumbing: + mcpp.cli.common project/workspace root discovery, target_dir, fs size helpers + mcpp.cli.install_ui xlings NDJSON → ui::DownloadProgress adapters, PathContext +``` + +Layering rules (enforced by module imports — cycles are compile errors): + +1. `mcpp.cli` imports only `cmd_*` modules (+ `pm.commands`, `ui`, `log`, `cmdline`, `toolchain.fingerprint` for the version string). +2. `cmd_*` modules never import each other; shared code lives in `common` / `install_ui` / `build`. +3. `mcpp.cli.build` is the single owner of `prepare_build`; consumers are `cmd_build`, `cmd_publish` (pack), `cmd_self` (doctor/why). +4. `mcpp.toolchain.post_install` is CLI-independent (imports only config/xlings/platform/log/ui) so future non-CLI callers (e.g. a daemonized installer) can reuse it. +5. Export surface is explicit per symbol (`export` on the declaration) — internals keep module linkage. + +### Module inventory + +| New file | Module | Exports | Body source (line ranges in pre-refactor `src/cli.cppm`) | +|---|---|---|---| +| `src/cli/common.cppm` | `mcpp.cli.common` | `find_manifest_root`, `find_workspace_root`, `merge_workspace_deps`, `target_dir`, `dir_size`, `human_bytes` | 116–181, 4630–4650 (drop `static`) | +| `src/cli/install_ui.cppm` | `mcpp.cli.install_ui` | `make_path_ctx`, `make_bootstrap_progress_callback`, `CliInstallProgress` | 300–517 | +| `src/toolchain/post_install.cppm` | `mcpp.toolchain.post_install` (ns `mcpp::toolchain`) | `patchelf_walk`, `fixup_clang_cfg`, `gcc_post_install_fixup` | 722–1038 | +| `src/cli/build.cppm` | `mcpp.cli.build` | `BuildContext`, `BuildOverrides`, `prepare_build` | 519–720, 1321–3561 | +| `src/cli/cmd_build.cppm` | `mcpp.cli.cmd_build` | `cmd_build`, `cmd_run`, `cmd_test`, `cmd_clean`, `cmd_dyndep` | 3563–3976, 4406–4570, 4608–4627, 5741–5789 | +| `src/cli/cmd_new.cppm` | `mcpp.cli.cmd_new` | `cmd_new` | 1051–1319 | +| `src/cli/cmd_registry.cppm` | `mcpp.cli.cmd_registry` | `cmd_search`, `cmd_index_{list,add,remove,update,pin,unpin}` | 3996–4402 | +| `src/cli/cmd_cache.cppm` | `mcpp.cli.cmd_cache` | `cmd_cache_{list,info,prune,clean}` | 4837–4976 | +| `src/cli/cmd_toolchain.cppm` | `mcpp.cli.cmd_toolchain` | `cmd_toolchain` | 183–298, 4978–5317 | +| `src/cli/cmd_publish.cppm` | `mcpp.cli.cmd_publish` | `cmd_publish`, `cmd_pack`, `cmd_emit_xpkg` | 4572–4606, 5319–5571 | +| `src/cli/cmd_self.cppm` | `mcpp.cli.cmd_self` | `cmd_env`, `cmd_doctor`, `cmd_why`, `cmd_explain`, `cmd_explain_action`, `cmd_self_{version,init,config}` | 3978–3994, 4652–4835, 5573–5739 | + +`src/cli.cppm` keeps: file header, `print_usage` (75–114), `run()` (5795–6190, minus the now-dead `using namespace mcpp::cli::detail;`). All moved code switches namespace `mcpp::cli::detail` → `mcpp::cli` (or `mcpp::toolchain` for post_install); unqualified cross-references keep working because callers share the namespace. The three post-install call sites outside the toolchain namespace gain explicit `mcpp::toolchain::` qualification. + +### Cross-platform notes + +- No platform-conditional code is touched; `if constexpr (mcpp::platform::is_macos/…)` blocks move verbatim, so Linux/macOS/Windows behavior is bit-identical. +- Source discovery is glob-based (`src/**/*`), so the new `src/cli/` directory needs no manifest change on any platform. +- Each new module keeps the `module; #include #include ` global-module-fragment prologue `cli.cppm` used (for `stderr`/`stdout` macros) — required on all three toolchains. + +## 3. Implementation plan + +The extraction is a single mechanical transformation of one immutable source revision, so it is scripted with `sed -n 'A,Bp'` range extraction (no hand-retyping; byte-identical bodies), then compiled and tested. Verification = full self-host build + unit suite + e2e suite. + +### Task 1: Architecture doc (this file) + +- [x] **Step 1:** Write this document. +- [x] **Step 2:** Commit on branch `refactor/cli-modularization`. + +### Task 2: Scripted extraction + +- [ ] **Step 1:** `cp src/cli.cppm /tmp/cli_orig.cppm` (immutable line-range source). +- [ ] **Step 2:** For each row of the module inventory: write the module header (GMF prologue, `export module`, imports per §2, `namespace … {`), then `sed -n 'A,Bp' /tmp/cli_orig.cppm >>` the body ranges in table order, then close the namespace. Import lists per module: + - common: `std, mcpp.manifest, mcpp.toolchain.detect, mcpp.toolchain.fingerprint` + - install_ui: `std, mcpp.ui, mcpp.log, mcpp.config, mcpp.fetcher` + - post_install: `std, mcpp.config, mcpp.xlings, mcpp.platform, mcpp.log, mcpp.ui` + - build: `std, mcpp.libs.json, mcpp.manifest, mcpp.modgraph.{graph,scanner,validate}, mcpp.toolchain.{clang,detect,fingerprint,registry,stdmod}, mcpp.toolchain.post_install, mcpp.build.plan, mcpp.lockfile, mcpp.config, mcpp.xlings, mcpp.platform, mcpp.fetcher, mcpp.pm.{resolver,index_spec,mangle,compat,dep_spec}, mcpp.ui, mcpp.log, mcpp.fallback.install_integrity, mcpp.bmi_cache, mcpp.cli.common, mcpp.cli.install_ui` + - cmd_build: `std, mcpplibs.cmdline, mcpp.cli.{build,common,install_ui}, mcpp.build.{plan,backend,ninja}, mcpp.bmi_cache, mcpp.dyndep, mcpp.manifest, mcpp.modgraph.scanner, mcpp.toolchain.stdmod, mcpp.xlings, mcpp.platform, mcpp.ui, mcpp.log` + - cmd_new: `std, mcpplibs.cmdline, mcpp.cli.install_ui, mcpp.scaffold, mcpp.config, mcpp.manifest, mcpp.fetcher, mcpp.pm.resolver, mcpp.ui` + - cmd_registry: `std, mcpplibs.cmdline, mcpp.cli.{common,install_ui}, mcpp.config, mcpp.xlings, mcpp.fetcher, mcpp.manifest, mcpp.lockfile, mcpp.ui` + - cmd_cache: `std, mcpplibs.cmdline, mcpp.cli.common, mcpp.toolchain.stdmod, mcpp.ui` + - cmd_toolchain: `std, mcpplibs.cmdline, mcpp.cli.{common,install_ui}, mcpp.toolchain.{detect,registry,post_install}, mcpp.config, mcpp.xlings, mcpp.fetcher, mcpp.manifest, mcpp.ui, mcpp.log` + - cmd_publish: `std, mcpplibs.cmdline, mcpp.cli.{common,build,install_ui}, mcpp.manifest, mcpp.modgraph.scanner, mcpp.publish.xpkg_emit, mcpp.pack, mcpp.build.{backend,ninja}, mcpp.config, mcpp.platform, mcpp.ui` + - cmd_self: `std, mcpplibs.cmdline, mcpp.cli.{common,build,install_ui}, mcpp.config, mcpp.xlings, mcpp.fallback.install_integrity, mcpp.toolchain.{detect,fingerprint,stdmod}, mcpp.build.plan, mcpp.ui` +- [ ] **Step 3:** Rewrite `src/cli.cppm`: header + imports (`std, mcpplibs.cmdline, mcpp.ui, mcpp.log, mcpp.toolchain.fingerprint, mcpp.pm.commands, mcpp.cli.cmd_*×7`) + exported `run()` decl + `print_usage` + `run()` body (drop the `using namespace …::detail;` line). +- [ ] **Step 4:** Add `export` keywords on the symbols listed in §2 (Edit per declaration); drop `static` from `dir_size`/`human_bytes`; qualify the three `mcpp::toolchain::` post-install call sites (one in build.cppm: `gcc_post_install_fixup`; two-plus-one in cmd_toolchain.cppm: `gcc_post_install_fixup`, `patchelf_walk`, `fixup_clang_cfg`). + +### Task 3: Build & fix loop + +- [ ] **Step 1:** `mcpp build` — fix any missing-import / linkage errors (expected failure mode: a helper referenced across modules that wasn't exported; fix = add `export` or import, never duplicate code). +- [ ] **Step 2:** `wc -l src/cli.cppm` — expected < 500. +- [ ] **Step 3:** Commit `refactor(cli): split cli.cppm into focused modules`. + +### Task 4: Verification + +- [ ] **Step 1:** `mcpp test` with the freshly built binary — expect all unit tests pass. +- [ ] **Step 2:** `MCPP= tests/e2e/run_all.sh` — expect all e2e tests pass (covers help/version text, exit codes 0/1/2/127, all command surfaces). +- [ ] **Step 3:** Sanity: `mcpp --help`, `mcpp version`, `mcpp self doctor`, unknown-command exit 127. + +### Task 5: PR + CI + +- [ ] **Step 1:** Push branch, open PR describing motivation, module map, zero-behavior-change guarantee, verification evidence. +- [ ] **Step 2:** Watch ci-linux / ci-macos / ci-windows / fresh-install lanes; fix-forward on any platform-specific module issue (most likely candidate: MSVC/clang module-linkage strictness on exported-vs-internal helpers). +- [ ] **Step 3:** Update this doc's status section. + +## 4. Follow-ups (out of scope here) + +- Decompose `prepare_build` internally (workspace / toolchain / dep-resolution / feature phases as named functions) now that it has a home module. +- Fold `pm.commands`' private `find_manifest_root` copy into a shared project-location module once a `cli`-independent home exists (`mcpp.cli.common` is still CLI-layer; a `mcpp.project` module would let pm import it without layering violations). +- Tighten `mcpp.cli.build`'s import list (it inherited the union of the old `cli.cppm` imports). + +## 5. Status + +- 2026-06-10: doc written; extraction in progress. From 20d49c78f52b483df229eac0983189fbb45c2b77 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 10 Jun 2026 09:13:53 +0800 Subject: [PATCH 2/6] refactor(cli): split cli.cppm into focused modules (dispatcher-only cli) cli.cppm (6192 lines) -> thin dispatcher (481 lines). All command implementations move into dedicated modules, byte-identical bodies: mcpp.cli.common project/workspace discovery + fs utils mcpp.cli.install_ui xlings NDJSON -> ui download-progress adapters mcpp.toolchain.post_install patchelf/specs/cfg payload fixups mcpp.cli.build BuildContext + prepare_build (build core) mcpp.cli.cmd_build build/run/test/clean/dyndep + fast-path cache mcpp.cli.cmd_new new + package templates mcpp.cli.cmd_registry search + index management mcpp.cli.cmd_cache cache list/info/prune/clean mcpp.cli.cmd_toolchain toolchain install/list/default/remove mcpp.cli.cmd_publish publish/pack/emit-xpkg mcpp.cli.cmd_self self */doctor/why/env/explain Zero behavior change (same statement order, messages, exit codes). Architecture + plan: .agents/docs/2026-06-10-cli-modularization.md --- src/cli.cppm | 5747 +------------------------------ src/cli/build.cppm | 2492 ++++++++++++++ src/cli/cmd_build.cppm | 686 ++++ src/cli/cmd_cache.cppm | 163 + src/cli/cmd_new.cppm | 296 ++ src/cli/cmd_publish.cppm | 322 ++ src/cli/cmd_registry.cppm | 435 +++ src/cli/cmd_self.cppm | 401 +++ src/cli/cmd_toolchain.cppm | 488 +++ src/cli/common.cppm | 110 + src/cli/install_ui.cppm | 241 ++ src/toolchain/post_install.cppm | 341 ++ 12 files changed, 5993 insertions(+), 5729 deletions(-) create mode 100644 src/cli/build.cppm create mode 100644 src/cli/cmd_build.cppm create mode 100644 src/cli/cmd_cache.cppm create mode 100644 src/cli/cmd_new.cppm create mode 100644 src/cli/cmd_publish.cppm create mode 100644 src/cli/cmd_registry.cppm create mode 100644 src/cli/cmd_self.cppm create mode 100644 src/cli/cmd_toolchain.cppm create mode 100644 src/cli/common.cppm create mode 100644 src/cli/install_ui.cppm create mode 100644 src/toolchain/post_install.cppm diff --git a/src/cli.cppm b/src/cli.cppm index c9ae75c..55c1919 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1,12 +1,11 @@ -// mcpp.cli — top-level command dispatch. +// mcpp.cli — top-level command dispatch (and nothing else). // -// MVP commands: -// mcpp new -// mcpp build [--verbose] [--print-fingerprint] [--no-cache] -// mcpp run [target] [-- args...] -// mcpp clean [--bmi-cache] -// mcpp emit xpkg [--version V] [--output FILE] (M2) -// mcpp --help / mcpp --version +// Every command implementation lives in a focused module: +// mcpp.cli.cmd_build / cmd_new / cmd_registry / cmd_cache / +// mcpp.cli.cmd_toolchain / cmd_publish / cmd_self (this layer) +// mcpp.pm.commands (add / remove / update) +// Shared plumbing: mcpp.cli.common, mcpp.cli.install_ui, mcpp.cli.build. +// See .agents/docs/2026-06-10-cli-modularization.md for the architecture. module; #include @@ -15,40 +14,18 @@ module; export module mcpp.cli; import std; -import mcpp.libs.json; -import mcpp.manifest; -import mcpp.modgraph.graph; -import mcpp.modgraph.scanner; -import mcpp.modgraph.validate; -import mcpp.toolchain.clang; -import mcpp.toolchain.detect; -import mcpp.toolchain.fingerprint; -import mcpp.toolchain.registry; -import mcpp.toolchain.stdmod; -import mcpp.build.plan; -import mcpp.build.backend; -import mcpp.build.ninja; -import mcpp.lockfile; -import mcpp.publish.xpkg_emit; -import mcpp.pack; -import mcpp.config; -import mcpp.xlings; -import mcpp.platform; -import mcpp.fetcher; -import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm -import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now -import mcpp.pm.index_spec; // IndexSpec for [indices] support -import mcpp.scaffold; // package-based project templates -import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexistence) -import mcpp.pm.compat; // 0.0.6: namespace field + dotted-name compat shims -import mcpp.pm.dep_spec; +import mcpplibs.cmdline; +import mcpp.cli.cmd_build; +import mcpp.cli.cmd_cache; +import mcpp.cli.cmd_new; +import mcpp.cli.cmd_publish; +import mcpp.cli.cmd_registry; +import mcpp.cli.cmd_self; +import mcpp.cli.cmd_toolchain; +import mcpp.pm.commands; +import mcpp.toolchain.fingerprint; // MCPP_VERSION import mcpp.ui; import mcpp.log; -import mcpp.fallback.install_integrity; -import mcpp.bmi_cache; -import mcpp.dyndep; -import mcpp.version_req; // SemVer constraint resolution -import mcpplibs.cmdline; // M6.1: dogfooded CLI parser export namespace mcpp::cli { @@ -56,15 +33,7 @@ int run(int argc, char** argv); } // namespace mcpp::cli -namespace mcpp::cli::detail { - -// ----- helpers ----- -// -// As of M6.1 phase 3, all CLI commands dispatch through a single -// `cmdline::App` declared in `run()` below. The previous per-command -// `cl::App` build + `parse_cmd_args(...)` double-parse is gone; each -// `cmd_*` now takes the already-parsed `ParsedArgs` and reads from it. -// `cmdline` handles `--help` / `--version` / unknown-option errors itself. +namespace mcpp::cli { // Custom top-level help. cmdline's auto-generated `print_help` is a fine // default but its layout (`USAGE:`, no command-specific blurbs) doesn't @@ -113,5687 +82,7 @@ void print_usage() { std::println("Docs: https://github.com/mcpp-community/mcpp/tree/main/docs"); } -// Locate mcpp.toml by walking upward from cwd. -std::optional find_manifest_root(std::filesystem::path start) { - auto p = std::filesystem::absolute(start); - while (true) { - if (std::filesystem::exists(p / "mcpp.toml")) return p; - auto parent = p.parent_path(); - if (parent == p) return std::nullopt; - p = parent; - } -} - -// Find the workspace root by walking upward from a member directory. -// Returns empty if no workspace root found. -std::filesystem::path find_workspace_root(const std::filesystem::path& memberRoot) { - auto p = memberRoot.parent_path(); - while (true) { - if (std::filesystem::exists(p / "mcpp.toml")) { - auto m = mcpp::manifest::load(p / "mcpp.toml"); - if (m && m->workspace.present) { - // Verify memberRoot is in members list - auto rel = std::filesystem::relative(memberRoot, p); - for (auto& member : m->workspace.members) { - if (rel == std::filesystem::path(member)) return p; - } - } - } - auto parent = p.parent_path(); - if (parent == p) break; - p = parent; - } - return {}; -} - -// Merge workspace.dependencies versions into a member's deps. -void merge_workspace_deps(mcpp::manifest::Manifest& member, - const mcpp::manifest::Manifest& workspace) { - auto merge_map = [&](std::map& deps) { - for (auto& [name, spec] : deps) { - if (!spec.inheritWorkspace) continue; - // Try exact key match first - auto it = workspace.workspace.dependencies.find(name); - if (it != workspace.workspace.dependencies.end()) { - spec.version = it->second.version; - spec.inheritWorkspace = false; - continue; - } - // Try short name for default-ns deps - auto shortIt = workspace.workspace.dependencies.find(spec.shortName); - if (shortIt != workspace.workspace.dependencies.end()) { - spec.version = shortIt->second.version; - spec.inheritWorkspace = false; - } - } - }; - merge_map(member.dependencies); - merge_map(member.devDependencies); - merge_map(member.buildDependencies); -} - -std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc, - const mcpp::toolchain::Fingerprint& fp, - const std::filesystem::path& root) -{ - auto triple = tc.targetTriple.empty() ? std::string{"unknown"} : tc.targetTriple; - return root / "target" / triple / fp.hex; -} - -// ─── Toolchain version-spec helpers ────────────────────────────────── -// -// Partial versions: `mcpp toolchain install gcc 15` must match -// the latest installed/available 15.x.y, `gcc 15.1` matches the latest -// 15.1.y, etc. Accept either ` ` (two positionals) or `@` -// (one positional with `@`) — both forms are normalised here. - -// Split "X.Y.Z…" into integer components. A trailing "-musl" (or any other -// non-numeric tail) is dropped — the caller has already handled the libc -// flavour and we only care about the numeric prefix for matching. -std::vector parse_version_components(std::string_view s) { - std::vector out; - int cur = 0; - bool any = false; - for (char c : s) { - if (c >= '0' && c <= '9') { cur = cur * 10 + (c - '0'); any = true; } - else if (c == '.') { - if (any) { out.push_back(cur); cur = 0; any = false; } - else { out.clear(); break; } - } else { - break; // non-numeric tail (e.g. "-musl") - } - } - if (any) out.push_back(cur); - return out; -} - -// Pick the version from `available` that best matches `partial`: -// "" → highest version overall -// "15" → highest 15.X.Y -// "15.1" → highest 15.1.Y -// "15.1.0" → exact match (or empty if not present) -// Empty result = no match. -std::optional -resolve_version_match(std::string_view partial, - std::vector available) -{ - if (available.empty()) return std::nullopt; - auto want = parse_version_components(partial); - auto matches = [&](const std::vector& cand) { - if (want.size() > cand.size()) return false; - for (std::size_t i = 0; i < want.size(); ++i) - if (cand[i] != want[i]) return false; - return true; - }; - std::optional best; - std::vector bestVec; - for (auto& v : available) { - auto comps = parse_version_components(v); - if (comps.empty()) continue; - if (!matches(comps)) continue; - if (!best || std::lexicographical_compare( - bestVec.begin(), bestVec.end(), comps.begin(), comps.end())) - { - best = v; - bestVec = std::move(comps); - } - } - return best; -} - -// Enumerate installed `/xim-x-//` subdirs. -std::vector -list_installed_versions(const std::filesystem::path& pkgsDir, - std::string_view ximName) -{ - std::vector out; - auto root = pkgsDir / std::format("xim-x-{}", ximName); - std::error_code ec; - if (!std::filesystem::exists(root, ec)) return out; - for (auto& v : std::filesystem::directory_iterator(root, ec)) { - if (v.is_directory(ec)) out.push_back(v.path().filename().string()); - } - return out; -} - -// Look up available versions for `xim:` from the locally synced index. -// Falls back to an empty list silently — the caller will then either error -// out with a clear message or just keep the partial as-is. -// -// Index layout in mcpp's sandbox is two-tier: -// /data/xim-pkgindex/pkgs//.lua — primary -// /data/xim-index-repos//pkgs//.lua -// We scan both so a package living in either tier resolves. -std::vector -list_available_xpkg_versions(const mcpp::config::GlobalConfig& cfg, - std::string_view ximName) -{ - if (ximName.empty()) return {}; - std::string subdir(1, ximName[0]); - std::string fname = std::string(ximName) + ".lua"; - - auto try_load = [&](const std::filesystem::path& p) - -> std::optional> - { - std::error_code ec; - if (!std::filesystem::exists(p, ec)) return std::nullopt; - std::ifstream is(p); - std::string body((std::istreambuf_iterator(is)), {}); - return mcpp::manifest::list_xpkg_versions(body, "linux"); - }; - - auto data = cfg.xlingsHome() / "data"; - if (auto v = try_load(data / "xim-pkgindex" / "pkgs" / subdir / fname); v) - return std::move(*v); - - std::error_code ec; - auto repos = data / "xim-index-repos"; - if (std::filesystem::exists(repos, ec)) { - for (auto& repo : std::filesystem::directory_iterator(repos, ec)) { - auto cand = repo.path() / "pkgs" / subdir / fname; - if (auto v = try_load(cand); v) return std::move(*v); - } - } - return {}; -} - -// ─── Install-time progress display ─────────────────────────────────── -// -// xlings emits NDJSON events on stdout via `xlings interface install_packages -// --args ...` (see fetcher.cppm). The events we care about for UX are: -// -// {"kind":"data","dataKind":"download_progress","payload":{ -// "elapsedSec": 2.0, -// "files": [{"name":"...", "downloadedBytes":..., "totalBytes":..., "finished":bool, ...}], -// ... -// }} -// -// We parse the first file in the `files` array (xlings serializes the -// currently-active download first) and feed (current, total) to a -// ui::ProgressBar so the user sees a "Downloading [==== ] -// 45 MB / 110 MB" line. - -struct InstallProgressFile { - std::string name; - double downloaded = 0; - double total = 0; - bool started = false; - bool finished = false; -}; - -namespace { - -// Extract one `{ ... }` object starting at payload[*pos], moving *pos past -// the closing `}`. Returns the slice or empty when no object is here. -std::string_view scan_one_object(std::string_view payload, std::size_t* pos) { - auto p = *pos; - while (p < payload.size() && (payload[p] == ' ' || payload[p] == '\n')) ++p; - if (p >= payload.size() || payload[p] != '{') { *pos = p; return {}; } - auto start = p; - int depth = 0; - bool in_string = false; - for (; p < payload.size(); ++p) { - char c = payload[p]; - if (in_string) { - if (c == '\\' && p + 1 < payload.size()) { ++p; continue; } - if (c == '"') in_string = false; - continue; - } - if (c == '"') in_string = true; - else if (c == '{') ++depth; - else if (c == '}') { if (--depth == 0) { ++p; break; } } - } - *pos = p; - return payload.substr(start, (p == payload.size() ? p : p) - start); -} - -InstallProgressFile parse_one_install_file(std::string_view obj) { - auto get_str = [&](std::string_view key) -> std::string { - std::string n = std::format("\"{}\":\"", key); - auto q = obj.find(n); - if (q == std::string_view::npos) return ""; - q += n.size(); - std::string out; - while (q < obj.size() && obj[q] != '"') { - if (obj[q] == '\\' && q + 1 < obj.size()) { out.push_back(obj[q+1]); q += 2; continue; } - out.push_back(obj[q++]); - } - return out; - }; - auto get_num = [&](std::string_view key) -> double { - std::string n = std::format("\"{}\":", key); - auto q = obj.find(n); - if (q == std::string_view::npos) return 0; - q += n.size(); - auto e = q; - while (e < obj.size() - && (std::isdigit(static_cast(obj[e])) - || obj[e] == '.' || obj[e] == '-' || obj[e] == '+' - || obj[e] == 'e' || obj[e] == 'E')) ++e; - try { return std::stod(std::string(obj.substr(q, e - q))); } - catch (...) { return 0; } - }; - auto get_bool = [&](std::string_view key) -> bool { - std::string n = std::format("\"{}\":", key); - auto q = obj.find(n); - if (q == std::string_view::npos) return false; - q += n.size(); - return obj.size() - q >= 4 && obj.substr(q, 4) == "true"; - }; - - InstallProgressFile f; - f.name = get_str("name"); - f.downloaded = get_num("downloadedBytes"); - f.total = get_num("totalBytes"); - f.started = get_bool("started"); - f.finished = get_bool("finished"); - return f; -} - -} // namespace - -// Parse every entry in the payload's `files` array. xlings emits an -// array-of-files for download_progress events even when only one is -// active, and during multi-package installs (gcc → glibc / binutils / -// linux-headers / gcc-runtime / gcc) the order of entries shifts as -// each file starts and finishes. Reading just the first one would -// flicker between names and re-emit the static "Downloading " -// line every time the first slot rotates. -std::vector -parse_all_install_files(std::string_view payload) -{ - std::vector out; - constexpr std::string_view kKey{"\"files\":["}; - auto p = payload.find(kKey); - if (p == std::string_view::npos) return out; - p += kKey.size(); - while (p < payload.size()) { - while (p < payload.size() && (payload[p] == ' ' || payload[p] == '\n' - || payload[p] == ',')) ++p; - if (p >= payload.size() || payload[p] == ']') break; - if (payload[p] != '{') break; - auto obj = scan_one_object(payload, &p); - if (obj.empty()) break; - auto f = parse_one_install_file(obj); - if (!f.name.empty()) out.push_back(std::move(f)); - } - return out; -} - -// Pull a top-level numeric field out of a payload JSON string. Cheap; -// only used for `elapsedSec` which we trust to be a plain number. -double extract_payload_number(std::string_view payload, std::string_view key) { - std::string n = std::format("\"{}\":", key); - auto q = payload.find(n); - if (q == std::string_view::npos) return 0; - q += n.size(); - auto e = q; - while (e < payload.size() - && (std::isdigit(static_cast(payload[e])) - || payload[e] == '.' || payload[e] == '-' || payload[e] == '+' - || payload[e] == 'e' || payload[e] == 'E')) ++e; - try { return std::stod(std::string(payload.substr(q, e - q))); } - catch (...) { return 0; } -} - -// Build the PathContext used to shorten user-visible paths in status -// output. project_root may be empty (for verbs that don't need it). -mcpp::ui::PathContext make_path_ctx(const mcpp::config::GlobalConfig* cfg, - std::filesystem::path project_root = {}) -{ - mcpp::ui::PathContext ctx; - ctx.project_root = std::move(project_root); - if (cfg) ctx.mcpp_home = cfg->mcppHome; - if (auto* h = std::getenv("HOME"); h && *h) ctx.home = h; - return ctx; -} - -// Map a decoded NDJSON `download_progress` files[] snapshot onto the neutral -// `mcpp::ui::DownloadFile` the centralized renderer consumes. -template -std::vector to_ui_download_files(const std::vector& files) { - std::vector out; - out.reserve(files.size()); - for (auto& f : files) { - if constexpr (requires { f.downloadedBytes; }) { - out.push_back({ f.name, - static_cast(f.downloadedBytes), - static_cast(f.totalBytes), - f.started, f.finished }); - } else { - out.push_back({ f.name, - static_cast(f.downloaded), - static_cast(f.total), - f.started, f.finished }); - } - } - return out; -} - -// Adapter from `mcpp::config::BootstrapProgress` (xlings download_progress -// event) to the centralized download renderer. Used by load_or_init() during -// the one-time sandbox bootstrap (xim:patchelf, xim:ninja + transitive deps). -mcpp::config::BootstrapProgressCallback make_bootstrap_progress_callback() { - auto progress = std::make_shared(); - return [progress](const mcpp::config::BootstrapProgress& ev) { - auto files = to_ui_download_files(ev.files); - progress->update(files, ev.elapsedSec); - }; -} - -// EventHandler that forwards xlings `download_progress` events to the same -// centralized renderer. Used for toolchain, builtin-index and custom-index -// installs alike, so all three show identical UI. -struct CliInstallProgress : mcpp::fetcher::EventHandler { - mcpp::ui::DownloadProgress progress_; - - void on_data(const mcpp::fetcher::DataEvent& d) override { - if (d.dataKind != "download_progress") return; - auto files = parse_all_install_files(d.payloadJson); - if (files.empty()) return; - double elapsed = extract_payload_number(d.payloadJson, "elapsedSec"); - auto ui_files = to_ui_download_files(files); - progress_.update(ui_files, elapsed); - } - - void on_log(const mcpp::fetcher::LogEvent& e) override { - if (e.level == "error") - mcpp::log::error("xlings", e.message); - else if (e.level == "warn") - mcpp::log::warn("xlings", e.message); - else - mcpp::log::info("xlings", e.message); - mcpp::log::verbose("xlings", std::format("[{}] {}", e.level, e.message)); - } - - void on_error(const mcpp::fetcher::ErrorEvent& e) override { - mcpp::log::error("xlings", std::format("{}: {}", e.code, e.message)); - if (!e.hint.empty()) - mcpp::log::info("xlings", std::format("hint: {}", e.hint)); - } - - // progress_'s own destructor finishes the active bar. - ~CliInstallProgress() override = default; -}; - -// Compose a stable canonical compile-flags string for fingerprinting. -std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { - std::string s; - s += "-std="; s += m.package.standard; - s += " -fmodules"; - // macOS deployment target changes the effective compile triple - // (arm64-apple-macosxNN) — a std.pcm built for one target cannot be - // loaded by a TU compiled for another. Fold the resolved value - // (env override > [build] macos_deployment_target manifest default) - // into the fingerprint so switching targets rebuilds the BMI cache - // instead of dying with a module config mismatch. - // - // The built-in default floor (rustc-style) lives in the single - // resolver (platform::macos::deployment_target), so this rule, the - // flags and the std-module prebuild always agree — the 0.0.50-era - // attempt to inject a default here alone left the test build's - // std.pcm unstaged (import std failed wholesale on macos CI). - if constexpr (mcpp::platform::is_macos) { - auto dtv = mcpp::platform::macos::deployment_target( - m.buildConfig.macosDeploymentTarget); - if (!dtv.empty()) { - s += " macos_deployment_target="; - s += dtv; - } - } - if (!m.buildConfig.cStandard.empty()) { - s += " c_standard="; - s += m.buildConfig.cStandard; - } - for (auto const& flag : m.buildConfig.cflags) { - s += " cflag:"; - s += flag; - } - for (auto const& flag : m.buildConfig.cxxflags) { - s += " cxxflag:"; - s += flag; - } - for (auto const& flag : m.buildConfig.ldflags) { - s += " ldflag:"; - s += flag; - } - return s; -} - -std::string canonical_package_build_metadata( - const std::vector& packages) -{ - std::string s; - for (auto const& pkg : packages) { - s += "\npackage:"; - s += pkg.manifest.package.namespace_; - s += "/"; - s += pkg.manifest.package.name; - s += "@"; - s += pkg.manifest.package.version; - if (!pkg.manifest.buildConfig.cStandard.empty()) { - s += " c_standard="; - s += pkg.manifest.buildConfig.cStandard; - } - for (auto const& flag : pkg.manifest.buildConfig.cflags) { - s += " cflag:"; - s += flag; - } - for (auto const& flag : pkg.manifest.buildConfig.cxxflags) { - s += " cxxflag:"; - s += flag; - } - for (auto const& flag : pkg.manifest.buildConfig.ldflags) { - s += " ldflag:"; - s += flag; - } - if (pkg.usageResolved) { - for (auto const& dir : pkg.privateBuild.includeDirs) { - s += " private_include:"; - s += dir.generic_string(); - } - for (auto const& dir : pkg.publicUsage.includeDirs) { - s += " public_include:"; - s += dir.generic_string(); - } - } - for (auto const& [path, content] : pkg.manifest.buildConfig.generatedFiles) { - s += " genfile:"; - s += path.generic_string(); - s += "="; - s += content; - } - } - return s; -} - -std::expected -materialize_generated_files(const std::filesystem::path& root, - const mcpp::manifest::Manifest& manifest) -{ - for (auto const& [relPath, content] : manifest.buildConfig.generatedFiles) { - if (relPath.empty()) { - return std::unexpected("generated_files contains an empty path"); - } - if (relPath.is_absolute()) { - return std::unexpected(std::format( - "generated_files path '{}' must be relative", relPath.generic_string())); - } - auto const genericPath = relPath.generic_string(); - for (std::size_t begin = 0; begin <= genericPath.size();) { - auto const end = genericPath.find('/', begin); - auto const part = genericPath.substr(begin, end == std::string::npos - ? std::string::npos - : end - begin); - if (part == "..") { - return std::unexpected(std::format( - "generated_files path '{}' must not escape the package root", - relPath.generic_string())); - } - if (end == std::string::npos) { - break; - } - begin = end + 1; - } - - auto out = root / relPath.lexically_normal(); - std::error_code ec; - std::filesystem::create_directories(out.parent_path(), ec); - if (ec) { - return std::unexpected(std::format( - "cannot create directory for generated file '{}': {}", - out.string(), ec.message())); - } - - std::ofstream os(out, std::ios::binary); - if (!os) { - return std::unexpected(std::format( - "cannot write generated file '{}'", out.string())); - } - os << content; - if (!os) { - return std::unexpected(std::format( - "failed while writing generated file '{}'", out.string())); - } - } - return {}; -} - -bool is_std_module(std::string_view name) { - return name == "std" || name == "std.compat"; -} - -std::string trim_copy(std::string s) { - while (!s.empty() && std::isspace(static_cast(s.front()))) - s.erase(0, 1); - while (!s.empty() && std::isspace(static_cast(s.back()))) - s.pop_back(); - return s; -} - -bool source_file_imports_std(const std::filesystem::path& path) { - std::ifstream is(path); - if (!is) return false; - - std::string line; - while (std::getline(is, line)) { - line = trim_copy(std::move(line)); - std::size_t i = std::string::npos; - if (line.starts_with("import ")) { - i = 7; - } else if (line.starts_with("export import ")) { - i = 14; - } - if (i == std::string::npos) continue; - while (i < line.size() && std::isspace(static_cast(line[i]))) - ++i; - - std::string name; - while (i < line.size() - && (std::isalnum(static_cast(line[i])) - || line[i] == '_' || line[i] == '.' || line[i] == ':')) { - name.push_back(line[i]); - ++i; - } - if (is_std_module(name)) return true; - } - return false; -} - -bool graph_or_targets_import_std(const mcpp::modgraph::Graph& graph, - const mcpp::manifest::Manifest& manifest, - const std::filesystem::path& projectRoot) { - for (auto& u : graph.units) { - for (auto& req : u.requires_) { - if (is_std_module(req.logicalName)) - return true; - } - } - - // Some target entry files can be added to the plan after the package scan. - // Check them here so std BMI setup matches what make_plan will compile. - for (auto& t : manifest.targets) { - if (!t.main.empty() && source_file_imports_std(projectRoot / t.main)) - return true; - } - return false; -} - -// Run patchelf on every dynamic ELF in `dir` (recursively): -// - Set PT_INTERP to `loader` (the sandbox-local glibc loader). -// - Set RUNPATH to `rpath` (colon-separated list of sandbox lib dirs). -// Idempotent; skips static binaries and shared libs without PT_INTERP. -// -// TODO(xlings/libxpkg-upstream): xim 0.4.10's `elfpatch.auto({interpreter=...})` -// is supposed to do this in install hooks but currently scans 0 files for -// some packages (verified empirically: `binutils: elfpatch auto: 0 0 0`). -// Once the upstream legacy elfpatch path is fixed, this mcpp-side walker -// can be deleted. -void patchelf_walk(const std::filesystem::path& dir, - const std::filesystem::path& loader, - const std::string& rpath, - const std::filesystem::path& patchelfBin) -{ - if (!std::filesystem::exists(dir) || !std::filesystem::exists(patchelfBin)) - return; - std::error_code ec; - for (auto it = std::filesystem::recursive_directory_iterator(dir, ec); - it != std::filesystem::recursive_directory_iterator{}; it.increment(ec)) - { - if (ec) { ec.clear(); continue; } - if (!it->is_regular_file(ec)) continue; - auto path = it->path(); - // Skip non-ELF (cheap magic check) - std::ifstream is(path, std::ios::binary); - char m[4]{}; - is.read(m, 4); - if (!is || m[0] != 0x7f || m[1] != 'E' || m[2] != 'L' || m[3] != 'F') - continue; - is.close(); - // Probe PT_INTERP — skip static binaries (no interp). - auto probe = std::format("{} --print-interpreter {} 2>/dev/null", - mcpp::platform::shell::quote(patchelfBin.string()), - mcpp::platform::shell::quote(path.string())); - auto probeResult = mcpp::platform::process::capture(probe); - bool hasInterp = (probeResult.exit_code == 0 && !probeResult.output.empty()); - if (hasInterp) { - (void)mcpp::platform::process::run_silent(std::format( - "{} --set-interpreter {} {} 2>/dev/null", - mcpp::platform::shell::quote(patchelfBin.string()), - mcpp::platform::shell::quote(loader.string()), - mcpp::platform::shell::quote(path.string()))); - } - // Always set RUNPATH (works on .so too — they need to find deps). - if (!rpath.empty()) { - (void)mcpp::platform::process::run_silent(std::format( - "{} --set-rpath {} {} 2>/dev/null", - mcpp::platform::shell::quote(patchelfBin.string()), - mcpp::platform::shell::quote(rpath), - mcpp::platform::shell::quote(path.string()))); - } - } -} - -// xim bakes the installing user's XLINGS_HOME into gcc specs at install -// time (as `--dynamic-linker` and `-rpath`). When mcpp uses its own -// isolated sandbox (MCPP_HOME/registry/), the baked-in paths point to -// xlings' home, not mcpp's sandbox glibc — binaries would fail to exec. -// -// Mcpp does a post-install spec rewrite: -// - Dynamically detects the baked-in lib dir from the specs file -// - Replaces the dynamic-linker path with /ld-linux-x86-64.so.2 -// - Replaces the rpath with : -// Idempotent — skips if already pointing at the correct glibc. -// Extract the baked-in lib directory from a gcc specs file by finding -// the dynamic-linker path that ends with `/ld-linux-x86-64.so.2`. -// xim bakes the installing user's XLINGS_HOME into specs at install -// time, so the path varies per machine — we cannot hardcode it. -std::string detect_baked_lib_dir(const std::string& specsContent) { - constexpr std::string_view kLoader = "/ld-linux-x86-64.so.2"; - auto pos = specsContent.find(kLoader); - if (pos == std::string::npos) return ""; - // Walk backwards to find start of the absolute path - auto start = pos; - while (start > 0 && specsContent[start - 1] != ' ' - && specsContent[start - 1] != ':' - && specsContent[start - 1] != ';' - && specsContent[start - 1] != '\n') { - --start; - } - auto dir = specsContent.substr(start, pos - start); - // Sanity: must be absolute - if (dir.empty() || dir[0] != '/') return ""; - // Skip if it already points to the target glibc (no fixup needed) - return dir; -} - -void fixup_gcc_specs(const std::filesystem::path& gccPkgRoot, - const std::filesystem::path& glibcLibDir, - const std::filesystem::path& gccLibDir) -{ - auto specsParent = gccPkgRoot / "lib" / "gcc" / "x86_64-linux-gnu"; - if (!std::filesystem::exists(specsParent)) return; - - auto loaderReplacement = (glibcLibDir / "ld-linux-x86-64.so.2").string(); - auto rpathReplacement = std::format("{}:{}", - glibcLibDir.string(), - gccLibDir.string()); - - auto replace_all = [](std::string& s, std::string_view needle, - std::string_view rep) - { - for (std::size_t pos = 0; - (pos = s.find(needle, pos)) != std::string::npos;) { - s.replace(pos, needle.size(), rep); - pos += rep.size(); - } - }; - - for (auto& sub : std::filesystem::directory_iterator(specsParent)) { - auto specs = sub.path() / "specs"; - if (!std::filesystem::exists(specs)) continue; - - std::ifstream is(specs); - std::stringstream ss; ss << is.rdbuf(); - std::string content = ss.str(); - - auto bakedDir = detect_baked_lib_dir(content); - if (bakedDir.empty()) continue; - // Already pointing at the right place — no fixup needed. - if (bakedDir == glibcLibDir.string()) continue; - - auto bakedLoader = bakedDir + "/ld-linux-x86-64.so.2"; - - // Order matters: replace the full loader file path first so the - // shorter dir pattern doesn't eat its prefix. - replace_all(content, bakedLoader, loaderReplacement); - replace_all(content, bakedDir, rpathReplacement); - - std::ofstream os(specs); - os << content; - } -} - -// 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; - } -} - -// 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) { - // 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; - 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 -// pinning workflows; users opt into resolution by writing `^1.2.3` etc. -// `is_version_constraint`, `kXpkgPlatform` and `resolve_semver` have moved -// to `mcpp.pm.resolver` (PR-R4 — see -// `.agents/docs/2026-05-08-pm-subsystem-architecture.md`). Call sites -// below reference the `mcpp::pm::` qualified names directly. - -// --- Commands --- - -// ─── Package-based templates (design v2: multi-level --template) ────── -// -// Resolve SPEC's package@version through the index, ensure the package -// sources are installed (same cache as dependencies), and return the -// package root (the directory containing mcpp.toml). -struct FetchedTemplatePackage { - std::filesystem::path root; - std::string name; // short package name (e.g. "imgui") - std::string version; // resolved exact version -}; - -std::expected -fetch_template_package(const mcpp::scaffold::TemplateSpec& spec) { - auto cfg = mcpp::config::load_or_init(/*quiet=*/false, - make_bootstrap_progress_callback()); - if (!cfg) return std::unexpected(cfg.error().message); - mcpp::pm::Fetcher fetcher(*cfg); - - // Namespace candidates mirror dependency lookup: index root first, - // then the compat namespace. - std::string ns; - std::optional lua; - for (std::string cand : {std::string{}, std::string{"compat"}}) { - if (auto l = fetcher.read_xpkg_lua(cand, spec.pkg)) { - ns = cand; - lua = std::move(*l); - break; - } - } - if (!lua) { - return std::unexpected(std::format( - "template package '{}' not found in the index " - "(check the name, or run `mcpp index update`)", spec.pkg)); - } - - std::string version = spec.version; - if (version.empty()) { - auto v = mcpp::pm::resolve_semver(ns, spec.pkg, "*", fetcher); - if (!v) return std::unexpected(v.error()); - version = *v; - } - - auto installed = fetcher.install_path(ns, spec.pkg, version); - if (!installed) { - auto fq = ns.empty() ? spec.pkg : std::format("{}.{}", ns, spec.pkg); - mcpp::ui::info("Downloading", std::format("{} v{}", fq, version)); - CliInstallProgress progress; - std::vector targets{ std::format("{}@{}", fq, version) }; - auto r = fetcher.install(targets, &progress); - if (!r) return std::unexpected(std::format( - "fetch '{}@{}': {}", fq, version, r.error().message)); - if (r->exitCode != 0) return std::unexpected(std::format( - "fetch '{}@{}' failed (exit {})", fq, version, r->exitCode)); - installed = fetcher.install_path(ns, spec.pkg, version); - if (!installed) return std::unexpected(std::format( - "package '{}@{}' install path missing after fetch", fq, version)); - } - - // Package root = the directory holding mcpp.toml (tarballs usually wrap - // everything in a single top-level directory). - std::filesystem::path root = *installed; - if (!std::filesystem::exists(root / "mcpp.toml")) { - std::error_code ec; - for (auto& e : std::filesystem::directory_iterator(root, ec)) { - if (e.is_directory() - && std::filesystem::exists(e.path() / "mcpp.toml")) { - root = e.path(); - break; - } - } - } - if (!std::filesystem::exists(root / "mcpp.toml")) { - return std::unexpected(std::format( - "package '{}@{}' has no mcpp.toml", spec.pkg, version)); - } - return FetchedTemplatePackage{root, spec.pkg, version}; -} - -void print_template_listing(const FetchedTemplatePackage& pkg, - const std::vector& entries) { - std::println("Templates in {}@{}:", pkg.name, pkg.version); - for (auto& t : entries) { - std::println(" {:<14}{}{}", t.name, - t.meta.isDefault ? "(default) " : " ", - t.meta.description); - } - std::println(""); - std::println("usage: mcpp new --template {}[@ver][: