Skip to content

Commit 6ef09b5

Browse files
committed
Merge branch 'feat/eight-t1' into feat/eight-reports
2 parents 18d2e93 + 2a1c744 commit 6ef09b5

9 files changed

Lines changed: 649 additions & 42 deletions

‎docs/07-workspace.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,28 @@ mcpp searches upward from the current directory; if it finds an `mcpp.toml` cont
350350

351351
### 5.3 The `-p, --package` Option
352352

353-
`-p` works with `build`, `test`, `run`, and other commands to select the target member. Its value is either the last path segment of a member's directory name or the full relative path:
353+
`-p` works with `build`, `test`, `run`, and other commands to select the target
354+
member. Its value is resolved in one order, because the option names a
355+
*package*:
356+
357+
1. a member's qualified name, `<namespace>.<name>` (only meaningful for a
358+
member that declares a namespace);
359+
2. otherwise, a member's bare `package.name` — refused, naming every match, if
360+
two or more members share it;
361+
3. otherwise, a member's path as written in `[workspace] members`, or its
362+
directory's last segment (the historical spellings, kept as a fallback).
354363

355364
```bash
356-
mcpp build -p server # matches apps/server
365+
mcpp build -p server # matches apps/server (by directory or package name)
357366
mcpp test -p core # matches libs/core
358367
mcpp run -p server -- --port 8080
359368
```
360369

370+
A value that is one member's package name and a different member's directory
371+
selects the member named by the package, with a warning naming the other one —
372+
the option promises a package, so an exact package-name match outranks a
373+
directory that merely happens to share the spelling.
374+
361375
`--workspace` (on `build` and `test`) is the fan-out form: it acts on **every**
362376
member. `mcpp test --workspace` reports each member separately and continues past a
363377
failing member, exiting non-zero if any member failed — ideal as a single,

‎docs/specs/manifest-semantics.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,15 @@ mcpp 2026.9.26.2,#703)。**
434434
## 9. 工作空间继承与构建需求的作用域
435435

436436
1. 工作空间成员**必须**恰好接收一次 `[workspace.package]`、`[workspace.build]` 与
437-
`x.workspace = true` 条目的继承,无论它是命令构建的包、另一个成员的 `path` 依赖、
438-
通过 `git` 引用的托管在 git 上的工作空间的成员,还是索引包归档内的成员(描述符的
439-
`mcpp` 字段指向该成员的清单)。后两种情况按该成员所在仓库或归档的工作空间根继承,
440-
相对路径以该根为锚点;在归档内查找工作空间根时**禁止**越出该版本的安装根。
437+
`x.workspace = true` 条目的继承,无论它是命令构建的包、带 `[package]` 的工作空间根
438+
自己的 `path` 依赖所到达的成员、另一个成员的 `path` 依赖、通过 `git` 引用的托管在
439+
git 上的工作空间的成员,还是索引包归档内的成员(描述符的 `mcpp` 字段指向该成员的
440+
清单)。后两种情况按该成员所在仓库或归档的工作空间根继承,相对路径以该根为锚点;
441+
在归档内查找工作空间根时**禁止**越出该版本的安装根。工作空间的上下文(它是哪个
442+
工作空间、工作空间根在哪里)取决于清单**在哪里**,与命令走的是哪条分支无关——带
443+
`[package]` 的工作空间根按自身构建时,同样要在解析任何依赖之前建立这一上下文。
444+
`-p`/`--package` 首先按成员的包身份(限定名 `<namespace>.<name>`,其次是裸包名)
445+
为其命名,目录路径与目录名是回落拼法。
441446
2. 向量按工作空间、成员、命中的 `[target.<selector>.build]` 的顺序追加;`defines` 按
442447
§8 的集合语义合并。标量仅在成员未**声明**该键时取工作空间的值。
443448
3. 继承**必须**在 `defines` 展开之前、在清单被固定进构建图之前完成。实现**必须**拒绝

