From 0651cc328113954c441a5a45f4d35576079028bf Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 22 May 2026 21:04:19 +0800 Subject: [PATCH] feat: fallback registry + install-first resolve + mcpp self fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fallback registry (src/fallback/registry.cppm): - Compile-time registry of all 17 fallback mechanisms in mcpp - Each entry: id, domain, lifecycle (permanent/compat/workaround), removeBy - Single source of truth for auditing and cleanup planning 2. resolve_xpkg_path priority fix (src/pm/package_fetcher.cppm): - Refactored from: sandbox → copy → install (copy short-circuits install) - Refactored to: sandbox → install → copy (install-first, copy is fallback) - Split into resolve_quick() + copy_from_global() for clarity - Fixes: install path was never exercised when ~/.xlings/ had the package 3. mcpp self fallbacks command (src/cli.cppm): - Lists all registered fallbacks grouped by lifecycle - Shows workarounds (need upstream fix), compat (remove by 1.0), permanent --- src/cli.cppm | 48 ++++++++-- src/fallback/registry.cppm | 175 ++++++++++++++++++++++++++++++++++++ src/pm/package_fetcher.cppm | 152 ++++++++++++++++--------------- 3 files changed, 296 insertions(+), 79 deletions(-) create mode 100644 src/fallback/registry.cppm diff --git a/src/cli.cppm b/src/cli.cppm index 6ca62ae..0a69bdb 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -42,6 +42,7 @@ 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.registry; import mcpp.bmi_cache; import mcpp.dyndep; import mcpp.version_req; // SemVer constraint resolution @@ -4268,6 +4269,40 @@ int cmd_self_version(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { return 0; } +int cmd_self_fallbacks(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { + namespace fb = mcpp::fallback; + + // Group by lifecycle + std::vector workarounds, compats, permanents; + for (std::size_t i = 0; i < fb::kEntryCount; ++i) { + auto* e = &fb::kEntries[i]; + switch (e->lifecycle) { + case fb::Lifecycle::workaround: workarounds.push_back(e); break; + case fb::Lifecycle::compat: compats.push_back(e); break; + case fb::Lifecycle::permanent: permanents.push_back(e); break; + } + } + + std::println("Fallback Registry ({} entries)\n", fb::kEntryCount); + + auto print_group = [](std::string_view title, const std::vector& entries) { + if (entries.empty()) return; + std::println("{}:", title); + for (auto* e : entries) { + std::print(" {:<40s} {}", e->id, e->description); + if (!e->removeBy.empty()) + std::print(" [remove by {}]", e->removeBy); + std::println(""); + } + std::println(""); + }; + + print_group("WORKAROUNDS (need upstream fix)", workarounds); + print_group("COMPAT (backward compatibility)", compats); + print_group("PERMANENT (architecturally required)", permanents); + return 0; +} + std::string upper_ascii(std::string s) { for (char& ch : s) { if (ch >= 'a' && ch <= 'z') ch = static_cast(ch - 'a' + 'A'); @@ -4648,13 +4683,16 @@ int run(int argc, char** argv) { .subcommand(cl::App("explain") .description("Show extended description for an error code") .arg(cl::Arg("code").help("Error code such as E0001").required())) + .subcommand(cl::App("fallbacks") + .description("List all registered fallback mechanisms")) .action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) { return dispatch_sub("self", p, { - {"doctor", cmd_doctor}, - {"env", cmd_env}, - {"config", cmd_self_config}, - {"version", cmd_self_version}, - {"explain", cmd_explain_action}, + {"doctor", cmd_doctor}, + {"env", cmd_env}, + {"config", cmd_self_config}, + {"version", cmd_self_version}, + {"explain", cmd_explain_action}, + {"fallbacks", cmd_self_fallbacks}, }); }))) diff --git a/src/fallback/registry.cppm b/src/fallback/registry.cppm new file mode 100644 index 0000000..db7376b --- /dev/null +++ b/src/fallback/registry.cppm @@ -0,0 +1,175 @@ +// mcpp.fallback.registry — compile-time fallback metadata registry. +// +// Every fallback/workaround in mcpp is registered here with its lifecycle, +// domain, and removal plan. This provides a single source of truth for +// auditing, logging, and the `mcpp self fallbacks` command. +// +// Usage: +// auto* fb = mcpp::fallback::find("xpkg.copy_from_global"); +// mcpp::log::verbose("fetcher", std::format("[fallback:{}] {}", fb->id, fb->description)); + +export module mcpp.fallback.registry; + +import std; + +export namespace mcpp::fallback { + +enum class Lifecycle { + permanent, // architecturally required (multi-platform, retry logic) + compat, // backward compatibility, remove by specified version + workaround, // works around external bug, remove when upstream fixed +}; + +struct Entry { + std::string_view id; // unique key, e.g. "xpkg.copy_from_global" + std::string_view domain; // "package" | "config" | "toolchain" | "build" | "dependency" | "manifest" + std::string_view description; // one-line human-readable description + Lifecycle lifecycle; + std::string_view removeBy; // version string "1.0" or "" (permanent/workaround) + std::string_view upstreamIssue; // "xlings#123" or "" or description of upstream issue +}; + +// ─── Registry entries ──────────────────────────────────────────────── + +inline constexpr Entry kEntries[] = { + + // ─── Package fetch & install ───────────────────────────────────── + + { .id = "xpkg.copy_from_global", + .domain = "package", + .description = "copy xpkg from ~/.xlings/ when sandbox install fails", + .lifecycle = Lifecycle::workaround, + .removeBy = {}, + .upstreamIssue = "xlings XLINGS_HOME propagation / NDJSON large-package install", + }, + { .id = "xpkg.install_direct_before_ndjson", + .domain = "package", + .description = "try direct xlings install before NDJSON interface for large packages", + .lifecycle = Lifecycle::workaround, + .removeBy = {}, + .upstreamIssue = "xlings NDJSON interface unreliable for large packages", + }, + { .id = "xpkg.install_dir_scan", + .domain = "package", + .description = "last-resort scan xpkgs/ directory for matching package dir", + .lifecycle = Lifecycle::compat, + .removeBy = "1.0", + }, + { .id = "xpkg.lua_candidates", + .domain = "package", + .description = "multi-candidate xpkg .lua file lookup for legacy naming", + .lifecycle = Lifecycle::compat, + .removeBy = "1.0", + }, + + // ─── xlings binary acquisition ─────────────────────────────────── + + { .id = "xlings_binary.vendored_env", + .domain = "config", + .description = "MCPP_VENDORED_XLINGS env override (Windows runtime workaround)", + .lifecycle = Lifecycle::workaround, + .removeBy = {}, + .upstreamIssue = "Windows xlings binary may lack runtime after copy", + }, + { .id = "xlings_binary.system_which", + .domain = "config", + .description = "find xlings in PATH when bundled binary unavailable", + .lifecycle = Lifecycle::permanent, + }, + + // ─── Toolchain probing ─────────────────────────────────────────── + + { .id = "probe.sysroot_compiler", + .domain = "toolchain", + .description = "gcc -print-sysroot direct probe", + .lifecycle = Lifecycle::permanent, + }, + { .id = "probe.sysroot_cfg", + .domain = "toolchain", + .description = "parse clang++.cfg for --sysroot line", + .lifecycle = Lifecycle::permanent, + }, + { .id = "probe.sysroot_xcrun", + .domain = "toolchain", + .description = "macOS xcrun --show-sdk-path fallback", + .lifecycle = Lifecycle::permanent, + }, + { .id = "probe.sysroot_xlings_remap", + .domain = "toolchain", + .description = "remap xlings build-time sysroot path to mcpp registry", + .lifecycle = Lifecycle::workaround, + .removeBy = {}, + .upstreamIssue = "xlings bakes build-host absolute path into gcc specs", + }, + { .id = "sysroot.symlink_kernel_headers", + .domain = "toolchain", + .description = "symlink linux-headers xpkg into sysroot when missing", + .lifecycle = Lifecycle::workaround, + .removeBy = {}, + .upstreamIssue = "xlings sysroot may lack kernel headers after init", + }, + { .id = "sysroot.symlink_glibc_headers", + .domain = "toolchain", + .description = "symlink glibc xpkg headers into sysroot when missing", + .lifecycle = Lifecycle::workaround, + .removeBy = {}, + .upstreamIssue = "xlings sysroot may lack glibc headers after init", + }, + + // ─── Build system ──────────────────────────────────────────────── + + { .id = "build.ninja_incremental_retry", + .domain = "build", + .description = "full rebuild when incremental ninja build fails", + .lifecycle = Lifecycle::permanent, + }, + { .id = "build.dyndep_opt_out", + .domain = "build", + .description = "MCPP_NINJA_DYNDEP=0 disables P1689 dynamic deps", + .lifecycle = Lifecycle::permanent, + }, + + // ─── Dependency resolution ─────────────────────────────────────── + + { .id = "deps.multi_version_mangle", + .domain = "dependency", + .description = "cross-major version coexistence via module name mangling", + .lifecycle = Lifecycle::permanent, + }, + + // ─── Backward compatibility / migration ────────────────────────── + + { .id = "compat.dotted_package_name", + .domain = "manifest", + .description = "split legacy 'ns.name' dotted package names", + .lifecycle = Lifecycle::compat, + .removeBy = "1.0", + }, + { .id = "compat.config_index_migration", + .domain = "config", + .description = "rename mcpp-index to mcpplibs in config files", + .lifecycle = Lifecycle::compat, + .removeBy = "1.0", + }, +}; + +inline constexpr std::size_t kEntryCount = sizeof(kEntries) / sizeof(kEntries[0]); + +// ─── Query API ─────────────────────────────────────────────────────── + +constexpr const Entry* find(std::string_view id) { + for (auto& e : kEntries) + if (e.id == id) return &e; + return nullptr; +} + +inline constexpr const char* lifecycle_str(Lifecycle l) { + switch (l) { + case Lifecycle::permanent: return "permanent"; + case Lifecycle::compat: return "compat"; + case Lifecycle::workaround: return "workaround"; + } + return "unknown"; +} + +} // namespace mcpp::fallback diff --git a/src/pm/package_fetcher.cppm b/src/pm/package_fetcher.cppm index 834d8dd..707c51e 100644 --- a/src/pm/package_fetcher.cppm +++ b/src/pm/package_fetcher.cppm @@ -15,6 +15,7 @@ export module mcpp.pm.package_fetcher; import std; import mcpp.config; import mcpp.log; +import mcpp.fallback.registry; import mcpp.pm.compat; import mcpp.pm.index_spec; import mcpp.xlings; @@ -620,65 +621,15 @@ Fetcher::resolve_xpkg_path(std::string_view target, p.sourceDir = (subs.size() == 1) ? subs.front() : p.root; }; - auto resolve = [&]() -> std::expected { - // Workaround: xlings may extract large packages (e.g. LLVM) into its - // global data dir instead of the mcpp sandbox, because the extraction - // subprocess doesn't always inherit XLINGS_HOME. Detect this and copy - // the payload into the sandbox so mcpp remains self-contained. - // Originally Windows-only; extended to all platforms for the same - // reason (xlings subprocess XLINGS_HOME propagation is unreliable). - if (!std::filesystem::exists(verdir)) { - mcpp::log::verbose("fetcher", "verdir not in sandbox, checking global xlings"); - const char* xhome = nullptr; -#if defined(_WIN32) - xhome = std::getenv("USERPROFILE"); -#endif - if (!xhome) xhome = std::getenv("HOME"); - if (xhome) { - mcpp::log::debug("fetcher", std::format("HOME={}", xhome)); - // xlings stores xpkgs at /.xlings/data/xpkgs/ or - // /.xlings/subos/default/data/xpkgs/ - auto pkgDir = verdir.parent_path().filename().string(); - auto verName = verdir.filename().string(); - std::filesystem::path candidates[] = { - std::filesystem::path(xhome) / ".xlings" / "data" / "xpkgs" / pkgDir / verName, - std::filesystem::path(xhome) / ".xlings" / "subos" / "default" / "data" / "xpkgs" / pkgDir / verName, - }; - for (auto& src : candidates) { - std::error_code ec; - bool srcExists = std::filesystem::exists(src, ec) && std::filesystem::is_directory(src, ec); - mcpp::log::debug("fetcher", std::format( - "candidate '{}' exists={}", src.string(), srcExists)); - if (srcExists) { - std::filesystem::create_directories(verdir.parent_path(), ec); - std::filesystem::copy(src, verdir, - std::filesystem::copy_options::recursive - | std::filesystem::copy_options::overwrite_existing, ec); - mcpp::log::verbose("fetcher", std::format( - "copied from global xlings: ec={}", ec.message())); - if (!ec) break; - } - } - } - } else { - mcpp::log::debug("fetcher", "verdir exists in sandbox, no copy needed"); - } - if (!std::filesystem::exists(verdir)) { - return std::unexpected(CallError{ - std::format("xpkg payload missing: {}", verdir.string())}); - } + // ─── resolve_quick: check sandbox only, no fallbacks ────────────── + auto resolve_quick = [&]() -> std::optional { + if (!std::filesystem::exists(verdir)) return std::nullopt; XpkgPayload payload; - // For xim packages (gcc, cmake, ...) the version dir IS the root. - // For mcpplibs packages the version dir contains an extracted - // tarball subdirectory; we treat the wrapper subdir as the root - // when its content includes bin/ or include/. std::error_code ec; std::vector subs; for (auto& e : std::filesystem::directory_iterator(verdir, ec)) { if (e.is_directory()) subs.push_back(e.path()); } - // If verdir directly contains bin/ or include/ → it's the root. - // Otherwise prefer the unique subdirectory. bool verdir_is_root = std::filesystem::exists(verdir / "bin") || std::filesystem::exists(verdir / "include") || std::filesystem::exists(verdir / "lib"); @@ -689,32 +640,85 @@ Fetcher::resolve_xpkg_path(std::string_view target, return payload; }; - auto p = resolve(); - if (p) return *p; + // ─── copy_from_global: last-resort fallback ───────────────────── + // [fallback:xpkg.copy_from_global] + // xlings may install packages into its global data dir (~/.xlings/) + // instead of the mcpp sandbox. Copy them into sandbox as a fallback. + auto copy_from_global = [&]() { + if (std::filesystem::exists(verdir)) return; + mcpp::log::verbose("fetcher", + "[fallback:xpkg.copy_from_global] checking global xlings"); + const char* xhome = nullptr; +#if defined(_WIN32) + xhome = std::getenv("USERPROFILE"); +#endif + if (!xhome) xhome = std::getenv("HOME"); + if (!xhome) return; + auto pkgDir = verdir.parent_path().filename().string(); + auto verName = verdir.filename().string(); + std::filesystem::path candidates[] = { + std::filesystem::path(xhome) / ".xlings" / "data" / "xpkgs" / pkgDir / verName, + std::filesystem::path(xhome) / ".xlings" / "subos" / "default" / "data" / "xpkgs" / pkgDir / verName, + }; + for (auto& src : candidates) { + std::error_code ec; + if (std::filesystem::exists(src, ec) && std::filesystem::is_directory(src, ec)) { + std::filesystem::create_directories(verdir.parent_path(), ec); + std::filesystem::copy(src, verdir, + std::filesystem::copy_options::recursive + | std::filesystem::copy_options::overwrite_existing, ec); + if (!ec) { + mcpp::log::verbose("fetcher", std::format( + "copied from '{}'", src.string())); + return; + } + } + } + }; + + // ─── Resolution chain: sandbox → install → copy fallback ──────── + // + // Priority order: + // 1. Already in sandbox? → use it + // 2. autoInstall? → xlings install into sandbox + // 3. Still missing? → copy from ~/.xlings/ (fallback) + // 4. Still missing? → error + + // 1. Quick check: already in sandbox + if (auto p = resolve_quick()) { + mcpp::log::debug("fetcher", "found in sandbox"); + return *p; + } - if (!autoInstall) { - return std::unexpected(p.error()); + // 2. Install via xlings (primary path) + if (autoInstall) { + std::vector targets { + std::format("{}:{}@{}", parsed.indexName, parsed.packageName, parsed.version) + }; + mcpp::log::verbose("fetcher", std::format("xlings install: {}", targets[0])); + auto inst = install(targets, handler); + if (inst && inst->exitCode == 0) { + if (auto p = resolve_quick()) { + mcpp::log::debug("fetcher", "found in sandbox after install"); + return *p; + } + } + if (inst && inst->exitCode != 0) { + mcpp::log::warn("fetcher", std::format( + "xlings install exit={}, will try fallback", inst->exitCode)); + } } - // Trigger install via xlings. - std::vector targets { - std::format("{}:{}@{}", parsed.indexName, parsed.packageName, parsed.version) - }; - mcpp::log::verbose("fetcher", std::format("triggering xlings install: {}", targets[0])); - auto inst = install(targets, handler); - if (!inst) return std::unexpected(inst.error()); - mcpp::log::verbose("fetcher", std::format( - "xlings install exitCode={} verdir_exists={}", - inst->exitCode, std::filesystem::exists(verdir))); - if (inst->exitCode != 0) { - std::string err = std::format( - "xlings install of '{}:{}@{}' failed (exit {})", - parsed.indexName, parsed.packageName, parsed.version, inst->exitCode); - if (inst->error) err += ": " + inst->error->message; - return std::unexpected(CallError{err}); + // 3. Fallback: copy from global xlings + copy_from_global(); + if (auto p = resolve_quick()) { + mcpp::log::verbose("fetcher", "resolved via copy fallback"); + return *p; } - return resolve(); + // 4. Nothing worked + return std::unexpected(CallError{ + std::format("xpkg payload missing: {}", verdir.string())}); } // ─── Namespace-aware install_path (canonical, 0.0.10+) ──────────────