// mcpp.project project/workspace location + workspace-dependency merging.
//
// Shared by the CLI layer and the pm subsystem (which previously kept a
// private copy of find_manifest_root to avoid importing mcpp.cli).
// Bodies moved verbatim from the CLI layer. Zero behavior change.
module;
#include
#include
export module mcpp.project;
import std;
import mcpp.manifest;
namespace mcpp::project {
// Locate mcpp.toml by walking upward from cwd.
export 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.
export 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 into a member's deps (`x.workspace = true`).
//
// #224: this used to propagate only `version`, so a workspace-level
// `[workspace.dependencies] x = { path = "..." }` inherited by a member was
// silently treated as a version/index dep (empty version) and failed to
// resolve. Now the location fields (path/git/*) travel too a dep spec is
// one of {version, path, git} so copying whichever the workspace declared
// is correct.
//
// `wsRoot` anchors a relative `path`: the workspace author wrote it
// relative to the WORKSPACE ROOT (where `[workspace.dependencies]` lives),
// not the inheriting member's own directory, so it is resolved to an
// absolute path here downstream path-dep resolution (relative to the
// member root) then sees an already-absolute path and leaves it alone.
export void merge_workspace_deps(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot = {}) {
auto copy_from = [&](mcpp::manifest::DependencySpec& spec,
const mcpp::manifest::DependencySpec& wsSpec) {
spec.version = wsSpec.version;
spec.path = wsSpec.path;
spec.git = wsSpec.git;
spec.gitRev = wsSpec.gitRev;
spec.gitRefKind = wsSpec.gitRefKind;
if (!spec.path.empty() && !wsRoot.empty()) {
std::filesystem::path p(spec.path);
if (p.is_relative()) {
spec.path = std::filesystem::weakly_canonical(wsRoot / p).string();
}
}
spec.inheritWorkspace = false;
};
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()) {
copy_from(spec, it->second);
continue;
}
// Try short name for default-ns deps
auto shortIt = workspace.workspace.dependencies.find(spec.shortName);
if (shortIt != workspace.workspace.dependencies.end()) {
copy_from(spec, shortIt->second);
}
}
};
merge_map(member.dependencies);
merge_map(member.devDependencies);
merge_map(member.buildDependencies);
}
// Inherit the workspace root's `[indices]` when the member declares none.
// A relative `[indices].path` was written at the WORKSPACE ROOT, so it must
// resolve against `wsRoot` and not the member directory otherwise every
// member needs its own `../`-prefixed copy of the same declaration (#224).
//
// Shared so every reader of `[indices]` sees the same effective map: the build
// path resolves dependencies through it, and `mcpp add` decides whether a
// package exists through it. Two copies of this rule is how the two ended up
// disagreeing about which packages are real.
export void inherit_workspace_indices(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot) {
if (!member.indices.empty() || workspace.indices.empty()) return;
member.indices = workspace.indices;
for (auto& [_, idx] : member.indices) {
if (idx.is_local() && idx.path.is_relative()) {
// This is an ownership/anchoring operation, not a request to
// resolve filesystem aliases. weakly_canonical can rewrite a
// Windows short/case-preserving workspace path into a different
// spelling before the inherited index is opened. Keep the path
// rooted exactly where the workspace manifest declared it; the
// normal reader remains responsible for existence/readability.
idx.path = (wsRoot / idx.path).lexically_normal();
}
}
}
// Resolve which member directory a workspace command acts on, for the
// single-member case. Shares the match rule (basename OR member path) with
// prepare_build's member switch, so `build -p X` and `test -p X` agree.
// Returns:
// - the member dir when `package_filter` names a member,
// - empty path when no switch applies (not a workspace, or a rooted
// workspace with no filter act on the root package),
// - error when the filter names an unknown member, or a *virtual*
// workspace is addressed with no filter (the caller must
// pick a member with -p or fan out with --workspace).
export std::expected
resolve_member_dir(const mcpp::manifest::Manifest& rootManifest,
const std::filesystem::path& rootDir,
std::string_view package_filter) {
if (!rootManifest.workspace.present) return std::filesystem::path{};
if (!package_filter.empty()) {
for (auto& mp : rootManifest.workspace.members) {
auto basename = std::filesystem::path(mp).filename().string();
if (basename == package_filter || mp == package_filter)
return rootDir / mp;
}
return std::unexpected(std::format(
"workspace member '{}' not found in [workspace].members", package_filter));
}
if (rootManifest.package.name.empty()) {
return std::unexpected(std::string(
"virtual workspace: specify -p or --workspace"));
}
return std::filesystem::path{}; // rooted workspace, no filter root package
}
} // namespace mcpp::project