Skip to content

Commit 2083588

Browse files
authored
2026.10.11.1: build dependencies on one pipeline — the build programs' graph, instances per configuration, build plugins, one library interface, CalVer deprecations (#798)
Closes #797. See the PR description for the design, the fixes and the tests.
1 parent 2bf99ee commit 2083588

236 files changed

Lines changed: 6449 additions & 4017 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md‎

Lines changed: 744 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env bash
2+
# Observations for .agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md.
3+
# Prints what each case does; asserts nothing. Usage: MCPP=/path/to/mcpp bash <this>
4+
#
5+
# R1 #797 body: a module wrapper over a header library, imported by build.mcpp
6+
# through `host-module = true` (its GMF #includes the package's own header).
7+
# R2 #797 comment: one package in [dependencies] and [build-dependencies].
8+
# R2b the same library split across two workspace members: m1 links it, m2's
9+
# build.mcpp imports it (works today; a regression guard).
10+
# R2c a library reached by the target through another library, and named
11+
# directly in [build-dependencies] (works today; a regression guard).
12+
# R3 a feature requested only by a [build-dependencies] edge reaches the
13+
# target instance of the same package.
14+
# R4 [workspace.xlings.overrides] on a rooted workspace whose root package
15+
# depends on a member by path.
16+
# R5 the same with the root's own [xlings] by position: the warning appears
17+
# on a plain `mcpp build` of the root package.
18+
# R6 a header that a path host module includes is edited; the build
19+
# program is neither recompiled nor rerun (its identity hashes the
20+
# interface file only).
21+
# R7 a build plugin built as the root of its own build (the shape of
22+
# examples/08-build-rules/rules-embed): the engine module does not exist.
23+
# R8 `mcpp emit build-database` and compile_commands.json do not describe
24+
# build.mcpp, the host modules or the engine module.
25+
# W1 a build.mcpp beside a workspace root without [package] is ignored
26+
# without a word (the package tables there are reported).
27+
# W2 `features` written in [workspace.dependencies] is dropped without a
28+
# word when a member inherits the entry.
29+
set -u
30+
MCPP="${MCPP:-mcpp}"
31+
T=$(mktemp -d)
32+
trap 'rm -rf "$T"' EXIT
33+
show() { echo "── $1"; shift; ( "$@" ) 2>&1 | grep -E "error|warning|fatal|FEATURE|ok:" | head -8; echo; }
34+
35+
# ── a header library with a module wrapper (the toml++ shape) ───────────────
36+
mklib() { # $1 = dir
37+
mkdir -p "$1/include/hl" "$1/src"
38+
cat > "$1/include/hl/hl.hpp" <<'EOF'
39+
#pragma once
40+
namespace hl { inline int answer() { return 42; } }
41+
EOF
42+
cat > "$1/src/hl.cppm" <<'EOF'
43+
module;
44+
#include <hl/hl.hpp>
45+
export module hl;
46+
export namespace hl { using hl::answer; }
47+
#ifdef HL_FEATURE_X
48+
export namespace hl { inline int feature_x() { return 1; } }
49+
#endif
50+
EOF
51+
cat > "$1/mcpp.toml" <<'EOF'
52+
[package]
53+
name = "hl"
54+
version = "0.1.0"
55+
[lib]
56+
path = "src/hl.cppm"
57+
[build]
58+
sources = ["src/hl.cppm"]
59+
include_dirs = ["include"]
60+
[features]
61+
x = { defines = ["HL_FEATURE_X"] }
62+
[targets.hl]
63+
kind = "lib"
64+
EOF
65+
}
66+
mklib "$T/hl"
67+
# The same library with the include made relative to the unit -- the shape
68+
# mcpp-index uses today to get past R1 -- so that R2 isolates the edge merge.
69+
mklib "$T/hlrel"
70+
sed -i 's|#include <hl/hl.hpp>|#include "../include/hl/hl.hpp"|' "$T/hlrel/src/hl.cppm"
71+
72+
mkapp() { # $1 = dir, $2 = manifest tail
73+
mkdir -p "$1/src"
74+
printf '[package]\nname = "app"\nversion = "0.1.0"\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n%s\n' "$2" > "$1/mcpp.toml"
75+
}
76+
77+
# R1
78+
mkapp "$T/r1" $'[build-dependencies]\nhl = { path = "../hl", host-module = true }'
79+
echo 'int main() { return 0; }' > "$T/r1/src/main.cpp"
80+
printf 'import std;\nimport hl;\nint main() { std::println("mcpp:cxxflag=-DANSWER={}", hl::answer()); }\n' > "$T/r1/build.mcpp"
81+
show "R1 host-module over a header library" bash -c "cd $T/r1 && $MCPP build"
82+
83+
# R2
84+
mkapp "$T/r2" $'[dependencies]\nhl = { path = "../hlrel" }\n[build-dependencies]\nhl = { path = "../hlrel", host-module = true }'
85+
printf 'import std;\nimport hl;\nint main() { std::println("ok: {}", hl::answer()); }\n' > "$T/r2/src/main.cpp"
86+
printf 'import std;\nimport hl;\nint main() { std::println("mcpp:cxxflag=-DANSWER={}", hl::answer()); }\n' > "$T/r2/build.mcpp"
87+
show "R2 one package in both tables (target import)" bash -c "cd $T/r2 && $MCPP build"
88+
89+
# R2b
90+
mkdir -p "$T/r2b"
91+
printf '[workspace]\nmembers = ["m1", "m2"]\n' > "$T/r2b/mcpp.toml"
92+
mkapp "$T/r2b/m1" $'[dependencies]\nhl = { path = "../../hlrel" }'
93+
sed -i 's/name = "app"/name = "m1"/; s/targets.app/targets.m1/' "$T/r2b/m1/mcpp.toml"
94+
printf 'import std;\nimport hl;\nint main() { std::println("ok: m1 {}", hl::answer()); }\n' > "$T/r2b/m1/src/main.cpp"
95+
mkapp "$T/r2b/m2" $'[build-dependencies]\nhl = { path = "../../hlrel", host-module = true }'
96+
sed -i 's/name = "app"/name = "m2"/; s/targets.app/targets.m2/' "$T/r2b/m2/mcpp.toml"
97+
printf 'import std;\nint main() { std::println("ok: m2 {}", ANSWER); }\n' > "$T/r2b/m2/src/main.cpp"
98+
printf 'import std;\nimport hl;\nint main() { std::println("mcpp:cxxflag=-DANSWER={}", hl::answer()); }\n' > "$T/r2b/m2/build.mcpp"
99+
show "R2b workspace split" bash -c "cd $T/r2b && $MCPP build --workspace && $MCPP run -p m1 && $MCPP run -p m2"
100+
101+
# R2c
102+
mkdir -p "$T/lib/src"
103+
printf '[package]\nname = "lib"\nversion = "0.1.0"\n[dependencies]\nhl = { path = "../hlrel" }\n[targets.lib]\nkind = "lib"\n' > "$T/lib/mcpp.toml"
104+
printf 'export module lib;\nimport hl;\nexport int via_lib() { return hl::answer(); }\n' > "$T/lib/src/lib.cppm"
105+
mkapp "$T/r2c" $'[dependencies]\nlib = { path = "../lib" }\n[build-dependencies]\nhl = { path = "../hlrel", host-module = true }'
106+
printf 'import std;\nimport lib;\nint main() { std::println("ok: {} {}", via_lib(), ANSWER); }\n' > "$T/r2c/src/main.cpp"
107+
cp "$T/r2b/m2/build.mcpp" "$T/r2c/build.mcpp"
108+
show "R2c transitive target use and a direct build dependency" bash -c "cd $T/r2c && $MCPP build && $MCPP run"
109+
110+
# R3 -- the library itself is a dependency of `mid`, which asks for feature x
111+
# only in its [build-dependencies]; the app imports hl for its target.
112+
mkdir -p "$T/mid/src"
113+
printf '[package]\nname = "mid"\nversion = "0.1.0"\n[build-dependencies]\nhl = { path = "../hl", features = ["x"] }\n[targets.mid]\nkind = "lib"\n' > "$T/mid/mcpp.toml"
114+
printf 'export module mid;\nexport int mid_v() { return 1; }\n' > "$T/mid/src/mid.cppm"
115+
mkapp "$T/r3" $'[dependencies]\nhl = { path = "../hl" }\nmid = { path = "../mid" }'
116+
cat > "$T/r3/src/main.cpp" <<'EOF'
117+
import std;
118+
import hl;
119+
int main() {
120+
#ifdef HL_FEATURE_X
121+
std::println("FEATURE x reached the target");
122+
#endif
123+
std::println("ok: {}", hl::answer());
124+
}
125+
EOF
126+
show "R3 a build-only feature request and the target instance" bash -c "cd $T/r3 && $MCPP build && $MCPP run"
127+
grep -rhoE -- "-DHL_FEATURE_X" "$T/r3/target" 2>/dev/null | head -1 | sed 's/^/FEATURE define in target compile: /'
128+
129+
# R4, R5
130+
mkws() { # $1 = dir, $2 = root tail
131+
mkdir -p "$1/src" "$1/sub/src"
132+
printf '[package]\nname = "root"\nversion = "0.1.0"\n[targets.root]\nkind = "bin"\nmain = "src/main.cpp"\n[dependencies]\nsub = { path = "sub" }\n[workspace]\nmembers = ["sub"]\n%s\n' "$2" > "$1/mcpp.toml"
133+
echo 'int main() { return 0; }' > "$1/src/main.cpp"
134+
printf '[package]\nname = "sub"\nversion = "0.1.0"\n[targets.sub]\nkind = "lib"\n' > "$1/sub/mcpp.toml"
135+
printf 'export module sub;\nexport int f() { return 1; }\n' > "$1/sub/src/sub.cppm"
136+
}
137+
mkws "$T/r4" $'[workspace.xlings.overrides]\n"xim:cmake" = "/usr/bin/cmake"'
138+
show "R4 [workspace.xlings.overrides] with the root depending on a member" bash -c "cd $T/r4 && $MCPP build"
139+
mkws "$T/r5" $'[xlings.overrides]\n"xim:cmake" = "/usr/bin/cmake"'
140+
show "R5 the root's own [xlings] by position, plain build of the root" bash -c "cd $T/r5 && $MCPP build"
141+
142+
# R6 -- uses the relative-include library so that the host compile succeeds.
143+
mkapp "$T/r6" $'[build-dependencies]\nhl = { path = "../hlrel", host-module = true }'
144+
printf 'import std;\nint main() { std::println("ok: ANSWER={}", ANSWER); }\n' > "$T/r6/src/main.cpp"
145+
printf 'import std;\nimport hl;\nint main() { std::println("mcpp:cxxflag=-DANSWER={}", hl::answer()); }\n' > "$T/r6/build.mcpp"
146+
show "R6 before the header edit" bash -c "cd $T/r6 && $MCPP build >/dev/null && $MCPP run"
147+
sed -i 's/return 42/return 43/' "$T/hlrel/include/hl/hl.hpp"
148+
show "R6 after editing the header to 43" bash -c "cd $T/r6 && $MCPP build >/dev/null && $MCPP run"
149+
150+
# R7
151+
mkdir -p "$T/r7/src"
152+
printf '[package]\nname = "rule"\nversion = "0.1.0"\n[targets.rule]\nkind = "lib"\n' > "$T/r7/mcpp.toml"
153+
printf 'export module rule;\nimport std;\nimport mcpp;\nexport namespace rule { inline bool compile() { return true; } }\n' > "$T/r7/src/rule.cppm"
154+
show "R7 a build plugin as the root of its own build" bash -c "cd $T/r7 && $MCPP build"
155+
156+
# R8 -- r6 has a build.mcpp importing a host module
157+
( cd "$T/r6" && $MCPP emit build-database --format json 2>/dev/null ) > "$T/db.json"
158+
python3 - "$T/db.json" <<'PY'
159+
import json, sys
160+
doc = json.load(open(sys.argv[1]))["data"]["database"]
161+
def sources(o):
162+
if isinstance(o, dict):
163+
for k, v in o.items():
164+
if k == "source" and isinstance(v, str): yield v.replace("\\", "/").split("/")[-1]
165+
else: yield from sources(v)
166+
elif isinstance(o, list):
167+
for v in o: yield from sources(v)
168+
units = sorted(set(sources(doc)))
169+
print("── R8 build database")
170+
print("ok: translation units:", units)
171+
print("ok: build.mcpp / hl.cppm / mcpp.cppm described:",
172+
"build.mcpp" in units, "hl.cppm" in units, "mcpp.cppm" in units)
173+
PY
174+
echo
175+
176+
# W1
177+
mkdir -p "$T/w1/m/src"
178+
printf '[workspace]\nmembers = ["m"]\n' > "$T/w1/mcpp.toml"
179+
printf 'int main() { return 1; }\n' > "$T/w1/build.mcpp" # would fail the build if it ran
180+
printf '[package]\nname = "m"\nversion = "0.1.0"\n[targets.m]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$T/w1/m/mcpp.toml"
181+
echo 'int main() { return 0; }' > "$T/w1/m/src/main.cpp"
182+
show "W1 build.mcpp beside a virtual root (no output = ignored silently)" bash -c "cd $T/w1 && $MCPP build"
183+
184+
# W2
185+
mkdir -p "$T/w2/m/src"
186+
printf '[workspace]\nmembers = ["m"]\n[workspace.dependencies]\nhl = { path = "../hl", features = ["x"] }\n' > "$T/w2/mcpp.toml"
187+
printf '[package]\nname = "m"\nversion = "0.1.0"\n[dependencies]\nhl.workspace = true\n[targets.m]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$T/w2/m/mcpp.toml"
188+
cat > "$T/w2/m/src/main.cpp" <<'CPP'
189+
import std;
190+
import hl;
191+
int main() {
192+
#ifdef HL_FEATURE_X
193+
std::println("ok: feature x inherited");
194+
#else
195+
std::println("FEATURE x from [workspace.dependencies] was dropped");
196+
#endif
197+
}
198+
CPP
199+
show "W2 features in [workspace.dependencies]" bash -c "cd $T/w2 && $MCPP build && $MCPP run -p m"

