Skip to content

Commit 23c9590

Browse files
authored
2026.10.1.2: a pack states its build, every drive takes the job count, and a unit's compile does not depend on the member selection (#751, #753) (#754)
Implements .agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md and resolves #751 and #753. - Every ninja drive takes the job count and the stale-token reclaim from the plan, and is reported whenever the command opened the report, unless its caller writes its own lines (mcpp test's per-test drives). - mcpp pack states its build: package lines, the status row, and Finished before the first Packing line; one Finished for several configurations; the dispatch pass is reported; a pack fills the global dependency cache. - Every package's BMIs lie below its own directory except the root's, as object files have since #233; a unit reads one module map of what it reaches through its imports (a GCC mapper, a clang argument file of -fmodule-file= lines, an MSVC command file of /reference lines). - A file a member lists from outside its directory belongs to that member, not to the workspace's virtual root. - Position independence follows the target: every unit of an ELF target that is not freestanding is compiled with -fPIC. Verification: CI on 71d09f0 (42 passed; the two xcode-27 jobs fail as on main, #669). GalTranslPP built with mcpp from this branch on windows-2025, run 36814777279 on the final head: mcpp run -p GPPCLI after build --workspace took 10 s (3m22s on 2026.9.30.2), both release packs 89 s.
1 parent 80d1fde commit 23c9590

37 files changed

Lines changed: 2431 additions & 276 deletions

‎.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md‎

