Skip to content

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

Closed

Description

@Sunrisepeak

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 into targetOverrides and are root-position keys: one value per graph, inherited where the member is the root of a build (inherit_workspace_root_position).
  • The .build sub-table — cflags, cxxflags, ldflags, defines, sources, include_dirs, ... — goes into a ConditionalConfig and is package-owned: merge_conditional_config (src/build/prepare/config.cpp) folds each manifest's own rows into that manifest's buildConfig. 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 diagnostic

mcpp 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.X is the sharing form of X, 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] via inherit_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; defines as a set keyed by macro name (a member entry for the same name replaces, !NAME removes); relative include dirs anchored at the workspace root (the Workspace path dependencies and inherited index paths cannot be centralized reliably #224 rule); diagnostics recorded in inheritedFromWorkspace with 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_config alone — 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.toml is 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].sources compat 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 the docs/zh mirrors)
  • 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 X grammar.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions