Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions .agents/docs/2026-10-01-pack-drive-and-selection-verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env bash
# Sandbox verification of the release that implements
# .agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md
# (#751, #753), run against the PUBLISHED release inside an xlings sandbox:
#
# B64=$(base64 -w0 .agents/docs/2026-10-01-pack-drive-and-selection-verify.sh)
# xlings subos new v1002 2>/dev/null || true
# xlings subos use v1002 --sandbox --cmd "echo $B64 | base64 -d > /tmp/v.sh && VER=<version> bash /tmp/v.sh"
#
# VER selects the release under test. Running it with VER=2026.10.1.1 is the
# control: every section marked CHANGE must fail there, and every other
# section must pass on both.
#
# Every probe directory is removed at the start of its section, because the
# sandbox's $HOME persists between runs of the same subos. A section that does
# not run is reported as SKIP and counted apart from a pass.
set -u
VER="${VER:?VER=<version under test>}"
W="${W:-$HOME/v1002}"
fails=0; passes=0; skips=0
pass() { echo "PASS $1"; passes=$((passes+1)); }
fail() { echo "FAIL $1"; [ -n "${2:-}" ] && [ -f "$2" ] && tail -20 "$2"; fails=$((fails+1)); }
skip() { echo "SKIP $1"; skips=$((skips+1)); }

# 0. The release under test, from the published channel, with the CN mirror.
if [ -n "${MCPP_OVERRIDE:-}" ]; then
MCPP="$MCPP_OVERRIDE"
else
xlings config --mirror CN >/dev/null 2>&1 || true
xlings update >/dev/null 2>&1 || true
xlings install "mcpp@$VER" -y > /tmp/v1002-install.log 2>&1 || true
MCPP="$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp"
fi
if [ ! -x "$MCPP" ]; then
echo "FATAL: mcpp $VER is not installable from the index"; tail -20 /tmp/v1002-install.log; exit 2
fi
got=$("$MCPP" --version 2>&1 | head -1)
case "$got" in *"$VER"*) pass "0 installed: $got";; *) fail "0 version: $got";; esac
[ -n "${MCPP_OVERRIDE:-}" ] || "$MCPP" self config --mirror CN >/dev/null 2>&1 || true
mkdir -p "$W"

# compiles_after <log> <lines>: the compile steps `.ninja_log` records after
# its first <lines> lines.
compiles_after() {
tail -n +"$(( $2 + 1 ))" "$1" | awk -F'\t' '$4 ~ /\.(o|obj)$/ { n++ } END { print n + 0 }'
}
# no_compile <label> <selection...>: the build of the selection compiles nothing.
no_compile() {
local label="$1"; shift
local log before n
log=$(find target -name .ninja_log | head -1)
before=$(wc -l < "$log")
"$MCPP" build "$@" > nc.log 2>&1 || { fail "$label: build $* failed" nc.log; return 1; }
n=$(compiles_after "$log" "$before")
[ "$n" = 0 ] || { fail "$label: build $* compiled $n units" nc.log; return 1; }
return 0
}
member() { # <dir> <name> <kind> [<dependency path>]
mkdir -p "$1/src"
{
printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$2"
[ -n "${4:-}" ] && printf '\n[dependencies]\ncore = { path = "%s" }\n' "$4"
printf '\n[targets.%s]\nkind = "%s"\n' "$2" "$3"
[ "$3" = bin ] && printf 'main = "src/main.cpp"\n'
} > "$1/mcpp.toml"
}

# 1. CHANGE (#751, GalTranslPP's shape): two members compile one file from
# outside their directories; after --workspace, no selection compiles again.
rm -rf "$W/s1"; mkdir -p "$W/s1/shared"; cd "$W/s1"
printf '[workspace]\nmembers = ["core", "app", "tool"]\n' > mcpp.toml
printf 'export module m;\nexport int answer() { return 42; }\n' > shared/m.cppm
member core core lib; member app app bin ../core; member tool tool bin
printf '\n[build]\nsources = ["src/*.cppm", "../shared/m.cppm"]\n' >> core/mcpp.toml
printf '\n[build]\nsources = ["src/*.cpp", "../shared/m.cppm"]\n' >> tool/mcpp.toml
printf 'export module corelib;\nimport m;\nexport int core_value() { return answer(); }\n' > core/src/core.cppm
printf '#include <cstdio>\nimport corelib;\nint main() { std::printf("%%d\\n", core_value()); }\n' > app/src/main.cpp
printf '#include <cstdio>\nimport m;\nint main() { std::printf("%%d\\n", answer() + 1); }\n' > tool/src/main.cpp
if "$MCPP" build --workspace > s1.log 2>&1; then
if no_compile "1" -p app && no_compile "1" -p tool && no_compile "1" --workspace && no_compile "1" -p app; then
pass "1 CHANGE: a file two members list is compiled once for every selection"
fi
else fail "1 the workspace did not build" s1.log; fi

