Skip to content

Commit 58b61b9

Browse files
committed
refactor(pm): extract resolver into pm/resolver.cppm (PR-R4)
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.
1 parent 8b53718 commit 58b61b9

2 files changed

Lines changed: 109 additions & 77 deletions

File tree

src/cli.cppm

Lines changed: 7 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import mcpp.publish.xpkg_emit;
3030
import mcpp.pack;
3131
import mcpp.config;
3232
import mcpp.fetcher;
33+
import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm
3334
import mcpp.ui;
3435
import mcpp.bmi_cache;
3536
import mcpp.dyndep;
@@ -665,81 +666,10 @@ void fixup_gcc_specs(const std::filesystem::path& gccPkgRoot,
665666
// it starts with one of `^~><=` or contains a comma (multi-part), or is `*`
666667
// or empty. Bare `1.2.3` is treated as exact for back-compat with pre-SemVer
667668
// pinning workflows; users opt into resolution by writing `^1.2.3` etc.
668-
bool is_version_constraint(std::string_view v) {
669-
if (v.empty()) return true;
670-
if (v == "*") return true;
671-
char c = v.front();
672-
if (c == '^' || c == '~' || c == '>' || c == '<' || c == '=') return true;
673-
if (v.find(',') != std::string_view::npos) return true;
674-
return false;
675-
}
676-
677-
// xpkg.lua's xpm.<key> uses these names. (Distinct from kCurrentPlatform
678-
// which is the [toolchain] table key — "macos" vs "macosx".)
679-
constexpr std::string_view kXpkgPlatform =
680-
#if defined(__linux__)
681-
"linux";
682-
#elif defined(__APPLE__)
683-
"macosx";
684-
#elif defined(_WIN32)
685-
"windows";
686-
#else
687-
"linux";
688-
#endif
689-
690-
// Resolve a SemVer constraint against the index entry's available versions.
691-
// Returns the chosen exact version string, or an error message.
692-
std::expected<std::string, std::string>
693-
resolve_semver(std::string_view name,
694-
std::string_view constraint,
695-
mcpp::fetcher::Fetcher& fetcher)
696-
{
697-
namespace vr = mcpp::version_req;
698-
699-
auto luaContent = fetcher.read_xpkg_lua(name);
700-
if (!luaContent) {
701-
return std::unexpected(std::format(
702-
"dependency '{}' has SemVer constraint '{}' but the index entry "
703-
"isn't cloned locally yet — run `mcpp index update` first",
704-
name, constraint));
705-
}
706-
707-
auto req = vr::parse_req(constraint);
708-
if (!req) {
709-
return std::unexpected(std::format(
710-
"dependency '{}': invalid version constraint '{}': {}",
711-
name, constraint, req.error()));
712-
}
713-
714-
auto rawVersions = mcpp::manifest::list_xpkg_versions(*luaContent, kXpkgPlatform);
715-
if (rawVersions.empty()) {
716-
return std::unexpected(std::format(
717-
"dependency '{}': index entry has no versions for platform '{}'",
718-
name, kXpkgPlatform));
719-
}
720-
721-
std::vector<vr::Version> parsed;
722-
parsed.reserve(rawVersions.size());
723-
for (auto& s : rawVersions) {
724-
auto v = vr::parse_version(s);
725-
if (!v) continue; // ignore unparseable entries
726-
parsed.push_back(*v);
727-
}
728-
if (parsed.empty()) {
729-
return std::unexpected(std::format(
730-
"dependency '{}': no valid versions in index", name));
731-
}
732-
733-
auto idx = vr::choose(*req, parsed);
734-
if (!idx) {
735-
std::string avail;
736-
for (auto& s : rawVersions) { if (!avail.empty()) avail += ", "; avail += s; }
737-
return std::unexpected(std::format(
738-
"dependency '{}': constraint '{}' matches none of: [{}]",
739-
name, constraint, avail));
740-
}
741-
return parsed[*idx].str();
742-
}
669+
// `is_version_constraint`, `kXpkgPlatform` and `resolve_semver` have moved
670+
// to `mcpp.pm.resolver` (PR-R4 — see
671+
// `.agents/docs/2026-05-08-pm-subsystem-architecture.md`). Call sites
672+
// below reference the `mcpp::pm::` qualified names directly.
743673

744674
// --- Commands ---
745675

@@ -1121,11 +1051,11 @@ prepare_build(bool print_fingerprint,
11211051
-> std::expected<void, std::string>
11221052
{
11231053
if (s.isPath() || s.isGit()) return {};
1124-
if (!is_version_constraint(s.version)) return {};
1054+
if (!mcpp::pm::is_version_constraint(s.version)) return {};
11251055
auto cfg = get_cfg();
11261056
if (!cfg) return std::unexpected(cfg.error());
11271057
mcpp::fetcher::Fetcher fetcher(**cfg);
1128-
auto resolved = resolve_semver(depName, s.version, fetcher);
1058+
auto resolved = mcpp::pm::resolve_semver(depName, s.version, fetcher);
11291059
if (!resolved) return std::unexpected(resolved.error());
11301060
mcpp::ui::info("Resolved",
11311061
std::format("{} {} → v{}", depName, s.version, *resolved));

src/pm/resolver.cppm

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// mcpp.pm.resolver — turn a SemVer constraint into a concrete version,
2+
// using the package's xpkg lua descriptor as the version inventory.
3+
//
4+
// Part of the package-management subsystem refactor (PR-R4 in
5+
// `.agents/docs/2026-05-08-pm-subsystem-architecture.md`). Strictly
6+
// pulled out of `cli.cppm` with no behavior change; the same
7+
// signatures, the same error strings, the same platform key picking.
8+
9+
export module mcpp.pm.resolver;
10+
11+
import std;
12+
import mcpp.manifest;
13+
import mcpp.pm.package_fetcher;
14+
import mcpp.version_req;
15+
16+
export namespace mcpp::pm {
17+
18+
// xpkg.lua's `xpm.<key>` uses these names. (Distinct from
19+
// `kCurrentPlatform` in cli.cppm, which is the [toolchain] table key —
20+
// "macos" vs "macosx".)
21+
inline constexpr std::string_view kXpkgPlatform =
22+
#if defined(__linux__)
23+
"linux";
24+
#elif defined(__APPLE__)
25+
"macosx";
26+
#elif defined(_WIN32)
27+
"windows";
28+
#else
29+
"linux";
30+
#endif
31+
32+
// Returns true if `v` is a SemVer constraint (caret, tilde, range, glob,
33+
// `*`, or empty) rather than a literal exact version. Empty counts as
34+
// "constraint" so callers re-resolve via the index — bare `1.2.3` is
35+
// treated as exact for back-compat with pre-SemVer pinning workflows;
36+
// users opt into resolution by writing `^1.2.3` etc.
37+
inline bool is_version_constraint(std::string_view v) {
38+
if (v.empty()) return true;
39+
if (v == "*") return true;
40+
char c = v.front();
41+
if (c == '^' || c == '~' || c == '>' || c == '<' || c == '=') return true;
42+
if (v.find(',') != std::string_view::npos) return true;
43+
return false;
44+
}
45+
46+
// Resolve a SemVer constraint against the index entry's available
47+
// versions. Returns the chosen exact version string, or an error
48+
// message. The fetcher is used to read the lua descriptor for the
49+
// requested package name.
50+
inline std::expected<std::string, std::string>
51+
resolve_semver(std::string_view name,
52+
std::string_view constraint,
53+
mcpp::pm::Fetcher& fetcher)
54+
{
55+
namespace vr = mcpp::version_req;
56+
57+
auto luaContent = fetcher.read_xpkg_lua(name);
58+
if (!luaContent) {
59+
return std::unexpected(std::format(
60+
"dependency '{}' has SemVer constraint '{}' but the index entry "
61+
"isn't cloned locally yet — run `mcpp index update` first",
62+
name, constraint));
63+
}
64+
65+
auto req = vr::parse_req(constraint);
66+
if (!req) {
67+
return std::unexpected(std::format(
68+
"dependency '{}': invalid version constraint '{}': {}",
69+
name, constraint, req.error()));
70+
}
71+
72+
auto rawVersions = mcpp::manifest::list_xpkg_versions(*luaContent, kXpkgPlatform);
73+
if (rawVersions.empty()) {
74+
return std::unexpected(std::format(
75+
"dependency '{}': index entry has no versions for platform '{}'",
76+
name, kXpkgPlatform));
77+
}
78+
79+
std::vector<vr::Version> parsed;
80+
parsed.reserve(rawVersions.size());
81+
for (auto& s : rawVersions) {
82+
auto v = vr::parse_version(s);
83+
if (!v) continue; // ignore unparseable entries
84+
parsed.push_back(*v);
85+
}
86+
if (parsed.empty()) {
87+
return std::unexpected(std::format(
88+
"dependency '{}': no valid versions in index", name));
89+
}
90+
91+
auto idx = vr::choose(*req, parsed);
92+
if (!idx) {
93+
std::string avail;
94+
for (auto& s : rawVersions) { if (!avail.empty()) avail += ", "; avail += s; }
95+
return std::unexpected(std::format(
96+
"dependency '{}': constraint '{}' matches none of: [{}]",
97+
name, constraint, avail));
98+
}
99+
return parsed[*idx].str();
100+
}
101+
102+
} // namespace mcpp::pm

0 commit comments

Comments
 (0)