Compile our own Java into the APK, so the classes Android constructs can exist
The APK's only dex was Slint's. `assemble-apk.sh` found `classes.dex` under the android-activity backend's build directory and copied it in, and there was no `javac` step and no `d8` of anything of ours — package.sh's header said so outright, on the reasoning that the app has no Java because android-activity calls `android_main` directly. That reasoning holds for everything the app *calls* and fails for everything Android *constructs*. A `ContentProvider` is instantiated by the system from its manifest entry; nothing in the process ever reaches its constructor, so there is no JNI route to writing one in Rust. The launch `Intent` is the same shape of problem from the other end: it arrives through `Activity.getIntent()`, and the activity android-activity hands out is a stock `NativeActivity` rather than a subclass with room for code. FR-PLAT-AND-6 needs both, and FR-PLAT-AND-4 needs a foreground `Service`, which is a third. So: everything under `apps/darkroom-android/android/java/` goes through javac against `android.jar`, and d8 merges the classes with Slint's finished dex into one `classes.dex`. Merging rather than emitting a second dex keeps the staging and zip steps as they are — multidex is native at API 28, but two files to keep in step buys nothing at this size. The step is skipped when the tree holds no Java, which is the state this commit leaves it in. Nothing about the APK changes until a `.java` file appears. `-source 8 -target 8 -bootclasspath android.jar` is not caution about language features. It is the last combination in which javac allows the boot class path to be replaced: from `-target 9` the flag is rejected, the platform classes come from the JDK instead of from android.jar, and the build stays green while the device raises `NoClassDefFoundError` for a class Android never shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,27 @@ inside app-private storage, `run-as` needs a debuggable build, and the app has n
|
|||||||
fetch. docs/faces.md §2.2a is the decision and its limits — these files come back out before anything
|
fetch. docs/faces.md §2.2a is the decision and its limits — these files come back out before anything
|
||||||
is published.
|
is published.
|
||||||
|
|
||||||
|
## Java in the APK
|
||||||
|
|
||||||
|
There is no Gradle here and there is no AndroidX, so `assemble-apk.sh` compiles Java itself:
|
||||||
|
everything under `apps/darkroom-android/android/java/` goes through `javac` against `android.jar`,
|
||||||
|
and `d8` merges the result with the dex Slint's build script produced for its own helper. One
|
||||||
|
`classes.dex` comes out. Drop a `.java` file in that tree and the next build picks it up; an empty
|
||||||
|
tree skips the step entirely and the APK carries Slint's dex alone, which is what it did before the
|
||||||
|
step existed.
|
||||||
|
|
||||||
|
**Java is for what Android constructs, and nothing else.** The system instantiates a
|
||||||
|
`ContentProvider` from its manifest entry, and `Activity.getIntent()` is only reachable on an
|
||||||
|
activity object — android-activity hands Rust a JNI handle to a stock `NativeActivity`, not a
|
||||||
|
subclass it could have put code in. Those cases need a class in the APK at any price. Everything
|
||||||
|
else stays in Rust, because a second language is a second place for the logic to live.
|
||||||
|
|
||||||
|
The step compiles `-source 8 -target 8` with `-bootclasspath android.jar`. That is not conservatism
|
||||||
|
about language features — it is the last combination in which javac lets the boot class path be
|
||||||
|
replaced. From `-target 9` the flag is rejected and the platform classes come from the JDK instead,
|
||||||
|
which compiles and then dies on the device with `NoClassDefFoundError` for a class Android never
|
||||||
|
shipped.
|
||||||
|
|
||||||
## Pinned versions
|
## Pinned versions
|
||||||
|
|
||||||
| Component | Version | Why this one |
|
| Component | Version | Why this one |
|
||||||
|
|||||||
@@ -103,6 +103,80 @@ DEX="$(find "${TARGET_DIR}/${RUST_TARGET}/release/build" \
|
|||||||
[[ -n "${DEX}" ]] || { echo "error: Slint classes.dex not found — did the backend build?" >&2; exit 1; }
|
[[ -n "${DEX}" ]] || { echo "error: Slint classes.dex not found — did the backend build?" >&2; exit 1; }
|
||||||
echo " dex: ${DEX}"
|
echo " dex: ${DEX}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Our own Java.
|
||||||
|
#
|
||||||
|
# Almost all of this app is Rust, and the classes here are the exceptions the
|
||||||
|
# platform forces: Android constructs some things itself, from a class named in
|
||||||
|
# the manifest, and hands the result back. A `ContentProvider` is one — the
|
||||||
|
# system instantiates it, nothing in the process ever calls its constructor —
|
||||||
|
# and reaching the launch `Intent` is another, because it arrives through
|
||||||
|
# `Activity.getIntent()` and android-activity gives Rust a JNI handle to a
|
||||||
|
# stock `NativeActivity` rather than a subclass it could have put code in.
|
||||||
|
# Neither can be written as Rust at any price, so the APK needs a dex of ours.
|
||||||
|
#
|
||||||
|
# Skipped when the tree has no Java, which is the state this build was in until
|
||||||
|
# FR-PLAT-AND-6 and the state a cut-down branch may return to. The step then
|
||||||
|
# costs nothing and the APK carries Slint's dex alone, exactly as before.
|
||||||
|
JAVA_SRC="${REPO}/apps/darkroom-android/android/java"
|
||||||
|
JAVA_FILES=()
|
||||||
|
if [[ -d "${JAVA_SRC}" ]]; then
|
||||||
|
mapfile -t JAVA_FILES < <(find "${JAVA_SRC}" -name '*.java' | sort)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#JAVA_FILES[@]} -gt 0 ]]; then
|
||||||
|
echo "==> compiling ${#JAVA_FILES[@]} Java source(s)"
|
||||||
|
mkdir -p "${OUT}/classes" "${OUT}/dex"
|
||||||
|
|
||||||
|
# `-source 8 -target 8` with an explicit `-bootclasspath`, because that is
|
||||||
|
# the last combination in which javac still lets the boot class path be
|
||||||
|
# replaced: from `-target 9` onwards it rejects the flag outright, and the
|
||||||
|
# platform classes then come from the *JDK* rather than from android.jar.
|
||||||
|
# That compiles cleanly and fails on the device — a JDK class Android does
|
||||||
|
# not ship raises NoClassDefFoundError the moment it is touched, with
|
||||||
|
# nothing at build time having said so. Compiling against android.jar and
|
||||||
|
# only android.jar is what makes "it compiled" mean "the device has it".
|
||||||
|
#
|
||||||
|
# `-Xlint:-options` silences one note, "source value 8 is obsolete", which
|
||||||
|
# is advice about a future JDK rather than about this code. The JDK is
|
||||||
|
# pinned in the Dockerfile, so the day it matters is a deliberate bump.
|
||||||
|
javac \
|
||||||
|
-source 8 -target 8 \
|
||||||
|
-bootclasspath "${ANDROID_JAR}" \
|
||||||
|
-classpath "${ANDROID_JAR}" \
|
||||||
|
-Xlint:-options \
|
||||||
|
-d "${OUT}/classes" \
|
||||||
|
"${JAVA_FILES[@]}"
|
||||||
|
|
||||||
|
mapfile -t CLASS_FILES < <(find "${OUT}/classes" -name '*.class' | sort)
|
||||||
|
|
||||||
|
# d8 merges, it does not only translate. Handing it Slint's finished
|
||||||
|
# classes.dex alongside our fresh .class files yields one dex holding both,
|
||||||
|
# which is what the zip step below already expects. The alternative — ours
|
||||||
|
# as a second classes2.dex — works at API 28, where multidex is native, but
|
||||||
|
# leaves two files to keep in step in the staging and zip steps for no gain
|
||||||
|
# at this size.
|
||||||
|
#
|
||||||
|
# `--min-api` is MIN_API for the same reason the linkers use it: d8 decides
|
||||||
|
# what it must desugar from the oldest device this APK may reach, and a
|
||||||
|
# higher number here emits bytecode that verifies against the build
|
||||||
|
# machine's idea of Android and not against that device's.
|
||||||
|
#
|
||||||
|
# `--lib` is android.jar rather than a copy of the classpath: desugaring
|
||||||
|
# needs to see the platform types it is desugaring against, and without it
|
||||||
|
# d8 reports missing classes for anything our code touches.
|
||||||
|
"${BT}/d8" \
|
||||||
|
--release \
|
||||||
|
--min-api "${MIN_API}" \
|
||||||
|
--lib "${ANDROID_JAR}" \
|
||||||
|
--output "${OUT}/dex" \
|
||||||
|
"${DEX}" \
|
||||||
|
"${CLASS_FILES[@]}"
|
||||||
|
|
||||||
|
DEX="${OUT}/dex/classes.dex"
|
||||||
|
echo " dex: ${DEX} (ours merged with Slint's)"
|
||||||
|
fi
|
||||||
|
|
||||||
# A debug keystore. CI points KEYSTORE at a throwaway directory so nothing is
|
# A debug keystore. CI points KEYSTORE at a throwaway directory so nothing is
|
||||||
# persisted or published; package.sh keeps one in the cache on purpose, because
|
# persisted or published; package.sh keeps one in the cache on purpose, because
|
||||||
# Android refuses to update an installed app whose signature changed and a new
|
# Android refuses to update an installed app whose signature changed and a new
|
||||||
@@ -234,7 +308,8 @@ echo " signing: ${SIGNING}"
|
|||||||
|
|
||||||
# The intermediates are not the artefact, and leaving them beside it invites
|
# The intermediates are not the artefact, and leaving them beside it invites
|
||||||
# the wrong file being picked up by a glob.
|
# the wrong file being picked up by a glob.
|
||||||
rm -rf "${OUT}/staging" "${OUT}/res.zip" "${OUT}/base.apk" "${OUT}/unaligned.apk"
|
rm -rf "${OUT}/staging" "${OUT}/res.zip" "${OUT}/base.apk" "${OUT}/unaligned.apk" \
|
||||||
|
"${OUT}/classes" "${OUT}/dex"
|
||||||
|
|
||||||
echo "==> ${OUT}/darkroom.apk"
|
echo "==> ${OUT}/darkroom.apk"
|
||||||
ls -la "${OUT}/darkroom.apk"
|
ls -la "${OUT}/darkroom.apk"
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
#
|
#
|
||||||
# Gradle would add a second build system, a second dependency tree, and a
|
# Gradle would add a second build system, a second dependency tree, and a
|
||||||
# second place for the toolchain versions to drift out of step with the
|
# second place for the toolchain versions to drift out of step with the
|
||||||
# Dockerfile. The four tools it would have driven — aapt2, d8, zipalign,
|
# Dockerfile. The tools it would have driven — javac, aapt2, d8, zipalign,
|
||||||
# apksigner — are in build-tools already and are enough on their own, because
|
# apksigner — are in the image already and are enough on their own, because
|
||||||
# the app has no Java of its own: android-activity's glue calls android_main
|
# the app is Rust: android-activity's glue calls android_main directly, and
|
||||||
# directly, and the only classes in the APK are the ones Slint's build script
|
# the only Java in the APK is Slint's helper plus the handful of classes
|
||||||
# compiles for its own helper.
|
# Android insists on constructing itself (see assemble-apk.sh's Java step).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|||||||
Reference in New Issue
Block a user