Skip to content

Commit 96fda4e

Browse files
committed
feat(build): a source glob may carry the accel it is for, and the build narrows to it
`[build] sources` accepts `{ glob = "...", accel = "..." }` entries. The glob joins the plain list, so every reader keeps working; the constraint is resolved in prepare after feature application. A constrained glob that matches nothing is refused, naming it. Under a build that asks for no accelerator the glob is excluded through the same `!` mechanism feature gates use, which is how one project yields its CPU-only variant. Under a build that targets something the constraint is not within, the build is refused naming the glob and both sides (`accel-mismatch`). Device-kind files the effective set matches reach the build program as MCPP_DEVICE_SOURCES; the engine has no compile rule for them and the rule package turns each into an action. Unit tests cover the table form and its refusals; e2e 606 measures the four outcomes with a backend nothing knows.
1 parent 0ac6397 commit 96fda4e

14 files changed

Lines changed: 403 additions & 2 deletions

docs/05-mcpp-toml.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ the package/feature boundary, not on an individual target.
215215
> mcpp 2026.8.18.1 the two were byte-identical, so there was no spelling for
216216
> "nothing" and any file left under `src/` was swept in.
217217
218+
> **A `sources` entry may carry the accelerator it is for** (2026.9.6+):
219+
> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`. The glob
220+
> joins the list like any other; the constraint decides whether it applies to a
221+
> given build. It must match at least one file (an empty match is refused: it
222+
> would leave nothing to compile for that device and say so only at the link).
223+
> Under `--no-accel` the glob is left out, which is how one project yields its
224+
> CPU-only variant. Under an `--accel` that does not cover the constraint the
225+
> build is refused naming both (`accel-mismatch`). Device-kind files (`.cu`,
226+
> `.hip`) the effective set matches are never compiled by the engine; they
227+
> reach the build program as `MCPP_DEVICE_SOURCES`, where the rule package the
228+
> project imports turns each into an `mcpp::action`.
229+
218230
```toml
219231
[build]
220232
sources = ["src/**/*.cppm", "src/**/*.cpp"] # Source globs (default: src/**/*.{cppm,cpp,cc,c,S,s,asm})

docs/07-build-mcpp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ The running program receives the build context as `MCPP_*` variables
533533
| `MCPP_HOST` | `mcpp::host()` | the host triple |
534534
| `MCPP_PROFILE` | `mcpp::profile()` | effective profile name (`dev`/`release`/…) |
535535
| `MCPP_ACCEL` *(2026.9.6+)* | `mcpp::accel()` | the device axis of this build, resolved — `--accel` / `--no-accel` over `[build] accel` — in the wire form `cuda12.9+{sm_89} ptx>=89`; empty when the build asks for no accelerator. A rule package derives its own flags (`-gencode`, `--offload-arch`) from it, so the architecture set is written once, in the manifest. The same value feeds the `cfg(accelerator = "…")` layer key |
536+
| `MCPP_DEVICE_SOURCES` *(2026.9.6+)* | `mcpp::device_sources()` | the device-kind sources (`.cu`, `.hip`, …) the package's effective `sources` match, package-root-relative, one per line; empty when there are none. The engine compiles none of them — the rule package this program imports turns each into an `mcpp::action`. Already narrowed: a `{ glob, accel }` entry the build does not cover contributes nothing, so `--no-accel` yields an empty list |
536537
| `MCPP_OUT_DIR` | `mcpp::out_dir()` | a writable scratch/output dir owned by mcpp |
537538
| `MCPP_MANIFEST_DIR` | `mcpp::manifest_dir()` | the package root (= CWD) |
538539
| `MCPP_FEATURE_<NAME>` | `mcpp::has_feature("name")` | set to `1` per active feature (same `<NAME>` sanitization as the `MCPP_FEATURE_` compile macro) |

docs/11-machine-output.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ a program classifying the outcome reads `reason`:
379379
| `layer-ordering` | the five layers do not stack |
380380
| `exclusive-capability` | two packages provide one capability and at least one declared it exclusive |
381381
| `version-floor-unmet` | a package requires more of the machine than the machine was declared to have |
382+
| `accel-mismatch` | a `[build] sources` entry is constrained to a device set this build does not cover |
382383
| `other` | a refusal whose branch has not been given a token yet |
383384