‎docs/zh/07-workspace.md‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,23 @@ mcpp 从当前目录向上搜索;若发现某个 `mcpp.toml` 含 `[workspace]`
332332

333333
### 5.3 `-p, --package` 选项
334334

335-
`-p` 可用于 `build`、`test`、`run` 等命令,指定目标成员。参数值可以是成员目录名
336-
的最后一段,也可以是完整相对路径:
335+
`-p` 可用于 `build`、`test`、`run` 等命令,指定目标成员。选项名说的是**包**,
336+
参数值按下述顺序解析:
337+
338+
1. 成员的限定名 `<namespace>.<name>`(只有声明了 namespace 的成员才有这个拼法);
339+
2. 否则,成员裸的 `package.name`——如果两个以上成员共享它,拒绝并点名每一个匹配;
340+
3. 否则,成员在 `[workspace] members` 里写的路径,或其目录名的最后一段(历史拼法,
341+
作为回落保留)。
337342

338343
```bash
339-
mcpp build -p server # matches apps/server
344+
mcpp build -p server # matches apps/server(按目录或包名)
340345
mcpp test -p core # matches libs/core
341346
mcpp run -p server -- --port 8080
342347
```
343348

349+
参数值若既是某个成员的包名,又是另一个成员的目录,选中包名所命名的那个成员,并给
350+
出警告点名另一个成员——选项名的是包,包名的精确匹配压过恰好同名的目录。
351+
344352
`--workspace`(用于 `build` 与 `test`)是扇出形式:作用于**每个**成员。
345353
`mcpp test --workspace` 逐成员分别汇报,遇失败继续,只要有任一成员失败就非零
346354
退出——很适合作为"一个测试众多库的工作空间"单条、无需 shell 的 CI 步骤。