# 2. CHANGE: two members each with their own module `m`; alternating
# selections compile nothing once each was built, and each program prints its
# own value.
rm -rf "$W/s2"; mkdir -p "$W/s2"; cd "$W/s2"
printf '[workspace]\nmembers = ["core", "app", "tool"]\n' > mcpp.toml
member core core lib; member app app bin ../core; member tool tool bin
printf 'export module m;\nexport int answer() { return 42; }\n' > core/src/m.cppm
printf 'export module corelib;\nimport m;\nexport int core_value() { return answer(); }\n' > core/src/core.cppm
printf '#include <cstdio>\nimport corelib;\nint main() { std::printf("%%d\\n", core_value()); }\n' > app/src/main.cpp
printf 'export module m;\nexport int answer() { return 7; }\n' > tool/src/m.cppm
printf '#include <cstdio>\nimport m;\nint main() { std::printf("%%d\\n", answer()); }\n' > tool/src/main.cpp
if "$MCPP" build --workspace > s2.log 2>&1; then
a=$(find target -path '*/bin/app/app' -type f | head -1); t=$(find target -path '*/bin/tool/tool' -type f | head -1)
if [ "$("$a")" = 42 ] && [ "$("$t")" = 7 ] \
&& no_compile "2" -p app && no_compile "2" -p tool && no_compile "2" -p app && no_compile "2" --workspace; then
pass "2 CHANGE: two modules of one name, no recompile across selections, each program its own value"
fi
else fail "2 the workspace did not build" s2.log; fi

# 3. CHANGE: a member that builds a shared library does not change the other
# members' commands.
rm -rf "$W/s3"; mkdir -p "$W/s3"; cd "$W/s3"
printf '[workspace]\nmembers = ["core", "app", "dso"]\n' > mcpp.toml
member core core lib; member app app bin ../core; member dso dso shared
printf 'export module corelib;\nexport int core_value() { return 3; }\n' > core/src/core.cppm
printf 'import corelib;\nint main() { return core_value() == 3 ? 0 : 1; }\n' > app/src/main.cpp
printf 'export module dsolib;\nexport int dso_value() { return 5; }\n' > dso/src/dso.cppm
if "$MCPP" build --workspace > s3.log 2>&1; then
if no_compile "3" -p app && no_compile "3" --workspace; then
pass "3 CHANGE: a shared-library member leaves the other members' commands alone"
fi
else fail "3 the workspace did not build" s3.log; fi

# 4. CHANGE (#753): a pack states its build, and Finished precedes Packing.
rm -rf "$W/s4"; mkdir -p "$W/s4"; cd "$W/s4"
member . p4 bin
printf 'int main() { return 0; }\n' > src/main.cpp
if "$MCPP" pack --format tar > s4.log 2>&1; then
fin=$(grep -n 'Finished' s4.log | head -1 | cut -d: -f1); pk=$(grep -n 'Packing' s4.log | head -1 | cut -d: -f1)
if grep -q 'Compiling p4' s4.log && [ -n "$fin" ] && [ -n "$pk" ] && [ "$fin" -lt "$pk" ]; then
pass "4 CHANGE: the pack states its build, Finished before Packing"
else fail "4 CHANGE: the pack's build is not stated" s4.log; fi
else fail "4 the pack failed" s4.log; fi

# 5. CHANGE: `[build] jobs = 1` bounds mcpp test's compiles.
rm -rf "$W/s5"; mkdir -p "$W/s5"; cd "$W/s5"
member . p5 bin
printf '\n[build]\njobs = 1\n' >> mcpp.toml
mkdir -p tests
for i in 1 2 3 4; do
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
done
printf 'int main() { return 0; }\n' > src/main.cpp
printf 'int main() { return 0; }\n' > tests/t.cpp
if "$MCPP" test > s5.log 2>&1; then
log=$(find target -name .ninja_log | head -1)
if awk -F'\t' 'NR > 1 && $4 ~ /\.o$/ { s[n] = $1; e[n] = $2; n++ }
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
pass "5 CHANGE: mcpp test runs one compile at a time under jobs = 1"
else fail "5 CHANGE: mcpp test overlapped compiles under jobs = 1" "$log"; fi
else fail "5 mcpp test failed" s5.log; fi