384385
⚠️ **Exit 0 whenever the question was answered, including "refused".** "Would

docs/zh/05-mcpp-toml.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,15 @@ mcpp 刻意不在一次构建里把同一个共享源编译成两份:一个源
201201
> `src/` 下剩下的任何文件都会被扫进来。
202202
203203

204+
> **`sources` 的条目可以带上它所面向的加速器**(2026.9.6+):
205+
> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`。glob 与其它条目一样
206+
> 进入列表;约束决定它是否适用于某一次构建。它必须至少匹配一个文件(空匹配会被拒绝:
207+
> 那会让这个设备无东西可编,而只在链接时才说话)。`--no-accel` 下该 glob 被排除,
208+
> 一个工程由此产出它的 CPU-only 变体。`--accel` 未覆盖该约束时构建被拒并给出两侧
209+
> (`accel-mismatch`)。有效集合匹配到的设备类源文件(`.cu``.hip`)引擎从不编译;
210+
> 它们以 `MCPP_DEVICE_SOURCES` 到达构建程序,由工程引入的规则包把每一个变成一条
211+
> `mcpp::action`
212+
204213
```toml
205214
[build]
206215
sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/**/*.{cppm,cpp,cc,c,S,s,asm})

docs/zh/07-build-mcpp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ mcpp 会把它自己构建时用的**同一份** std 模块暂存过来,缓存
463463
| `MCPP_HOST` | `mcpp::host()` | 宿主三元组 |
464464
| `MCPP_PROFILE` | `mcpp::profile()` | 生效 profile 名(`dev`/`release`/…) |
465465
| `MCPP_ACCEL` *(2026.9.6+)* | `mcpp::accel()` | 本次构建的设备轴,已解析 —— `--accel` / `--no-accel` 优先于 `[build] accel` —— 线上形态 `cuda12.9+{sm_89} ptx>=89`;不要加速器时为空串。规则包从它推导自己的开关(`-gencode``--offload-arch`),架构集合因此只在 manifest 写一次。同一个值也喂给 `cfg(accelerator = "…")` 这个 layer 键 |
466+
| `MCPP_DEVICE_SOURCES` *(2026.9.6+)* | `mcpp::device_sources()` | 本包有效 `sources` 匹配到的设备类源文件(`.cu``.hip`…),相对包根,一行一个;没有时为空串。引擎一个都不编译 —— 由本程序引入的规则包把每一个变成一条 `mcpp::action`。已经过收窄:构建未覆盖的 `{ glob, accel }` 条目贡献为空,因此 `--no-accel` 得到空列表 |
466467
| `MCPP_OUT_DIR` | `mcpp::out_dir()` | mcpp 提供的可写输出/暂存目录 |
467468
| `MCPP_MANIFEST_DIR` | `mcpp::manifest_dir()` | 包根(= CWD) |
468469
| `MCPP_FEATURE_<NAME>` | `mcpp::has_feature("name")` | 每个活跃 feature 置 `1`(`<NAME>` 消毒规则与 `MCPP_FEATURE_` 编译宏一致) |

docs/zh/11-machine-output.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ mcpp why toolchain [--target <triple>] [--toolchain <spec>] --format json
333333
| `layer-ordering` | 五层叠不起来 |
334334
| `exclusive-capability` | 一个能力有多个提供者,而其中至少一个声明了独占 |
335335
| `version-floor-unmet` | 一个包对机器的要求高于机器被声明拥有的 |
336+
| `accel-mismatch` | 一条 `[build] sources` 条目被约束到本次构建未覆盖的设备集合 |
336337
| `other` | 一处还没有被命名的拒绝分支 |
337338

338339
⚠️ **只要问题被回答了就退 0,包括答案是「拒绝」。** 「它能不能构建,不能的话

