Skip to content

Commit 87cc232

Browse files
authored
2026.9.30.2: a build exits after ninja, the status row counts the work of the build, planning walks each tree once, a vendored xlings is replaced from the newest source (#744), and a module name is unique within one program (#732) (#745)
Five reports on 2026.9.30.1 while building xlings, analysed and planned in .agents/docs/2026-09-30-build-wall-time-progress-count-and-hang-plan.md. - A build no longer hangs after ninja: every loop of the stack animation grows the stack or ends; a property test drives every animation and game under a watchdog. - The status row counts the work of the build: the cache pass is reported by its Cached lines and not counted, scans that wait on no action run first as `Scanning f/t`, and `Building f/t` counts the main pass. - A vendored xlings is replaced from the newest source (#744): the override, otherwise the newer of the released copy and the PATH copy; Updating and Note once per process; the version is kept per process and under the home. - Planning walks each package tree once, revalidated by directory modification times, and states its phases under build/stage in the log. - A module name is unique within one program, not within one build (#732): imports are resolved in the importer's closure; two BMIs of one name lie below their packages' directories and each compile is bound to its provider (a module map for GCC, -fmodule-file= for clang, /reference for MSVC). Without a collision the build directory is byte-identical. - A BMI served from the global cache waits for the modules it imports that the build compiles (a build-program-generated module of a cached package). - Version 2026.9.30.2, CHANGELOG, guides in both languages, SPEC-005 v1.7. On xlings: ninja starts at 1.3 s instead of 4.5 s on a clean build; an edit of one source builds in 8.0 s instead of 10.5 s.
1 parent cc24173 commit 87cc232

41 files changed

Lines changed: 3571 additions & 269 deletions

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-09-30-build-wall-time-progress-count-and-hang-plan.md‎

Lines changed: 1062 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env bash
2+
# Sandbox verification of mcpp 2026.9.30.2 (.agents/docs/2026-09-30-build-
3+
# wall-time-progress-count-and-hang-plan.md), run against the PUBLISHED release
4+
# inside an xlings sandbox:
5+
#
6+
# B64=$(base64 -w0 .agents/docs/2026-09-30-build-wall-time-verify.sh)
7+
# xlings subos new v9302 2>/dev/null || true
8+
# xlings subos use v9302 --sandbox --cmd "echo $B64 | base64 -d > /tmp/v.sh && VER=2026.9.30.2 bash /tmp/v.sh"
9+
#
10+
# VER selects the release under test. Running it with VER=2026.9.30.1 is the
11+
# control: every section marked CHANGE must fail there, and every other section
12+
# 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 separately from a pass.
17+
set -u
18+
VER="${VER:-2026.9.30.2}"
19+
W="$HOME/v9302"
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 -15 "$2"; fails=$((fails+1)); }
23+
skip() { echo "SKIP $1"; skips=$((skips+1)); }
24+
25+
# ── 0. the release under test, from the published channel ──────────────────
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/v9302-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/v9302-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+
"$MCPP" self config --mirror CN >/dev/null 2>&1 || true
40+
mkdir -p "$W"
41+
42+
module_file() { printf 'export module boost;\nexport int value() { return %s; }\n' "$2" > "$1"; }
43+
main_file() { printf '#include <cstdio>\nimport boost;\nint main() { std::printf("%%d\\n", value()); }\n' > "$1"; }
44+
bin_of() { find "$1" -path '*/bin/*' -name "$2" -type f | head -1; }
45+
46+
# ── 1. CHANGE: #732, an artifacts program with its own module of one name ──
47+
rm -rf "$W/s1"; mkdir -p "$W/s1/app/src" "$W/s1/updater/src"; cd "$W/s1"
48+
module_file app/src/boost.cppm 1; main_file app/src/main.cpp
49+
module_file updater/src/boost.cppm 2; main_file updater/src/main.cpp
50+
printf '[package]\nname = "updater"\nversion = "0.1.0"\n\n[targets.updater]\nkind = "bin"\nmain = "src/main.cpp"\n' > updater/mcpp.toml
51+
printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nupdater = { path = "../updater", artifacts = ["updater"] }\n\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > app/mcpp.toml
52+
if (cd app && "$MCPP" build > ../s1.log 2>&1) \
53+
&& [ "$("$(bin_of app/target app)")" = 1 ] && [ "$("$(bin_of app/target updater)")" = 2 ]; then
54+
pass "1 CHANGE: the app and its artifacts updater each have their own module boost"
55+
else fail "1 CHANGE: two programs, one module name" s1.log; fi
56+
57+
# ── 2. #732, two providers in one program are refused ───────────────────────
58+
rm -rf "$W/s2"; mkdir -p "$W/s2/lib1/src" "$W/s2/lib2/src" "$W/s2/prog/src"; cd "$W/s2"
59+
module_file lib1/src/boost.cppm 1; module_file lib2/src/boost.cppm 2
60+
for l in lib1 lib2; do printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$l" > $l/mcpp.toml; done
61+
printf 'int main() { return 0; }\n' > prog/src/main.cpp
62+
printf '[package]\nname = "prog"\nversion = "0.1.0"\n\n[dependencies]\nlib1 = { path = "../lib1" }\nlib2 = { path = "../lib2" }\n\n[targets.prog]\nkind = "bin"\nmain = "src/main.cpp"\n' > prog/mcpp.toml
63+
if (cd prog && "$MCPP" build > ../s2.log 2>&1); then fail "2 two providers in one program were accepted" s2.log
64+
elif grep -q "module 'boost' is provided by package" s2.log; then pass "2 two providers in one program are refused"
65+
else fail "2 the refusal does not name the module" s2.log; fi
66+
67+
# ── 3. CHANGE: #732, two workspace members with one module name ─────────────
68+
rm -rf "$W/s3"; mkdir -p "$W/s3/one/src" "$W/s3/two/src"; cd "$W/s3"
69+
module_file one/src/boost.cppm 5; main_file one/src/main.cpp
70+
module_file two/src/boost.cppm 6; main_file two/src/main.cpp
71+
printf '[workspace]\nmembers = ["one", "two"]\n' > mcpp.toml
72+
for m in one two; do printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' "$m" "$m" > $m/mcpp.toml; done
73+
if "$MCPP" build --workspace > s3.log 2>&1 \
74+
&& [ "$("$(bin_of target one)")" = 5 ] && [ "$("$(bin_of target two)")" = 6 ]; then
75+
pass "3 CHANGE: two workspace members each have their own module boost"
76+
else fail "3 CHANGE: two workspace members, one module name" s3.log; fi
77+
78+
# ── 4. index packages build and run with the release ───────────────────────
79+
rm -rf "$W/s4"; mkdir -p "$W/s4/src"; cd "$W/s4"
80+
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' > mcpp.toml
81+
cat > src/main.cpp <<'EOF'
82+
#include <cstdio>
83+
#include <zlib.h>
84+
import mcpplibs.cmdline;
85+
int heavy();
86+
int main() { std::printf("zlib %s\n", zlibVersion()); return heavy() == 0; }
87+
EOF
88+
# One unit that takes a few seconds to compile, so that section 5's build
89+
# outlives the half second before the status row is first drawn.
90+
cat > src/heavy.cpp <<'EOF'
91+
#include <format>
92+
#include <regex>
93+
#include <string>
94+
int heavy() {
95+
std::regex r("([a-z]+)-([0-9]+)");
96+
std::smatch m;
97+
std::string s = std::format("{}-{}", "mcpp", 2026);
98+
return std::regex_match(s, m, r) ? static_cast<int>(m.size()) : 0;
99+
}
100+
EOF
101+
if "$MCPP" build > s4.log 2>&1 && "$(bin_of target eco)" | grep -q '^zlib '; then
102+
pass "4 compat.zlib and mcpplibs.cmdline from the index build and run"
103+
else fail "4 index packages" s4.log; fi
104+
105+
# ── 5. CHANGE: the status row counts the work of the build ─────────────────
106+
# A pty makes the status row appear; the project of section 4 has a
107+
# dependency the global cache serves after its first build.
108+
cd "$W/s4"
109+
if command -v script > /dev/null 2>&1 && [ -f s4.log ] && grep -q 'Finished' s4.log; then
110+
"$MCPP" clean > /dev/null 2>&1
111+
MCPP_PROGRESS=plain script -qefc "stty cols 160 rows 40; $MCPP build" s5.pty > /dev/null 2>&1
112+
rows=$(sed 's/\x1b\[[0-9;?]*[A-Za-z]//g' s5.pty | tr '\r' '\n')
113+
# The units the cache placed, from the `Cached ... (N units)` lines; the
114+
# Building total must not include them (2026.9.30.1 counted each one).
115+
placed=$(echo "$rows" | grep -a '^ *Cached ' | grep -ao '[0-9]* units\?)' | grep -o '^[0-9]*' \
116+
| awk '{s += $1} END {print s + 0}')
117+
building=$(echo "$rows" | grep -ao 'Building [0-9]*/[0-9]*' | head -1)
118+
total=${building##*/}
119+
if [ -n "$total" ] && [ "$placed" -gt 0 ] && [ "$total" -lt "$placed" ]; then
120+
pass "5 CHANGE: Building counts $total steps, not the $placed units placed from the cache"
121+
else fail "5 CHANGE: the status row (first Building: '$building', units placed: $placed)" s5.pty; fi
122+
else skip "5 no script(1) or section 4 did not build"; fi
123+
124+
# ── 6. CHANGE: #744, the vendored xlings comes from the newest source ──────
125+
rm -rf "$W/s6"; mkdir -p "$W/s6/rel/bin" "$W/s6/rel/registry/bin" "$W/s6/pathbin"; cd "$W/s6"
126+
NINJA=$(ls "$HOME"/.mcpp/registry/data/xpkgs/xim-x-ninja/*/ninja 2>/dev/null | head -1)
127+
REAL="$HOME/.mcpp/registry/bin/xlings"
128+
if [ -n "$NINJA" ] && [ -x "$REAL" ]; then
129+
older=$("$NINJA" --version | head -1)
130+
export_home="$W/s6/home"
131+
cp "$MCPP" rel/bin/mcpp
132+
cp "$NINJA" rel/registry/bin/xlings
133+
cp "$REAL" pathbin/xlings
134+
MCPP_HOME="$export_home" MCPP_OFFLINE=1 rel/bin/mcpp self env > setup.log 2>&1 || true
135+
mkdir -p "$export_home/registry/bin"; rm -f "$export_home/registry/bin/xlings"; cp "$NINJA" "$export_home/registry/bin/xlings"
136+
env -u MCPP_VENDORED_XLINGS MCPP_HOME="$export_home" MCPP_OFFLINE=1 PATH="$W/s6/pathbin:/usr/bin:/bin" \
137+
rel/bin/mcpp self env > s6.out 2> s6.err || true
138+
if grep -q "vendored xlings $older -> .* from PATH" s6.err; then
139+
pass "6 CHANGE: a newer xlings on PATH replaced the vendored one past an older released copy"
140+
else fail "6 CHANGE: #744" s6.err; fi
141+
else skip "6 no ninja payload or vendored xlings to stand in"; fi
142+
143+
# ── 7. planning states its phases ──────────────────────────────────────────
144+
cd "$W/s4" && touch src/main.cpp
145+
LOGFILE="$HOME/.mcpp/log/mcpp.log"
146+
before=$(wc -c < "$LOGFILE" 2>/dev/null || echo 0)
147+
if MCPP_LOG_LEVEL=info "$MCPP" build > s7.log 2>&1 \
148+
&& tail -c +$((before + 1)) "$LOGFILE" 2>/dev/null | grep -q 'build/stage: plan scan'; then
149+
pass "7 CHANGE: planning states its phases under build/stage"
150+
else fail "7 CHANGE: the planning phase timers" s7.log; fi
151+
152+
# ── 8. the largest consumer: xlings builds from its main branch ────────────
153+
rm -rf "$W/s8"; mkdir -p "$W/s8"; cd "$W/s8"
154+
if git clone -q --depth 1 https://github.com/openxlings/xlings.git xlings > s8-clone.log 2>&1; then
155+
cd xlings
156+
if "$MCPP" build > ../s8.log 2>&1 && "$(bin_of target xlings)" --version 2>/dev/null | grep -q '^xlings '; then
157+
pass "8 xlings builds from its main branch and runs: $(grep -a 'Finished' ../s8.log | tail -1 | sed 's/\x1b\[[0-9;]*m//g; s/^ *//')"
158+
else fail "8 xlings from its main branch" ../s8.log; fi
159+
else skip "8 xlings could not be cloned"; fi
160+
161+
echo
162+
echo "RESULT: $passes passed, $fails failed, $skips skipped (mcpp $VER)"
163+
[ "$fails" -eq 0 ]

‎.agents/docs/README.md‎

Lines changed: 3 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-
319 records.
21+
320 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+
- [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
3334
- [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
3435
- [The workspace as the unit of build: one graph per configuration, one scheduler, product directories, and a reusable graph module](2026-09-29-workspace-build-graph-design.md) — landed
3536
- [Build progress: each step's line states its outcome, and one status line states the build](2026-09-29-build-progress-display-design.md) — landed
@@ -110,6 +111,7 @@ Records that declare one. Everything else is listed by date below.
110111

111112
### 2026-09
112113

114+
- [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
113115
- [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
114116
- [The workspace as the unit of build: one graph per configuration, one scheduler, product directories, and a reusable graph module](2026-09-29-workspace-build-graph-design.md) — landed
115117
- [Build progress: each step's line states its outcome, and one status line states the build](2026-09-29-build-progress-display-design.md) — landed

‎CHANGELOG.md‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,75 @@
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.9.30.2] - 2026-09-30
8+
9+
This release answers five reports on 2026.9.30.1 while building xlings
10+
(`.agents/docs/2026-09-30-build-wall-time-progress-count-and-hang-plan.md`): a
11+
build that did not exit after its status row stopped, a status row whose count
12+
was mostly bookkeeping, planning that preceded every edit's compile by three
13+
seconds, mcpp#744, and mcpp#732. On a clean build of xlings the command starts
14+
ninja at 1.3 s instead of 4.5 s; an edit of one source builds in 8.0 s instead
15+
of 10.5 s; a build with nothing to do is unchanged at 0.05 s.
16+
17+
### Fixed
18+
19+
- **A build no longer hangs after ninja.** The stack animation could spawn
20+
pieces onto cells it already held once its stack reached the right edge
21+
short of its target, and its loop then never ended while the status row held
22+
the terminal's line lock; the build joined the row's thread and waited for
23+
ever (about 1% of interactive builds). Every loop of the animation now grows
24+
the stack or ends. A property test drives every animation over 2000 seeds and
25+
every game over 500 under a watchdog; it fails on the previous animation.
26+
- **The status row counts the work of the build.** A clean build of xlings
27+
counted 1195 steps, of which 503 placed files the global cache serves and 460
28+
were dependency scans, and read 967/1195 when its first compile began. The
29+
cache pass is reported by its `Cached` lines and not counted; the scans that
30+
wait on no action run first, shown as `Scanning f/t`; and `Building f/t`
31+
counts the compiles, links, archives and actions: 0/232 at the first compile
32+
of the same build. The fast path runs the same passes (e2e 842, 843).
33+
- **A vendored xlings is replaced from the newest source (mcpp#744).**
34+
`MCPP_VENDORED_XLINGS` when set, otherwise the newer of the xlings released
35+
with mcpp and the xlings on `PATH`; the note that no newer source is
36+
available was false when a newer xlings was on `PATH`. `Updating` and `Note`
37+
are each stated once per process (e2e 846).
38+
- **A module name is unique within one program, not within one build
39+
(mcpp#732).** An `artifacts` program, or a workspace member that shares no
40+
program with another, may provide a module of the same name as another
41+
program of the build. An import is resolved in the importing package's
42+
closure; two BMIs of one name lie below their packages' directories, and each
43+
compile is told which one a name means (a module map for GCC,
44+
`-fmodule-file=` for Clang, `/reference` for MSVC). Two providers that one
45+
program links are refused, naming that program's package, and one file
46+
reached as two packages is recognised whatever its spelling. When every name
47+
has one provider, `build.ninja` and `compile_commands.json` are byte-identical
48+
to 2026.9.30.1's (e2e 847, 848).
49+
- **A BMI served from the global cache waits for the modules it imports that
50+
the build compiles.** A module that a package's build program generates lies
51+
below the consumer's target directory and is compiled in every build, also
52+
when the rest of the package is staged from the cache (xpkg's `lua_stdlib`,
53+
imported by its cached `executor`). Nothing ordered a consumer of the staged
54+
BMI after that compile, so a fresh build could compile the consumer first and
55+
fail with `failed to read compiled module`. The stage edge of such a BMI now
56+
waits for those BMIs (e2e 849).
57+
58+
### Changed
59+
60+
- **Planning walks each package tree once.** A source pattern with an empty
61+
literal prefix walked the whole package tree: the 127 patterns of
62+
`compat.libarchive`, expanded about three times per plan, opened its 35
63+
directories 13,406 times. A walk is now kept per tree for the command and
64+
revalidated by its directories' modification times. A planned edit of one
65+
xlings source opens 1,749 package directories instead of 30,372, and its
66+
scan phase takes 43 ms instead of 0.91 s.
67+
- **The version of the vendored xlings is asked once.** It is kept per
68+
process, and under the home keyed by the binary's path, size and
69+
modification time, so a command that loads its configuration no longer runs
70+
`xlings --version` (0.35 s) when the binary has not changed.
71+
- **Planning states where its time goes.** Each phase of planning, and each
72+
step of its last phase, logs its duration under `build/stage` in the log
73+
file, which `--verbose` or `MCPP_LOG_LEVEL=info` enables. The backend's own
74+
stage lines are recorded in the file under the same condition.
75+
776
## [2026.9.30.1] - 2026-09-30
877

978
This release revises what a build prints, from a report on `mcpp build` in the

‎docs/05-dependencies.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,35 @@ updater = { path = "../updater", artifacts = ["updater"] }
525525
> by nothing that made a decision: writing it produced a manifest that loaded,
526526
> no diagnostic, and no effect.
527527
528+
### One module per name in each program (mcpp 2026.9.30.2+)
529+
530+
A module name identifies one module within one program. The compilers name a
531+
module's entities and its initializer after the module, so a program cannot
532+
link two modules of one name. A build can hold several programs (a package and
533+
the programs it ships through `artifacts`, the members of a workspace), and
534+
each of them may have its own module of one name.
535+
536+
- An import is resolved within the importing package's closure: the package
537+
and every package it reaches through its dependencies. The closure does not
538+
follow `artifacts`, `tools` or `[build-dependencies]` edges, whose programs
539+
are built separately.
540+
- Two packages that one program links may not provide the same name. The build
541+
is refused, and the message names the package whose closure holds both.
542+
- When two packages of one build provide a name, each BMI lies below its
543+
package's directory in the build directory, and every compile that may import
544+
the name is told which one it means: through a module map with GCC, through
545+
`-fmodule-file=` with Clang, and through `/reference` with MSVC. When every
546+
name has one provider, the build directory and every command are as they
547+
were before.
548+
- A package that provides such a name is compiled in the project, not served
549+
from the global dependency cache.
550+
- clangd finds a module by its name in the compilation database, so for a name
551+
two packages provide it may show the other program's module. The build is
552+
not affected.
553+
554+
A module that several programs share is best provided by one package that the
555+
others depend on; it is then compiled once.
556+
528557
## Current limitations
529558

530559
- **Two things need the network, and only two:** resolving a branch that has no

‎docs/07-workspace.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,11 @@ member that several members use is compiled once.
451451
`compile_commands.json` once, as the union of their databases (2026.9.29.5+).
452452
- **No-op builds.** A command repeated with nothing changed is answered by one
453453
check per configuration, without planning.
454-
- **Module names.** Members built in one graph share one module namespace:
455-
two members that each provide a module of the same name cannot be built in
456-
one `--workspace` command; build each with `-p`.
454+
- **Module names.** A module name is unique within one program, not within one
455+
graph (2026.9.30.2+). Two members that share no program may each provide a
456+
module of the same name, and one `--workspace` command builds both; a member
457+
that links both is refused. See
458+
[05 — One module per name in each program](05-dependencies.md#one-module-per-name-in-each-program-mcpp-20269302).
457459

458460
## 6. Directory Layout
459461

0 commit comments

Comments
 (0)