Skip to content

The action lists have no size limit, and ${mcpp.self} names the engine (2026.9.13.1) - #629

Merged
Sunrisepeak merged 6 commits into
mainfrom
feat/action-heap-buffers-and-mcpp-self
Sep 12, 2026
Merged

The action lists have no size limit, and ${mcpp.self} names the engine (2026.9.13.1)#629
Sunrisepeak merged 6 commits into
mainfrom
feat/action-heap-buffers-and-mcpp-self

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

Implements P1-P4 of §9.2 in .agents/docs/2026-09-13-four-upstream-asks-from-a-ui-framework.md (mcpp repository), one release, 0.9.0.

  • dist-apk (dist/apk.cppm), P1 -- options::manifest_template. A project file, package-root-relative, rendered through six tokens substituted verbatim wherever they appear: {{application_id}}, {{label}}, {{activity}}, {{lib_name}}, {{min_sdk}}, {{target_sdk}}. {{application_id}} and {{activity}} are required always; {{lib_name}} joins them at level 0. A missing required token, or an unknown {{...}} token, is refused at plan time (applies = false, reason set, printed to the child's own stderr exactly as this member's other refusals are) naming the token -- the missing-token message also names assets/mcpp-run.json, which adb-run reads to start the application. The built-in default is manifest_xml's 0.8.0 output expressed with these tokens; level 0 with no template renders byte-identical to 0.8.0's. The template is declared with mcpp::rerun_if_changed. The unknown-token check is deliberately dist-apk's own and not proposed for dist-web (a page template may legitimately carry {{ }} for a front-end framework); the asymmetry is recorded in a comment in apk.cppm.
  • dist-apk, P2 -- options::java_sources becomes std::vector<std::string>. One javac over every root's .java files, one d8. rerun_if_changed_glob is declared only for a root under mcpp::manifest_dir(); a root outside it (a dependency's own tree) is not walked by the glob fingerprint regardless, its files are already javac action inputs, and its version is already in the build's fingerprint.
  • dist-web (dist/web.cppm), P3. The two copy steps become argv { "${mcpp.self}", "stage", "--verify", "content", "--output", dst, src }; the plan-time create_directories is removed (stage creates the parent); the POSIX-only note leaves the header and the README row; the header states ${mcpp.self}/mcpp stage's argument shape as the engine contract since 2026.9.13.1.
  • P4 -- release plumbing. README rows for dist-apk and dist-web (floor 2026.9.13.1 for both, one sentence each on why); mcpp.toml and src/plugins.cppm to 0.9.0; .github/workflows/ci.yml MCPP_VERSION to 2026.9.13.1.

Tests

tests/apk-consumer is extended (not a new, HuxerUI-specific fixture): build.mcpp reads two environment variables (APK_CONSUMER_TEMPLATE, APK_CONSUMER_LEVEL1) to reach five configurations without a second project. New fixtures: manifest-template-good.xml (a uses-permission and a receiver), manifest-template-missing-appid.xml, manifest-template-bogus-token.xml, a project-local Java root (tests/apk-consumer/java) and a sibling directory outside the fixture (tests/apk-consumer-external-java) reached by an absolute path build.mcpp computes, standing in for a path dependency's own Java tree. check-apk-features.sh runs criteria (a)-(e) and is wired into CI beside the existing "dist-apk produces a signed APK, level 0 and level 1" step. tests/web-consumer/check-web-idempotent.sh is a new sibling of check-web-plan.sh (which is unchanged and still passes) asserting a second, no-op pack copies nothing; wired in beside the existing dist-web step.

Criteria and local results (engine: a build carrying this batch's engine half, reporting 2026.9.12.4 but with ${mcpp.self} and the unbounded action lists -- the release will be 2026.9.13.1)

criterion what ran result
(a) level 0, no template built the 0.8.0 code against this engine on the same fixture, captured AndroidManifest.xml as a checked-in fixture; built this branch's code and cmp'd the two identical
(b) template with permission + receiver mcpp pack --format apk with manifest-template-good.xml; aapt2 dump xmltree on the linked base.apk lists both android.permission.INTERNET and org.mcpp.apkconsumer.SampleReceiver
(c) missing {{application_id}} re-invoked the compiled build.mcpp binary directly (see note below); mcpp pack itself also refused, "no action claimed --format 'apk'" message: the manifest template does not use '{{application_id}}', and assets/mcpp-run.json -- which adb-run starts the application from -- is written from the same value
(d) unknown {{bogus}} same message: the manifest template names an unknown token '{{bogus}}' -- expected one of application_id, label, activity, lib_name, min_sdk, target_sdk
(e) two Java roots mcpp pack --format apk with APK_CONSUMER_LEVEL1=1; dexdump -l plain on the produced classes.dex lists both org.mcpp.apkconsumer.MainActivity; (project root) and org.mcpp.apkconsumer.ExternalHelper; (external root)
dist-web second pack two mcpp pack --format web runs 1s apart; every staged file's mtime compared unchanged; build.ninja's command line confirmed as <engine path> stage --verify content --output <dst> <src>
tests/web-consumer/check-web-plan.sh run unmodified PASS: dist-web produces a static directory, node runs it, 1-2-3
existing "dist-apk ... level 0 and level 1" step re-run against this branch still passes (unaffected)
tests/all-rules-compile mcpp build && mcpp run compiles and runs; all-rules-compile ok
"the collection states its own version" mcpp.toml vs src/plugins.cppm both 0.9.0

Note on (c)/(d): mcpp discards a build program's captured stdout/stderr when it exits 0 (build_program.cppm: the capture is surfaced only on a non-zero exit or a timeout), and a plan_for refusal never makes build.mcpp exit non-zero -- submit() returns true for applies = false exactly as every other refusal in this member does, which is the behavior the design record asks this change to match ("printing to stderr exactly as its other refusals do"). So the refusal TEXT itself is checked by re-invoking the already-compiled build.mcpp binary directly with the documented MCPP_* build-program contract (docs/30) reconstructed from a prior, real build's resolved payload paths -- check-apk-features.sh's own header explains this. The FUNCTIONAL refusal (the overall command failing) is additionally checked through the ordinary mcpp pack CLI, which fails with "no action claimed --format 'apk'" in both cases.

Note on the web idempotency check: the design record's ninja -n leg was not usable as written -- this repository's .ninja_log is written by mcpp's own internal ninja, and the system ninja (a different version) on this machine reads that log, decides it is stale, and reports every edge as pending regardless of whether anything reran. The design record's alternative (unchanged mtimes) was used instead and is what check-web-idempotent.sh asserts.

Not verified

  • Real device/emulator runs of the level-1 APK (adb-run) -- this sandbox has neither an emulator nor an attached device, consistent with the existing CI step's own note.
  • CI itself: mcpp 2026.9.13.1 does not exist yet, so this PR's CI will be red on the fetch-the-release step until that engine release ships. Do not lower the pin to make CI green.

Design record: .agents/docs/2026-09-13-four-upstream-asks-from-a-ui-framework.md (mcpp repository), §3, §4, §9.2 (P1-P4).

Local full suite

tests/e2e/run_all.sh against a build of this branch before the guard fix: 353 passed, 37 skipped, 1 failed, the failure being 659 at N=600 refused by the old guard on Linux (the reverse leg of the guard fix). 659 passes against the build that carries the fix; 188, 313, 631 and 660 pass on both. The first Windows shard run of this PR (head ef96d829) measured the guard defect; head 88bd3136 carries the fix.

The bundled mcpp module held an action's six list fields in fixed arrays
(8192 bytes of serialised JSON for inputs and outputs, 16384 for command)
and refused a declaration that did not fit. The bound was in bytes, so a
consumer's checkout depth decided whether a list of 44 resource files was
accepted (HuxerUI#130 measured the margin at 45 bytes), and outputs is the
one list an author cannot shorten: an output the program does not name
cannot be built, and there is no depfile for outputs.

The arrays were fixed because one constraint was read as two. The module
must not import std, and its exported interface must name no std type;
neither forbids the heap, and <cstdlib> was already in the global module
fragment. The six arrays become one growable std-free buffer over realloc.
The exported surface and the protocol version are unchanged, and the
payload of every action that fit before is byte-identical, so the cache key
is untouched. The overflow marker keeps its wire form and now means
allocation failure; the engine's message says so.

e2e 659 declares 200 inputs and 200 outputs and a 19200-byte command and
reads the action's edge out of build.ninja; under 2026.9.12.4 the same
fixture is refused.
…able copy

An action's command is an argv with no shell, so a build program had no
portable way to copy a file: cp is absent on Windows, cmd /c copy is a
shell and an 8191-character limit, and a copier carried by a package is a
host-tool sub-build for one copy. The engine is the one program present
wherever a build runs, and mcpp stage --verify content --output <dst> <src>
is the copy every stage_file edge already performs. ${mcpp.self} joins the
argv substitution family and is replaced by the engine's absolute path, as
the check wrapper already bakes it in.

mcpp stage's argument shape is a contract from here on; its help text now
states the real default (content). e2e 660 copies the linked program
through the token on every shard; under 2026.9.12.4 the token stays
literal and the edge fails.
…dge line

check_inline_command_lengths read each build line as a proxy for the
command. That is right for a rule that expands $in and $out and wrong for
an action rule, whose command is a literal argv and whose inputs and
outputs exist only so that ninja can order and re-run it. e2e 659's first
run on the Windows shard refused an action with 200 outputs and 200 inputs
with the whole edge line counted as argv.

The guard now measures a literal command's own text. The refusal names the
edge by its first output and the count of the rest, instead of printing
every output. e2e 659 declares 600 inputs and 600 outputs, so the edge
line crosses the POSIX 128 KiB limit as well as Windows's 32767 and the
guard is measured on every shard; its negative leg declares a 140800-byte
literal command and expects the refusal to name the edge.
…arget

The Windows job of this workflow was green with one sandbox lineage and
red with the next on the same sources and the same runner image (PR #629:
lld could not find -lntdll and its neighbours). On a Windows host
openkal-windows's build program generates no import libraries, so where
lld finds them is a property of the host, the sandbox or the payload, and
nothing in the job said which. This step prints it.
@Sunrisepeak
Sunrisepeak merged commit a965a05 into main Sep 12, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants