Skip to content

Commit 95cbc36

Browse files
fix(dist-apk): link a project's res/ as the base, and pin android-platform 36-r2 (#21)
* fix(dist-apk): link a project's res/ as the base, and pin android-platform 36-r2 Two defects an application with a launcher icon hits, measured by HuxerUI's Android row against 0.9.0. `-R` is aapt2's overlay: every resource in that unit must override one the base already defines. A project's `res/` IS the base, so its first colour failed with `color/ic_launcher_background does not override an existing resource`, and no icon, colour or string could be supplied at all. The compiled unit is now passed positionally. `xim:android-platform` was pinned at 35-r2 while the index has carried 36-r2 since openxlings/xim-pkgindex#834; a project declaring 36 for its own targetSdkVersion got a two-versions warning and the nearer pin. The option pins 36-r2, and the level-0 fixture manifest follows (`targetSdkVersion="36"` -- the one byte in it that is read from the platform, everything else is the template 0.8.0 rendered). `tests/apk-consumer` gains criterion (g): with APK_CONSUMER_RES set, its own `res/values/{strings,colors}.xml` -- two resources nothing else defines -- link, and `aapt2 dump resources` on the apk lists both. check-apk-features.sh (a) to (g) pass locally; HuxerUI's example 01 with a launcher `res/` packs to an APK carrying `res/mipmap-*`, `resources.arsc` and `application-icon-*` in its badging. Version 0.9.1. * fix: the version the module states follows the manifest (0.9.1) --------- Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
1 parent fe1db3e commit 95cbc36

10 files changed

