@@ -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+
116164std::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.
774822struct 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
@@ -1073,6 +1210,7 @@ prepare_build(bool print_fingerprint,
10731210 std::string requestedBy; // who asked for it
10741211 std::string originalConstraint; // spec.version BEFORE pinning (for SemVer merge)
10751212 std::size_t consumerDepIndex; // dep_manifests slot of who pushed this child; kMainConsumer for main
1213+ std::filesystem::path resolveRoot; // base dir for relative path deps (empty = use project root)
10761214 };
10771215 std::deque<WorkItem> worklist;
10781216
@@ -1317,12 +1455,12 @@ prepare_build(bool print_fingerprint,
13171455 // caller wants them; they're never propagated transitively.
13181456 const std::string mainPkgLabel = m->package .name ;
13191457 for (auto & [n, s] : m->dependencies ) {
1320- worklist.push_back ({n, s, mainPkgLabel, s.version , kMainConsumer });
1458+ worklist.push_back ({n, s, mainPkgLabel, s.version , kMainConsumer , {} });
13211459 }
13221460 if (includeDevDeps) {
13231461 for (auto & [n, s] : m->devDependencies ) {
13241462 worklist.push_back ({n, s, mainPkgLabel + " (dev-dep)" ,
1325- s.version , kMainConsumer });
1463+ s.version , kMainConsumer , {} });
13261464 }
13271465 }
13281466
@@ -1529,14 +1667,15 @@ prepare_build(bool print_fingerprint,
15291667 {
15301668 const std::string& expectedShort =
15311669 spec.shortName .empty () ? name : spec.shortName ;
1532- std::string expectedComposite;
1533- if (!spec. namespace_ . empty ()
1534- && spec. namespace_ != mcpp::manifest:: kDefaultNamespace ) {
1535- expectedComposite = std::format ( " {}.{} " ,
1536- spec. namespace_ , expectedShort);
1537- }
1670+ // Also accept the fully-qualified form (ns.short) since
1671+ // synthesize_from_xpkg_lua may set package.name to the
1672+ // composite name for backward compat.
1673+ auto expectedComposite = spec. namespace_ . empty ()
1674+ ? std::string{}
1675+ : std::format ( " {}.{} " , spec. namespace_ , expectedShort);
15381676 const bool nameOk =
15391677 newManifest.package .name == expectedShort
1678+ || newManifest.package .name == name
15401679 || (!expectedComposite.empty ()
15411680 && newManifest.package .name == expectedComposite);
15421681 if (!nameOk) {
@@ -1571,7 +1710,7 @@ prepare_build(bool print_fingerprint,
15711710 dep_manifests[it->second .depIndex ]->dependencies ) {
15721711 worklist.push_back ({child_name, child_spec, newLabel,
15731712 child_spec.version ,
1574- it->second .depIndex });
1713+ it->second .depIndex , {} });
15751714 }
15761715 continue ;
15771716 }
@@ -1583,9 +1722,12 @@ prepare_build(bool print_fingerprint,
15831722 std::filesystem::path dep_root;
15841723
15851724 if (spec.isPath ()) {
1586- // Path-based: resolve relative to project root.
1725+ // Path-based: resolve relative to the consumer's root dir.
1726+ // For top-level deps this is the project root; for transitive
1727+ // deps it's the parent dep's directory (stored in resolveRoot).
15871728 dep_root = spec.path ;
1588- if (dep_root.is_relative ()) dep_root = *root / dep_root;
1729+ auto base = item.resolveRoot .empty () ? *root : item.resolveRoot ;
1730+ if (dep_root.is_relative ()) dep_root = base / dep_root;
15891731 dep_root = std::filesystem::weakly_canonical (dep_root);
15901732 } else if (spec.isGit ()) {
15911733 // Git-based (M4 #5): clone into ~/.mcpp/git/<hash>/<rev>/
@@ -1720,7 +1862,7 @@ prepare_build(bool print_fingerprint,
17201862 const std::size_t selfIdx = dep_manifests.size () - 1 ;
17211863 for (auto & [child_name, child_spec] : dep_manifests.back ()->dependencies ) {
17221864 worklist.push_back ({child_name, child_spec, thisDepLabel,
1723- child_spec.version , selfIdx});
1865+ child_spec.version , selfIdx, dep_root });
17241866 }
17251867 }
17261868
@@ -2053,6 +2195,7 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
20532195
20542196 BuildOverrides ov;
20552197 if (auto t = parsed.value (" target" )) ov.target_triple = *t;
2198+ if (auto p = parsed.value (" package" )) ov.package_filter = *p;
20562199 ov.force_static = parsed.is_flag_set (" static" );
20572200
20582201 // P0: try fast-path if inputs haven't changed.
@@ -3533,6 +3676,8 @@ int run(int argc, char** argv) {
35333676 " Build for <triple> (e.g. x86_64-linux-musl); looks up [target.<triple>] in mcpp.toml" ))
35343677 .option (cl::Option (" static" ).help (
35353678 " Force static linking (-static). On Linux, prefer pairing with --target <arch>-linux-musl" ))
3679+ .option (cl::Option (" package" ).short_name (' p' ).takes_value ().value_name (" NAME" )
3680+ .help (" Build only the named workspace member" ))
35363681 .action (wrap_rc (cmd_build)))
35373682 .subcommand (cl::App (" run" )
35383683 .description (" Build + run a binary target (after `--`, args are passed to it)" )
0 commit comments