// mcpp.cli — top-level command dispatch. // // 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 module; #include #include 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 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 { 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. // Custom top-level help. cmdline's auto-generated `print_help` is a fine // default but its layout (`USAGE:`, no command-specific blurbs) doesn't // match what the e2e tests assert against — they check for `Usage:` // (mixed case) plus `mcpp new` / `mcpp build` literals. We keep the // canonical printer here so the docs/CHANGELOG examples don't drift // every time cmdline tweaks its formatting. void print_usage() { std::println("mcpp v{} — modern C++23 build tool", mcpp::toolchain::MCPP_VERSION); std::println(""); std::println("Usage:"); std::println("Project commands:"); std::println(" mcpp new Create a new package skeleton"); std::println(" mcpp build [options] Build the current package"); std::println(" mcpp run [target] [-- args...] Build + run a binary target"); std::println(" mcpp test [-- args...] Build + run all tests/**/*.cpp"); std::println(" mcpp clean [--bmi-cache] Remove target/ (and optionally BMI cache)"); std::println(" mcpp add [@] Add a dependency to mcpp.toml"); std::println(" mcpp remove Remove a dependency from mcpp.toml"); std::println(" mcpp update [pkg] Re-resolve deps and rewrite mcpp.lock"); std::println(" mcpp search Search packages in registries"); std::println(" mcpp publish [--dry-run] Publish package to default registry"); std::println(" mcpp pack [--mode ] Build + bundle into a distributable tarball"); std::println(" mcpp emit xpkg [-V VER] [-o FILE] Generate xpkg Lua entry"); std::println(""); std::println("Resource management:"); std::println(" mcpp toolchain install|list|default Manage mcpp's private toolchains"); std::println(" mcpp cache list|prune|clean|info Inspect/manage the global BMI cache"); std::println(" mcpp index list|add|remove|update Manage package registries"); std::println(""); std::println("About mcpp itself:"); std::println(" mcpp self doctor Diagnose mcpp environment health"); std::println(" mcpp self env Print mcpp paths and toolchain"); std::println(" mcpp self config [--mirror CN|GLOBAL] Show or modify mcpp's xlings config"); std::println(" mcpp self version Show mcpp version"); std::println(" mcpp self explain Show extended description for an error code"); std::println(" mcpp --help / --version Help / version"); std::println(""); std::println("Build options:"); std::println(" --verbose, -v Verbose compiler output"); std::println(" --quiet, -q Suppress status output"); std::println(" --print-fingerprint Show toolchain fingerprint and 10 inputs"); std::println(" --no-cache Force-clear target/ before building"); std::println(" --no-color Disable colored output"); std::println(""); 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; } // Stateless adapter from `mcpp::config::BootstrapProgress` (xlings // download_progress event) to a sticky ProgressBar. Used by // load_or_init() during the one-time sandbox bootstrap (xim:patchelf, // xim:ninja, plus their transitive deps). // // Two xlings quirks the callback has to absorb: // 1. Each file's `finished=true` event arrives twice in a row. // 2. During multi-package installs the `files[]` array reshuffles // between events (the active download isn't always at slot 0). // The fix mirrors CliInstallProgress: dedupe via a `finished_` set and // always pick "active first if still in event, else first // started+unfinished" rather than reading slot 0 blindly. mcpp::config::BootstrapProgressCallback make_bootstrap_progress_callback() { auto bar = std::make_shared>(); auto active = std::make_shared(); auto finished = std::make_shared>(); return [bar, active, finished](const mcpp::config::BootstrapProgress& ev) { // Process newly-finished entries. for (auto& f : ev.files) { if (finished->contains(f.name)) continue; if (!f.finished) continue; if (*active == f.name) { if (*bar) (*bar)->finish(); bar->reset(); active->clear(); } finished->insert(f.name); } // Pick what to display: prefer continuing with `*active` if it's // still in the array and not finished, otherwise the first // started+unfinished entry. const mcpp::config::BootstrapFile* current = nullptr; for (auto& f : ev.files) { if (f.name == *active && !f.finished && !finished->contains(f.name)) { current = &f; break; } } if (!current) { for (auto& f : ev.files) { if (finished->contains(f.name)) continue; if (f.started && !f.finished) { current = &f; break; } } } if (!current) return; if (current->name != *active) { if (*bar) (*bar)->finish(); *active = current->name; bar->emplace("Downloading", current->name); } if (current->totalBytes > 0) { (*bar)->update_bytes(static_cast(current->downloadedBytes), static_cast(current->totalBytes), ev.elapsedSec); } }; } struct CliInstallProgress : mcpp::fetcher::EventHandler { std::optional bar_; std::string active_; std::unordered_set finished_; 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; // 1. Process any newly-finished entries. Each file is reported // twice with finished=true (xlings quirk); the `finished_` // set dedupes both that AND the rotation case where the // same file shows up at a different array slot in a later // event. for (auto& f : files) { if (finished_.contains(f.name)) continue; if (!f.finished) continue; if (active_ == f.name) { if (bar_) bar_->finish(); bar_.reset(); active_.clear(); } finished_.insert(f.name); } // 2. Pick what to display. Prefer continuing with the current // `active_` if it's still in the array and not finished — // otherwise the first started+unfinished entry. This stops // the bar from flickering between names when xlings reshuffles // files[] across events during a multi-package install. const InstallProgressFile* current = nullptr; for (auto& f : files) { if (f.name == active_ && !f.finished && !finished_.contains(f.name)) { current = &f; break; } } if (!current) { for (auto& f : files) { if (finished_.contains(f.name)) continue; if (f.started && !f.finished) { current = &f; break; } } } if (!current) return; if (current->name != active_) { if (bar_) bar_->finish(); active_ = current->name; bar_.emplace("Downloading", current->name); } if (current->total > 0) { double elapsed = extract_payload_number(d.payloadJson, "elapsedSec"); bar_->update_bytes(static_cast(current->downloaded), static_cast(current->total), 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)); } ~CliInstallProgress() override { if (bar_) bar_->finish(); } }; // 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][: