Skip to content

Commit d9d3686

Browse files
committed
feat(workspace): parse [workspace] section and .workspace = true
- Add WorkspaceConfig struct to Manifest (members, exclude, dependencies) - Parse [workspace] section with members array and version declarations - Parse [workspace.dependencies] and [workspace.dependencies.<ns>] - Support .workspace = true in dependency specs for version inheritance - Make [package] optional when [workspace] is present (virtual workspace) - Add 3 unit tests for workspace parsing
1 parent a3012cc commit d9d3686

3 files changed

Lines changed: 130 additions & 10 deletions

File tree

src/manifest.cppm

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ struct PackConfig {
139139
std::vector<std::string> forceBundle; // libs to bundle even if PEP 600 says skip
140140
};
141141

142+
// `[workspace]` — multi-package workspace support (0.0.11+).
143+
//
144+
// A workspace root mcpp.toml declares member packages. Members share
145+
// a unified lock file, target directory, and can inherit dependency
146+
// versions via `.workspace = true`.
147+
//
148+
// Virtual workspace (no [package]): pure management node.
149+
// Rooted workspace ([package] + [workspace]): root is also a package.
150+
struct WorkspaceConfig {
151+
std::vector<std::string> members; // relative paths to member dirs
152+
std::vector<std::string> exclude; // paths to exclude
153+
std::map<std::string, DependencySpec> dependencies; // [workspace.dependencies]
154+
bool present = false;
155+
};
156+
142157
struct Manifest {
143158
std::filesystem::path sourcePath; // mcpp.toml's filesystem path
144159

@@ -156,9 +171,6 @@ struct Manifest {
156171
BuildConfig buildConfig;
157172

158173
// [target.<triple>] tables — empty if user didn't declare any.
159-
// Triple keys are accepted in either GCC form (x86_64-linux-musl)
160-
// or Rust form (x86_64-unknown-linux-musl); both are normalised by
161-
// stripping `-unknown-` on read.
162174
std::map<std::string, TargetEntry> targetOverrides;
163175

164176
// [pack] — `mcpp pack` config (see docs/35-pack-design.md).
@@ -167,6 +179,9 @@ struct Manifest {
167179
// [lib] — library root interface convention (M5.x+).
168180
LibConfig lib;
169181

182+
// [workspace] — multi-package workspace.
183+
WorkspaceConfig workspace;
184+
170185
// M5.0: post-parse computed/inferred state
171186
bool usesModules = true; // refined by scanner
172187
bool usesImportStd = true; // refined by scanner
@@ -267,22 +282,26 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
267282
Manifest m;
268283
m.sourcePath = origin;
269284

270-
// [package]
285+
// [package] — required unless [workspace] is present (virtual workspace).
271286
auto* pkg_t = doc->get_table("package");
272-
if (!pkg_t) return std::unexpected(error(origin, "missing required [package] section"));
287+
bool has_workspace = (doc->get_table("workspace") != nullptr);
288+
if (!pkg_t && !has_workspace)
289+
return std::unexpected(error(origin, "missing required [package] section"));
273290

274291
auto name = doc->get_string("package.name");
275-
if (!name) return std::unexpected(error(origin, "missing required field 'package.name'"));
276-
m.package.name = *name;
292+
if (!name && !has_workspace)
293+
return std::unexpected(error(origin, "missing required field 'package.name'"));
294+
if (name) m.package.name = *name;
277295

278296
// 0.0.6+: explicit namespace field (xpkg V1 style).
279297
// If present, [package].name is the short name.
280298
// If absent, compat.cppm::resolve_package_name infers from dotted name.
281299
if (auto v = doc->get_string("package.namespace")) m.package.namespace_ = *v;
282300

283301
auto version = doc->get_string("package.version");
284-
if (!version) return std::unexpected(error(origin, "missing required field 'package.version'"));
285-
m.package.version = *version;
302+
if (!version && !has_workspace)
303+
return std::unexpected(error(origin, "missing required field 'package.version'"));
304+
if (version) m.package.version = *version;
286305

287306
if (auto v = doc->get_string("package.description")) m.package.description = *v;
288307
if (auto v = doc->get_string("package.license")) m.package.license = *v;
@@ -395,7 +414,7 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
395414
auto is_dep_spec_key = [](std::string_view k) {
396415
return k == "path" || k == "version" || k == "git"
397416
|| k == "rev" || k == "tag" || k == "branch"
398-
|| k == "features";
417+
|| k == "features" || k == "workspace";
399418
};
400419
auto looks_like_inline_dep_spec = [&](const t::Table& sub) {
401420
if (sub.empty()) return false;
@@ -423,6 +442,10 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
423442
spec.gitRev = it->second.as_string();
424443
spec.gitRefKind = "branch";
425444
}
445+
if (auto it = sub.find("workspace"); it != sub.end() && it->second.is_bool() && it->second.as_bool()) {
446+
spec.inheritWorkspace = true;
447+
return {}; // version will be filled in by workspace merge
448+
}
426449
if (spec.path.empty() && spec.version.empty() && spec.git.empty()) {
427450
return std::unexpected(error(origin, std::format(
428451
"[{}.\"{}\"] must specify 'path', 'version', or 'git'", section, fqName)));
@@ -597,6 +620,46 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
597620
}
598621
}
599622

623+
// [workspace] — multi-package workspace support (0.0.11+).
624+
if (doc->get_table("workspace")) {
625+
m.workspace.present = true;
626+
if (auto v = doc->get_string_array("workspace.members"))
627+
m.workspace.members = *v;
628+
if (auto v = doc->get_string_array("workspace.exclude"))
629+
m.workspace.exclude = *v;
630+
631+
// [workspace.dependencies] — versions that members inherit via .workspace = true.
632+
if (auto* wdeps = doc->get_table("workspace.dependencies")) {
633+
for (auto& [k, v] : *wdeps) {
634+
if (v.is_string()) {
635+
DependencySpec spec;
636+
spec.version = v.as_string();
637+
if (k.find('.') != std::string::npos) {
638+
auto pos = k.find('.');
639+
spec.namespace_ = k.substr(0, pos);
640+
spec.shortName = k.substr(pos + 1);
641+
} else {
642+
spec.namespace_ = std::string{kDefaultNamespace};
643+
spec.shortName = k;
644+
}
645+
m.workspace.dependencies[k] = std::move(spec);
646+
continue;
647+
}
648+
if (!v.is_table()) continue;
649+
// Namespaced subtable: [workspace.dependencies.<ns>]
650+
const std::string ns = k;
651+
for (auto& [sk, sv] : v.as_table()) {
652+
if (!sv.is_string()) continue;
653+
DependencySpec spec;
654+
spec.namespace_ = ns;
655+
spec.shortName = sk;
656+
spec.version = sv.as_string();
657+
m.workspace.dependencies[std::format("{}.{}", ns, sk)] = std::move(spec);
658+
}
659+
}
660+
}
661+
}
662+
600663
return m;
601664
}
602665

src/pm/dep_spec.cppm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ struct DependencySpec {
3232
std::string gitRev; // commit / tag / branch (any one)
3333
std::string gitRefKind; // "rev" / "tag" / "branch" (for clarity)
3434

35+
bool inheritWorkspace = false; // .workspace = true
36+
3537
bool isPath() const { return !path.empty(); }
3638
bool isGit() const { return !git.empty(); }
3739
bool isVersion() const { return !isPath() && !isGit() && !version.empty(); }

tests/unit/test_manifest.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,61 @@ package = {
330330
EXPECT_EQ(b.version, "0.0.2");
331331
}
332332

333+
TEST(Manifest, WorkspaceSectionParsed) {
334+
constexpr auto src = R"(
335+
[workspace]
336+
members = ["libs/core", "libs/http", "apps/server"]
337+
exclude = ["libs/experimental"]
338+
339+
[workspace.dependencies]
340+
cmdline = "0.0.2"
341+
342+
[workspace.dependencies.compat]
343+
gtest = "1.15.2"
344+
mbedtls = "3.6.1"
345+
)";
346+
auto m = mcpp::manifest::parse_string(src);
347+
ASSERT_TRUE(m.has_value()) << m.error().format();
348+
EXPECT_TRUE(m->workspace.present);
349+
ASSERT_EQ(m->workspace.members.size(), 3u);
350+
EXPECT_EQ(m->workspace.members[0], "libs/core");
351+
EXPECT_EQ(m->workspace.members[1], "libs/http");
352+
EXPECT_EQ(m->workspace.members[2], "apps/server");
353+
ASSERT_EQ(m->workspace.exclude.size(), 1u);
354+
EXPECT_EQ(m->workspace.exclude[0], "libs/experimental");
355+
ASSERT_EQ(m->workspace.dependencies.size(), 3u);
356+
auto& gt = m->workspace.dependencies.at("compat.gtest");
357+
EXPECT_EQ(gt.version, "1.15.2");
358+
EXPECT_EQ(gt.namespace_, "compat");
359+
}
360+
361+
TEST(Manifest, WorkspaceTrueInDependency) {
362+
constexpr auto src = R"(
363+
[package]
364+
name = "x"
365+
version = "0.1.0"
366+
[dependencies.compat]
367+
mbedtls = { workspace = true }
368+
)";
369+
auto m = mcpp::manifest::parse_string(src);
370+
ASSERT_TRUE(m.has_value()) << m.error().format();
371+
auto& s = m->dependencies.at("compat.mbedtls");
372+
EXPECT_TRUE(s.inheritWorkspace);
373+
EXPECT_EQ(s.namespace_, "compat");
374+
EXPECT_EQ(s.shortName, "mbedtls");
375+
}
376+
377+
TEST(Manifest, NoWorkspaceSectionMeansNotPresent) {
378+
constexpr auto src = R"(
379+
[package]
380+
name = "x"
381+
version = "0.1.0"
382+
)";
383+
auto m = mcpp::manifest::parse_string(src);
384+
ASSERT_TRUE(m.has_value());
385+
EXPECT_FALSE(m->workspace.present);
386+
}
387+
333388
TEST(Manifest, LibRootInferredFromPackageName) {
334389
constexpr auto src = R"(
335390
[package]

0 commit comments

Comments
 (0)