# 6. Index packages that provide modules build, run, and are served from the
# global cache by a second project, their BMIs below their packages' directories.
for d in s6a s6b; do
rm -rf "$W/$d"; mkdir -p "$W/$d/src"
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"
cat > "$W/$d/src/main.cpp" <<'EOF'
#include <cstdio>
#include <zlib.h>
import mcpplibs.cmdline;
int main() { std::printf("zlib %s\n", zlibVersion()); return 0; }
EOF
done
cd "$W/s6a"
if "$MCPP" run > s6a.log 2>&1 && grep -q '^zlib ' s6a.log; then
cd "$W/s6b"
if "$MCPP" run > s6b.log 2>&1 && grep -q '^zlib ' s6b.log && grep -q 'Cached mcpplibs.cmdline' s6b.log; then
pass "6a index packages build, run, and are served from the cache"
if [ -n "$(find target -path '*.cache/*cmdline*/*' -type f | head -1)" ]; then
pass "6b CHANGE: a cached dependency's BMIs are staged below its package's directory"
else fail "6b CHANGE: the cached BMIs are staged at their names" s6b.log; fi
else fail "6a the second project was not served from the cache" s6b.log; fi
else fail "6 index packages" s6a.log; fi

# 7. xlings, built from its source with the release under test.
rm -rf "$W/s7"; mkdir -p "$W/s7"; cd "$W/s7"
if git clone -q --depth 1 https://github.com/openxlings/xlings.git xlings > s7-clone.log 2>&1 \
|| git clone -q --depth 1 https://gitee.com/openxlings/xlings.git xlings > s7-clone.log 2>&1; then
cd xlings
if "$MCPP" build > s7.log 2>&1; then
x=$(find target -path '*/bin/xlings' -type f | head -1)
if [ -n "$x" ] && "$x" --version > s7v.log 2>&1; then
if no_compile "7" ; then pass "7 xlings builds with the release, runs, and a second build compiles nothing: $(head -1 s7v.log)"; fi
else fail "7 the xlings that was built does not run" s7v.log; fi
else fail "7 xlings does not build" s7.log; fi
else skip "7 xlings could not be cloned"; fi

echo "---- $passes passed, $fails failed, $skips skipped"
[ "$fails" = 0 ]
6 changes: 5 additions & 1 deletion .agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```

321 records.
322 records.

## By subject

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

### design

- [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
- [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
- [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
- [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
Expand Down Expand Up @@ -110,6 +111,9 @@ Records that declare one. Everything else is listed by date below.

## By date

### 2026-10

- [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
### 2026-09

- [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
Expand Down
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,72 @@
> Each `## [<version>]` section is that release's notes. Entries are written in English
> from 2026.9.28.3 on; earlier entries remain as written.

## [2026.10.1.2] - 2026-10-01

This release implements the design for a pack's build and a compile that does
not depend on the member selection
(`.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md`),
and resolves mcpp#751 and mcpp#753. A project that imports a module of a
dependency, and every project on an ELF target, is compiled once more after the
upgrade, because its commands change (both under **Changed**).

### Fixed

