Two coupled deliverables toward "把对库的测试做成真实工程、基于 mcpp 自包含、消除所有 shell":
- Phase 1 (repo
mcpp) — makemcpp testworkspace-aware:mcpp test -p <member>,mcpp build|test --workspace, and fix baremcpp testat a workspace root. - Phase 2 (repo
mcpp-index) — turn eachtests/<lib>/into a real mcpp test project (mcpp test+ behavioral assertions), migrate thesmoke_compat_*.shheredocs into them, and delete every shell driver — CI becomesmcpp test --workspace.
Pairs with the typed import mcpp; build library
(2026-06-30-l3-build-mcpp-implementation-design.md §forward-note, task #20) as the
0.0.79 "workspace-test + build.mcpp library" release.
mcpp build is workspace-aware; mcpp test is not.
mcpp build -p <member>works (src/cli.cppmbuild subcommand has.option("package").short_name('p');cmd_buildcopies it toBuildOverrides::package_filter—src/cli/cmd_build.cppm;prepare_buildresolves the member and reassignsrootto the member dir —src/build/prepare.cppm:415-509).mcpp testhas no-p/--workspace(src/cli.cppmtest subcommand).- The bug:
run_tests(src/build/execute.cppm:421-459) callsfind_manifest_root()→ gets the workspace root →expand_glob(*root, "tests/**/*.cpp")→ collects every member'stests/.../main.cpp→seenNames.insert("main")collides →error: duplicate test name 'main'. Test discovery runs before any member selection, on the unscoped workspace root.
Proof:
$ mcpp build # virtual ws root → "Workspace building member 'tests/examples/build-mcpp'" (picks ONE, arbitrarily)
$ mcpp test # virtual ws root → error: duplicate test name 'main' (globs ALL, unscoped)
Scope, don't duplicate. The root-test bug is missing member scoping, not a
missing second globber. The fix is to run the same member resolution build
uses before test discovery, so tests/**/*.cpp is globbed from the member
dir. --workspace is thin orchestration: a loop over members that calls the
existing per-member pipeline once each — no parallel build/test path.
Concretely, extract today's inline member logic into one shared helper and have
both build and test consume it:
// src/build/workspace.cppm (new, or fold into project.cppm)
namespace mcpp::build {
struct MemberRef { std::string name; std::filesystem::path dir; };
// Resolve which members a command acts on, from the (already-loaded) root
// manifest + overrides. Encapsulates the selection rules in §3.
std::expected<std::vector<MemberRef>, std::string>
select_members(const manifest::Manifest& root, const std::filesystem::path& rootDir,
const BuildOverrides& ov, bool wantAll);
}-p <name>→ 1 member (match by directory basename or member path — the rule already inprepare.cppm:430-447).--workspace(or bare at a virtual workspace) → all members.- Bare at a rooted workspace (
[package]+[workspace]) → the root package (members only via--workspace), matching today.
prepare_build's existing per-member switch (load member manifest, merge_workspace_deps,
inherit toolchain/target/indices, root = memberDir) stays as the single-member
mechanism; select_members just decides the set, and the orchestration loop calls
prepare_build(package_filter = member.name) per member.
| invocation | virtual workspace | rooted workspace | plain package |
|---|---|---|---|
mcpp build / mcpp test |
all members | root package | the package |
... -p <m> |
member <m> |
member <m> |
error (no members) |
... --workspace |
all members | all members (+root) | error |
mcpp run (+-p) |
default/-p member |
root/-p |
the package |
Change from today: bare build/test at a virtual workspace goes from "pick one
arbitrary member" → all members (Cargo-consistent; the pick-one was a
placeholder). run stays single-target (no --workspace). This is the only
behavior change; verify/adjust the workspace e2e (tests/e2e/*workspace*).
- CLI (
src/cli.cppm, test subcommand ~L244-256): addand add.option(cl::Option("package").short_name('p').takes_value().value_name("NAME") .help("Run tests only for the named workspace member")) .option(cl::Option("workspace").help("Run tests for all workspace members"))
.option("workspace")to the build subcommand (~L215-237). - Parse (
src/cli/cmd_build.cppm): incmd_test, copy-p→ov.package_filter; read--workspace→bool all. Same--workspaceread incmd_build. select_membershelper (newsrc/build/workspace.cppm): the §2 logic, extracted fromprepare.cppm:422-453so both paths share it.run_testsscoping (src/build/execute.cppm:421-459): before theexpand_glob, resolve the member root for the single-member case (whenpackage_filterset, via the shared helper) and glob from the member dir, not the workspace root. This kills the "duplicate main" bug by scoping.--workspaceorchestration (src/cli/cmd_build.cppm): whenall, callselect_members(..., wantAll=true)and loop —cmd_buildrunsprepare_build+run_build_planper member;cmd_testrunsrun_testsper member (each withpackage_filter = member.name). Aggregate exit codes (first non-zero wins; print a per-member summary). Continue-on-failure so one member's failure still reports the rest.- Tests (
tests/e2e/90_workspace_test.sh): a virtual workspace with 2 members each havingtests/<distinct>.cppasserting behavior; assertmcpp test -p memberAruns only A's tests,mcpp test --workspaceruns both (no duplicate-stem error), baremcpp testat the root runs both. - Docs (
docs/06-workspace.md+ zh): document-p/--workspacefor build/test and the bare-at-root semantics.
mcpp-index/
pkgs/ # the index (recipes) — unchanged
mcpp.toml # [workspace] members=tests/* ⊕ [indices] compat={path="."}
tests/
cjson/
mcpp.toml # [package] cjson-tests; [dependencies] compat.cjson
tests/parse.cpp # mcpp test — assert cJSON_Parse fields
eigen/ tests/matmul.cpp # assert A*B values
nlohmann.json/ tests/roundtrip.cpp # parse→dump→parse equality
openblas/ # [target.'cfg(windows)'.dependencies] openblas; tests/dgemm.cpp asserts [19 22;43 50] (no-op gate off-Windows)
build-mcpp/ # build.mcpp generates a source; tests/ assert it linked
mcpp's native mcpp test discovers tests/**/*.cpp, builds each as a test binary,
runs it (non-zero = fail). Members use plain assertion .cpp (a failing assert/
non-zero return), no external framework required — keeps members dependency-light
and host-portable. (gtest remains available via [dev-dependencies] if a member
wants richer output, but is not required.)
- Convert each
smoke_compat_*.shheredoc body into the matching member'stests/*.cpp(the heredocs already contain the usage snippets). - Delete
tests/smoke_compat_{core,imgui,archive,imgui_window}.sh,tests/smoke_imgui_module.sh,tests/smoke_compat_portable.sh,tests/run_workspace.sh,tests/run_example.sh. validate.ymlcollapses: the per-suite jobs become one matrix or a singlemcpp test --workspacestep:Windows/macOS jobs run the same command (platform-gated members like openblas self-gate via- run: mcpp test --workspace # linux; the whole index, self-contained
cfg(windows); thedetectjob can still narrow to-p <lib>on PRs touching one recipe). No shell driver remains.
The index repo is simultaneously (a) the package index and (b) a mcpp workspace
whose members really use and test every recipe — driven entirely by mcpp, no
.sh. "基于 mcpp 自包含" achieved.
- build.mcpp cwd bug — fixed in 0.0.79 (see §6.1); the
build-mcppmember drove it out (itsbuild.mcppwrote to the wrong dir under-p). - Pure test projects work — a member with only
[dependencies]+tests/*.cpp(nosrc/) builds + tests cleanly; the dep's headers/lib reach the test binary. - Feature-built dependency objects don't link into test binaries — the eigen
eigen_blasfeature compiles Eigen's reference BLAS into compat.eigen's lib, but the test binary callingdgemm_fails to link it (worked when the member was abin). The eigen member tests header-only Eigen for now; linking feature-gated dependency objects intomcpp testbinaries is a separate mcpp follow-up. - Display/GL smokes stay shell for now —
smoke_compat_{imgui,imgui_window, glfw}+ the portable matrix need a display / broader libs; migrating them to headlessmcpp testmembers is a later increment. Phase 2 converts the 5 headless example members + switches their CI tomcpp test.
A typed module bundled in the mcpp binary, emitting the existing stdout
mcpp: wire protocol, implemented with C-level I/O so neither it nor build.mcpp
needs import std;. De-risking confirmed the module itself works (GCC 16):
module; #include <cstdio>
export module mcpp;
export namespace mcpp {
inline void cxxflag(const char* f) { std::printf("mcpp:cxxflag=%s\n", f); }
inline void link_lib(const char* n) { std::printf("mcpp:link-lib=%s\n", n); }
// ...
}g++ -std=c++23 --sysroot=… -fmodules -c mcpp.cppm -o mcpp.o → gcm.cache/mcpp.gcm
mcpp.o; theng++ … -fmodules -x c++ build.mcpp -x none mcpp.o -o bin(run from the dir containinggcm.cache/) →import mcpp;resolves; the binary emits the directives. Noimport std;needed.
Why deferred (not in 0.0.79): one remaining piece this release won't rush:
cwd-capable spawn.DONE in 0.0.79 —capture_execgained acwdparameter (Linuxposix_spawn_file_actions_addchdir_np; elsecd … &&), added for the build.mcpp-cwd correctness fix. This is exactly what the typed lib needs to stagegcm.cache/<m>.gcm(GCC C++ finds modules only relative to the compile CWD — the named-fmodule-file=mcpp=<path>form is rejected "valid for D not C++";-fmodule-output=is absent on GCC 16). So the GCC path is now unblocked.- Clang path. Clang uses
.pcm+--precompile+-fmodule-file=mcpp=<pcm>(different ABI/flags), untested here. Without it,build.mcppusingimport mcpp;on a Clang host (macOS/Windows) would fail to compile — a partial feature. Both compiler paths must land together → still a focused 0.0.80.
Plus: embed the module source in the binary, compile it once into
target/.build-mcpp/ keyed on the toolchain (cache; don't rebuild), then convert
the build.mcpp docs/examples/test + the mcpp-index build-mcpp member to
import mcpp;. Tracked as task #20 for a focused 0.0.80. The string-protocol
substrate already ships build.mcpp today, so this is a pure ergonomic layer — no
functionality is blocked by deferring it.
- mcpp PR A — Phase 1 (workspace-aware test) + e2e + docs → release 0.0.79 → full ecosystem loop (mirror → index → verify → pin).
- mcpp-index PR B — Phase 2 restructure on 0.0.79; delete all shell; CI =
mcpp test --workspace→ its CI green → merge. (Primary user goal: zero-shell, self-contained,-p-addressable index.) - (follow-up) mcpp 0.0.80 — typed
import mcpp;library (§6), with the cwd-capable spawn + Clang.pcmpath; then convert build.mcpp docs/examples and the mcpp-indexbuild-mcppmember toimport mcpp;.
- Behavior change (bare virtual-ws build/test → all members): the one
compatibility-affecting change; covered by updating the workspace e2e and is the
more-correct semantics.
runis untouched (single-target). - No parallel code path:
--workspace/-pare orchestration over the proven per-memberprepare_build/run_tests; the helper centralizes selection so the rules can't drift betweenbuildandtest(the class of bug we're fixing). - Aggregate reporting:
--workspacemust continue-on-failure and print a per-member pass/fail summary, else one red member hides the rest. - Per-member dep isolation is the reason for the workspace (vs one mega
[package]): members resolve independently, so conflicting transitive deps and platform-only libs (openblas/Windows) don't couple. Recorded as the rejected single-project alternative.