Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c33eedf
docs: plan for member selection, build programs prepared once, a pack…
speak-agent Sep 30, 2026
ecff599
platform: starting a child is safe for concurrent callers (#748, B2-0)
speak-agent Sep 30, 2026
380a6c9
feat: one member selection for build, test, emit build-database and r…
speak-agent Sep 30, 2026
d0645c2
test: e2e 852 to 856 for the member selection, --exclude, a test plan…
speak-agent Sep 30, 2026
bfcb033
feat: a member missing from its plan is reported instead of selecting…
speak-agent Sep 30, 2026
f0eabb2
docs: repeatable -p, --exclude, a test over several members planned o…
speak-agent Sep 30, 2026
5683fa4
Merge T1: member selection and a test over a selection planned once
speak-agent Sep 30, 2026
52548c0
feat: --workspace together with -p is refused, naming both, rather th…
speak-agent Sep 30, 2026
21feffa
ui: narration on standard error and results on standard output; a fai…
speak-agent Sep 30, 2026
5815c42
e2e: the output streams (862 to 866), and 122 asserts the exact progr…
speak-agent Sep 30, 2026
e589b14
docs: which stream carries what, the exit status of mcpp run, and the…
speak-agent Sep 30, 2026
5bef4ee
build: compiled build-program imports are kept by key, and a workspac…
speak-agent Sep 30, 2026
bcc8584
Merge T3: narration on standard error, results on standard output, an…
speak-agent Sep 30, 2026
2164627
Merge T2: build programs compile the engine's module and host modules…
speak-agent Sep 30, 2026
9a5fa38
fix: the c++fly summary of a build is narration, on standard error
speak-agent Sep 30, 2026
4c5f8dc
pack: mcpp pack over several members plans once per configuration, bu…
speak-agent Sep 30, 2026
a23a754
e2e: a pack over several members (867 to 869)
speak-agent Sep 30, 2026
d934e39
docs: packing several members, and the pack envelope of several members
speak-agent Sep 30, 2026
dea4260
pack: the paths of a pack of several members are shown from the works…
speak-agent Sep 30, 2026
8f3c1a6
e2e: a provider one packed member reaches acts for that member, besid…
speak-agent Sep 30, 2026
79aed8b
fix: review of the selection, build-program and stream changes
speak-agent Sep 30, 2026
6b3fd49
build: the attribution of a package to a packed member is a method of…
speak-agent Sep 30, 2026
3ee4cd1
pack: a configuration that cannot be planned fails for its members al…
speak-agent Sep 30, 2026
ecb6811
cli: the help of pack's --output says what it names when several memb…
speak-agent Sep 30, 2026
a8c2de1
Merge T4: mcpp pack over several members plans once per configuration…
speak-agent Sep 30, 2026
8052078
2026.10.1.1: the version, the release notes, the plan's implementatio…
speak-agent Sep 30, 2026
87d7b0d
ci: the Windows msvc step reads narration from both streams, and e2e …
speak-agent Sep 30, 2026
4cdf04d
fix: a pack of several members resolves a target name for the member …
speak-agent Sep 30, 2026
3a8d3d5
docs: the implementation record counts e2e 870
speak-agent Sep 30, 2026
72e238d
refactor: the refusals of references a plan cannot answer are mcpp::b…
speak-agent Sep 30, 2026
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.

156 changes: 156 additions & 0 deletions .agents/docs/2026-10-01-member-selection-verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# Sandbox verification of the release that implements
# .agents/docs/2026-09-30-member-selection-and-build-program-cost-plan.md
# (#748, #749, #750), run against the PUBLISHED release inside an xlings
# sandbox:
#
# B64=$(base64 -w0 .agents/docs/2026-10-01-member-selection-verify.sh)
# xlings subos new v1001 2>/dev/null || true
# xlings subos use v1001 --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.9.30.2 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/v1001}"
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/v1001-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/v1001-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
# The sandbox's home is empty, so the mirror is set inside it. A run on a host
# with MCPP_OVERRIDE leaves the host's configuration as it is.
[ -n "${MCPP_OVERRIDE:-}" ] || "$MCPP" self config --mirror CN >/dev/null 2>&1 || true
mkdir -p "$W"

lib_member() { # <dir> <name> <value>: a library member with one module and one test
mkdir -p "$1/src" "$1/tests"
printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[targets.%s]\nkind = "lib"\n' "$2" "$2" > "$1/mcpp.toml"
printf 'export module %s;\nexport int %s_value() { return %s; }\n' "$2" "$2" "$3" > "$1/src/$2.cppm"
printf 'import %s;\nint main() { return %s_value() == %s ? 0 : 1; }\n' "$2" "$2" "$3" > "$1/tests/test_$2.cpp"
}

# 1. CHANGE (#750): a repeated -p selects every member it names.
rm -rf "$W/s1"; mkdir -p "$W/s1"; cd "$W/s1"
printf '[workspace]\nmembers = ["a", "b", "c"]\n' > mcpp.toml
lib_member a a 1; lib_member b b 2; lib_member c c 3
if "$MCPP" test -p a -p b > s1.log 2>&1 \
&& grep -q 'test_a' s1.log && grep -q 'test_b' s1.log && ! grep -q 'test_c' s1.log; then
pass "1 CHANGE: mcpp test -p a -p b tests a and b, and not c"
else fail "1 CHANGE: a repeated -p" s1.log; fi

# 2. CHANGE: --exclude, and a member name that matches nothing.
cd "$W/s1"
if "$MCPP" test --workspace --exclude c > s2a.log 2>&1 \
&& grep -q 'test_a' s2a.log && grep -q 'test_b' s2a.log && ! grep -q 'test_c' s2a.log; then
pass "2a CHANGE: --workspace --exclude c tests a and b"
else fail "2a CHANGE: --exclude" s2a.log; fi
if "$MCPP" build -p nosuch > s2b.log 2>&1; then fail "2b an unknown member was accepted" s2b.log
elif grep -q 'nosuch' s2b.log; then pass "2b an unknown member is refused by name"
else fail "2b the refusal does not name the member" s2b.log; fi

# 3. CHANGE (R1, R2): run -q writes exactly the program's output, and a
# failed build exits 101.
rm -rf "$W/s3"; mkdir -p "$W/s3/src"; cd "$W/s3"
printf '[package]\nname = "p3"\nversion = "0.1.0"\n\n[targets.p3]\nkind = "bin"\nmain = "src/main.cpp"\n' > mcpp.toml
printf '#include <cstdio>\nint main() { std::puts("OUT"); std::fputs("ERR\\n", stderr); return 0; }\n' > src/main.cpp
"$MCPP" run -q > s3.out 2> s3.err; rc=$?
if [ "$rc" = 0 ] && [ "$(od -c s3.out | head -1)" = "$(printf 'OUT\n' | od -c | head -1)" ]; then
pass "3a CHANGE: run -q writes exactly the program's stdout"
else fail "3a CHANGE: run -q stdout (rc=$rc)" s3.out; fi
printf 'int main() { x }\n' > src/main.cpp
"$MCPP" run -q > s3b.log 2>&1; rc=$?
if [ "$rc" = 101 ]; then pass "3b CHANGE: a run whose build failed exits 101"
else fail "3b CHANGE: exit status $rc for a failed build" s3b.log; fi

# 4. CHANGE (R3): a build narrates on stderr.
cd "$W/s1"
out=$("$MCPP" build --workspace 2>/dev/null)
if [ -z "$out" ] && "$MCPP" build --workspace 2>&1 >/dev/null | grep -q 'Finished'; then
pass "4 CHANGE: status lines are on stderr and stdout is empty"
else fail "4 CHANGE: status stream"; echo "$out" | head -5; fi

# 5. index packages build and run with the release.
rm -rf "$W/s5"; mkdir -p "$W/s5/src"; cd "$W/s5"
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
cat > src/main.cpp <<'EOF'
#include <cstdio>
#include <zlib.h>
import mcpplibs.cmdline;
int main() { std::printf("zlib %s\n", zlibVersion()); return 0; }
EOF
if "$MCPP" run > s5.log 2>&1 && grep -q '^zlib ' s5.log; then
pass "5 compat.zlib and mcpplibs.cmdline from the index build and run"
else fail "5 index packages" s5.log; fi

# 6. CHANGE (#748): four members' build programs import one host module of a
# path dependency; the module is compiled once for the four, and the engine's
# own module is kept in the global cache.
rm -rf "$W/s6"; mkdir -p "$W/s6/rules/src"; cd "$W/s6"
printf '[workspace]\nmembers = ["m1", "m2", "m3", "m4"]\n' > mcpp.toml
printf '[package]\nname = "rule6"\nversion = "0.1.0"\n\n[targets.rule6]\nkind = "lib"\n' > rules/mcpp.toml
printf 'export module v6.rules;\nimport mcpp;\nexport void apply(const char* d) { mcpp::define(d); }\n' > rules/src/rule6.cppm
for m in m1 m2 m3 m4; do
mkdir -p $m/src
printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[build-dependencies]\nrule6 = { path = "../rules", host-module = true }\n\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' $m $m > $m/mcpp.toml
printf 'int main() { return 0; }\n' > $m/src/main.cpp
printf 'import mcpp;\nimport v6.rules;\nint main() { apply("V6_%s=1"); return 0; }\n' $m > $m/build.mcpp
done
if MCPP_VERBOSE=1 "$MCPP" build --workspace > s6.log 2>&1; then
compiles=$(grep -cE "host module 'v6\.rules' (precompile|compile):" s6.log || true)
ran=$(grep -cE "^ *build\.mcpp m[1-4] .* ran " s6.log || true)
if [ "$compiles" = 1 ] && [ "$ran" = 4 ]; then
pass "6 CHANGE: one host module compile for four build programs"
else fail "6 CHANGE: $compiles host module compiles for $ran programs" s6.log; fi
else fail "6 the workspace of four build programs did not build" s6.log; fi

# 7. CHANGE (#749): one pack of two program members that share a member with a
# build program: one package per member, and the shared program runs once.
rm -rf "$W/s7"; mkdir -p "$W/s7/core/src"; cd "$W/s7"
printf '[workspace]\nmembers = ["core", "cli", "gui"]\n' > mcpp.toml
printf '[package]\nname = "core"\nversion = "0.1.0"\n\n[targets.core]\nkind = "lib"\n' > core/mcpp.toml
printf 'export module v7core;\nexport int core_answer() { return 42; }\n' > core/src/core.cppm
cat > core/build.mcpp <<'EOF'
import mcpp;
#include <fstream>
#include <string>
int main() {
std::ofstream log(std::string(mcpp::out_dir()) + "/runs.log", std::ios::app);
log << "ran\n";
return 0;
}
EOF
for m in cli gui; do
mkdir -p $m/src
printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[dependencies]\ncore = { path = "../core" }\n\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n' $m $m > $m/mcpp.toml
printf '#include <cstdio>\nimport v7core;\nint main() { std::printf("%s %%d\\n", core_answer()); }\n' $m > $m/src/main.cpp
done
if "$MCPP" pack --workspace --format dir > s7.log 2>&1; then
packed=$(grep -cE '^ *Packed ' s7.log || true)
runs=$(find core -name runs.log -exec cat {} + 2>/dev/null | grep -c ran || true)
if [ "$packed" = 2 ] && [ "$runs" = 1 ]; then
pass "7 CHANGE: one pack of two members, and the shared build program ran once"
else fail "7 CHANGE: $packed packages, the shared program ran $runs times" s7.log; fi
else fail "7 CHANGE: mcpp pack --workspace" s7.log; fi

echo "---- $passes passed, $fails failed, $skips skipped"
[ "$fails" = 0 ]
4 changes: 3 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
---
```

320 records.
321 records.

## By subject

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

### design

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

### 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
- [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
- [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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,9 @@ jobs:
# the global default in `toolchain list` / doctor output.
TMP=$(mktemp -d); cd "$TMP"
out=$("$MCPP_SELF" toolchain default msvc); echo "$out"
# Both streams: `Detected` and `Default set to` are narration, on
# standard error since 2026.10.1.1.
out=$("$MCPP_SELF" toolchain default msvc 2>&1); echo "$out"
grep -q "Detected" <<<"$out"
grep -q "msvc@system" <<<"$out"
Expand Down
92 changes: 92 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,98 @@
> 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.1] - 2026-10-01

This release implements the plan for member selection, build programs prepared
once, a pack over several members, and the output streams
(`.agents/docs/2026-09-30-member-selection-and-build-program-cost-plan.md`),
and resolves mcpp#748, mcpp#749 and mcpp#750. Two changes are observable by
scripts: narration moves to standard error, and a `mcpp run` whose build failed
exits 101 (both under **Changed**).

### Changed

- **Narration is written to standard error, and a command's result to
standard output.** The narration is what says what a command is doing: the
status lines that begin with a verb (`Resolving`, `Compiling`, `Finished`,
`Running`, `Packed`, `Downloading` and the others), the progress bars, the
status row of a terminal, and the blank line after `Running`. The result is
what the command was asked to produce: the program's output under `mcpp run`,
a document or a listing, and for `mcpp test` each verdict, the `test result`
line and the `workspace result` line. Releases up to 2026.9.30.2 wrote the
narration to standard output. A script that read a status line from standard
output changes: `mcpp build | tee log` becomes `mcpp build 2>&1 | tee log`. A
CI log, which captures both streams, is unaffected. The live status row is
drawn on standard error, whether or not standard output is a terminal
(e2e 864 to 866).
- **A `mcpp run` whose planning or build failed exits 101**, as `cargo run`
does. It exited 1 or 2, which a program that returns 1 also does. The
program's own status still passes through, a refused start keeps 125 to 127,
and `build`, `test` and `pack` keep their statuses. A program or runner that
itself returns 101 reads as a failed build (e2e 863).
- **`mcpp test` over several members plans once per configuration.** The
members of one configuration are one plan with each member's tests, so a
package they share is compiled once with the union of their features and its
build program runs once; then each member's tests run in `[workspace]
members` order with that member's runtime directories, continuing past a
member that fails. The JSON stream adds a `group_build` record before a
group's first test and a `build_group` field in each member's summary, whose
`build_ms` is the group's build time; each test record adds `build_ms`, the
build time of its own binary. `--workspace-timeout` bounds the runs and
`--build-timeout` the build (e2e 854 to 856).
- **A workspace's build programs share what they import and compile at the
same time (mcpp#748).** The bundled `mcpp` module and each host module are
compiled once per key: the host compiler, the standard and every flag of the
compile, the imported BMIs and the interface's content. The engine's module
and the host modules of index packages are kept in the global cache, where
`mcpp cache gc` collects them; a host module of a path or git dependency or
of a workspace member is kept under the workspace's
`target/.build-mcpp/host-modules/`. The members' programs are compiled
together, up to the job count, and run in the order they always ran, so the
plan is the one a serial build writes. On a fixture of #748's shape the whole
build takes 0.33 to 0.48 s instead of 0.89 to 1.22 s (e2e 857 to 861).

### Added

- **A repeated `-p` selects every member it names (mcpp#750).** `build`,
`test`, `pack` and `mcpp emit build-database` read one selection: a set of
members in `[workspace] members` order, whatever the order of the `-p`
values. A value that names no member is refused before planning, and the
refusal lists the members. `mcpp run` executes one program and refuses a
second `-p`, naming both; `--workspace` together with `-p` is refused
(e2e 852, 853).
- **`--exclude <member>`** removes members from a whole-workspace selection
(`--workspace`, or a virtual root without `-p`), on `build`, `test`, `pack`
and `mcpp emit build-database`. It is refused with `-p`, for a name that
matches no member, and when it leaves no member (e2e 853).
- **`mcpp pack` over several members (mcpp#749).** `--workspace`, a repeated
`-p` and `--exclude` pack several members with one plan per configuration and
one build; each member is staged in a tree of its own, its build program sees
its own `pack_stage_dir()`, and a dispatched format runs one second pass per
configuration. A positional target, several `--target` values, an `--output`
that is a file, a member no provider of the requested format acts for, and
two members that would write one destination are refused before anything is
compiled. A member whose distribution step fails is reported by name and the
others are packed; the members are reported in `[workspace] members` order,
and `${mcpp.target_file:<name>}` names the target of the member an action is
for. `mcpp pack -p <member>` keeps its meaning. The JSON envelope adds
`data.stages` and a `member` field per artifact when several members are
packed (e2e 867 to 870).

### Fixed

- **A repeated `-p` no longer keeps only its last value (mcpp#750).**
`mcpp test -p a -p b` tested `b` alone and exited 0.
- **Starting a child process is safe for concurrent callers.** A pipe created
by one thread could be inherited by a child another thread started, and the
first thread's reader then waited for end of file until the unrelated child
exited. Pipes are created close-on-exec on Linux; on macOS and Windows the
creation of a pipe, the start of the child and the parent's close of the
write end form one critical section. The registry of children for signal
forwarding is serialised and holds 256 entries.
- **`mcpp run -q` writes exactly the program's standard output.** It began
with an empty line (e2e 862).

## [2026.9.30.2] - 2026-09-30

This release answers five reports on 2026.9.30.1 while building xlings
Expand Down
7 changes: 7 additions & 0 deletions docs/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,13 @@ What is **not** cached: `path` and `git` dependencies, at any depth, and
workspace members. Their sources can change without their `name@version`
changing, so no key over that identity could notice the change.

The modules a build program imports follow the same rule (2026.10.1.1+). The
bundled `mcpp` module and the host modules of index packages are kept in the
global cache, and a host module of a `path` or `git` dependency or of a workspace
member is kept under `target/.build-mcpp/host-modules/`, never in the global
cache. `--cache local` and `--cache off` keep all of them in the project. See
[30 — Placement of compiled host modules](30-build-mcpp.md).

Inspection and reclamation:

```
Expand Down
Loading
Loading