- **A unit is compiled by the same command in every selection of a
workspace.** The selections of one configuration share a build directory,
and three facts about the whole graph reached the commands of members they
did not concern, so each switch between `--workspace` and `-p` recompiled
them (mcpp#751; GalTranslPP 3.1.3 recompiled its core for 3m22s):
- a module name that two members provide moved every BMI of that name below
its provider's directory, and told the importers so, only when the graph
held both providers;
- a file that a member lists from outside its own directory was owned by the
workspace's virtual root, whose object census depended on how many members
listed it;
- a member that builds a shared library put `-fPIC` on every unit of the
graph.
A command now depends on the unit's package, the packages it reaches, the
features the selection activates for it and the declarations a selected
member holds as the root, and on nothing else in the graph. `mcpp build
--workspace` followed by `mcpp build -p <member>` compiles nothing, and so
does any alternation of selections (e2e 872).
- **`mcpp pack` states the build it performs.** Its build wrote no package
line, no status row and no `Finished`, so a release job whose pack
recompiled mcpp showed `Planning` for six minutes (mcpp#753). A pack is now
stated as `mcpp build` states one, with `Finished` before the first `Packing`
line and one `Finished` for a pack over several configurations; the second
pass of a dispatched `--format` is stated in the same way (e2e 871).
- **`[build] jobs` bounds every command that compiles.** `mcpp test` and
`mcpp pack` ran ninja's default number of jobs whatever `[build] jobs`,
`--jobs` or `MCPP_JOBS` said, which on a machine with little memory exceeded
the bound the key exists to enforce. The backend reads the job count from
the plan, and the reclaim of stale `bmi_schedule = "on"` tokens, which ran
only under `mcpp build`, runs before the first drive of every build
directory (e2e 871).
- **A pack fills the global dependency cache** with the dependencies its
build compiled, as `mcpp build` and `mcpp test` do.

### Changed

- **Every package's BMIs lie below the package's directory, except the root
package's.** The rule is the one object files have followed since mcpp#233:
`gcm.cache/<package>/<module>.gcm` (`pcm.cache` with clang). A unit that
imports a module of another package reads one module map of the modules it
reaches through its imports: a mapper file with GCC, and an argument file
`@<build directory>/modmap/<package>-<hash>.modmap` of `-fmodule-file=` or
`/reference` lines with clang or MSVC. A project whose modules are all its
own is laid out, and every command spelled, as before. A package whose
module name another package of the graph provides is served from the global
dependency cache again; 2026.9.30.2 compiled it in the project.
- **Every unit of an ELF target that is not freestanding is compiled with
`-fPIC`,** whether or not the graph links a shared library, as rustc's
default relocation model does on these targets: an object compiled once
serves a program and a shared object. Mach-O compilers produce
position-independent code by default, PE has no such flag, and nothing
changes on those targets or on freestanding ones. The global dependency
cache keys these entries by the flag, so each is compiled once more and
then served again.

## [2026.10.1.1] - 2026-10-01

This release implements the plan for member selection, build programs prepared
Expand Down
4 changes: 3 additions & 1 deletion docs/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,9 @@ jobs = "auto" # or a positive number; --jobs / MCPP_JOBS override it
bmi_schedule = "off" # auto (default, = off) | on | off
```

`jobs` is how many compiles run at once. `"auto"` is resolved **against the
`jobs` is how many compiles run at once, in every command that compiles:
`build`, `run`, `test` and `pack` (2026.10.1.2+; before it, `test` and `pack`
ran ninja's default number of jobs whatever the key said). `"auto"` is resolved **against the
machine doing the build**, never frozen into the manifest: it takes the physical
core count on a heterogeneous CPU (a 13900K is 8 P-cores + 16 E-cores, so its 32
threads are not 32 equal workers) and clamps that by free memory, because a
Expand Down
24 changes: 16 additions & 8 deletions docs/05-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,14 +551,22 @@ each of them may have its own module of one name.
are built separately.
- Two packages that one program links may not provide the same name. The build
is refused, and the message names the package whose closure holds both.
- When two packages of one build provide a name, each BMI lies below its
package's directory in the build directory, and every compile that may import
the name is told which one it means: through a module map with GCC, through
`-fmodule-file=` with Clang, and through `/reference` with MSVC. When every
name has one provider, the build directory and every command are as they
were before.
- A package that provides such a name is compiled in the project, not served
from the global dependency cache.
- Every package's BMIs lie below the package's directory in the build
directory, `gcm.cache/<package>/` (`pcm.cache` with Clang), except the root
package's, which lie at their names, as object files do (2026.10.1.2+). A
compile that imports a module of another package is told where its BMI is,
and where the BMIs of the modules that one imports are, through one module
map: a mapper file with GCC (`-fmodule-mapper=`), and an argument file of
`-fmodule-file=` lines with Clang or `/reference` lines with MSVC
(`@<build directory>/modmap/<package>-<hash>.modmap`). The map lists what the
unit reaches through its imports and nothing else, so the command does not
change with the rest of the graph. A project whose modules are all its own
is laid out, and every command spelled, as before.
- Until 2026.10.1.2 a BMI moved below its package's directory only when two
packages of the graph provided its name, and such a package was not served
from the global dependency cache. Both depended on which packages a command
selected, so a workspace member was compiled again at each switch between
`-p` and `--workspace` (mcpp#751).
- clangd finds a module by its name in the compilation database, so for a name
two packages provide it may show the other program's module. The build is
not affected.
Expand Down
Loading
Loading