‎src/build/prepare/manifest.cpp‎

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,19 +228,14 @@ std::expected<void, std::string> phase0_manifest_and_workspace(PrepareState& sta
228228
std::string targetMember;
229229

230230
if (!state.overrides.package_filter.empty()) {
231-
// -p <name>: find matching member by directory basename or path
232-
for (auto& mp : state.m->workspace.members) {
233-
auto basename = std::filesystem::path(mp).filename().string();
234-
if (basename == state.overrides.package_filter || mp == state.overrides.package_filter) {
235-
targetMember = mp;
236-
break;
237-
}
238-
}
239-
if (targetMember.empty()) {
240-
return std::unexpected(std::format(
241-
"workspace member '{}' not found in [workspace].members",
242-
state.overrides.package_filter));
243-
}
231+
// `-p <name>`: the package identity first, the member's
232+
// directory as a fallback -- one resolver shared with every
233+
// other `-p`/`--package` command
234+
// (mcpp::project::resolve_member_dir, #725).
235+
auto matched = mcpp::project::resolve_member_dir(
236+
*state.m, *state.root, state.overrides.package_filter);
237+
if (!matched) return std::unexpected(matched.error());
238+
targetMember = matched->lexically_relative(*state.root).generic_string();
244239
} else if (state.m->package.name.empty()) {
245240
// Virtual workspace: find a member with a program target ("is
246241
// this the program", #622 A3's `is_program()`, so a member whose
@@ -265,8 +260,17 @@ std::expected<void, std::string> phase0_manifest_and_workspace(PrepareState& sta
265260
}
266261
// else: rooted workspace with [package] — build root normally. Its own
267262
// `x.workspace = true` entries name its own [workspace.dependencies].
268-
else if (state.m->workspace.present)
263+
// The workspace context is set here too (#725): it is a property of
264+
// where the manifest lives, not of the branch that was taken, so a
265+
// member this package reaches through its OWN `path` dependencies
266+
// (`depIsMember`, graph.cpp) is recognised as a member and receives
267+
// `[workspace.package]`, `[workspace.build]` and `x.workspace = true`
268+
// the same way a sibling's `path` dependency does.
269+
else if (state.m->workspace.present) {
270+
state.runtimeWorkspaceRoot = *state.root;
271+
state.wsManifest = *state.m;
269272
mcpp::project::merge_workspace_deps(*state.m, *state.m, *state.root);
273+
}
270274

271275
if (!targetMember.empty()) {
272276
auto memberDir = *state.root / targetMember;

‎src/cli.cppm‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ int run(int argc, char** argv) {
375375
.option(cl::Option("static").help(
376376
"Force static linking (-static). On Linux, prefer pairing with --target <arch>-linux-musl"))
377377
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
378-
.help("Build only the named workspace member"))
378+
.help("Build only the named workspace member (package name first, then directory)"))
379379
.option(cl::Option("profile").takes_value().value_name("NAME")
380380
.help("Build profile: dev (default) | release | dist | <[profile.*] name>"))
381381
.option(cl::Option("release").help("Shorthand for --profile release"))
@@ -419,7 +419,7 @@ int run(int argc, char** argv) {
419419
.option(cl::Option("target-triple").takes_value().value_name("TRIPLE")
420420
.help("Alias for --target"))
421421
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
422-
.help("Run only the named workspace member (single-member; no --workspace fan-out)"))
422+
.help("Run only the named workspace member (package name first, then directory; single-member, no --workspace fan-out)"))
423423
// DECLARED ON THE THREE COMMANDS THAT BUILD BEFORE THEY ACT, AS ON
424424
// `build`. The value has always reached them: the pre-parse loop
425425
// above publishes it as MCPP_TOOLCHAIN for every command, and
@@ -519,7 +519,7 @@ int run(int argc, char** argv) {
519519
.option(cl::Option("strict")
520520
.help("Treat manifest schema warnings (unknown feature/platform) as errors"))
521521
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
522-
.help("Run tests only for the named workspace member"))
522+
.help("Run tests only for the named workspace member (package name first, then directory)"))
523523
.option(cl::Option("toolchain").takes_value().value_name("SPEC")
524524
.help("Build the tests with this toolchain for one invocation, e.g. llvm@22.1.8"))
525525
.option(cl::Option("cache").takes_value().value_name("MODE")
@@ -684,7 +684,7 @@ int run(int argc, char** argv) {
684684
.help("Describe the variant built for no accelerator"))
685685
.option(cl::Option("static").help("Describe the build with --static"))
686686
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
687-
.help("Describe only the named workspace member"))
687+
.help("Describe only the named workspace member (package name first, then directory)"))
688688
.option(cl::Option("profile").takes_value().value_name("NAME")
689689
.help("Build profile: dev (default) | release | dist | <[profile.*] name>"))
690690
.option(cl::Option("release").help("Shorthand for --profile release"))

‎src/project.cppm‎

Lines changed: 147 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export module mcpp.project;
1212

1313
import std;
1414
import mcpp.manifest;
15+
import mcpp.ui;
1516