modules/manifest/src/toml.cppm

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,14 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
485485
// find_disallowed_array_of_tables above.
486486
static constexpr std::string_view kAllowedArraysOfTables[] = {
487487
"build.flags",
488+
// A `sources` list whose every entry is a table (`[{ glob, accel }]`)
489+
// has the same Value shape as `[[build.sources]]`; the reader
490+
// type-checks every entry, so nothing is silently taken. The
491+
// conditional axis is allowlisted for the same reason: its reader
492+
// refuses a table entry with a message that says where it belongs,
493+
// which this guard's generic sentence would pre-empt.
494+
"build.sources",
495+
"target.*.build.sources",
488496
"features.*.flags", // #253 — the middle segment is the feature name
489497
"target.*.build.flags", // #258 — middle segment is the cfg predicate
490498
"runtime.requirements",
@@ -594,8 +602,48 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
594602
// mean "compile nothing", and only the key's existence can say that (see
595603
// BuildConfig::sourcesDeclared). Set from either spelling, because the
596604
// legacy one has to be able to express it too.
597-
if (auto v = doc->get_string_array("build.sources")) {
598-
m.buildConfig.sources = *v;
605+
// A mixed list: plain globs, and tables that carry a constraint. Read
606+
// element by element rather than through get_string_array, which answers
607+
// "not a string array" for a list with one table in it -- and then the
608+
// key would count as absent, the default glob would apply, and the
609+
// constrained entry would vanish without a word.
610+
if (auto* sv = doc->get("build.sources")) {
611+
if (!sv->is_array()) {
612+
return std::unexpected(error(origin,
613+
"[build].sources must be an array of globs and/or inline tables "
614+
"({ glob = \"...\", accel = \"...\" })"));
615+
}
616+
for (auto& ev : sv->as_array()) {
617+
if (ev.is_string()) { m.buildConfig.sources.push_back(ev.as_string()); continue; }
618+
if (!ev.is_table()) {
619+
return std::unexpected(error(origin,
620+
"[build].sources entries must be strings or inline tables "
621+
"with a `glob` key"));
622+
}
623+
mcpp::manifest::BuildConfig::SourceConstraint sc;
624+
for (auto& [k, v] : ev.as_table()) {
625+
bool ok = false;
626+
if (k == "glob") { ok = v.is_string(); if (ok) sc.glob = v.as_string(); }
627+
else if (k == "accel") { ok = v.is_string(); if (ok) sc.accel = v.as_string(); }
628+
if (!ok) {
629+
return std::unexpected(error(origin, std::format(
630+
"[build].sources: invalid key '{}' in a table entry "
631+
"(expected glob = \"...\" and optionally accel = \"...\")", k)));
632+
}
633+
}
634+
if (sc.glob.empty()) {
635+
return std::unexpected(error(origin,
636+
"[build].sources: a table entry is missing its `glob` key"));
637+
}
638+
if (sc.glob.starts_with("!")) {
639+
return std::unexpected(error(origin, std::format(
640+
"[build].sources: a constrained entry cannot be an exclusion "
641+
"('{}'); write the exclusion as a plain string", sc.glob)));
642+
}
643+
m.buildConfig.sources.push_back(sc.glob);
644+
if (!sc.accel.empty())
645+
m.buildConfig.sourceConstraints.push_back(std::move(sc));
646+
}
599647
m.buildConfig.sourcesDeclared = true;
600648
}
601649
if (auto v = doc->get_string_array("modules.sources")) {
@@ -2451,6 +2499,17 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
24512499
read_list("cflags", cc.inputs.cflags);
24522500
read_list("cxxflags", cc.inputs.cxxflags);
24532501
read_list("ldflags", cc.inputs.ldflags);
2502+
// A constrained entry (`{ glob, accel }`) is refused here rather
2503+
// than skipped: read_list keeps strings only, and a table that
2504+
// silently vanished would be a device glob nobody ever narrowed.
2505+
if (auto f = bt.find("sources"); f != bt.end() && f->second.is_array())
2506+
for (auto& v : f->second.as_array())
2507+
if (!v.is_string()) {
2508+
return std::unexpected(error(origin,
2509+
"[target.'cfg(...)'.build].sources entries must be "
2510+
"plain globs; a constrained entry ({ glob, accel }) "
2511+
"belongs in [build].sources"));
2512+
}
24542513
read_list("sources", cc.inputs.sources);
24552514
// #296: package-level macros are a build input like any other,
24562515
// so the cfg axis carries them too — a platform-only macro

modules/manifest/src/types.cppm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,26 @@ struct BuildConfig : BuildInputs {
550550
// the "main" feature) without it being linked by default — see
551551
// .agents/docs/2026-06-25-gtest-main-feature-and-add-dev-design.md.
552552
std::map<std::string, std::vector<std::string>> featureSources;
553+
// `[build] sources` entries written as a table:
554+
//
555+
// sources = ["src/**/*.cppm", { glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }]
556+
//
557+
// The glob ALSO appears in `sources`, so every reader that walks the plain
558+
// list sees it; this carries the constraint that decides whether it
559+
// applies to a given build. Resolved in prepare_build: the glob must match
560+
// at least one file (an empty match is a typo, not a no-op); when the
561+
// build asks for no accelerator the glob is excluded, which is how
562+
// `--no-accel` yields the CPU-only variant of a project; and when it does
563+
// ask for one, the constraint must lie within what the build targets, or
564+
// the build is refused naming both. Device-kind files the effective source
565+
// set matches are handed to the package's build program
566+
// (MCPP_DEVICE_SOURCES) rather than compiled by the engine, which has no
567+
// rule for them: that is the rule package's business.
568+
struct SourceConstraint {
569+
std::string glob;
570+
std::string accel; // wire form, the mcpp.pack.abi_tag grammar
571+
};
572+
std::vector<SourceConstraint> sourceConstraints;
553573
// feature name → package-owned preprocessor defines (e.g. "-DEIGEN_USE_BLAS").
554574
// Feature System v2 Stage 1: when the feature is active these are appended to
555575
// the package's compile flags alongside the automatic -DMCPP_FEATURE_<NAME>

src/build/build_program.cppm

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ struct BuildProgramEnv {
8484
// (`-gencode`, `--offload-arch`) from here and the architecture set is
8585
// written once, in the manifest, and never again in a build program.
8686
std::string accel;
87+
// The device-kind sources (`.cu`, `.hip`, ...) this package's effective
88+
// source set matches, package-root-relative with `/` separators, one per
89+
// line. The engine has no compile rule for them and hands the list to the
90+
// build program, where the rule package the package imports turns each
91+
// one into an `mcpp::action`. Already narrowed: a glob whose `accel`
92+
// constraint the build does not satisfy contributes nothing.
93+
std::vector<std::string> deviceSources;
8794
// Artifact home (bin/cache/out). Empty → <root>/target/.build-mcpp (the
8895
// root-project default). Dependencies MUST point this into the CONSUMING
8996
// project's tree — a registry package root is shared and may be read-only.
@@ -434,6 +441,14 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv
434441
e.emplace_back("MCPP_TARGET_LIBC", env.targetLibc);
435442
e.emplace_back("MCPP_PROFILE", env.profile);
436443
e.emplace_back("MCPP_ACCEL", env.accel);
444+
{
445+
std::string joined;
446+
for (auto const& d : env.deviceSources) {
447+
if (!joined.empty()) joined += '\n';
448+
joined += d;
449+
}
450+
e.emplace_back("MCPP_DEVICE_SOURCES", joined);
451+
}
437452
e.emplace_back("MCPP_OUT_DIR", outDir.string());
438453
e.emplace_back("MCPP_MANIFEST_DIR", root.string());
439454
std::string csv;

src/build/hostprogram.cppm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ inline const char* profile() { return env_or("MCPP_PROFILE"
243243
// means beyond "backend, version, architectures, floor" is the package's
244244
// business: the engine never learns what `sm_89` is.
245245
inline const char* accel() { return env_or("MCPP_ACCEL"); }
246+
// The device-kind sources (`.cu`, `.hip`, ...) this package's `sources` match
247+
// under the current accel, package-root-relative, one per line, "" when there
248+
// are none. The engine compiles none of them; the rule package this program
249+
// imports turns each into an `mcpp::action`. Already narrowed: a glob written
250+
// as `{ glob = "...", accel = "..." }` whose constraint the build does not
251+
// satisfy contributes nothing, so `--no-accel` yields an empty list.
252+
inline const char* device_sources() { return env_or("MCPP_DEVICE_SOURCES"); }
246253
inline const char* out_dir() { return env_or("MCPP_OUT_DIR"); }
247254
248255
// Where the TOOLCHAIN mcpp resolved for this build lives — the payload root,

0 commit comments

Comments
 (0)