Repository navigation
feat: [workspace.target.<selector>.build] — per-target build inputs shared with workspace members, and a warning for package-owned tables at virtual workspace roots #785
Description
Context
While reviewing the workspace inheritance semantics (src/project.cppm), we traced exactly what the workspace root's [target.<triplet>] and [target.<triplet>.build] tables do, and found one real gap, one silent corner, and one discoverability cost worth discussing together. Evidence first, then a proposal.
How [target.<selector>] splits today. The parser splits one table header into two key classes (modules/manifest/src/toml.cppm):
- Scalar selection rows —
toolchain,linkage,cxx_runtime,sysroot,min_api_level,runner(s)— go intotargetOverridesand are root-position keys: one value per graph, inherited where the member is the root of a build (inherit_workspace_root_position). - The
.buildsub-table —cflags,cxxflags,ldflags,defines,sources,include_dirs, ... — goes into aConditionalConfigand is package-owned:merge_conditional_config(src/build/prepare/config.cpp) folds each manifest's own rows into that manifest'sbuildConfig. It appears in no inheritance list, so it travels nowhere.
Who a root's [target.<triple>.build] serves, per shape:
| written at | who gets it |
|---|---|
| standalone project | the project itself |
rooted workspace root (has [package]) |
the root package only |
virtual workspace root (only [workspace]) |
nobody — silently |
| a member's own manifest | that member, also when pulled in as a path dependency |
Minimal repro (virtual root):
# ws/mcpp.toml
[workspace]
members = ["libs/core", "apps/server"]
[target.x86_64-linux-musl.build]
defines = ["PLATFORM_MUSL"] # reaches no package; no diagnosticmcpp build --workspace --target x86_64-linux-musl succeeds; neither member's compile line carries -DPLATFORM_MUSL. The rows are not even dead in a loud way — virtual_workspace_root carries no conditional configs, and nothing warns.
The gap. There is no "per-platform × all members" channel for build inputs. [workspace.build] is target-blind; [target.<selector>.build] is package-owned. The only correct spelling today is repeating the rows in every member. Compare Cargo, which fills this exact slot with .cargo/config.toml's [target.<triple>] rustflags/linker/runner — an ambient, upward-discovered environment layer shared by everything built beneath a directory — while Cargo's manifest-level [target.'cfg(...)'.dependencies] (which mcpp already mirrors) is deliberately per-package. mcpp merges both roles into one file and separates them by key class; that is coherent per key, but the shared table header makes the split easy to misread, and docs/07 §4's key-class table is the only thing telling the two apart.
Proposal
A. Add [workspace.target.<selector>.build] — the shared form of [target.<selector>.build]
# workspace root
[workspace.target.x86_64-linux-musl.build]
defines = ["PLATFORM_MUSL"]
cxxflags = ["-fno-exceptions"]
[workspace.target.'cfg(os = "windows")'.build]
defines = ["USE_WIN32"]Design points, each with in-repo precedent:
workspace.Xis the sharing form ofX, exactly as[workspace.build]relates to[build]. One mental rule, already taught by docs/07.- Scope: workspace members — including a rooted workspace's own root package, like
[workspace.build]viainherit_as_root_package— and not fetched index/git dependencies, matching the[workspace.build]ownership boundary. State this in docs; it is the line users will most likely mis-expect. - Merge semantics mirror
[workspace.build]: vectors prepend with the workspace first;definesas a set keyed by macro name (a member entry for the same name replaces,!NAMEremoves); relative include dirs anchored at the workspace root (the Workspace path dependencies and inherited index paths cannot be centralized reliably #224 rule); diagnostics recorded ininheritedFromWorkspacewith the selector in the key (WS3: every inherited word names its origin). - Selectors: literal triple or
cfg(...), evaluated against the resolved build target. Inputs carry no layer-key circularity — only dependency edges do (docs/14 already notes build inputs under a layer predicate do apply). - Implementation home:
inherit_workspace_configalone — that function exists precisely because hand-copied merge lists drift ([Bug & RFC] 系统工具链 build.mcpp 执行失败缺陷、原生宿主模式与工作区继承规范提案 #527 Bug 2). - Guard rail: scalar selection keys on
[workspace.target.<selector>]are refused, with the error pointing at[target.<triple>](see B).
B. Do not rename [target.<triple>] scalar rows to workspace.<triple>
The class's accurate name is root position, not workspace:
- A member built as a host tool is the root of its own sub-build (A host tool's sub-build resolves its own toolchain, not the one its key records, so a workspace pinned to one LLVM installs a second #710) and its own rows apply there.
- A non-workspace project states its cross-build environment in the same table; a
workspace.prefix would strand it (Cargo avoids this only because.cargo/config.tomlis location-based, not manifest-based). - Any spelling migration in manifests means permanently dual-reading old spellings — manifests are hashed into build keys, packed and published. That is a real, permanent vocabulary cost (
[modules].sources→[build].sourcescompat shows the pattern) paid for a purely syntactic benefit, when the per-key semantics are already consistent.
The incoherence is the shared header, not the semantics. It is cheaper to teach (docs/07 §4 already does) than to rename away.
C. Warn when package-owned tables sit at a virtual workspace root
A manifest with [workspace] but no [package] that carries [target.<selector>.build], [build], [targets], ... applies those rows to no package, silently. A schema warning — "this table applies to no package here; write it in a member, or under [workspace...] to share it" — turns the only silent-swallow corner into a reported one. This also fixes the misreading risk of rooted roots, where the rows look workspace-wide but serve only the root package.
Affected modules
modules/manifest— new table vocabulary + schema warnings (selector grammar reused from[target.<selector>.build])src/project.cppm—inherit_workspace_config(single site)src/build/prepare/config.cpp— merge + diagnostics reuse (merge_conditional_config,inheritedFromWorkspace)docs/04-mcpp-toml.md,docs/07-workspace.md§4 (and thedocs/zhmirrors)- tests: manifest parser unit tests; e2e asserting workspace rows reach every member and the root package, do not reach fetched dependencies, and a member's own rows win
Prior art
- Cargo manifest:
[target.'cfg(...)'.dependencies]— per-package, target-conditional; mcpp already mirrors this. - Cargo environment:
.cargo/config.toml [target.<triple>]rustflags/linker/runner— ambient, shared by everything built under the directory, literal triples only. - Proposal A is the manifest-level equivalent of the config.toml slot, expressed in mcpp's
workspace.X = shared Xgrammar.
Activity