Lines changed: 76 additions & 24 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,8 +1216,10 @@ jobs:
12161216
# refusal's exit code of 0 (the script's own header says why); (e) two
12171217
# Java roots, one under this project and one a sibling directory
12181218
# reached by an absolute path, dex into one file carrying classes from
1219-
# both.
1220-
- name: dist-apk's manifest template and Java roots, (a) to (e)
1219+
# both; (g) a project's own res/ links as the BASE -- two resources
1220+
# nothing else defines reach the apk (0.9.0 linked res/ as an aapt2
1221+
# overlay, which refuses any resource the base does not already have).
1222+
- name: dist-apk's manifest template, Java roots and res/, (a) to (g)
12211223
working-directory: tests/apk-consumer
12221224
run: MCPP="$MCPP" ./check-apk-features.sh
12231225

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ engine's own module family and is not used here.
7575
| `dist-wix` | `mcpp.dist.wix` | 2026.9.11.1 | `xim:wix`, which this feature declares on the Windows target axis; the .NET 6 runtime the tool needs is a Windows component the payload does not carry, and `wix --version` names it when it is missing. Windows only. Renders a `.wxs` and passes the program in as a preprocessor variable, because a bind path that resolves to nothing is silent |
7676
| `dist-apple` | `mcpp.dist.apple` | 2026.9.11.2 (macOS), 2026.9.12.3 (iOS) | the base macOS install (`ditto`, and `codesign` only when an identity is given). macOS: `Contents/`-shaped, as always. iOS (`aarch64-ios-sim`, `aarch64-ios`): a flat bundle at the same call site -- no separate feature, no separate module -- with `MinimumOSVersion` from `mcpp::min_platform_version()` (#622 A11), `CFBundleSupportedPlatforms` read from `env == "sim"`, `UIDeviceFamily`, `LSRequiresIPhoneOS`, and a directory of flat PNGs listed under `CFBundleIcons` in place of macOS's single `.icns` file. Signing is skipped on the simulator row (`options::identity` is ignored, with a `mcpp::warning` naming why) and unchanged on the device row. The iOS row is measured end to end on `macos-15`: a real `mcpp build`, `mcpp pack --format app` and `mcpp run` against `aarch64-ios-sim`, through `xim:apple-simulator-tools`' `simctl-run`. **The macOS floor is one release higher than its siblings** and the reason is not this member: under 2026.9.11.1 `mcpp pack` staged before dispatching and let a staging failure fail the command, so on a Mach-O program -- which the built-in closure walk refuses, because it uses `LD_TRACE_LOADED_OBJECTS` and dyld answers that by running the program -- every dispatched format was unreachable, including one that reads no staged tree. 2026.9.11.2 makes staging a service to the provider |
7777
| `dist-web` | `mcpp.dist.web` | 2026.9.13.1, the release that carries `${mcpp.self}` and `mcpp stage`'s argument shape as an engine contract (`stage --verify content --output <dst> <src>`) -- what lets this member's copy run on every host mcpp does, Windows included, in place of the `cp` this member used through 0.8.0 | nothing beyond mcpp: `wasm32-emscripten` only. Copies `${mcpp.stage_dir}/bin/` -- the `.js` launcher, the implicit `.wasm`, the `.data` when present, and every `mcpp::deploy`'d file, all of which #622 A5 and A4 already stage there -- to `<out_dir>/web/`, dropping the `bin/` prefix a browser has no use for, and writes an `index.html` rendered from a project template or a built-in default that loads the script with a plain `<script src>`. Each staged file and the rendered page are copied with `${mcpp.self} stage --verify content --output <dst> <src>`, the same copier every `stage_file` edge in `build.ninja` already runs -- no host-specific copy tool, no plan-time `create_directories` (`stage` creates the destination's parent), and a second `mcpp pack --format web` with nothing changed copies nothing |
78-
| `dist-apk` | `mcpp.dist.apk` | 2026.9.13.1, raised alongside `dist-web` in the same 0.9.0 release: this member'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 | `xim:android-build-tools`, `xim:android-platform` (versioned by API level, read back for `targetSdkVersion`), `xim:jdk-temurin` (`javac`/`jar`; `android-build-tools`' own runtime dependency provisions a JDK for its OWN wrappers only), `xim:android-debug-keystore`, all on the `cfg(env = "android")` axis. Generates `AndroidManifest.xml` and signs with the published Android debug key by default. Level 0 needs no Java (`hasCode="false"`, `android.app.NativeActivity`); `options::java_sources` adds `javac` + `d8` and a real `<activity>`. `options::manifest_template` renders a project manifest with six tokens substituted verbatim; `{{application_id}}` and `{{activity}}` are required always and `{{lib_name}}` at level 0, each refused by name at plan time when missing (naming `assets/mcpp-run.json`, which `adb-run` reads them from too) or when the template names an unknown token; empty renders 0.8.0's manifest byte-identically. `options::java_sources` is an array: one `javac` over every root's `.java` files and one `d8` over the result, so a project's own sources and a path dependency's join without being merged into one directory first, and `rerun_if_changed_glob` is declared only for a root under `mcpp::manifest_dir()` -- a dependency root's files are already inputs of the `javac` action and its version is already in the build's fingerprint. Android only -- an `app` target is a shared object on this row (#622 A3), and this member reads its native library from the staged tree and whatever `mcpp::deploy` placed beside the ordinary build's link output, because Android's own closure stages neither the dependency set nor the deployed files (see `dist/apk.cppm`'s header). CI packages both level 0 and level 1 and checks the archive (`libc++_shared.so` bundled only when the closure's `NEEDED` entries require it, `mcpp::deploy`'d files under `assets/`); the runner has no emulator or device, so the two rows that actually run were measured locally on 2026-09-12, through `adb-run`, against a KVM-accelerated x86_64 emulator and a physical arm64-v8a phone, both printing `1-2-3` and exiting 0 |
78+
| `dist-apk` | `mcpp.dist.apk` | 2026.9.13.1, raised alongside `dist-web` in the same 0.9.0 release: this member'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 | `xim:android-build-tools`, `xim:android-platform` (versioned by API level, read back for `targetSdkVersion`), `xim:jdk-temurin` (`javac`/`jar`; `android-build-tools`' own runtime dependency provisions a JDK for its OWN wrappers only), `xim:android-debug-keystore`, all on the `cfg(env = "android")` axis. Generates `AndroidManifest.xml` and signs with the published Android debug key by default. Level 0 needs no Java (`hasCode="false"`, `android.app.NativeActivity`); `options::java_sources` adds `javac` + `d8` and a real `<activity>`. `options::manifest_template` renders a project manifest with six tokens substituted verbatim; `{{application_id}}` and `{{activity}}` are required always and `{{lib_name}}` at level 0, each refused by name at plan time when missing (naming `assets/mcpp-run.json`, which `adb-run` reads them from too) or when the template names an unknown token; empty renders 0.8.0's manifest byte-identically. `options::resources` is a project's own `res/`, linked as the application's base resources from 0.9.1 (0.9.0 linked it as an aapt2 overlay, which refuses every resource the base does not already define -- a launcher icon could not be supplied). `options::java_sources` is an array: one `javac` over every root's `.java` files and one `d8` over the result, so a project's own sources and a path dependency's join without being merged into one directory first, and `rerun_if_changed_glob` is declared only for a root under `mcpp::manifest_dir()` -- a dependency root's files are already inputs of the `javac` action and its version is already in the build's fingerprint. Android only -- an `app` target is a shared object on this row (#622 A3), and this member reads its native library from the staged tree and whatever `mcpp::deploy` placed beside the ordinary build's link output, because Android's own closure stages neither the dependency set nor the deployed files (see `dist/apk.cppm`'s header). CI packages both level 0 and level 1 and checks the archive (`libc++_shared.so` bundled only when the closure's `NEEDED` entries require it, `mcpp::deploy`'d files under `assets/`); the runner has no emulator or device, so the two rows that actually run were measured locally on 2026-09-12, through `adb-run`, against a KVM-accelerated x86_64 emulator and a physical arm64-v8a phone, both printing `1-2-3` and exiting 0 |
7979

8080
### Each rule brings its own environment
8181

dist/apk.cppm

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ struct options {
134134
// manifest byte-identical to 0.8.0's (`tests/apk-consumer`).
135135
std::string manifest_template;
136136

137-
// A `res/`-shaped directory `aapt2 compile --dir` compiles. Empty means
138-
// no resources at all -- a legal, common case for a NativeActivity
139-
// application that draws everything itself.
137+
// A `res/`-shaped directory `aapt2 compile --dir` compiles and `aapt2
138+
// link` links as the application's OWN resources -- its launcher icon,
139+
// colours, strings. Empty means no resources at all -- a legal, common
140+
// case for a NativeActivity application that draws everything itself.
140141
std::string resources;
141142

142143
// LEVEL 1. One or more directories of `.java` sources; one `javac` over
@@ -358,12 +359,12 @@ inline std::string label_for(const options& opt) {
358359
}
359360

360361
// `xim:android-platform`'s own resolved directory is named after the
361-
// version it resolved -- "35-r2", "34-r3" (`pkgs/a/android-platform.lua`'s
362-
// own `extract_dir()` recovers the API level the identical way, from the
363-
// leading digits of its OWN version string) -- so the level a project
364-
// pinned is read back from the directory `xpkg_dir` already answered,
365-
// rather than duplicated as a second option this member could disagree
366-
// with.
362+
// version it resolved -- "36-r2", "35-r2", "34-r3" (`pkgs/a/android-
363+
// platform.lua`'s own `extract_dir()` recovers the API level the identical
364+
// way, from the leading digits of its OWN version string) -- so the level a
365+
// project pinned is read back from the directory `xpkg_dir` already
366+
// answered, rather than duplicated as a second option this member could
367+
// disagree with.
367368
inline std::string api_level_from_platform_dir(const std::string& dir) {
368369
if (dir.empty()) return {};
369370
const std::string leaf = fs::path(dir).filename().string();
@@ -995,7 +996,13 @@ inline plan plan_for(options opt = {}) {
995996
link.output = (outDir / "base.apk").string();
996997
link.argv = { aapt2, "link", "-I", androidJar, "--manifest", manifestPath,
997998
"--min-sdk-version", minSdk, "--target-sdk-version", targetSdk };
998-
if (!assembled.empty()) { link.argv.push_back("-R"); link.argv.push_back(assembled.back()); }
999+
// POSITIONAL, NOT `-R`. `-R` is aapt2's overlay: a compilation unit whose
1000+
// resources must each override one the base already defines, and a
1001+
// project's `res/` IS the base -- linked with `-R`, its first colour
1002+
// failed as `color/ic_launcher_background does not override an existing
1003+
// resource` (0.9.0, measured by HuxerUI's Android row). A positional
1004+
// unit is the base.
1005+
if (!assembled.empty()) link.argv.push_back(assembled.back());
9991006
link.argv.push_back("-o"); link.argv.push_back(link.output);
10001007
link.inputs = { manifestPath, androidJar };
10011008
if (!assembled.empty()) link.inputs.push_back(assembled.back());

mcpp.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "plugins"
33
namespace = "mcpp"
4-
version = "0.9.0"
4+
version = "0.9.1"
55
description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature"
66
license = "Apache-2.0"
77
authors = ["mcpp-community"]
@@ -425,14 +425,14 @@ implies = ["surface"]
425425
# key can be handed back as a dependency's "version" and joined to a store
426426
# path that does not exist, and this rule's own tools inherit the identical
427427
# hazard if they pin the alias. `xim:android-platform`'s table is keyed by
428-
# alias too (`"35" = { ref = "35-r2" }`), so this declares the resolved key
428+
# alias too (`"36" = { ref = "36-r2" }`), so this declares the resolved key
429429
# directly for the same reason -- `targetSdkVersion` is read back from the
430430
# directory this entry resolves to (`api_level_from_platform_dir`), so an
431431
# alias that silently failed to resolve would surface as a build-time
432432
# refusal rather than a wrong number, but there is no reason to court it.
433433
[target.'cfg(env = "android")'.feature-xlings.dist-apk]
434434
"xim:android-build-tools" = ">=37.0.0"
435-
"xim:android-platform" = "35-r2"
435+
"xim:android-platform" = "36-r2"
436436
"xim:jdk-temurin" = "25.0.4+7"
437437
"xim:android-debug-keystore" = "1.0.0"
438438

src/plugins.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export namespace mcpp::plugins {
4949
//
5050
// One package, one version: the number lives in mcpp.toml, and the CI step
5151
// `the collection states its own version` compares the two.
52-
inline constexpr std::string_view version = "0.9.0";
52+
inline constexpr std::string_view version = "0.9.1";
5353

5454
} // namespace mcpp::plugins
5555

tests/apk-consumer/build.mcpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
// -- see dist/apk.cppm's header for why that source is the ordinary build
55
// tree and not the pack pipeline's own staged tree on this row.
66
//
7-
// TWO ENVIRONMENT VARIABLES, READ ONLY HERE, SELECT THE FIVE CRITERIA OF
8-
// design record §3.5 / §9.2 P1-P2 WITHOUT A SECOND FIXTURE.
7+
// THREE ENVIRONMENT VARIABLES, READ ONLY HERE, SELECT THE SIX CRITERIA OF
8+
// design record §3.5 / §9.2 P1-P2 AND check-apk-features.sh's (g) WITHOUT A
9+
// SECOND FIXTURE.
910
//
1011
// APK_CONSUMER_TEMPLATE a manifest template file, package-root-relative.
1112
// Unset is the byte-identity criterion (a): the
@@ -15,8 +16,11 @@
1516
// sibling directory this build program reaches by
1617
// an absolute path, standing in for a path
1718
// dependency's own Java tree (design record §3.3).
19+
// APK_CONSUMER_RES non-empty links this package's own `res/` -- two
20+
// resources nothing else defines, so they link only
21+
// as the BASE (positional), never as an overlay.
1822
//
19-
// Neither variable is read by `dist-apk` itself -- both are this fixture's
23+
// No variable is read by `dist-apk` itself -- both are this fixture's
2024
// own choice of which of its options to set, the same way any project would
2125
// choose them from its own configuration.
2226
import std;
@@ -44,6 +48,10 @@ int main() {
4448
if (const char* tpl = std::getenv("APK_CONSUMER_TEMPLATE"); tpl && *tpl) {
4549
opt.manifest_template = tpl;
4650
}
51+
// (g) a project's own res/ -- the base, not an overlay.
52+
if (const char* res = std::getenv("APK_CONSUMER_RES"); res && *res) {
53+
opt.resources = root + "/res";
54+
}
4755
if (const char* level1 = std::getenv("APK_CONSUMER_LEVEL1"); level1 && *level1) {
4856
opt.activity = "org.mcpp.apkconsumer.MainActivity";
4957
// The project's own Java root, and a SIBLING of this fixture's own

tests/apk-consumer/check-apk-features.sh

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
# End-to-end checks for dist-apk's manifest template (design record
33
# `2026-09-13-four-upstream-asks-from-a-ui-framework.md`, §3.2 / §9.2 P1) and
44
# Java-array (§3.3 / P2) changes, criteria (a) to (e) of §3.5 read against
5-
# this fixture. `build.mcpp` here reads two environment variables this
6-
# script sets to reach each configuration without a second fixture -- see
7-
# its own header.
5+
# this fixture, plus (g): a project's own res/ links as the base. `build.mcpp`
6+
# here reads three environment variables this script sets to reach each
7+
# configuration without a second fixture -- see its own header.
88
#
99
# Usage: MCPP=<mcpp 2026.9.13.1+> ./check-apk-features.sh (run from this
1010
# directory, after `tests/apk-consumer`'s own level-0 CI step, whose target/
@@ -25,6 +25,11 @@ DEXDUMP="$BT/dexdump"
2525
[ -x "$DEXDUMP" ] || fail "dexdump not found under $BT"
2626

2727
# ── (a) level 0, no template: byte-identical to 0.8.0's manifest ───────────
28+
#
29+
# One byte is not 0.8.0's: `targetSdkVersion` is read back from the pinned
30+
# `xim:android-platform` (`api_level_from_platform_dir`), 36 since 0.9.1
31+
# (35 in 0.8.0 and 0.9.0). The fixture carries the current level; every
32+
# other byte is the template 0.8.0 rendered.
2833
echo "== (a) level 0, no template =="
2934
rm -rf target
3035
unset APK_CONSUMER_TEMPLATE APK_CONSUMER_LEVEL1 || true
@@ -193,3 +198,24 @@ unset APK_CONSUMER_LEVEL1
193198

194199
rm -f build-*.log pack-*.log xmltree-*.log refusal-*.log dexdump-*.log mcpp-env.txt
195200
echo "PASS: dist-apk's manifest template and Java-array criteria (a) to (f)"
201+
202+
# ── (g) a project's res/ is the base, so a NEW resource links ──────────────
203+
#
204+
# 0.9.0 handed the compiled res/ to `aapt2 link` as `-R`, which is aapt2's
205+
# overlay semantics: every resource must override one the base already
206+
# defines, so the first colour of a real res/ failed with `does not override
207+
# an existing resource`. A project's res/ is the base and is linked
208+
# positionally; the two resources here are defined by nothing else.
209+
echo "== (g) a project res/ links as the base =="
210+
rm -rf target
211+
unset APK_CONSUMER_TEMPLATE APK_CONSUMER_LEVEL1 || true
212+
export APK_CONSUMER_RES=1
213+
"$MCPP" build --target "$TARGET" > build-f.log 2>&1 || fail "build failed" build-f.log
214+
"$MCPP" pack --format apk --target "$TARGET" > pack-f.log 2>&1 || fail "pack failed" pack-f.log
215+
APK=$(find target -name 'apk-consumer.apk' | head -1)
216+
[ -n "$APK" ] || fail "no apk-consumer.apk" pack-f.log
217+
"$AAPT2" dump resources "$APK" > resources-f.log 2>&1 || fail "aapt2 dump resources failed" resources-f.log
218+
grep -q 'string/apk_consumer_title' resources-f.log || fail "the project's string did not link" resources-f.log
219+
grep -q 'color/apk_consumer_background' resources-f.log || fail "the project's colour did not link" resources-f.log
220+
unset APK_CONSUMER_RES
221+
echo "ok: a project res/ with resources nobody else defines links as the base"

tests/apk-consumer/fixtures/expected-manifest-level0-0.8.0.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
package="app.apk_consumer">
4-
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35"/>
4+
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="36"/>
55
<application android:label="apk-consumer" android:hasCode="false">
66
<activity android:name="android.app.NativeActivity" android:exported="true">
77
<meta-data android:name="android.app.lib_name" android:value="apk-consumer"/>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- A NEW resource, defined by nobody else: what an overlay link refuses. -->
4+
<color name="apk_consumer_background">#FF3F51B5</color>
5+
</resources>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<string name="apk_consumer_title">apk-consumer</string>
4+
</resources>

0 commit comments

Comments
 (0)