Skip to content

Commit 063c025

Browse files
committed
feat(workspace): discovery, build orchestration, -p flag, e2e test
- Add find_workspace_root() for upward workspace discovery - Add merge_workspace_deps() for .workspace = true resolution - Virtual workspace: auto-select binary member as build target - Workspace toolchain/target overrides inherit to members - -p/--package flag for selective member builds - Members inside a workspace auto-inherit workspace config - Add e2e test: 3-member workspace (core + greeter + hello)
1 parent d9d3686 commit 063c025

2 files changed

Lines changed: 245 additions & 2 deletions

File tree

src/cli.cppm

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,54 @@ std::optional<std::filesystem::path> find_manifest_root(std::filesystem::path st
113113
}
114114
}
115115

116+
// Find the workspace root by walking upward from a member directory.
117+
// Returns empty if no workspace root found.
118+
std::filesystem::path find_workspace_root(const std::filesystem::path& memberRoot) {
119+
auto p = memberRoot.parent_path();
120+
while (true) {
121+
if (std::filesystem::exists(p / "mcpp.toml")) {
122+
auto m = mcpp::manifest::load(p / "mcpp.toml");
123+
if (m && m->workspace.present) {
124+
// Verify memberRoot is in members list
125+
auto rel = std::filesystem::relative(memberRoot, p);
126+
for (auto& member : m->workspace.members) {
127+
if (rel == std::filesystem::path(member)) return p;
128+
}
129+
}
130+
}
131+
auto parent = p.parent_path();
132+
if (parent == p) break;
133+
p = parent;
134+
}
135+
return {};
136+
}
137+
138+
// Merge workspace.dependencies versions into a member's deps.
139+
void merge_workspace_deps(mcpp::manifest::Manifest& member,
140+
const mcpp::manifest::Manifest& workspace) {
141+
auto merge_map = [&](std::map<std::string, mcpp::manifest::DependencySpec>& deps) {
142+
for (auto& [name, spec] : deps) {
143+
if (!spec.inheritWorkspace) continue;
144+
// Try exact key match first
145+
auto it = workspace.workspace.dependencies.find(name);
146+
if (it != workspace.workspace.dependencies.end()) {
147+
spec.version = it->second.version;
148+
spec.inheritWorkspace = false;
149+
continue;
150+
}
151+
// Try short name for default-ns deps
152+
auto shortIt = workspace.workspace.dependencies.find(spec.shortName);
153+
if (shortIt != workspace.workspace.dependencies.end()) {
154+
spec.version = shortIt->second.version;
155+
spec.inheritWorkspace = false;
156+
}
157+
}
158+
};
159+
merge_map(member.dependencies);
160+
merge_map(member.devDependencies);
161+
merge_map(member.buildDependencies);
162+
}
163+
116164
std::filesystem::path target_dir(const mcpp::toolchain::Toolchain& tc,
117165
const mcpp::toolchain::Fingerprint& fp,
118166
const std::filesystem::path& root)
@@ -772,8 +820,9 @@ struct BuildContext {
772820
// Command-level overrides (--target / --static).
773821
// Empty defaults preserve pre-existing behaviour exactly.
774822
struct BuildOverrides {
775-
std::string target_triple; // empty = host triple, fall through to [toolchain]
776-
bool force_static = false; // --static (or implied by musl target)
823+
std::string target_triple; // empty = host triple, fall through to [toolchain]
824+
bool force_static = false; // --static (or implied by musl target)
825+
std::string package_filter; // -p <name>: only build this workspace member
777826
};
778827

779828
// `prepare_build` builds the BuildContext for any verb that compiles.
@@ -795,6 +844,94 @@ prepare_build(bool print_fingerprint,
795844
auto m = mcpp::manifest::load(*root / "mcpp.toml");
796845
if (!m) return std::unexpected(m.error().format());
797846

847+
// ─── Workspace handling ────────────────────────────────────────────
848+
// If the manifest has [workspace] and is a virtual workspace (no [package]),
849+
// or if -p filter is set, switch to the target member's manifest.
850+
std::optional<mcpp::manifest::Manifest> wsManifest; // keep workspace manifest alive
851+
if (m->workspace.present) {
852+
std::string targetMember;
853+
854+
if (!overrides.package_filter.empty()) {
855+
// -p <name>: find matching member by directory basename or path
856+
for (auto& mp : m->workspace.members) {
857+
auto basename = std::filesystem::path(mp).filename().string();
858+
if (basename == overrides.package_filter || mp == overrides.package_filter) {
859+
targetMember = mp;
860+
break;
861+
}
862+
}
863+
if (targetMember.empty()) {
864+
return std::unexpected(std::format(
865+
"workspace member '{}' not found in [workspace].members",
866+
overrides.package_filter));
867+
}
868+
} else if (m->package.name.empty()) {
869+
// Virtual workspace: find a member with a binary target, or use last member.
870+
for (auto& mp : m->workspace.members) {
871+
auto memberDir = *root / mp;
872+
auto mm = mcpp::manifest::load(memberDir / "mcpp.toml");
873+
if (!mm) continue;
874+
for (auto& t : mm->targets) {
875+
if (t.kind == mcpp::manifest::Target::Binary) {
876+
targetMember = mp;
877+
break;
878+
}
879+
}
880+
if (!targetMember.empty()) break;
881+
}
882+
if (targetMember.empty() && !m->workspace.members.empty()) {
883+
targetMember = m->workspace.members.back();
884+
}
885+
}
886+
// else: rooted workspace with [package] — build root normally.
887+
888+
if (!targetMember.empty()) {
889+
auto memberDir = *root / targetMember;
890+
if (!std::filesystem::exists(memberDir / "mcpp.toml")) {
891+
return std::unexpected(std::format(
892+
"workspace member '{}' has no mcpp.toml", targetMember));
893+
}
894+
wsManifest = std::move(*m); // preserve workspace manifest
895+
m = mcpp::manifest::load(memberDir / "mcpp.toml");
896+
if (!m) return std::unexpected(std::format(
897+
"workspace member '{}': {}", targetMember, m.error().format()));
898+
899+
// Merge workspace dependency versions
900+
merge_workspace_deps(*m, *wsManifest);
901+
902+
// Inherit workspace toolchain if member doesn't define one
903+
if (m->toolchain.byPlatform.empty()) {
904+
m->toolchain = wsManifest->toolchain;
905+
}
906+
// Inherit workspace target overrides
907+
for (auto& [triple, entry] : wsManifest->targetOverrides) {
908+
if (!m->targetOverrides.contains(triple)) {
909+
m->targetOverrides[triple] = entry;
910+
}
911+
}
912+
913+
mcpp::ui::status("Workspace", std::format("building member '{}'", targetMember));
914+
root = memberDir;
915+
}
916+
} else {
917+
// Not at workspace root — check if we're inside a workspace
918+
auto wsRoot = find_workspace_root(*root);
919+
if (!wsRoot.empty()) {
920+
auto wsm = mcpp::manifest::load(wsRoot / "mcpp.toml");
921+
if (wsm && wsm->workspace.present) {
922+
merge_workspace_deps(*m, *wsm);
923+
if (m->toolchain.byPlatform.empty()) {
924+
m->toolchain = wsm->toolchain;
925+
}
926+
for (auto& [triple, entry] : wsm->targetOverrides) {
927+
if (!m->targetOverrides.contains(triple)) {
928+
m->targetOverrides[triple] = entry;
929+
}
930+
}
931+
}
932+
}
933+
}
934+
798935
// Inject synthetic targets (e.g. test binaries from `mcpp test`).
799936
for (auto& t : extraTargets) m->targets.push_back(t);
800937

@@ -2053,6 +2190,7 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
20532190

20542191
BuildOverrides ov;
20552192
if (auto t = parsed.value("target")) ov.target_triple = *t;
2193+
if (auto p = parsed.value("package")) ov.package_filter = *p;
20562194
ov.force_static = parsed.is_flag_set("static");
20572195

20582196
// P0: try fast-path if inputs haven't changed.
@@ -3533,6 +3671,8 @@ int run(int argc, char** argv) {
35333671
"Build for <triple> (e.g. x86_64-linux-musl); looks up [target.<triple>] in mcpp.toml"))
35343672
.option(cl::Option("static").help(
35353673
"Force static linking (-static). On Linux, prefer pairing with --target <arch>-linux-musl"))
3674+
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
3675+
.help("Build only the named workspace member"))
35363676
.action(wrap_rc(cmd_build)))
35373677
.subcommand(cl::App("run")
35383678
.description("Build + run a binary target (after `--`, args are passed to it)")

tests/e2e/35_workspace.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Test: workspace with two library members and one binary member.
5+
# Verifies:
6+
# 1. `mcpp build` at workspace root builds all members
7+
# 2. Path deps between members work
8+
# 3. Virtual workspace (no [package]) works
9+
10+
TMP=$(mktemp -d)
11+
trap "rm -rf $TMP" EXIT
12+
cd "$TMP"
13+
14+
# ── Create workspace structure ──────────────────────────
15+
mkdir -p libs/core/src libs/greeter/src apps/hello/src
16+
17+
# Workspace root (virtual — no [package])
18+
cat > mcpp.toml << 'EOF'
19+
[workspace]
20+
members = ["libs/core", "libs/greeter", "apps/hello"]
21+
EOF
22+
23+
# libs/core — a simple library
24+
cat > libs/core/mcpp.toml << 'EOF'
25+
[package]
26+
namespace = "demo"
27+
name = "core"
28+
version = "0.1.0"
29+
30+
[targets.core]
31+
kind = "lib"
32+
EOF
33+
34+
cat > libs/core/src/core.cppm << 'EOF'
35+
export module demo.core;
36+
import std;
37+
38+
export namespace demo::core {
39+
inline std::string greet_target() { return "World"; }
40+
}
41+
EOF
42+
43+
# libs/greeter — depends on core via path
44+
cat > libs/greeter/mcpp.toml << 'EOF'
45+
[package]
46+
namespace = "demo"
47+
name = "greeter"
48+
version = "0.1.0"
49+
50+
[targets.greeter]
51+
kind = "lib"
52+
53+
[dependencies]
54+
core = { path = "../core" }
55+
EOF
56+
57+
cat > libs/greeter/src/greeter.cppm << 'EOF'
58+
export module demo.greeter;
59+
import std;
60+
import demo.core;
61+
62+
export namespace demo::greeter {
63+
inline std::string greet() {
64+
return "Hello, " + demo::core::greet_target() + "!";
65+
}
66+
}
67+
EOF
68+
69+
# apps/hello — binary that uses greeter
70+
cat > apps/hello/mcpp.toml << 'EOF'
71+
[package]
72+
namespace = "demo"
73+
name = "hello"
74+
version = "0.1.0"
75+
76+
[dependencies]
77+
greeter = { path = "../../libs/greeter" }
78+
EOF
79+
80+
cat > apps/hello/src/main.cpp << 'EOF'
81+
import std;
82+
import demo.greeter;
83+
84+
int main() {
85+
std::println("{}", demo::greeter::greet());
86+
return 0;
87+
}
88+
EOF
89+
90+
# ── Build from workspace root ───────────────────────────
91+
echo "=== Building from workspace root ==="
92+
"$MCPP" build
93+
echo "workspace build: ok"
94+
95+
# ── Verify the binary runs correctly ────────────────────
96+
BIN=$(find target -type f -name hello | head -1)
97+
test -n "$BIN" || { echo "FAIL: hello binary not found"; exit 1; }
98+
OUT=$("$BIN" 2>&1)
99+
echo "output: $OUT"
100+
test "$OUT" = "Hello, World!" || { echo "FAIL: unexpected output '$OUT'"; exit 1; }
101+
echo "workspace run: ok"
102+
103+
echo "ALL WORKSPACE TESTS PASSED"

0 commit comments

Comments
 (0)