Skip to content

Commit c765c8c

Browse files
Sunrisepeakspeak-agentclaude
authored
Android, iOS and Web for a UI framework: the engine half of #622 (2026.9.12.3) (#623)
* docs(design): a UI framework on Android, iOS and Web, and the plan that lands it (#622) * wasm32-emscripten names its launcher .js and carries the .wasm as a link output (#622 A5) - artifact_naming grows an `emscripten` row (triple.cppm): exeSuffix ".js", lib prefix/ext as ELF's, sharedLibExt empty with the shared-unsupported marker set. Independent of host naming (Linux/Windows both answer ".js"), and a "none" OS still falls back to the host answer unchanged. - LinkIntentFlavor gains Wasm, rendered exactly as Elf (-L, -l); the switch in flags.cppm has no default arm. - ninja_backend.cppm declares bin/<name>.wasm as an implicit output of the same link edge that writes bin/<name>.js, for every Binary/TestBinary link unit on a Wasm-object-format triple. - prepare.cppm refuses `kind = "shared"` on a Wasm triple by name, before any toolchain is resolved (same site/reasoning as the Apple SDK check it sits beside), naming -sSIDE_MODULE as the unrendered mechanism. - stage_tree.cppm adds emscripten_stem_family(): the launcher is the executable, the family is every other <name>.<anything> the link also wrote, read from the directory rather than a fixed extension list. pack.cppm gains a wasm packaging path (Plan::targetIsWasm, run_wasm) that stages the launcher plus the whole stem family, with no dependency-closure step (an ordinary Emscripten link is one static image). Verified end to end on this machine's installed emsdk@6.0.9 payload: `mcpp build/run/pack --target wasm32-emscripten` names bin/app.js + bin/app.wasm, runs and prints its marker (fixture declares its own `[target.wasm32-emscripten] runner = ["node"]`), and stages both files under `--format dir` and `--format tar` with both listed in .stage-manifest. The `shared` refusal is reached with MCPP_NO_AUTO_INSTALL=1 (no network). Tests: tests/unit/test_artifact_naming.cpp (new emscripten/none-OS cases), tests/unit/test_pack_stage_tree.cpp (emscripten_stem_family, positive and negative), tests/e2e/650_wasm_row_names_its_launcher.sh. * an application is a kind, and its link form is the row's (#622 A3) - modules/manifest/src/types.cppm: `Target::Kind` gains `Application`; `Target::is_program()` answers "is this a program" ({Binary, Application}) so every "is this the program" call site stops asking `kind == Binary`. - modules/manifest/src/toml.cppm: `kind = "app"` parses; the refusal for an unrecognized kind lists all four; `main` is required for `app` as for `bin`; `windows_subsystem`/`windows_entry` accept `is_program()`, not `Binary` alone; target inference from a bare `src/main.cpp` stays `Binary` on purpose (a platform fact an author states, never a default guessed). - modules/manifest/src/xpkg.cppm: a Form B descriptor's target can be `app`. - modules/toolchain-model/src/triple.cppm: `ApplicationForm` and `application_form(const Triple&)`, beside `artifact_naming` -- `env == "android"` answers `SharedObject`, every other row answers `Executable`. A manifest carries no predicate for this; the row alone decides. - src/build/plan.cppm: the link-unit builder maps `Application` to a shared link when `application_form(targetTriple)` is `SharedObject` and to an ordinary executable link otherwise; `target_output` grows an `asSharedObject` parameter so the Android row's `app` links as `lib<name>.so`; the entry-`main` census and the entry-synthesis gate widen from `is_program()` so an Android `app`'s `main` still compiles as a translation unit of the shared library. - src/build/prepare.cppm: a virtual workspace's default member picks a program (`is_program()`); the host-tool provisioning site stays literally `Binary` on purpose -- a build-time tool is exec'd directly and must be the row's executable form, not whatever an `app` resolves to. - modules/buildmcpp/src/directives.cppm: `windows-subsystem`/`windows-entry` directives reach a program (`is_program()`), not only `Binary`. - src/pack/route.cppm: `"app"` joins the kind-name table; an `Application` is a program route on every row, exactly as `Binary` is. - src/build/execute.cppm: `mcpp run` of an `app` whose row form is a shared library, with no `--format`, is refused by name rather than falling into the generic "no binary target" message, which would blame the user for a target that does exist. - tests/unit/{test_manifest,test_artifact_naming,test_build_directives}.cpp: parsing, the four-kind refusal, `is_program()`'s exact membership, `windows_subsystem`/`entry` acceptance, `application_form` per row. - tests/e2e/652_an_application_is_a_kind.sh (`# requires: elf gcc`): the host build, `mcpp run`, `mcpp pack`, and the older engine's three-kind refusal (verified by hand against mcpp 2026.9.12.2, recorded in the header rather than run). - tests/e2e/652b_an_application_on_android_is_a_shared_library.sh (`# requires: android-ndk`, new capability registered in run_all.sh): the Android row links `lib<name>.so` and no executable, `bin` is unaffected, and `mcpp run` without `--format` is refused with the exact sentence. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * build programs deploy what they generated, and read the platform floor (#622 A4, A11) Two build-program surfaces, protocol 11: - `mcpp::deploy(from, to)` places a file the program produced or selected beside the artifact at a path relative to the executable -- the build- program form of `[runtime] deploy` (#615). `from` may be an action's own absolute declared output, which the manifest key can never name because it refuses an absolute path. Wire: `mcpp:deploy=<from>\t<to>` (tab-separated, because an absolute Windows `from` contains `:`). `to` obeys `deploy_path_problem`, checked before `apply` on both the run path and the cache-hit path. - `mcpp::min_platform_version()` / `MCPP_TARGET_MIN_PLATFORM_VERSION` hands a build program the same platform floor the compiler's `--target` flag and the fingerprint slot already resolve: `14.0` on macOS, `18.0` on iOS, an API level on Android, empty elsewhere. `dist-apple` and `dist-apk` read this instead of restating the value, where it was measured drifting (`dist/apple.cppm:140-143`). The deploy directive surfaced a pre-existing gap in the root package's directive-tail mirror (prepare.cppm): `packages[0]` is snapshotted from the root manifest BEFORE the root's own build.mcpp runs, so `apply()`'s later mutations reach `*m` but not `packages[0].manifest` -- the object `resolve_runtime_contract` (plan.cppm) actually reads. The existing mirror already repairs this for sources/cflags/cxxflags/includeDirs/ldflags; this change adds `runtimeConfig.linkIntent.deploy` to that list. Without it a directive-sourced deploy entry landed in `*m` and produced no copy edge. Docs are T7's. The two surfaces this task adds: `mcpp::deploy(from, to)` and `mcpp::min_platform_version()` / `MCPP_TARGET_MIN_PLATFORM_VERSION`. * frameworks is per target, and platforms names the rows that exist (#622 A2, A7) * exceptions is the second abi member, and a requirement can sit on the target axis (#622 A1, A6) * mcpp run --format hands the distributable to the runner (#622 A10) `mcpp run --format <name>` is `mcpp pack --format <name>` -- the two prepares, the build, the staging, the provider's action -- followed by the ordinary run, with the artifact the pack pipeline reported as the operand. Runner resolution is unchanged (project runner, then a dependency's, then the payload descriptor's); `--format` with `--no-runner` is refused, and an unknown format is refused by the pack pipeline's own message naming the available set. `build_and_pack` now returns `PackOutcome{rc, artifacts}` instead of a bare exit code, so the artifact(s) it reported travel to the caller instead of only being printed; `mcpp pack` itself only ever needed the exit code (cmd_publish.cppm). The runner-resolution + exec tail of `build_run_target` is factored into `run_artifact_via_runner`, shared between the ordinary link-output run and the `--format` run. Second half of #622 A3: the pack pipeline's program-target selection asks `Target::is_program()` rather than `LinkUnit::Binary` alone, so an `app` whose row links it as a shared object (Android) is still found as the program target; `make_plan`/`Plan` carry that fact (`programIsSharedObject`), and `pack::run` stages such a file under `lib/`, as the closure stages any shared object, without attempting a dependency closure walk on an object this host cannot execute at all. Tests: new tests/e2e/656_run_hands_the_distributable_to_the_runner.sh; tests/e2e/652b_an_application_on_android_is_a_shared_library.sh extended with the pack + MCPP_TARGET_MIN_PLATFORM_VERSION assertions. * docs(design): the sandbox verification script for #622 * docs: the keys, the row and the directive of #622 * chore: bump version to 2026.9.12.3 * a build program under a target row's pin resolves the host's compiler, not the row's (#622) * changelog: the build program's host compiler under a row's pin * e2e 67: the platform vocabulary is asserted on the warning's absence, not on --strict * the dlopen-surface check counts an application on a library-form row as a program (#622 A3) --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85df514 commit c765c8c

58 files changed

Lines changed: 5635 additions & 318 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-12-622-a-ui-framework-on-android-ios-and-web.md

Lines changed: 1001 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-09-12-622-implementation-plan.md

Lines changed: 413 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env bash
2+
# Ecosystem verification for #622 against a PUBLISHED mcpp: the engine items
3+
# A1 to A7, A10 and A11 of the design record, each read from the published
4+
# binary rather than from a development tree, and (when the plugin release
5+
# exists) `mcpp:plugins`' dist-web through the index.
6+
#
7+
# B64=$(base64 -w0 .agents/docs/2026-09-12-622-verify.sh)
8+
# xlings subos use verify-622 --sandbox --cmd \
9+
# "echo $B64 | base64 -d > /tmp/v.sh && MCPP_VERIFY_VERSION=<ver> bash /tmp/v.sh"
10+
#
11+
# The sandbox has an empty $HOME on first use and a fresh /tmp; it shares the
12+
# xlings data directory, so the published mcpp is addressed by its store path
13+
# and the payloads the machine holds (emsdk, android-ndk) are visible. Every
14+
# section clears its own probe directory first, so a second run reads the
15+
# release and not the first run's residue. A section that cannot run says so
16+
# and is listed again in the summary.
17+
set -u
18+
19+
VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}"
20+
STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}"
21+
PLUGINS="${MCPP_VERIFY_PLUGINS:-}" # e.g. 0.7.0; empty skips section J
22+
23+
fails=0
24+
skipped=""
25+
fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); }
26+
ok() { printf 'ok: %s\n' "$1"; }
27+
section() { printf '\n== %s ==\n' "$1"; }
28+
skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped
29+
- $1"; }
30+
has_line() { grep -qxF -- "$2" "$1"; }
31+
has_text() { grep -qF -- "$2" "$1"; }
32+
33+
root=/tmp/verify-622
34+
rm -rf "$root"; mkdir -p "$root"
35+
36+
section "A. the published mcpp answers for itself, with the CN mirror configured"
37+
if [ ! -x "$STORE" ]; then
38+
fail "no mcpp at $STORE"; printf '\nfails=%d (nothing else can run)\n' "$fails"; exit 1
39+
fi
40+
got=$("$STORE" --version 2>&1 | head -1)
41+
case "$got" in *"$VER"*) ok "mcpp --version says $got" ;; *) fail "mcpp --version says '$got', expected $VER" ;; esac
42+
"$STORE" self config --mirror CN >/dev/null 2>&1 && ok "mcpp self config --mirror CN" || fail "mcpp self config --mirror CN"
43+
44+
# A small program every section reuses.
45+
mkprog() { # $1 dir, $2 name, $3 extra manifest text
46+
rm -rf "$1"; mkdir -p "$1/src"
47+
printf '[package]\nname = "%s"\nversion = "0.1.0"\n\n[targets.%s]\nkind = "bin"\nmain = "src/main.cpp"\n%s\n' "$2" "$2" "$3" > "$1/mcpp.toml"
48+
printf '#include <cstdio>\nint main() { std::puts("1-2-3"); return 0; }\n' > "$1/src/main.cpp"
49+
}
50+
51+
section "B. the wasm row: bin/<name>.js, the .wasm sibling, the staged family, shared refused"
52+
d=$root/b; mkprog "$d" w ""
53+
if (cd "$d" && "$STORE" build --target wasm32-emscripten > build.log 2>&1); then
54+
js=$(find "$d/target/wasm32-emscripten" -path '*/bin/w.js' | head -1)
55+
wasm=$(find "$d/target/wasm32-emscripten" -path '*/bin/w.wasm' | head -1)
56+
bare=$(find "$d/target/wasm32-emscripten" -path '*/bin/w' | head -1)
57+
[ -n "$js" ] && ok "launcher is bin/w.js" || fail "no bin/w.js"
58+
[ -n "$wasm" ] && ok "bin/w.wasm beside it" || fail "no bin/w.wasm"
59+
[ -z "$bare" ] && ok "no bare bin/w" || fail "a bare bin/w exists"
60+
(cd "$d" && "$STORE" run --target wasm32-emscripten > run.log 2>&1); has_line "$d/run.log" "1-2-3" && ok "mcpp run prints 1-2-3 through the payload's runner" || { fail "mcpp run did not print 1-2-3"; tail -3 "$d/run.log"; }
61+
(cd "$d" && "$STORE" pack --target wasm32-emscripten --format dir > pack.log 2>&1)
62+
staged=$(find "$d/target/dist" -name 'w.js' | head -1)
63+
if [ -n "$staged" ] && [ -f "${staged%.js}.wasm" ]; then ok "pack --format dir stages w.js and w.wasm"; else fail "pack did not stage the family"; tail -3 "$d/pack.log"; fi
64+
printf '\n[targets.wshared]\nkind = "shared"\n' >> "$d/mcpp.toml"; printf 'int f() { return 1; }\n' > "$d/src/lib.cpp"
65+
(cd "$d" && "$STORE" build --target wasm32-emscripten > shared.log 2>&1) && fail "kind = shared on wasm was accepted" || { has_text "$d/shared.log" 'a side module needs -sSIDE_MODULE' && ok "shared refused naming -sSIDE_MODULE" || { fail "shared refused for another reason"; tail -3 "$d/shared.log"; }; }
66+
else
67+
fail "wasm build failed"; tail -5 "$d/build.log"
68+
fi
69+
70+
section "C. a build program deploys what it generated (mcpp::deploy), and the copy survives a cache hit"
71+
d=$root/c; mkprog "$d" dep ""
72+
mkdir -p "$d/gen"; printf 'generated-1\n' > "$d/gen/in.txt"
73+
cat > "$d/build.mcpp" <<'CPP'
74+
import mcpp;
75+
#include <string>
76+
int main() {
77+
const std::string out = std::string(mcpp::out_dir()) + "/res.bin";
78+
mcpp::action a; a.id = "gen:res"; a.role = "source";
79+
a.arg("/bin/cp").arg("gen/in.txt").arg(out.c_str()).input("gen/in.txt").output(out.c_str()).submit();
80+
mcpp::deploy(out.c_str(), "dep.resources");
81+
return 0;
82+
}
83+
CPP
84+
if (cd "$d" && "$STORE" build > build.log 2>&1); then
85+
f=$(find "$d/target" -path '*/bin/dep.resources/res.bin' | head -1)
86+
[ -n "$f" ] && has_line "$f" "generated-1" && ok "bin/dep.resources/res.bin carries the generated content" || { fail "deploy'd file absent or wrong"; tail -3 "$d/build.log"; }
87+
bindir=$(dirname "$(dirname "$f")"); rm -rf "$bindir"
88+
(cd "$d" && "$STORE" build > build2.log 2>&1); [ -f "$f" ] && ok "restored after bin/ was deleted (replay on a cache hit)" || fail "not restored on the rebuild"
89+
(cd "$d" && "$STORE" pack --format dir > pack.log 2>&1); [ -n "$(find "$d/target/dist" -path '*dep.resources/res.bin' | head -1)" ] && ok "pack --format dir stages it" || fail "pack did not stage the deploy'd file"
90+
else
91+
fail "build with mcpp::deploy failed"; tail -5 "$d/build.log"
92+
fi
93+
94+
section "D. kind = app: a program on the host, a shared library on Android, run refused without --format"
95+
d=$root/d; rm -rf "$d"; mkdir -p "$d/src"
96+
printf '[package]\nname = "myapp"\nversion = "0.1.0"\n\n[targets.myapp]\nkind = "app"\nmain = "src/main.cpp"\n\n[targets.tool]\nkind = "bin"\nmain = "src/tool.cpp"\n\n[target.x86_64-linux-android]\nmin_api_level = 24\n' > "$d/mcpp.toml"
97+
printf '#include <cstdio>\nint main() { std::puts("app-1-2-3"); return 0; }\n' > "$d/src/main.cpp"
98+
printf '#include <cstdio>\nint main() { std::puts("tool-1-2-3"); return 0; }\n' > "$d/src/tool.cpp"
99+
if (cd "$d" && "$STORE" run myapp > run.log 2>&1) && has_line "$d/run.log" "app-1-2-3"; then ok "an app is a program on the host"; else fail "host run of the app"; tail -3 "$d/run.log"; fi
100+
if (cd "$d" && "$STORE" build --target x86_64-linux-android > android.log 2>&1); then
101+
[ -n "$(find "$d/target/x86_64-linux-android" -name 'libmyapp.so' | head -1)" ] && ok "libmyapp.so on Android" || fail "no libmyapp.so"
102+
[ -n "$(find "$d/target/x86_64-linux-android" -path '*/bin/tool' | head -1)" ] && ok "bin keeps meaning binary on Android" || fail "no bin/tool on Android"
103+
(cd "$d" && "$STORE" run myapp --target x86_64-linux-android > arun.log 2>&1) && fail "run of a library-form app was accepted" || { has_text "$d/arun.log" 'an application is a shared library that a package installs' && ok "run refused naming --format" || { fail "refused for another reason"; tail -3 "$d/arun.log"; }; }
104+
else
105+
skip "D (Android leg): build for x86_64-linux-android failed; is the NDK payload visible? $(tail -1 "$d/android.log")"
106+
fi
107+
108+
section "E. requires_abi on the target axis, and the two-member abi table"
109+
d=$root/e; rm -rf "$d"; mkdir -p "$d/lib/src" "$d/src"
110+
printf '[package]\nname = "needsthreads"\nversion = "0.1.0"\n\n[lib]\npath = "src/lib.cppm"\n\n[target.'"'"'cfg(linux)'"'"']\nrequires_abi = { threads = true }\n' > "$d/lib/mcpp.toml"
111+
printf 'export module needsthreads;\nexport int nt() { return 3; }\n' > "$d/lib/src/lib.cppm"
112+
printf '[package]\nname = "e"\nversion = "0.1.0"\n\n[dependencies]\nneedsthreads = { path = "lib" }\n\n[targets.e]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$d/mcpp.toml"
113+
printf 'import needsthreads;\n#include <cstdio>\nint main() { std::printf("1-2-%%d\\n", nt()); return 0; }\n' > "$d/src/main.cpp"
114+
(cd "$d" && "$STORE" build > b1.log 2>&1) && fail "a selector-scoped requirement was not enforced" || { has_text "$d/b1.log" "requires the artefact's ABI to have threads" && has_text "$d/b1.log" "cfg(linux)" && ok "refused naming the member and the selector" || { fail "refused for another reason"; tail -4 "$d/b1.log"; }; }
115+
printf '\n[target.'"'"'cfg(linux)'"'"'.abi]\nthreads = true\n' >> "$d/mcpp.toml"
116+
(cd "$d" && "$STORE" run > r.log 2>&1) && has_line "$d/r.log" "1-2-3" && ok "satisfied by the root's table" || { fail "build with the table"; tail -3 "$d/r.log"; }
117+
printf '\n[target.'"'"'cfg(linux)'"'"'.abi]\nfrobnicate = true\n' >> "$d/mcpp.toml"
118+
(cd "$d" && "$STORE" build > b3.log 2>&1) && fail "an unknown abi member was accepted" || { has_text "$d/b3.log" "threads, exceptions" && ok "unknown member refused naming both members" || { fail "refused without naming the members"; tail -2 "$d/b3.log"; }; }
119+
120+
section "F. frameworks per target: a Linux build is byte-identical with and without the tables"
121+
d=$root/f; mkprog "$d" fw ""
122+
(cd "$d" && "$STORE" build > b1.log 2>&1) && n1=$(find "$d/target" -name build.ninja | head -1) && cp "$n1" "$d/n1"
123+
printf '\n[runtime]\nframeworks = ["Foundation"]\n\n[target.macos.runtime]\nframeworks = ["AppKit"]\n\n[target.'"'"'cfg(os = "ios")'"'"'.runtime]\nframeworks = ["UIKit"]\n' >> "$d/mcpp.toml"
124+
rm -rf "$d/target"; (cd "$d" && "$STORE" build > b2.log 2>&1) && n2=$(find "$d/target" -name build.ninja | head -1)
125+
if [ -n "${n2:-}" ] && cmp -s "$d/n1" "$n2"; then ok "build.ninja identical on Linux"; else fail "build.ninja differs or build failed"; fi
126+
grep -q "unsupported key 'frameworks'" "$d/b2.log" && fail "frameworks reported as unsupported under a target table" || ok "frameworks accepted under [target.<sel>.runtime]"
127+
128+
section "G. platforms names the rows that exist"
129+
d=$root/g; mkprog "$d" pl 'platforms = ["linux", "ios", "android", "emscripten"]'
130+
sed -i 's/^\[targets.pl\]/[targets.pl]/' "$d/mcpp.toml"; python3 - "$d/mcpp.toml" <<'PY'
131+
import sys,re; p=sys.argv[1]; s=open(p).read()
132+
s=s.replace('version = "0.1.0"\n','version = "0.1.0"\nplatforms = ["linux", "ios", "android", "emscripten"]\n',1).replace('\nplatforms = ["linux", "ios", "android", "emscripten"]\n\n','\n\n')
133+
open(p,'w').write(s)
134+
PY
135+
(cd "$d" && "$STORE" build --strict > b1.log 2>&1) && ok "the six-word vocabulary passes --strict" || { fail "a known platform name was refused"; grep -i platform "$d/b1.log" | head -2; }
136+
sed -i 's/platforms = \[.*\]/platforms = ["web"]/' "$d/mcpp.toml"; rm -rf "$d/target"
137+
(cd "$d" && "$STORE" build --strict > b2.log 2>&1) && fail "'web' was accepted" || { has_text "$d/b2.log" "unknown platform 'web'" && has_text "$d/b2.log" "linux | macos | windows | ios | android | emscripten" && ok "'web' refused naming the six" || { fail "refused without the vocabulary"; tail -2 "$d/b2.log"; }; }
138+
139+
section "H. mcpp run --format hands the distributable to the runner"
140+
d=$root/h; mkprog "$d" rf ""
141+
host=$("$STORE" --version >/dev/null 2>&1; uname -m)-linux-gnu
142+
cat > "$d/runner.sh" <<'SH'
143+
#!/usr/bin/env bash
144+
echo "RUNNER: $1"; chmod +x "$1"; exec "$1"
145+
SH
146+
chmod +x "$d/runner.sh"
147+
printf '\n[target.%s]\nrunner = ["%s"]\n' "$host" "$d/runner.sh" >> "$d/mcpp.toml"
148+
cat > "$d/build.mcpp" <<'CPP'
149+
import mcpp;
150+
#include <string>
151+
#include <string_view>
152+
int main() {
153+
mcpp::provides_pack_format("blob");
154+
if (std::string_view(mcpp::pack_format()) != "blob") return 0;
155+
const std::string out = std::string(mcpp::out_dir()) + "/rf.blob";
156+
mcpp::action a; a.id = "blob"; a.role = "artifact";
157+
a.arg("/bin/cp").arg("${mcpp.target_file:rf}").arg(out.c_str()).input("${mcpp.target_file:rf}").output(out.c_str()).submit();
158+
return 0;
159+
}
160+
CPP
161+
(cd "$d" && "$STORE" run --format blob > r1.log 2>&1); has_text "$d/r1.log" "RUNNER: " && has_text "$d/r1.log" "rf.blob" && has_line "$d/r1.log" "1-2-3" && ok "run --format blob ran the blob through the runner" || { fail "run --format blob"; tail -4 "$d/r1.log"; }
162+
(cd "$d" && "$STORE" run > r2.log 2>&1); has_text "$d/r2.log" "RUNNER: " && ! has_text "$d/r2.log" "rf.blob" && ok "plain run hands the link output" || fail "plain run"
163+
(cd "$d" && "$STORE" run --format bogus > r3.log 2>&1) && fail "--format bogus accepted" || { has_text "$d/r3.log" "blob" && ok "unknown format refused naming blob" || fail "unknown format refusal"; }
164+
(cd "$d" && "$STORE" run --format blob --no-runner > r4.log 2>&1) && fail "--format with --no-runner accepted" || { has_text "$d/r4.log" "cannot be combined" && ok "--format with --no-runner refused" || fail "wrong refusal"; }
165+
166+
section "I. abi.exceptions on the Web row"
167+
d=$root/i; rm -rf "$d"; mkdir -p "$d/src"
168+
printf '[package]\nname = "ex"\nversion = "0.1.0"\n\n[targets.ex]\nkind = "bin"\nmain = "src/main.cpp"\n\n[target.'"'"'cfg(os = "emscripten")'"'"'.abi]\nexceptions = true\n' > "$d/mcpp.toml"
169+
printf '#include <cstdio>\n#include <stdexcept>\nint main() { try { throw std::runtime_error("x"); } catch (const std::exception&) { std::puts("1-2-3"); } return 0; }\n' > "$d/src/main.cpp"
170+
if (cd "$d" && "$STORE" run --target wasm32-emscripten > r1.log 2>&1) && has_line "$d/r1.log" "1-2-3"; then
171+
ok "throw and catch under node with exceptions = true"
172+
n=$(find "$d/target/wasm32-emscripten" -name build.ninja | head -1); grep -q -- '-fexceptions' "$n" && ok "-fexceptions in build.ninja" || fail "-fexceptions absent from build.ninja"
173+
sed -i '/\.abi\]/,$d' "$d/mcpp.toml"; rm -rf "$d/target"
174+
(cd "$d" && "$STORE" run --target wasm32-emscripten > r2.log 2>&1) && has_line "$d/r2.log" "1-2-3" && fail "the marker printed without the table" || ok "without the table the run does not print the marker"
175+
else
176+
fail "exceptions on the Web row"; tail -4 "$d/r1.log"
177+
fi
178+
179+
section "J. mcpp:plugins dist-web through the index"
180+
if [ -z "$PLUGINS" ]; then
181+
skip "J: MCPP_VERIFY_PLUGINS not set (the plugin release does not exist yet)"
182+
else
183+
d=$root/j; mkprog "$d" web ""
184+
printf '\n[build-dependencies.mcpp]\nplugins = { version = "%s", features = ["dist-web"], host-module = true }\n' "$PLUGINS" >> "$d/mcpp.toml"
185+
printf 'import mcpp;\nimport mcpp.dist.web;\nint main() { return mcpp::dist::web::generate({ .target = "web" }) ? 0 : 1; }\n' > "$d/build.mcpp"
186+
if (cd "$d" && "$STORE" pack --target wasm32-emscripten --format web > p.log 2>&1); then
187+
idx=$(find "$d/target" -path '*/web/index.html' | head -1)
188+
[ -n "$idx" ] && [ -f "$(dirname "$idx")/web.js" ] && [ -f "$(dirname "$idx")/web.wasm" ] && ok "dist-web wrote index.html, web.js, web.wasm" || { fail "dist-web output incomplete"; ls "$(dirname "${idx:-$d}")"; }
189+
else
190+
fail "pack --format web through mcpp:plugins@$PLUGINS"; tail -5 "$d/p.log"
191+
fi
192+
fi
193+
194+
printf '\nfails=%d\n' "$fails"
195+
if [ -n "$skipped" ]; then printf 'sections NOT RUN:%s\nA pass with sections not run is not a pass for those sections.\n' "$skipped"; fi
196+
[ "$fails" -eq 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-
280 records.
21+
282 records.
2222

2323
## By subject
2424

@@ -47,6 +47,8 @@ Records that declare one. Everything else is listed by date below.
4747
### targets
4848

4949
- [A verified Web run that asked the host for node](2026-09-12-a-verified-web-run-that-asked-the-host-for-node.md) — landed
50+
- [Implementation plan: a UI framework on Android, iOS and Web (#622)](2026-09-12-622-implementation-plan.md) — active
51+
- [A UI framework on Android, iOS and Web: where each item of #622 lands, and the three it does not list](2026-09-12-622-a-ui-framework-on-android-ios-and-web.md) — active
5052
- [SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web](2026-09-11-sdk-toolchains-and-ios-local-verification.md) — landed
5153
- [Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins](2026-09-11-platform-targets-design-review.md) — active
5254

@@ -61,6 +63,8 @@ Records that declare one. Everything else is listed by date below.
6163

6264
- [The engine gaps left open after the SDK batch](2026-09-12-engine-gaps-after-the-sdk-batch.md) — landed
6365
- [A verified Web run that asked the host for node](2026-09-12-a-verified-web-run-that-asked-the-host-for-node.md) — landed
66+
- [Implementation plan: a UI framework on Android, iOS and Web (#622)](2026-09-12-622-implementation-plan.md) — active
67+
- [A UI framework on Android, iOS and Web: where each item of #622 lands, and the three it does not list](2026-09-12-622-a-ui-framework-on-android-ios-and-web.md) — active
6468
- [Six open issues: what each one actually is, and what would answer it](2026-09-11-six-open-issues-analysis.md) — active
6569
- [SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web](2026-09-11-sdk-toolchains-and-ios-local-verification.md) — landed
6670
- [Where a platform's knowledge belongs: iOS, Android and Web across the engine, the index and the plugins](2026-09-11-platform-targets-design-review.md) — active

0 commit comments

Comments
 (0)