Skip to content

Commit f0d2f19

Browse files
committed
feat(build): root build.mcpp runs after dep resolution and receives MCPP_DEP_*_DIR
Design §3.1 item 4 (+ #230-#243 ledger follow-up): the ROOT project's build.mcpp used to run BEFORE dependency resolution (prepare.cppm ~1180), so unlike a dependency's it never saw MCPP_DEP_<NAME>_DIR — consumer-side synthesis (ffmpeg-m reading compat.ffmpeg's payload tree) was impossible. The call now sits right after the dep build.mcpp loop / usage fixpoint / capability binding, BEFORE the [targets.*] gate and the modgraph scan, and populates env.depDirs from the SAME authoritative dependencyEdges graph as the dep path (consumer index 0 = root; canonical + namespace-stripped spellings; same collision guard in contract_env). Ordering invariants, each verified at the new call point: (1) generated=/source= registration precedes the modgraph scan — the scan walks packages[0].manifest, so the new-source TAILS are mirrored into packages[0].manifest.{buildConfig.sources,modules.sources} (see wrinkle below); the scan call sits ~200 lines further down. (2) directive flags precede canonicalization/fingerprint — fpi.compileFlags = canonical_compile_flags(*m) + canonical_package_build_metadata(packages) is computed AFTER the scan, i.e. after this call; flag tails are mirrored into packages[0].privateBuild (per-TU assembly) AND packages[0].manifest.buildConfig (fingerprint metadata parity). (3) materialize_generated_files (root) still runs earlier — untouched at its pre-move position, so [generated_files] may still produce build.mcpp. (4) L1 cfg-conditional merge ordering unchanged — it still runs before makePackageRoot; ONLY the build.mcpp call moved later (e2e 108 green). (5) root features env — recomputed with the byte-identical expression (feature_closure(*m, parse_feature_request(overrides.features))) so the contract hash / build.mcpp cache is stable across the move. The wrinkle the old ordering hid: apply() used to mutate *m before `packages[0] = makePackageRoot(*root, *m)` snapshotted buildConfig into privateBuild/manifest — the copies the scan and per-TU flag assembly read. Post-move the snapshot (and root feature activation on it) already happened, so the directive tails are mirrored explicitly, dep-loop style: sources → manifest.buildConfig.sources + manifest.modules.sources; c/cxx flags → privateBuild + manifest.buildConfig; ldflags → linkUsage + manifest.buildConfig (final link reads *m, already applied); include-dir → privateBuild.includeDirs ONLY (private discipline — under the old order they also leaked into root publicUsage via the snapshot, harmless with no consumers but now deliberately private); include-dir-after → manifest.buildConfig.includeDirsAfter. Known benign delta: directive flags now append AFTER feature/usage-propagated flags in privateBuild instead of before them (order within one flags vector). e2e 145_root_build_mcpp_dep_dirs.sh (modeled on 125): root with a path dep; the ROOT's build.mcpp hard-fails unless dep_dir("datad") is set, generates a source returning it; asserts the dir exists and is datad's root. Docs: MCPP_DEP_<NAME>_DIR documented in the env table (en/zh) — previously undocumented — with the root now receiving it. Verified: unit 35/35; e2e 145/110/111/112/125/143/144 green + regression batch 106/107/108/109/114/100/04/09/15/126/128 green.
1 parent c8bc6af commit f0d2f19

4 files changed

Lines changed: 191 additions & 31 deletions

File tree

docs/07-build-mcpp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ The running program receives the build context as `MCPP_*` variables
120120
| `MCPP_MANIFEST_DIR` | `mcpp::manifest_dir()` | the package root (= CWD) |
121121
| `MCPP_FEATURE_<NAME>` | `mcpp::has_feature("name")` | set to `1` per active feature (same `<NAME>` sanitization as the `MCPP_FEATURE_` compile macro) |
122122
| `MCPP_FEATURES` || comma-separated active feature list |
123+
| `MCPP_DEP_<NAME>_DIR` | `mcpp::dep_dir("name")` | the resolved install dir of each declared dependency (canonical **and** namespace-stripped name spellings; same `<NAME>` sanitization as `MCPP_FEATURE_`). Received by dependencies' build.mcpp **and** the root project's (the root runs after dependency resolution) |
123124

124125
These values are folded into the re-run key **unconditionally** — changing the
125126
target, profile, or feature set re-runs the program without any

docs/zh/07-build-mcpp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ int main() {
112112
| `MCPP_MANIFEST_DIR` | `mcpp::manifest_dir()` | 包根(= CWD) |
113113
| `MCPP_FEATURE_<NAME>` | `mcpp::has_feature("name")` | 每个活跃 feature 置 `1`(`<NAME>` 消毒规则与 `MCPP_FEATURE_` 编译宏一致) |
114114
| `MCPP_FEATURES` || 活跃 feature 逗号列表 |
115+
| `MCPP_DEP_<NAME>_DIR` | `mcpp::dep_dir("name")` | 每个已声明依赖解析后的安装目录(canonical 名与去命名空间短名两种拼写都可用;`<NAME>` 消毒规则同 `MCPP_FEATURE_`)。依赖包的 build.mcpp ****根工程的 build.mcpp 都能拿到(根工程的 build.mcpp 在依赖解析之后运行) |
115116

116117
这些契约值**无条件**折入重跑键——换 target、换 profile、开关 feature 都会触发重跑,
117118
不需要任何 `rerun-if-env-changed` 声明。

src/build/prepare.cppm

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,20 +1129,24 @@ prepare_build(bool print_fingerprint,
11291129
// self-describing. See docs: 2026-05-21-linux-sysroot-missing-kernel-headers.md
11301130

11311131
// ── L3: project-local `build.mcpp` imperative build program ─────────────
1132-
// Compiled with the HOST toolchain and run now — after target resolution
1133-
// + the L1 cfg-flag merge (buildConfig flags are final) and BEFORE the
1134-
// modgraph scan (so its `generated=` sources are picked up). Its stdout
1135-
// directives augment buildConfig; a declared-input cache re-runs it only
1136-
// when its source/inputs/env/contract change. It cannot gate the top-level
1137-
// dependency graph (leaf-only rule). Under a cross --target it runs with a
1138-
// host-resolved toolchain and sees MCPP_TARGET = the cross triple (G3).
1139-
// Dependencies' build.mcpp run in a later pass (G2), after their features
1140-
// are known. See .agents/docs/2026-06-30-l3-build-mcpp-implementation-design.md
1141-
// and 2026-07-17-asm-sources-and-general-build-capabilities-design.md §2.4.
1132+
// The ROOT program is compiled with the HOST toolchain and run AFTER
1133+
// dependency resolution + feature activation (so it receives
1134+
// MCPP_DEP_<NAME>_DIR like a dependency's does — design §3.1 item 4) and
1135+
// BEFORE the modgraph scan (so its `generated=`/`source=` sources are
1136+
// picked up) — see the call site further below, after the dep build.mcpp
1137+
// loop. Its stdout directives augment buildConfig; a declared-input cache
1138+
// re-runs it only when its source/inputs/env/contract change. It cannot
1139+
// gate the top-level dependency graph (leaf-only rule). Under a cross
1140+
// --target it runs with a host-resolved toolchain and sees MCPP_TARGET =
1141+
// the cross triple (G3).
1142+
// See .agents/docs/2026-06-30-l3-build-mcpp-implementation-design.md,
1143+
// 2026-07-17-asm-sources-and-general-build-capabilities-design.md §2.4 and
1144+
// 2026-07-19-large-source-pkg-platform-fixes-and-buildmcpp-generation-design.md.
11421145
// Root [generated_files]: materialize before build.mcpp and the modgraph
1143-
// scan so synthesized sources are globbed like any on-disk file. (The
1144-
// per-dependency call sits in the dep resolution loop below; the root
1145-
// manifest needs its own.)
1146+
// scan so synthesized sources are globbed like any on-disk file — and
1147+
// BEFORE dependency resolution, since generated_files may produce
1148+
// build.mcpp itself. (The per-dependency call sits in the dep resolution
1149+
// loop below; the root manifest needs its own.)
11461150
if (!m->buildConfig.generatedFiles.empty()) {
11471151
if (auto r = materialize_generated_files(*root, *m); !r) {
11481152
return std::unexpected(r.error());
@@ -1204,21 +1208,6 @@ prepare_build(bool print_fingerprint,
12041208
return *hostTcCache;
12051209
};
12061210

1207-
if (std::filesystem::exists(*root / "build.mcpp")) {
1208-
auto host = host_tc_for_build_program();
1209-
if (!host) return std::unexpected(host.error());
1210-
mcpp::build::BuildProgramEnv bpEnv;
1211-
bpEnv.targetTriple = resolvedTargetCanonical;
1212-
bpEnv.profile = effectiveProfile;
1213-
bpEnv.features = feature_closure(*m, parse_feature_request(overrides.features));
1214-
if (auto bp = mcpp::build::run_build_program(
1215-
*m, *root, host->first, host->second,
1216-
m->cppStandard.canonical, bpEnv);
1217-
!bp) {
1218-
return std::unexpected(bp.error());
1219-
}
1220-
}
1221-
12221211
// Resolve dependencies: walk the **transitive** graph from the main
12231212
// manifest, BFS-style. Each unique `(namespace, shortName)` is fetched
12241213
// once, its `[build].include_dirs` are propagated to the main
@@ -3051,9 +3040,8 @@ prepare_build(bool print_fingerprint,
30513040
// under BOTH its canonical package name AND its namespace-stripped
30523041
// short name, so `mcpp::dep_dir("compat.zlib")` and
30533042
// `mcpp::dep_dir("zlib")` both resolve regardless of which spelling
3054-
// the author used in `deps`. (The ROOT project's own build.mcpp runs
3055-
// before dependency resolution, so it does not yet receive these —
3056-
// tracked as a follow-up in the #230-#243 ledger.)
3043+
// the author used in `deps`. (The ROOT project's build.mcpp gets
3044+
// the same treatment at its own call site right after this loop.)
30573045
for (auto const& edge : dependencyEdges) {
30583046
if (edge.consumerPackageIndex != i) continue;
30593047
auto const& depPkg = packages[edge.dependencyPackageIndex];
@@ -3176,6 +3164,96 @@ prepare_build(bool print_fingerprint,
31763164
}
31773165
}
31783166

3167+
// ── L3: ROOT build.mcpp (moved after dependency resolution, design §3.1
3168+
// item 4) ────────────────────────────────────────────────────────────────
3169+
// Runs HERE — after dep resolution + feature activation (so the contract
3170+
// env can expose MCPP_DEP_<NAME>_DIR exactly like the dep loop above does)
3171+
// and BEFORE the modgraph scan / flag canonicalization / fingerprint (so
3172+
// its generated=/source= sources and flag directives are fully visible).
3173+
// Ordering invariants preserved relative to the pre-move call site:
3174+
// materialize_generated_files (may produce build.mcpp itself) and the L1
3175+
// cfg merge still run earlier — ONLY this call moved later.
3176+
//
3177+
// One wrinkle the old ordering hid: back then apply() mutated *m BEFORE
3178+
// `packages[0] = makePackageRoot(*root, *m)` snapshotted buildConfig into
3179+
// privateBuild/manifest — the copies the scan and per-TU flag assembly
3180+
// actually read. Now the snapshot (and root feature activation on it)
3181+
// already happened, so mirror the directive TAILS into packages[0]
3182+
// explicitly, the same way the dep loop does for its package.
3183+
if (std::filesystem::exists(*root / "build.mcpp")) {
3184+
auto host = host_tc_for_build_program();
3185+
if (!host) return std::unexpected(host.error());
3186+
mcpp::build::BuildProgramEnv bpEnv;
3187+
bpEnv.targetTriple = resolvedTargetCanonical;
3188+
bpEnv.profile = effectiveProfile;
3189+
// Same expression as the pre-move call site (and same order), so the
3190+
// contract hash — and therefore the build.mcpp cache — is unchanged
3191+
// across the move for feature-identical builds.
3192+
bpEnv.features = feature_closure(*m, parse_feature_request(overrides.features));
3193+
// mcpp#241 (root): the root's resolved direct deps, from the same
3194+
// authoritative edge graph as the dep loop (consumer index 0 = root),
3195+
// emitted under canonical AND namespace-stripped names.
3196+
for (auto const& edge : dependencyEdges) {
3197+
if (edge.consumerPackageIndex != 0) continue;
3198+
auto const& depPkg = packages[edge.dependencyPackageIndex];
3199+
const auto& canon = depPkg.manifest.package.name;
3200+
bpEnv.depDirs.emplace_back(canon, depPkg.root);
3201+
if (auto dot = canon.rfind('.'); dot != std::string::npos
3202+
&& dot + 1 < canon.size())
3203+
bpEnv.depDirs.emplace_back(canon.substr(dot + 1), depPkg.root);
3204+
}
3205+
auto& bcRoot = m->buildConfig;
3206+
const auto rcN = bcRoot.cflags.size(), rcxN = bcRoot.cxxflags.size(),
3207+
rldN = bcRoot.ldflags.size(), rsrcN = bcRoot.sources.size(),
3208+
rincN = bcRoot.includeDirs.size(),
3209+
rincAfterN = bcRoot.includeDirsAfter.size(),
3210+
rmodN = m->modules.sources.size();
3211+
if (auto bp = mcpp::build::run_build_program(
3212+
*m, *root, host->first, host->second,
3213+
m->cppStandard.canonical, bpEnv);
3214+
!bp) {
3215+
return std::unexpected(bp.error());
3216+
}
3217+
auto& pkg0 = packages[0];
3218+
// Sources → the scan walks packages[0].manifest, not *m.
3219+
pkg0.manifest.buildConfig.sources.insert(
3220+
pkg0.manifest.buildConfig.sources.end(),
3221+
bcRoot.sources.begin() + rsrcN, bcRoot.sources.end());
3222+
pkg0.manifest.modules.sources.insert(
3223+
pkg0.manifest.modules.sources.end(),
3224+
m->modules.sources.begin() + rmodN, m->modules.sources.end());
3225+
// Compile flags → the root's TUs read privateBuild (and the
3226+
// fingerprint folds packages[].manifest.buildConfig via
3227+
// canonical_package_build_metadata) — mirror both, as the old
3228+
// pre-snapshot ordering implicitly did.
3229+
pkg0.privateBuild.cflags.insert(pkg0.privateBuild.cflags.end(),
3230+
bcRoot.cflags.begin() + rcN, bcRoot.cflags.end());
3231+
pkg0.privateBuild.cxxflags.insert(pkg0.privateBuild.cxxflags.end(),
3232+
bcRoot.cxxflags.begin() + rcxN, bcRoot.cxxflags.end());
3233+
pkg0.manifest.buildConfig.cflags.insert(
3234+
pkg0.manifest.buildConfig.cflags.end(),
3235+
bcRoot.cflags.begin() + rcN, bcRoot.cflags.end());
3236+
pkg0.manifest.buildConfig.cxxflags.insert(
3237+
pkg0.manifest.buildConfig.cxxflags.end(),
3238+
bcRoot.cxxflags.begin() + rcxN, bcRoot.cxxflags.end());
3239+
// Link flags → the final link reads *m (already applied); keep the
3240+
// linkUsage snapshot equivalent too.
3241+
pkg0.linkUsage.ldflags.insert(pkg0.linkUsage.ldflags.end(),
3242+
bcRoot.ldflags.begin() + rldN, bcRoot.ldflags.end());
3243+
pkg0.manifest.buildConfig.ldflags.insert(
3244+
pkg0.manifest.buildConfig.ldflags.end(),
3245+
bcRoot.ldflags.begin() + rldN, bcRoot.ldflags.end());
3246+
// include-dir directives: PRIVATE (already absolute from parse_line) —
3247+
// privateBuild only, never publicUsage (see the dep loop's rationale).
3248+
for (auto it = bcRoot.includeDirs.begin() + rincN;
3249+
it != bcRoot.includeDirs.end(); ++it)
3250+
appendUniquePath(pkg0.privateBuild.includeDirs, *it);
3251+
pkg0.manifest.buildConfig.includeDirsAfter.insert(
3252+
pkg0.manifest.buildConfig.includeDirsAfter.end(),
3253+
bcRoot.includeDirsAfter.begin() + rincAfterN,
3254+
bcRoot.includeDirsAfter.end());
3255+
}
3256+
31793257
// [targets.*] required_features gate: a target is emitted only when ALL its
31803258
// required features are active in this build; otherwise it is silently
31813259
// skipped. A pure build-selection knob — it runs before the modgraph/plan
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# P1 (large-source-pkg design §3.1 item 4): the ROOT project's build.mcpp now
4+
# runs AFTER dependency resolution and receives MCPP_DEP_<NAME>_DIR exactly
5+
# like a dependency's build.mcpp does (mcpp#241 contract, previously dep-only:
6+
# the root ran before resolution and saw none). Scenario mirrors
7+
# 125_build_mcpp_dep_dir.sh, but the build.mcpp asserting dep_dir() is the
8+
# ROOT's own: root -> datad (path dep); root build.mcpp reads
9+
# dep_dir("datad"), fails loudly if unset, and generates a source returning
10+
# it; main prints it; the test asserts it is datad's real root.
11+
set -e
12+
13+
TMP=$(mktemp -d)
14+
trap "rm -rf $TMP" EXIT
15+
cd "$TMP"
16+
17+
# data-asset dependency
18+
mkdir -p datad/src
19+
echo 'int datad_touch() { return 1; }' > datad/src/d.cpp
20+
cat > datad/mcpp.toml <<'EOF'
21+
[package]
22+
name = "datad"
23+
version = "0.1.0"
24+
[modules]
25+
sources = ["src/**/*.cpp"]
26+
[targets.datad]
27+
kind = "lib"
28+
EOF
29+
30+
# ROOT project whose own build.mcpp locates datad's dir
31+
mkdir -p rootp/src
32+
cat > rootp/build.mcpp <<'EOF'
33+
#include <cstdio>
34+
#include <string>
35+
import mcpp;
36+
int main() {
37+
const char* d = mcpp::dep_dir("datad");
38+
if (d == nullptr || d[0] == '\0') {
39+
std::fprintf(stderr, "build.mcpp: MCPP_DEP_DATAD_DIR not set for the ROOT\n");
40+
return 1; // the moved call point must expose the contract var
41+
}
42+
std::string out = "src/depdir.cpp";
43+
std::FILE* f = std::fopen(out.c_str(), "w");
44+
std::fprintf(f, "const char* root_datad_dir() { return \"%s\"; }\n", d);
45+
std::fclose(f);
46+
mcpp::generated("src/depdir.cpp");
47+
return 0;
48+
}
49+
EOF
50+
cat > rootp/src/main.cpp <<'EOF'
51+
import std;
52+
extern const char* root_datad_dir();
53+
int main() {
54+
std::println("DEPDIR={}", root_datad_dir());
55+
return 0;
56+
}
57+
EOF
58+
cat > rootp/mcpp.toml <<'EOF'
59+
[package]
60+
name = "rootp"
61+
version = "0.1.0"
62+
[modules]
63+
sources = ["src/**/*.cpp"]
64+
[dependencies]
65+
datad = { path = "../datad" }
66+
[targets.rootp]
67+
kind = "bin"
68+
main = "src/main.cpp"
69+
EOF
70+
71+
cd rootp
72+
"$MCPP" build > build.log 2>&1 || { cat build.log; echo "FAIL: build failed"; exit 1; }
73+
74+
out="$("$MCPP" run 2>&1 | grep '^DEPDIR=' | tail -1)"
75+
dir="${out#DEPDIR=}"
76+
[[ -n "$dir" ]] || { echo "FAIL: empty dep dir (root contract var missing)"; exit 1; }
77+
[[ -d "$dir" ]] || { echo "FAIL: dep dir does not exist: $dir"; exit 1; }
78+
[[ "$(basename "$dir")" == "datad" ]] || { echo "FAIL: dep dir is not datad's root: $dir"; exit 1; }
79+
80+
echo "OK"

0 commit comments

Comments
 (0)