0.9.0: dist-apk takes a manifest template and several Java roots, dist-web copies through the engine - #20
Merged
Sunrisepeak merged 5 commits intoSep 12, 2026
Conversation
…es as an array
options::manifest_template renders a project AndroidManifest.xml through
six tokens substituted verbatim: {{application_id}}, {{label}},
{{activity}}, {{lib_name}}, {{min_sdk}}, {{target_sdk}}. Three are
required, not merely substituted, because their value is also written
to assets/mcpp-run.json, which adb-run reads to start the application:
{{application_id}} and {{activity}} always, {{lib_name}} at level 0. A
missing required token or an unknown {{...}} token is refused at plan
time, naming it. The built-in default is 0.8.0's manifest_xml output
expressed with these tokens, so level 0 with no template renders
byte-identical to 0.8.0's.
options::java_sources becomes std::vector<std::string>: one javac over
every root's .java files and one d8 over the result. rerun_if_changed_glob
is declared only for a root under mcpp::manifest_dir(); a root outside
it (a dependency's own Java tree) is not walked by the glob fingerprint
regardless, and its files are already inputs of the javac action while
its version is already in the build's fingerprint.
Design record: .agents/docs/2026-09-13-four-upstream-asks-from-a-ui-framework.md, §3.
The two copy steps become argv { "${mcpp.self}", "stage", "--verify",
"content", "--output", dst, src }, the same copier every stage_file edge
in build.ninja already runs: it creates the destination's parent,
compares content and writes only on a difference. Plan-time
create_directories is removed with it. The POSIX-only note leaves the
header; this member's floor rises to the mcpp release that carries
${mcpp.self} and mcpp stage's argument shape as an engine contract.
Design record: .agents/docs/2026-09-13-four-upstream-asks-from-a-ui-framework.md, §4.
…s idempotent pack
tests/apk-consumer: build.mcpp reads APK_CONSUMER_TEMPLATE and
APK_CONSUMER_LEVEL1 to reach five configurations without a second
fixture. check-apk-features.sh runs all five: (a) level 0 with no
template, byte-identical to the 0.8.0 manifest fixture; (b) a template
naming a uses-permission and a receiver, both present in aapt2 dump
xmltree on the linked base.apk; (c) a template missing
{{application_id}}, refused naming the token and assets/mcpp-run.json;
(d) a template naming {{bogus}}, refused naming it; (e) two Java roots,
one under the project (tests/apk-consumer/java) and one a sibling
directory reached by an absolute path (tests/apk-consumer-external-java),
producing one classes.dex with classes from both. (c) and (d) read
build.mcpp's own stdout/stderr by re-invoking the compiled binary
directly with the documented MCPP_* contract, because mcpp discards a
build program's captured output on a plan-time refusal's exit code of 0.
tests/web-consumer/check-web-idempotent.sh: a second, no-op
mcpp pack --format web changes no staged file's mtime, the criterion
${mcpp.self} stage gives for free and cp never could. A sibling of
check-web-plan.sh, which keeps passing unchanged.
.github/workflows/ci.yml: both wired in beside the existing
"dist-apk produces a signed APK, level 0 and level 1" and
"dist-web produces a static directory, and node runs it" steps;
MCPP_VERSION raised to 2026.9.13.1, the release both P1-P3 need.
Design record: .agents/docs/2026-09-13-four-upstream-asks-from-a-ui-framework.md, §3.5, §4.3, §9.2.
README rows for dist-apk and dist-web state the new floor and why:
dist-web needs ${mcpp.self} and mcpp stage's argument shape as an
engine contract; dist-apk's own manifest-template and Java-array
changes ask nothing new of the engine, but this collection publishes
one package at one version, and this is the release CI verifies it
under from here on.
mcpp.toml and src/plugins.cppm: version 0.9.0.
The mcpp 2026.9.13.1 release this depends on does not exist yet, so
CI is red on the fetch step until it does (see the pull request body).
mcpp::rerun_if_changed_glob((root + "/**/*.java").c_str()) declared an ABSOLUTE pattern for a root under mcpp::manifest_dir(), because opt.java_sources's own roots (manifest_dir() composed with a subdirectory) are absolute. The engine matches a glob by comparing the candidate file made relative to the package root against the pattern (modules/manifest/src/glob.cppm, path_matches_glob); an absolute pattern is compared against a relative candidate and never matches anything, so the fingerprint is always the empty set and a .java file appearing under the project root never changes it -- "a .java added under the project root re-runs the build program" was not true. root_in_project now returns the manifest-relative form (std::optional <std::string>) instead of a bool, and the glob is declared with that relative path. Measured directly on the recorded cache fingerprint (build.mcpp.cache's "glob <hash> java/**/*.java" line), which is the precise evidence: with the pre-fix absolute pattern, the recorded hash was identical (cbf29ce484222325) whether or not a .java file existed under the project root; with the fix, the hash changes (78b24cb88185e8c9 -> 4cd5de0f1766a171) when one is added. tests/apk-consumer/ check-apk-features.sh's new leg (f) asserts this directly, plus the external root's continuing absence from any recorded glob, plus (as a functional sanity check, not a caching claim -- this engine reruns build.mcpp on every `mcpp pack` of this fixture regardless of any glob, so a dex-content check alone cannot distinguish the two) that classes.dex picks up a class added under either root.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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,reasonset, printed to the child's own stderr exactly as this member's other refusals are) naming the token -- the missing-token message also namesassets/mcpp-run.json, whichadb-runreads to start the application. The built-in default ismanifest_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 withmcpp::rerun_if_changed. The unknown-token check is deliberatelydist-apk's own and not proposed fordist-web(a page template may legitimately carry{{ }}for a front-end framework); the asymmetry is recorded in a comment inapk.cppm.dist-apk, P2 --options::java_sourcesbecomesstd::vector<std::string>. Onejavacover every root's.javafiles, oned8.rerun_if_changed_globis declared only for a root undermcpp::manifest_dir(); a root outside it (a dependency's own tree) is not walked by the glob fingerprint regardless, its files are alreadyjavacaction 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-timecreate_directoriesis removed (stagecreates 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.dist-apkanddist-web(floor2026.9.13.1for both, one sentence each on why);mcpp.tomlandsrc/plugins.cppmto0.9.0;.github/workflows/ci.ymlMCPP_VERSIONto2026.9.13.1.Post-review fix: the java_sources rerun glob was absolute
Review found that P2's
mcpp::rerun_if_changed_glob((root + "/**/*.java").c_str())declared an absolute pattern for a root undermcpp::manifest_dir(), becauseopt.java_sources's own roots (composed frommcpp::manifest_dir()) are absolute. The engine matches a glob by comparing the candidate file made relative to the package root against the pattern (modules/manifest/src/glob.cppm,path_matches_glob); an absolute pattern is compared against a relative candidate and never matches anything, so the fingerprint is always the empty set and a.javafile appearing under the project root never changed it -- "a.javaadded to the project root re-runs the build program on the next build" (design record §3.5) was not true. 0.8.0 had the same shape under one root; this PR is where it is stated and fixed.Reverse leg, measured before the fix, exactly as asked:
build.mcpp.cache's recorded fingerprint for the pattern was identical (cbf29ce484222325) whether or notAddedLater.javaexisted under the project root -- direct proof the fingerprint never saw the file. (The literal "doesmcpp packskip a rebuild and so miss the class" reading could not be measured throughmcpp packon this fixture: this engine re-runsbuild.mcppon everymcpp packof this project regardless of the glob -- confirmed by running it three times in a row with nothing changed at all, always "build.mcpp compiling/build.mcpp running", never "up to date (cached)" -- which is a property of this fixture's dispatch pass, not of the glob, and true both before and after this fix. A plain, non-packagingmcpp builddoes cache correctly, but never declares the glob at all, sinceplan_forreturns before thejava_sourcesbranch whenpack_format()is empty. Soclasses.dexpicking up an added file is not, by itself, evidence the glob works -- it would look identical either way. The cache's own recorded fingerprint is the evidence that actually distinguishes the two, and is what the fix and its test rely on.)Fix:
root_in_projectnow returnsstd::optional<std::string>-- the manifest-relative form -- instead of abool; the glob is declared with that relative path (*rel + "/**/*.java"), never withrootitself. After the fix, the recorded hash changes with the real file set:78b24cb88185e8c9(2 files) ->4cd5de0f1766a171(3 files,AddedLater.javaadded).New test leg (f) in
tests/apk-consumer/check-apk-features.sh, both directions:java/**/*.javaglob hash changes when a file is added under the project root (the precise, caching-independent evidence above);classes.dexcontains the added class in both cases -- true regardless of the fix, but still worth asserting since it is the actual thing a project observes.Tests
tests/apk-consumeris extended (not a new, HuxerUI-specific fixture):build.mcppreads two environment variables (APK_CONSUMER_TEMPLATE,APK_CONSUMER_LEVEL1) to reach five configurations without a second project. New fixtures:manifest-template-good.xml(auses-permissionand areceiver),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 pathbuild.mcppcomputes, standing in for a path dependency's own Java tree.check-apk-features.shruns criteria (a)-(f) 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.shis a new sibling ofcheck-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.4but with${mcpp.self}and the unbounded action lists -- the release will be2026.9.13.1)AndroidManifest.xmlas a checked-in fixture; built this branch's code andcmp'd the twomcpp pack --format apkwithmanifest-template-good.xml;aapt2 dump xmltreeon the linkedbase.apkandroid.permission.INTERNETandorg.mcpp.apkconsumer.SampleReceiver{{application_id}}build.mcppbinary directly (see note below);mcpp packitself also refused, "no action claimed --format 'apk'"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{{bogus}}the manifest template names an unknown token '{{bogus}}' -- expected one of application_id, label, activity, lib_name, min_sdk, target_sdkmcpp pack --format apkwithAPK_CONSUMER_LEVEL1=1;dexdump -l plainon the producedclasses.dexorg.mcpp.apkconsumer.MainActivity;(project root) andorg.mcpp.apkconsumer.ExternalHelper;(external root)java/**/*.javaglob hash identical with/withoutAddedLater.java(cbf29ce484222325both times)78b24cb88185e8c9->4cd5de0f1766a171);classes.dexcontainsAddedLaterapk-consumer-external-java, before or after adding a file thereclasses.dexstill contains the added class (a functional fact, not a caching claim -- see the note above)dist-websecond packmcpp pack --format webruns 1s apart; every staged file's mtime comparedbuild.ninja's command line confirmed as<engine path> stage --verify content --output <dst> <src>tests/web-consumer/check-web-plan.shPASS: dist-web produces a static directory, node runs it, 1-2-3tests/all-rules-compilemcpp build && mcpp runall-rules-compile okmcpp.tomlvssrc/plugins.cppm0.9.0Note on (c)/(d):
mcppdiscards 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 aplan_forrefusal never makesbuild.mcppexit non-zero --submit()returnstrueforapplies = falseexactly 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-compiledbuild.mcppbinary directly with the documentedMCPP_*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 ordinarymcpp packCLI, which fails with "no action claimed --format 'apk'" in both cases.Note on (f): see "Post-review fix" above -- the cache's own recorded glob fingerprint, not
classes.dexcontent, is the evidence that actually distinguishes the pre-fix and post-fix behavior on this fixture, becausemcpp packrerunsbuild.mcppunconditionally here regardless of any cache.Note on the web idempotency check: the design record's
ninja -nleg was not usable as written -- this repository's.ninja_logis written by mcpp's own internal ninja, and the systemninja(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 whatcheck-web-idempotent.shasserts.Not verified
adb-run) -- this sandbox has neither an emulator nor an attached device, consistent with the existing CI step's own note.mcpp2026.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).