From 58b61b9e3e74dfd4467fb1095d1bf90b22490d5e Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 8 May 2026 23:34:30 +0800 Subject: [PATCH] refactor(pm): extract resolver into pm/resolver.cppm (PR-R4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step four of the package-management subsystem refactor described in `.agents/docs/2026-05-08-pm-subsystem-architecture.md`. Strictly zero behavior change. * New module `mcpp.pm.resolver` (`src/pm/resolver.cppm`) owns `resolve_semver`, `is_version_constraint`, and the `kXpkgPlatform` constant — previously sitting in `cli.cppm`'s anonymous namespace. Same signatures, same error strings, same platform key picking. * `cli.cppm` drops the originals (~80 lines) and qualifies the two call sites with `mcpp::pm::`. Verification: * `mcpp build` compiles unchanged. * `mcpp test` — 9/9 unit binaries pass. * e2e subset (02 / 09 / 12 / 23 / 27) all pass. --- src/cli.cppm | 84 +++-------------------------------- src/pm/resolver.cppm | 102 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 77 deletions(-) create mode 100644 src/pm/resolver.cppm diff --git a/src/cli.cppm b/src/cli.cppm index 2da8d50..99692e3 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -30,6 +30,7 @@ import mcpp.publish.xpkg_emit; import mcpp.pack; import mcpp.config; import mcpp.fetcher; +import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm import mcpp.ui; import mcpp.bmi_cache; import mcpp.dyndep; @@ -665,81 +666,10 @@ void fixup_gcc_specs(const std::filesystem::path& gccPkgRoot, // 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. -bool is_version_constraint(std::string_view v) { - if (v.empty()) return true; - if (v == "*") return true; - char c = v.front(); - if (c == '^' || c == '~' || c == '>' || c == '<' || c == '=') return true; - if (v.find(',') != std::string_view::npos) return true; - return false; -} - -// xpkg.lua's xpm. uses these names. (Distinct from kCurrentPlatform -// which is the [toolchain] table key — "macos" vs "macosx".) -constexpr std::string_view kXpkgPlatform = -#if defined(__linux__) - "linux"; -#elif defined(__APPLE__) - "macosx"; -#elif defined(_WIN32) - "windows"; -#else - "linux"; -#endif - -// Resolve a SemVer constraint against the index entry's available versions. -// Returns the chosen exact version string, or an error message. -std::expected -resolve_semver(std::string_view name, - std::string_view constraint, - mcpp::fetcher::Fetcher& fetcher) -{ - namespace vr = mcpp::version_req; - - auto luaContent = fetcher.read_xpkg_lua(name); - if (!luaContent) { - return std::unexpected(std::format( - "dependency '{}' has SemVer constraint '{}' but the index entry " - "isn't cloned locally yet — run `mcpp index update` first", - name, constraint)); - } - - auto req = vr::parse_req(constraint); - if (!req) { - return std::unexpected(std::format( - "dependency '{}': invalid version constraint '{}': {}", - name, constraint, req.error())); - } - - auto rawVersions = mcpp::manifest::list_xpkg_versions(*luaContent, kXpkgPlatform); - if (rawVersions.empty()) { - return std::unexpected(std::format( - "dependency '{}': index entry has no versions for platform '{}'", - name, kXpkgPlatform)); - } - - std::vector parsed; - parsed.reserve(rawVersions.size()); - for (auto& s : rawVersions) { - auto v = vr::parse_version(s); - if (!v) continue; // ignore unparseable entries - parsed.push_back(*v); - } - if (parsed.empty()) { - return std::unexpected(std::format( - "dependency '{}': no valid versions in index", name)); - } - - auto idx = vr::choose(*req, parsed); - if (!idx) { - std::string avail; - for (auto& s : rawVersions) { if (!avail.empty()) avail += ", "; avail += s; } - return std::unexpected(std::format( - "dependency '{}': constraint '{}' matches none of: [{}]", - name, constraint, avail)); - } - return parsed[*idx].str(); -} +// `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 --- @@ -1121,11 +1051,11 @@ prepare_build(bool print_fingerprint, -> std::expected { if (s.isPath() || s.isGit()) return {}; - if (!is_version_constraint(s.version)) return {}; + if (!mcpp::pm::is_version_constraint(s.version)) return {}; auto cfg = get_cfg(); if (!cfg) return std::unexpected(cfg.error()); mcpp::fetcher::Fetcher fetcher(**cfg); - auto resolved = resolve_semver(depName, s.version, fetcher); + auto resolved = mcpp::pm::resolve_semver(depName, s.version, fetcher); if (!resolved) return std::unexpected(resolved.error()); mcpp::ui::info("Resolved", std::format("{} {} → v{}", depName, s.version, *resolved)); diff --git a/src/pm/resolver.cppm b/src/pm/resolver.cppm new file mode 100644 index 0000000..3603f80 --- /dev/null +++ b/src/pm/resolver.cppm @@ -0,0 +1,102 @@ +// mcpp.pm.resolver — turn a SemVer constraint into a concrete version, +// using the package's xpkg lua descriptor as the version inventory. +// +// Part of the package-management subsystem refactor (PR-R4 in +// `.agents/docs/2026-05-08-pm-subsystem-architecture.md`). Strictly +// pulled out of `cli.cppm` with no behavior change; the same +// signatures, the same error strings, the same platform key picking. + +export module mcpp.pm.resolver; + +import std; +import mcpp.manifest; +import mcpp.pm.package_fetcher; +import mcpp.version_req; + +export namespace mcpp::pm { + +// xpkg.lua's `xpm.` uses these names. (Distinct from +// `kCurrentPlatform` in cli.cppm, which is the [toolchain] table key — +// "macos" vs "macosx".) +inline constexpr std::string_view kXpkgPlatform = +#if defined(__linux__) + "linux"; +#elif defined(__APPLE__) + "macosx"; +#elif defined(_WIN32) + "windows"; +#else + "linux"; +#endif + +// Returns true if `v` is a SemVer constraint (caret, tilde, range, glob, +// `*`, or empty) rather than a literal exact version. Empty counts as +// "constraint" so callers re-resolve via the index — 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. +inline bool is_version_constraint(std::string_view v) { + if (v.empty()) return true; + if (v == "*") return true; + char c = v.front(); + if (c == '^' || c == '~' || c == '>' || c == '<' || c == '=') return true; + if (v.find(',') != std::string_view::npos) return true; + return false; +} + +// Resolve a SemVer constraint against the index entry's available +// versions. Returns the chosen exact version string, or an error +// message. The fetcher is used to read the lua descriptor for the +// requested package name. +inline std::expected +resolve_semver(std::string_view name, + std::string_view constraint, + mcpp::pm::Fetcher& fetcher) +{ + namespace vr = mcpp::version_req; + + auto luaContent = fetcher.read_xpkg_lua(name); + if (!luaContent) { + return std::unexpected(std::format( + "dependency '{}' has SemVer constraint '{}' but the index entry " + "isn't cloned locally yet — run `mcpp index update` first", + name, constraint)); + } + + auto req = vr::parse_req(constraint); + if (!req) { + return std::unexpected(std::format( + "dependency '{}': invalid version constraint '{}': {}", + name, constraint, req.error())); + } + + auto rawVersions = mcpp::manifest::list_xpkg_versions(*luaContent, kXpkgPlatform); + if (rawVersions.empty()) { + return std::unexpected(std::format( + "dependency '{}': index entry has no versions for platform '{}'", + name, kXpkgPlatform)); + } + + std::vector parsed; + parsed.reserve(rawVersions.size()); + for (auto& s : rawVersions) { + auto v = vr::parse_version(s); + if (!v) continue; // ignore unparseable entries + parsed.push_back(*v); + } + if (parsed.empty()) { + return std::unexpected(std::format( + "dependency '{}': no valid versions in index", name)); + } + + auto idx = vr::choose(*req, parsed); + if (!idx) { + std::string avail; + for (auto& s : rawVersions) { if (!avail.empty()) avail += ", "; avail += s; } + return std::unexpected(std::format( + "dependency '{}': constraint '{}' matches none of: [{}]", + name, constraint, avail)); + } + return parsed[*idx].str(); +} + +} // namespace mcpp::pm