1617
namespace mcpp::project {
1718

@@ -468,35 +469,166 @@ load_effective_manifest(const std::filesystem::path& dir) {
468469
return EffectiveManifest{ std::move(*m), std::move(*ws), wsRoot, true };
469470
}
470471

472+
// One declared workspace member, with the identity its own manifest states.
473+
//
474+
// #725: `-p, --package <NAME>` promises a package (SPEC-001's `(namespace,
475+
// name)`), so resolving it needs each member's OWN declaration, not only the
476+
// directory `[workspace] members` names it with.
477+
export struct WorkspaceMember {
478+
std::string memberPath; // as written in [workspace] members
479+
std::filesystem::path dir; // rootDir / memberPath
480+
std::string namespace_; // "" when undeclared, or unreadable
481+
std::string name; // "" when the manifest could not be
482+
// read -- such a member still matches
483+
// by directory or path (step 3 below)
484+
};
485+
486+
// A member's qualified name the way SPEC-001 spells one: `<namespace>.<name>`.
487+
// Meaningless (and not attempted by the resolver below) when the member
488+
// declares no namespace -- SPEC-001 §3.1: an unnamespaced package's bare name
489+
// already IS its full identity, so there is no separate dotted spelling to
490+
// match against.
491+
export std::string qualified_member_name(const WorkspaceMember& m) {
492+
return m.namespace_.empty() ? m.name : m.namespace_ + "." + m.name;
493+
}
494+
495+
// Every declared member, with its own package identity. Loaded the way a
496+
// member is always loaded (`insideWorkspace = true`, so an omitted
497+
// `package.version` is not refused here); a member whose manifest fails to
498+
// parse keeps an empty name rather than aborting the listing, because `-p`
499+
// still owes an answer about the members that DO parse.
500+
export std::vector<WorkspaceMember>
501+
workspace_members(const mcpp::manifest::Manifest& rootManifest,
502+
const std::filesystem::path& rootDir) {
503+
std::vector<WorkspaceMember> out;
504+
out.reserve(rootManifest.workspace.members.size());
505+
for (auto& mp : rootManifest.workspace.members) {
506+
WorkspaceMember wm;
507+
wm.memberPath = mp;
508+
wm.dir = rootDir / mp;
509+
if (auto mm = mcpp::manifest::load(wm.dir / "mcpp.toml", {.insideWorkspace = true})) {
510+
wm.namespace_ = mm->package.namespace_;
511+
wm.name = mm->package.name;
512+
}
513+
out.push_back(std::move(wm));
514+
}
515+
return out;
516+
}
517+
471518
// Resolve which member directory a workspace command acts on, for the
472-
// single-member case. Shares the match rule (basename OR member path) with
473-
// prepare_build's member switch, so `build -p X` and `test -p X` agree.
519+
// single-member case. Shared by every `-p`/`--package` reader (build, test,
520+
// run, emit, and prepare_build's own root-level switch, #725), so
521+
// `build -p X` and `test -p X` always agree on what X means.
522+
//
523+
// A value is resolved in this order, because the option names a package
524+
// (SPEC-001) and a directory is a fallback spelling docs/07 §5.3 has always
525+
// documented:
526+
// 1. a member's qualified name, `<namespace>.<name>` (only attempted for a
527+
// member that declares a namespace -- see `qualified_member_name`);
528+
// 2. otherwise, a member's bare package name -- refused, naming every
529+
// match's qualified name, when two or more members share it;
530+
// 3. otherwise, a member's path as written in `[workspace] members`, or its
531+
// directory's last segment. Two members sharing a directory basename
532+
// keep today's first-match selection (a script written against it keeps
533+
// working), with a warning naming the others and their paths.
534+
// A value that is one member's package name (step 2) and a different
535+
// member's directory (step 3) selects the step-2 member, with a warning
536+
// naming the other member and its path.
537+
//
474538
// Returns:
475539
// - the member dir when `package_filter` names a member,
476540
// - empty path when no switch applies (not a workspace, or a rooted
477541
// workspace with no filter → act on the root package),
478-
// - error when the filter names an unknown member, or a *virtual*
479-
// workspace is addressed with no filter (the caller must
480-
// pick a member with -p or fan out with --workspace).
542+
// - error when the filter names no member, names more than one,
543+
// or a *virtual* workspace is addressed with no filter
544+
// (the caller must pick a member with -p or fan out with
545+
// --workspace).
481546
export std::expected<std::filesystem::path, std::string>
482547
resolve_member_dir(const mcpp::manifest::Manifest& rootManifest,
483548
const std::filesystem::path& rootDir,
484549
std::string_view package_filter) {
485550
if (!rootManifest.workspace.present) return std::filesystem::path{};
486-
if (!package_filter.empty()) {
487-
for (auto& mp : rootManifest.workspace.members) {
488-
auto basename = std::filesystem::path(mp).filename().string();
489-
if (basename == package_filter || mp == package_filter)
490-
return rootDir / mp;
551+
if (package_filter.empty()) {
552+
if (rootManifest.package.name.empty()) {
553+
return std::unexpected(std::string(
554+
"virtual workspace: specify -p <member> or --workspace"));
491555
}
556+
return std::filesystem::path{}; // rooted workspace, no filter → root
557+
}
558+
559+
auto members = workspace_members(rootManifest, rootDir);
560+
561+
// Step 1: the qualified name. Unique by construction (SPEC-001 §3.3 asks
562+
// a single index to keep `(namespace, name)` unique; two workspace
563+
// members sharing one is a manifest defect this resolver does not
564+
// adjudicate), so the first hit is taken without a warning.
565+
for (auto const& m : members)
566+
if (!m.namespace_.empty() && qualified_member_name(m) == package_filter)
567+
return m.dir;
568+
569+
// Step 2: the bare package name, ignoring namespace.
570+
std::vector<std::size_t> byName;
571+
for (std::size_t i = 0; i < members.size(); ++i)
572+
if (!members[i].name.empty() && members[i].name == package_filter)
573+
byName.push_back(i);
574+
if (byName.size() > 1) {
575+
std::string names;
576+
for (auto i : byName)
577+
names += (names.empty() ? "" : ", ") + qualified_member_name(members[i]);
492578
return std::unexpected(std::format(
493-
"workspace member '{}' not found in [workspace].members", package_filter));
579+
"-p '{}' is ambiguous: it is the package name of {} members ({}). "
580+
"Write the qualified name (<namespace>.<name>) to select one.",
581+
package_filter, byName.size(), names));
582+
}
583+
584+
// Step 3: the directory path or basename `[workspace] members` writes.
585+
std::vector<std::size_t> byPath;
586+
for (std::size_t i = 0; i < members.size(); ++i) {
587+
auto basename = members[i].dir.filename().string();
588+
if (basename == package_filter || members[i].memberPath == package_filter)
589+
byPath.push_back(i);
590+
}
591+
592+
if (byName.size() == 1) {
593+
const auto& picked = members[byName.front()];
594+
if (!byPath.empty() && byPath.front() != byName.front()) {
595+
const auto& other = members[byPath.front()];
596+
mcpp::ui::warning(std::format(
597+
"-p '{}' is the package name of member '{}' ({}) and also the "
598+
"directory of member '{}' ({}); the package is selected. Write "
599+
"'{}' to select the other member.",
600+
package_filter, picked.memberPath, qualified_member_name(picked),
601+
other.memberPath, qualified_member_name(other), other.memberPath));
602+
}
603+
return picked.dir;
604+
}
605+
606+
if (!byPath.empty()) {
607+
if (byPath.size() > 1) {
608+
std::string others;
609+
for (std::size_t k = 1; k < byPath.size(); ++k)
610+
others += std::format("{}'{}'", others.empty() ? "" : ", ",
611+
members[byPath[k]].memberPath);
612+
mcpp::ui::warning(std::format(
613+
"-p '{}' matches more than one member's directory; '{}' is "
614+
"selected (the first listed in [workspace] members). Also "
615+
"matched: {}.",
616+
package_filter, members[byPath.front()].memberPath, others));
617+
}
618+
return members[byPath.front()].dir;
494619
}
495-
if (rootManifest.package.name.empty()) {
496-
return std::unexpected(std::string(
497-
"virtual workspace: specify -p <member> or --workspace"));
620+
621+
std::string list;
622+
for (auto const& m : members) {
623+
auto label = m.name.empty() ? std::string("?")
624+
: (m.namespace_.empty() ? m.name
625+
: std::format("{} ({})", m.name,
626+
qualified_member_name(m)));
627+
list += std::format("\n {} at '{}'", label, m.memberPath);
498628
}
499-
return std::filesystem::path{}; // rooted workspace, no filter → root package
629+
return std::unexpected(std::format(
630+
"workspace member '{}' not found. [workspace] members:{}",
631+
package_filter, list));
500632
}
501633

502634
} // namespace mcpp::project

0 commit comments

Comments
 (0)