‎.agents/docs/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
330 records.
21+
331 records.
2222

2323
## By subject
2424

2525
Records that declare one. Everything else is listed by date below.
2626

2727
### build-program
2828

29+
- [构建依赖的一条管线:配置与产物两轴、包的实例、库接口、兼容系统与工作空间收尾(#797)](2026-10-11-build-dependencies-one-pipeline-design.md) — active
2930
- [Four upstream asks from a UI framework: what each one is under mcpp's design, and the combined plan](2026-09-13-four-upstream-asks-from-a-ui-framework.md) — landed
3031

3132
### design
@@ -124,6 +125,7 @@ Records that declare one. Everything else is listed by date below.
124125

125126
### 2026-10
126127

128+
- [构建依赖的一条管线:配置与产物两轴、包的实例、库接口、兼容系统与工作空间收尾(#797)](2026-10-11-build-dependencies-one-pipeline-design.md) — active
127129
- [一处来源,一种含义:工作空间的传递语义、生成输出、模块角色与首次运行(#786、#785、#778、#790)](2026-10-10-one-source-one-meaning-design.md) — landed
128130
- [LLVM 23.1.3 Part 2:Linux aarch64 默认工具链与 glibc 生态闭环](2026-10-08-llvm-2313-linux-aarch64-ecosystem-part2-design.md) — active
129131
- [LLVM 23.1.3 全平台统一默认:跨仓库联动方案(mcpp × xim-pkgindex)](2026-10-07-llvm-2313-unified-default-cross-repo-design.md) — active

‎.edited‎

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env python3
2+
"""The deprecation registry, the code and docs/51 agree (mcpp#797).
3+
4+
modules/manifest/src/compat/registry.cppm is the one list of readings a user
5+
can write and that are to be removed. Three things are derived from it and can
6+
drift from it, so each is checked here:
7+
8+
1. Every `COMPAT(<id>)` marker in the code names a row, and every row has at
9+
least one marker -- the reading is somewhere, and it is registered.
10+
`COMPAT(internal:<topic>)` names engine-internal compatibility that no
11+
manifest can spell; it has no row and is not checked.
12+
2. docs/51 (English and Chinese) lists every row with its `since` and its
13+
`floor`.
14+
3. Each row's fields are present.
15+
16+
The removal itself -- that no reading outlives its floor -- is the unit test
17+
modules/manifest/tests/test_compat_registry.cpp.
18+
"""
19+
import pathlib
20+
import re
21+
import sys
22+
23+
ROOT = pathlib.Path(__file__).resolve().parents[2]
24+
REGISTRY = ROOT / "modules/manifest/src/compat/registry.cppm"
25+
DOCS = [ROOT / "docs/51-supported-versions.md", ROOT / "docs/zh/51-supported-versions.md"]
26+
27+
28+
def rows():
29+
text = REGISTRY.read_text(encoding="utf-8")
30+
floor = re.search(r'kRemovalFloor\s*=\s*"([^"]+)"', text).group(1)
31+
body = text[text.index("kDeprecations[] = {"):]
32+
body = body[:body.index("\n};")]
33+
out = []
34+
for block in re.findall(r"\{(.*?)\n \}", body, re.S):
35+
fields = re.findall(r'"((?:[^"\\]|\\.)*)"|(kRemovalFloor)', block)
36+
values = []
37+
pending = ""
38+
# Adjacent string literals concatenate, as in C++.
39+
tokens = re.findall(r'"((?:[^"\\]|\\.)*)"\s*|(kRemovalFloor)|(,)', block)
40+
for lit, name, comma in tokens:
41+
if comma:
42+
values.append(pending)
43+
pending = ""
44+
elif name:
45+
pending = floor
46+
else:
47+
pending += lit
48+
if pending:
49+
values.append(pending)
50+
values = [v for v in values]
51+
out.append(values)
52+
return out
53+
54+
55+
def main() -> int:
56+
failures = []
57+
registered = {}
58+
for r in rows():
59+
if len(r) < 7 or not all(r[:7]):
60+
failures.append(f"registry: a row has an empty field: {r}")
61+
continue
62+
registered[r[0]] = (r[1], r[2])
63+
64+
markers = {}
65+
for path in list(ROOT.glob("src/**/*.cpp*")) + list(ROOT.glob("modules/**/*.cpp*")):
66+
text = path.read_text(encoding="utf-8", errors="replace")
67+
for m in re.finditer(r"COMPAT\(([a-z0-9:<>-]+)\)", text):
68+
ident = m.group(1)
69+
if ident.startswith("internal:") or ident.startswith("<"):
70+
continue
71+
markers.setdefault(ident, []).append(str(path.relative_to(ROOT)))
72+
for ident, where in sorted(markers.items()):
73+
if ident not in registered:
74+
failures.append(f"COMPAT({ident}) in {where[0]} names no row of {REGISTRY.relative_to(ROOT)}")
75+
for ident in sorted(registered):
76+
if ident not in markers:
77+
failures.append(f"the row '{ident}' has no COMPAT({ident}) marker in the code")
78+
79+
for doc in DOCS:
80+
text = doc.read_text(encoding="utf-8")
81+
for ident, (since, floor) in sorted(registered.items()):
82+
line = next((l for l in text.splitlines() if f"`{ident}`" in l), None)
83+
if line is None:
84+
failures.append(f"{doc.relative_to(ROOT)} does not list `{ident}`")
85+
elif since not in line or floor not in line:
86+
failures.append(f"{doc.relative_to(ROOT)}: the row of `{ident}` does not state "
87+
f"{since} and {floor}")
88+
89+
for f in failures:
90+
print(f"FAIL: {f}")
91+
if failures:
92+
return 1
93+
print(f"ok: {len(registered)} deprecations, their markers and docs/51 agree")
94+
return 0
95+
96+
97+
if __name__ == "__main__":
98+
sys.exit(main())

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ jobs:
134134
- name: The key registry and the field reference agree
135135
run: python3 .github/tools/check_manifest_key_classes.py
136136

137+
- name: The deprecation registry, the code and docs/51 agree
138+
run: python3 .github/tools/check_compat_registry.py
139+
137140
- name: Documented target tiers agree with the table
138141
run: python3 .github/tools/check_target_tiers.py
139142

0 commit comments

Comments
 (0)