Lines changed: 662 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env bash
2+
# Sandbox verification of the release that implements
3+
# .agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md
4+
# (#751, #753), run against the PUBLISHED release inside an xlings sandbox:
5+
#
6+
# B64=$(base64 -w0 .agents/docs/2026-10-01-pack-drive-and-selection-verify.sh)
7+
# xlings subos new v1002 2>/dev/null || true
8+
# xlings subos use v1002 --sandbox --cmd "echo $B64 | base64 -d > /tmp/v.sh && VER=<version> bash /tmp/v.sh"
9+
#
10+
# VER selects the release under test. Running it with VER=2026.10.1.1 is the
11+
# control: every section marked CHANGE must fail there, and every other
12+
# section must pass on both.
13+
#
14+
# Every probe directory is removed at the start of its section, because the
15+
# sandbox's $HOME persists between runs of the same subos. A section that does
16+
# not run is reported as SKIP and counted apart from a pass.
17+
set -u
18+
VER="${VER:?VER=<version under test>}"
19+
W="${W:-$HOME/v1002}"
20+
fails=0; passes=0; skips=0
21+
pass() { echo "PASS $1"; passes=$((passes+1)); }
22+
fail() { echo "FAIL $1"; [ -n "${2:-}" ] && [ -f "$2" ] && tail -20 "$2"; fails=$((fails+1)); }
23+
skip() { echo "SKIP $1"; skips=$((skips+1)); }
24+
25+
# 0. The release under test, from the published channel, with the CN mirror.
26+
if [ -n "${MCPP_OVERRIDE:-}" ]; then
27+
MCPP="$MCPP_OVERRIDE"
28+
else
29+
xlings config --mirror CN >/dev/null 2>&1 || true
30+
xlings update >/dev/null 2>&1 || true
31+
xlings install "mcpp@$VER" -y > /tmp/v1002-install.log 2>&1 || true
32+
MCPP="$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp"
33+
fi
34+
if [ ! -x "$MCPP" ]; then
35+
echo "FATAL: mcpp $VER is not installable from the index"; tail -20 /tmp/v1002-install.log; exit 2
36+
fi
37+
got=$("$MCPP" --version 2>&1 | head -1)
38+
case "$got" in *"$VER"*) pass "0 installed: $got";; *) fail "0 version: $got";; esac
39+
[ -n "${MCPP_OVERRIDE:-}" ] || "$MCPP" self config --mirror CN >/dev/null 2>&1 || true
40+
mkdir -p "$W"
41+
42+
# compiles_after <log> <lines>: the compile steps `.ninja_log` records after
43+
# its first <lines> lines.
44+
compiles_after() {
45+
tail -n +"$(( $2 + 1 ))" "$1" | awk -F'\t' '$4 ~ /\.(o|obj)$/ { n++ } END { print n + 0 }'
46+
}
47+
# no_compile <label> <selection...>: the build of the selection compiles nothing.
48+
no_compile() {
49+
local label="$1"; shift
50+
local log before n
51+
log=$(find target -name .ninja_log | head -1)
52+
before=$(wc -l < "$log")
53+
"$MCPP" build "$@" > nc.log 2>&1 || { fail "$label: build $* failed" nc.log; return 1; }
54+
n=$(compiles_after "$log" "$before")
55+
[ "$n" = 0 ] || { fail "$label: build $* compiled $n units" nc.log; return 1; }
56+
return 0
57+
}
58+
member() { # <dir> <name> <kind> [<dependency path>]
59+
mkdir -p "$1/src"
60+
{
61+
printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$2"
62+
[ -n "${4:-}" ] && printf '\n[dependencies]\ncore = { path = "%s" }\n' "$4"
63+
printf '\n[targets.%s]\nkind = "%s"\n' "$2" "$3"
64+
[ "$3" = bin ] && printf 'main = "src/main.cpp"\n'
65+
} > "$1/mcpp.toml"
66+
}
67+
68+
# 1. CHANGE (#751, GalTranslPP's shape): two members compile one file from
69+
# outside their directories; after --workspace, no selection compiles again.
70+
rm -rf "$W/s1"; mkdir -p "$W/s1/shared"; cd "$W/s1"
71+
printf '[workspace]\nmembers = ["core", "app", "tool"]\n' > mcpp.toml
72+
printf 'export module m;\nexport int answer() { return 42; }\n' > shared/m.cppm
73+
member core core lib; member app app bin ../core; member tool tool bin
74+
printf '\n[build]\nsources = ["src/*.cppm", "../shared/m.cppm"]\n' >> core/mcpp.toml
75+
printf '\n[build]\nsources = ["src/*.cpp", "../shared/m.cppm"]\n' >> tool/mcpp.toml
76+
printf 'export module corelib;\nimport m;\nexport int core_value() { return answer(); }\n' > core/src/core.cppm
77+
printf '#include <cstdio>\nimport corelib;\nint main() { std::printf("%%d\\n", core_value()); }\n' > app/src/main.cpp
78+
printf '#include <cstdio>\nimport m;\nint main() { std::printf("%%d\\n", answer() + 1); }\n' > tool/src/main.cpp
79+
if "$MCPP" build --workspace > s1.log 2>&1; then
80+
if no_compile "1" -p app && no_compile "1" -p tool && no_compile "1" --workspace && no_compile "1" -p app; then
81+
pass "1 CHANGE: a file two members list is compiled once for every selection"
82+
fi
83+
else fail "1 the workspace did not build" s1.log; fi
84+
85+
# 2. CHANGE: two members each with their own module `m`; alternating
86+
# selections compile nothing once each was built, and each program prints its
87+
# own value.
88+
rm -rf "$W/s2"; mkdir -p "$W/s2"; cd "$W/s2"
89+
printf '[workspace]\nmembers = ["core", "app", "tool"]\n' > mcpp.toml
90+
member core core lib; member app app bin ../core; member tool tool bin
91+
printf 'export module m;\nexport int answer() { return 42; }\n' > core/src/m.cppm
92+
printf 'export module corelib;\nimport m;\nexport int core_value() { return answer(); }\n' > core/src/core.cppm
93+
printf '#include <cstdio>\nimport corelib;\nint main() { std::printf("%%d\\n", core_value()); }\n' > app/src/main.cpp
94+
printf 'export module m;\nexport int answer() { return 7; }\n' > tool/src/m.cppm
95+
printf '#include <cstdio>\nimport m;\nint main() { std::printf("%%d\\n", answer()); }\n' > tool/src/main.cpp
96+
if "$MCPP" build --workspace > s2.log 2>&1; then
97+
a=$(find target -path '*/bin/app/app' -type f | head -1); t=$(find target -path '*/bin/tool/tool' -type f | head -1)
98+
if [ "$("$a")" = 42 ] && [ "$("$t")" = 7 ] \
99+
&& no_compile "2" -p app && no_compile "2" -p tool && no_compile "2" -p app && no_compile "2" --workspace; then
100+
pass "2 CHANGE: two modules of one name, no recompile across selections, each program its own value"
101+
fi
102+
else fail "2 the workspace did not build" s2.log; fi
103+
104+
# 3. CHANGE: a member that builds a shared library does not change the other
105+
# members' commands.
106+
rm -rf "$W/s3"; mkdir -p "$W/s3"; cd "$W/s3"
107+
printf '[workspace]\nmembers = ["core", "app", "dso"]\n' > mcpp.toml
108+
member core core lib; member app app bin ../core; member dso dso shared
109+
printf 'export module corelib;\nexport int core_value() { return 3; }\n' > core/src/core.cppm
110+
printf 'import corelib;\nint main() { return core_value() == 3 ? 0 : 1; }\n' > app/src/main.cpp
111+
printf 'export module dsolib;\nexport int dso_value() { return 5; }\n' > dso/src/dso.cppm
112+
if "$MCPP" build --workspace > s3.log 2>&1; then
113+
if no_compile "3" -p app && no_compile "3" --workspace; then
114+
pass "3 CHANGE: a shared-library member leaves the other members' commands alone"
115+
fi
116+
else fail "3 the workspace did not build" s3.log; fi
117+
118+
# 4. CHANGE (#753): a pack states its build, and Finished precedes Packing.
119+
rm -rf "$W/s4"; mkdir -p "$W/s4"; cd "$W/s4"
120+
member . p4 bin
121+
printf 'int main() { return 0; }\n' > src/main.cpp
122+
if "$MCPP" pack --format tar > s4.log 2>&1; then
123+
fin=$(grep -n 'Finished' s4.log | head -1 | cut -d: -f1); pk=$(grep -n 'Packing' s4.log | head -1 | cut -d: -f1)
124+
if grep -q 'Compiling p4' s4.log && [ -n "$fin" ] && [ -n "$pk" ] && [ "$fin" -lt "$pk" ]; then
125+
pass "4 CHANGE: the pack states its build, Finished before Packing"
126+
else fail "4 CHANGE: the pack's build is not stated" s4.log; fi
127+
else fail "4 the pack failed" s4.log; fi
128+
129+
# 5. CHANGE: `[build] jobs = 1` bounds mcpp test's compiles.
130+
rm -rf "$W/s5"; mkdir -p "$W/s5"; cd "$W/s5"
131+
member . p5 bin
132+
printf '\n[build]\njobs = 1\n' >> mcpp.toml
133+
mkdir -p tests
134+
for i in 1 2 3 4; do
135+
printf 'constexpr unsigned long long s%s() { unsigned long long x = %s; for (unsigned k = 0; k < 600; ++k) for (unsigned j = 0; j < 600; ++j) x = x * 6364136223846793005ull + 1; return x; }\nunsigned long long v%s() { constexpr auto v = s%s(); return v; }\n' $i $i $i $i > src/u$i.cpp
136+
done
137+
printf 'int main() { return 0; }\n' > src/main.cpp
138+
printf 'int main() { return 0; }\n' > tests/t.cpp
139+
if "$MCPP" test > s5.log 2>&1; then
140+
log=$(find target -name .ninja_log | head -1)
141+
if awk -F'\t' 'NR > 1 && $4 ~ /\.o$/ { s[n] = $1; e[n] = $2; n++ }
142+
END { for (i = 0; i < n; i++) for (j = i + 1; j < n; j++) if (s[i] < e[j] && s[j] < e[i]) exit 1; exit (n < 4) }' "$log"; then
143+
pass "5 CHANGE: mcpp test runs one compile at a time under jobs = 1"
144+
else fail "5 CHANGE: mcpp test overlapped compiles under jobs = 1" "$log"; fi
145+
else fail "5 mcpp test failed" s5.log; fi
146+
147+
# 6. Index packages that provide modules build, run, and are served from the
148+
# global cache by a second project, their BMIs below their packages' directories.
149+
for d in s6a s6b; do
150+
rm -rf "$W/$d"; mkdir -p "$W/$d/src"
151+
printf '[package]\nname = "eco"\nversion = "0.1.0"\n\n[dependencies]\n"compat.zlib" = "*"\n"mcpplibs.cmdline" = "*"\n\n[targets.eco]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$W/$d/mcpp.toml"
152+
cat > "$W/$d/src/main.cpp" <<'EOF'
153+
#include <cstdio>
154+
#include <zlib.h>
155+
import mcpplibs.cmdline;
156+
int main() { std::printf("zlib %s\n", zlibVersion()); return 0; }
157+
EOF
158+
done
159+
cd "$W/s6a"
160+
if "$MCPP" run > s6a.log 2>&1 && grep -q '^zlib ' s6a.log; then
161+
cd "$W/s6b"
162+
if "$MCPP" run > s6b.log 2>&1 && grep -q '^zlib ' s6b.log && grep -q 'Cached mcpplibs.cmdline' s6b.log; then
163+
pass "6a index packages build, run, and are served from the cache"
164+
if [ -n "$(find target -path '*.cache/*cmdline*/*' -type f | head -1)" ]; then
165+
pass "6b CHANGE: a cached dependency's BMIs are staged below its package's directory"
166+
else fail "6b CHANGE: the cached BMIs are staged at their names" s6b.log; fi
167+
else fail "6a the second project was not served from the cache" s6b.log; fi
168+
else fail "6 index packages" s6a.log; fi
169+
170+
# 7. xlings, built from its source with the release under test.
171+
rm -rf "$W/s7"; mkdir -p "$W/s7"; cd "$W/s7"
172+
if git clone -q --depth 1 https://github.com/openxlings/xlings.git xlings > s7-clone.log 2>&1 \
173+
|| git clone -q --depth 1 https://gitee.com/openxlings/xlings.git xlings > s7-clone.log 2>&1; then
174+
cd xlings
175+
if "$MCPP" build > s7.log 2>&1; then
176+
x=$(find target -path '*/bin/xlings' -type f | head -1)
177+
if [ -n "$x" ] && "$x" --version > s7v.log 2>&1; then
178+
if no_compile "7" ; then pass "7 xlings builds with the release, runs, and a second build compiles nothing: $(head -1 s7v.log)"; fi
179+
else fail "7 the xlings that was built does not run" s7v.log; fi
180+
else fail "7 xlings does not build" s7.log; fi
181+
else skip "7 xlings could not be cloned"; fi
182+
183+
echo "---- $passes passed, $fails failed, $skips skipped"
184+
[ "$fails" = 0 ]

‎.agents/docs/README.md‎

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

21-
321 records.
21+
322 records.
2222

2323
## By subject
2424

@@ -30,6 +30,7 @@ Records that declare one. Everything else is listed by date below.
3030

3131
### design
3232

33+
- [A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751)](2026-10-01-pack-drive-and-selection-independent-compile-design.md) — active
3334
- [Member selection, build programs prepared once, a pack over several members, and the output streams of `mcpp run`: the plan for the release after 2026.9.30.2 (#748, #749, #750)](2026-09-30-member-selection-and-build-program-cost-plan.md) — landed
3435
- [The build's wall time, its progress count, a hang after the build, and #732 and #744: measurements and a remediation plan](2026-09-30-build-wall-time-progress-count-and-hang-plan.md) — landed
3536
- [Build output, revision 3: every package that does work is named, the live display is one line drawn in one write, and a repeated warning is stated once per file](2026-09-30-build-output-refinement-design.md) — landed
@@ -110,6 +111,9 @@ Records that declare one. Everything else is listed by date below.
110111

111112
## By date
112113

114+
### 2026-10
115+
116+
- [A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751)](2026-10-01-pack-drive-and-selection-independent-compile-design.md) — active
113117
### 2026-09
114118

115119
- [Member selection, build programs prepared once, a pack over several members, and the output streams of `mcpp run`: the plan for the release after 2026.9.30.2 (#748, #749, #750)](2026-09-30-member-selection-and-build-program-cost-plan.md) — landed

‎CHANGELOG.md‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,72 @@
44
> Each `## [<version>]` section is that release's notes. Entries are written in English
55
> from 2026.9.28.3 on; earlier entries remain as written.
66

7+
## [2026.10.1.2] - 2026-10-01
8+
9+
This release implements the design for a pack's build and a compile that does
10+
not depend on the member selection
11+
(`.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md`),
12+
and resolves mcpp#751 and mcpp#753. A project that imports a module of a
13+
dependency, and every project on an ELF target, is compiled once more after the
14+
upgrade, because its commands change (both under **Changed**).
15+
16+
### Fixed
17+
18+
- **A unit is compiled by the same command in every selection of a
19+
workspace.** The selections of one configuration share a build directory,
20+
and three facts about the whole graph reached the commands of members they
21+
did not concern, so each switch between `--workspace` and `-p` recompiled
22+
them (mcpp#751; GalTranslPP 3.1.3 recompiled its core for 3m22s):
23+
- a module name that two members provide moved every BMI of that name below
24+
its provider's directory, and told the importers so, only when the graph
25+
held both providers;
26+
- a file that a member lists from outside its own directory was owned by the
27+
workspace's virtual root, whose object census depended on how many members
28+
listed it;
29+
- a member that builds a shared library put `-fPIC` on every unit of the
30+
graph.
31+
A command now depends on the unit's package, the packages it reaches, the
32+
features the selection activates for it and the declarations a selected
33+
member holds as the root, and on nothing else in the graph. `mcpp build
34+
--workspace` followed by `mcpp build -p <member>` compiles nothing, and so
35+
does any alternation of selections (e2e 872).
36+
- **`mcpp pack` states the build it performs.** Its build wrote no package
37+
line, no status row and no `Finished`, so a release job whose pack
38+
recompiled mcpp showed `Planning` for six minutes (mcpp#753). A pack is now
39+
stated as `mcpp build` states one, with `Finished` before the first `Packing`
40+
line and one `Finished` for a pack over several configurations; the second
41+
pass of a dispatched `--format` is stated in the same way (e2e 871).
42+
- **`[build] jobs` bounds every command that compiles.** `mcpp test` and
43+
`mcpp pack` ran ninja's default number of jobs whatever `[build] jobs`,
44+
`--jobs` or `MCPP_JOBS` said, which on a machine with little memory exceeded
45+
the bound the key exists to enforce. The backend reads the job count from
46+
the plan, and the reclaim of stale `bmi_schedule = "on"` tokens, which ran
47+
only under `mcpp build`, runs before the first drive of every build
48+
directory (e2e 871).
49+
- **A pack fills the global dependency cache** with the dependencies its
50+
build compiled, as `mcpp build` and `mcpp test` do.
51+
52+
### Changed
53+
54+
- **Every package's BMIs lie below the package's directory, except the root
55+
package's.** The rule is the one object files have followed since mcpp#233:
56+
`gcm.cache/<package>/<module>.gcm` (`pcm.cache` with clang). A unit that
57+
imports a module of another package reads one module map of the modules it
58+
reaches through its imports: a mapper file with GCC, and an argument file
59+
`@<build directory>/modmap/<package>-<hash>.modmap` of `-fmodule-file=` or
60+
`/reference` lines with clang or MSVC. A project whose modules are all its
61+
own is laid out, and every command spelled, as before. A package whose
62+
module name another package of the graph provides is served from the global
63+
dependency cache again; 2026.9.30.2 compiled it in the project.
64+
- **Every unit of an ELF target that is not freestanding is compiled with
65+
`-fPIC`,** whether or not the graph links a shared library, as rustc's
66+
default relocation model does on these targets: an object compiled once
67+
serves a program and a shared object. Mach-O compilers produce
68+
position-independent code by default, PE has no such flag, and nothing
69+
changes on those targets or on freestanding ones. The global dependency
70+
cache keys these entries by the flag, so each is compiled once more and
71+
then served again.
72+
773
## [2026.10.1.1] - 2026-10-01
874

975
This release implements the plan for member selection, build programs prepared

‎docs/04-mcpp-toml.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,9 @@ jobs = "auto" # or a positive number; --jobs / MCPP_JOBS override it
819819
bmi_schedule = "off" # auto (default, = off) | on | off
820820
```
821821

822-
`jobs` is how many compiles run at once. `"auto"` is resolved **against the
822+
`jobs` is how many compiles run at once, in every command that compiles:
823+
`build`, `run`, `test` and `pack` (2026.10.1.2+; before it, `test` and `pack`
824+
ran ninja's default number of jobs whatever the key said). `"auto"` is resolved **against the
823825
machine doing the build**, never frozen into the manifest: it takes the physical
824826
core count on a heterogeneous CPU (a 13900K is 8 P-cores + 16 E-cores, so its 32
825827
threads are not 32 equal workers) and clamps that by free memory, because a

‎docs/05-dependencies.md‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -551,14 +551,22 @@ each of them may have its own module of one name.
551551
are built separately.
552552
- Two packages that one program links may not provide the same name. The build
553553
is refused, and the message names the package whose closure holds both.
554-
- When two packages of one build provide a name, each BMI lies below its
555-
package's directory in the build directory, and every compile that may import
556-
the name is told which one it means: through a module map with GCC, through
557-
`-fmodule-file=` with Clang, and through `/reference` with MSVC. When every
558-
name has one provider, the build directory and every command are as they
559-
were before.
560-
- A package that provides such a name is compiled in the project, not served
561-
from the global dependency cache.
554+
- Every package's BMIs lie below the package's directory in the build
555+
directory, `gcm.cache/<package>/` (`pcm.cache` with Clang), except the root
556+
package's, which lie at their names, as object files do (2026.10.1.2+). A
557+
compile that imports a module of another package is told where its BMI is,
558+
and where the BMIs of the modules that one imports are, through one module
559+
map: a mapper file with GCC (`-fmodule-mapper=`), and an argument file of
560+
`-fmodule-file=` lines with Clang or `/reference` lines with MSVC
561+
(`@<build directory>/modmap/<package>-<hash>.modmap`). The map lists what the
562+
unit reaches through its imports and nothing else, so the command does not
563+
change with the rest of the graph. A project whose modules are all its own
564+
is laid out, and every command spelled, as before.
565+
- Until 2026.10.1.2 a BMI moved below its package's directory only when two
566+
packages of the graph provided its name, and such a package was not served
567+
from the global dependency cache. Both depended on which packages a command
568+
selected, so a workspace member was compiled again at each switch between
569+
`-p` and `--workspace` (mcpp#751).
562570
- clangd finds a module by its name in the compilation database, so for a name
563571
two packages provide it may show the other program's module. The build is
564572
not affected.

0 commit comments

Comments
 (0)