E5: package app/shellApp without Gradle (cargo xtask apk)
New xtask/ crate (no deps) runs cargo ndk -> javac -> d8 -> aapt2 -> zipalign -> apksigner directly, signed with the same key build-apk.sh uses. Both pass conditions proved on the ai-app-2 emulator: the xtask APK installs over the Gradle-built shellApp, and the notification service starts and posts a real notification while backgrounded. Adds one printRuntimeClasspathJars task to shellApp/build.gradle.kts (and a matching signingConfig) -- the one disclosed Gradle call the xtask still makes, to resolve the AndroidX/:link dependency graph. That call's Kotlin compilation of :link as a side effect also answers E3's open kotlinc question, so no Java port of ServerStore was needed. Wires a second Apk component into .dev-updater.ron beside the existing one. Full writeup in RUST.md's E5 box. Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
1 parent
32a5256a0d
commit
ceabd00805
11 files changed
+1289
-25
No files matched your search
@@ -39,10 +39,24 @@ session spending an afternoon on them again.
|
||||
- **Done**: E0 (toolchain), E1 (Masonry on android-view, which found the
|
||||
keyboard gap — now explained, see below), E2 (a transcript in Masonry,
|
||||
which found that Masonry has no touch-scroll on Android at all — see
|
||||
below), **E3 (the Kotlin/Java shell over a JNI bridge into Rust, both
|
||||
pass conditions proved on the emulator — see its own box)**, I0a, I0b
|
||||
(iris builds on a pinned nightly and runs), I1 (parley + glyph atlas),
|
||||
I2 (iris on android-view), I3 (`iris::widget::List`).
|
||||
below), E3 (the Kotlin/Java shell over a JNI bridge into Rust, both
|
||||
pass conditions proved on the emulator — see its own box), **E5 (the
|
||||
Gradle-free packaging xtask, both pass conditions proved — see its own
|
||||
box)**, I0a, I0b (iris builds on a pinned nightly and runs), I1 (parley +
|
||||
glyph atlas), I2 (iris on android-view), I3 (`iris::widget::List`).
|
||||
- **E5 done, 2026-09-05.** `cargo xtask apk` (new `xtask/` crate at the
|
||||
repo root, zero dependencies) replaces Gradle for packaging
|
||||
`app/shellApp`: `cargo ndk` → `javac`/`d8` → `aapt2` → `zipalign` →
|
||||
`apksigner`, signed with the same key `app/build-apk.sh` uses. Both pass
|
||||
conditions held on this checkout's emulator: `adb install -r` over the
|
||||
Gradle-built `shellApp` succeeded (same key, so the signatures matched),
|
||||
and the notification service reached its follow-loop and posted a real
|
||||
notification while the app was backgrounded. E3's open `kotlinc`
|
||||
question resolved itself as a side effect of the one Gradle call still
|
||||
needed for AndroidX dependency resolution — see E5's own box for the
|
||||
full account, including the one disclosed place Gradle still runs and
|
||||
what was deliberately left undone (a real-device `arm64-v8a` install,
|
||||
dex shrinking).
|
||||
- **E3 done, 2026-09-05, and unlike E1/E2 it is committed to this repo**
|
||||
(`android-shell/` — a JNI-bridge crate on `client-core` — plus a new
|
||||
Gradle module `app/shellApp/`, left deliberately separate from
|
||||
@@ -1235,28 +1249,194 @@ accepted.
|
||||
a mock.
|
||||
- [ ] **E4 — the same screen on the desktop** in a winit window, from the
|
||||
same crate, with only the layout differing.
|
||||
- [ ] **E5 — the packaging xtask**: `cargo ndk` → `javac`/`d8` → `aapt2`
|
||||
→ `zipalign` → `apksigner`, signed with the existing release key,
|
||||
installed through Dev Updater. Pass: the APK installs over the
|
||||
Gradle-built one and the notification service starts.
|
||||
- [x] **E5 — the packaging xtask (2026-09-05).** Both pass-condition
|
||||
proofs held on this checkout's own emulator: `adb install -r` of the
|
||||
xtask-built APK over the Gradle-built one succeeded, and the
|
||||
notification service reached its follow-loop and posted a real
|
||||
notification while the app was backgrounded. `cargo xtask apk` at
|
||||
the repo root (`.cargo/config.toml`'s alias for `cargo run
|
||||
--manifest-path xtask/Cargo.toml --`) runs `cargo ndk` →
|
||||
`javac`/`d8` → `aapt2` → `zipalign` → `apksigner` with no Gradle
|
||||
driving the packaging itself -- one disclosed exception, below.
|
||||
|
||||
**Not blocked by E3's layout**, and one thing E5 will need to
|
||||
account for. `app/shellApp/`'s two Java classes and generated
|
||||
`PinnedCa.java` are plain `javac` input with no Kotlin of their own
|
||||
(`android-shell/src/settings.rs`'s doc explains why `kotlinc` still
|
||||
appears in the graph regardless: `:link`, the shared `wg-app-link`
|
||||
submodule, is Kotlin and is a real dependency of this module, not
|
||||
something E3 introduced). `android-shell` itself builds with plain
|
||||
`cargo ndk build --release -o .../jniLibs/`, exactly the shape "Can
|
||||
the APK be built without Gradle?" assumed. What E5 will actually
|
||||
need to solve that E3 did not: producing `:link`'s classes (or an
|
||||
equivalent Keystore-sealed-token implementation) without inflating
|
||||
the toolchain E5 is trying to shrink -- either accept `kotlinc` for
|
||||
that one shared submodule, prebuild it once into a jar/aar E5
|
||||
consumes as a binary input, or reimplement the Keystore sealing in
|
||||
Rust after all (rejected in E3 for reuse and phone-compatibility
|
||||
reasons, but the calculus is different once Kotlin is otherwise
|
||||
gone).
|
||||
*Where it lives.* `xtask/` (new, independent crate at the repo
|
||||
root -- **no Cargo workspace**, matching every other crate here;
|
||||
`run-tests.sh` already `cd`s into each rather than assuming one).
|
||||
**Zero dependencies**: every step is "run this SDK/JDK tool with
|
||||
these arguments and check its exit status," which needs nothing a
|
||||
crate would add (AGENTS.md's "new dependencies need a reason").
|
||||
`src/sdk.rs` finds the SDK root/build-tools/`android.jar` the same
|
||||
way `app/android-env.sh` does ($ANDROID_HOME, then
|
||||
$ANDROID_SDK_ROOT, then `~/Android/Sdk`); `src/keystore.rs`
|
||||
finds-or-generates the release key with the exact recipe
|
||||
`app/build-apk.sh` uses (same env vars, same path, same `keytool`
|
||||
invocation) so the two tools sign with the *same* key, plus a
|
||||
`--debug` path using the conventional `~/.android/debug.keystore`;
|
||||
`src/apk.rs` is the pipeline itself, `src/main.rs` the ~40-line CLI.
|
||||
About 420 lines total against RUST.md's earlier "about 150" guess --
|
||||
the difference is almost entirely dependency handling (below), which
|
||||
the earlier estimate didn't anticipate.
|
||||
|
||||
*`:link`'s `kotlinc` question, resolved.* Checked first, since E3
|
||||
left it open: no standalone `kotlinc` exists on this machine (not on
|
||||
PATH, not under any SDK -- only `kotlin-compiler-embeddable` jars
|
||||
inside Gradle's own distributions). So the choice was never
|
||||
"invoke kotlinc" versus "port `ServerStore`/`ServerSettings` to
|
||||
Java" as originally framed -- a third route fell out of solving the
|
||||
*other* open dependency problem (androidx, next paragraph): the one
|
||||
Gradle call already needed for that also compiles `:link`'s Kotlin
|
||||
as a side effect, via Gradle's own embedded compiler, and hands back
|
||||
the resulting `classes.jar` in the same resolved-jars list. That is
|
||||
RUST.md's own "prebuild it once into a jar/aar E5 consumes as a
|
||||
binary input" option, arrived at for free rather than built
|
||||
specially -- no Java port of `ServerStore` was written, and
|
||||
`android-shell/src/settings.rs`'s JNI class-by-name lookup
|
||||
(`com/example/wgapplink/ServerStore`) needed no change.
|
||||
|
||||
*One disclosed exception to "no Gradle in the loop": dependency
|
||||
resolution.* `app/shellApp` depends on `:link` (Kotlin, above) and
|
||||
on `androidx.core:core-ktx` -- not a compile-time dependency of the
|
||||
two Java stub classes (`MainActivity`/`NotificationService` import
|
||||
only `android.*`), but a **runtime** one: `android-shell/src/notify.rs`
|
||||
reaches `NotificationCompat`/`NotificationChannelCompat`/
|
||||
`NotificationManagerCompat`/`ServiceCompat`/`ContextCompat` by class
|
||||
name over JNI, so their bytecode has to be in the final dex even
|
||||
though nothing in this pipeline's own Java source mentions them.
|
||||
Reimplementing a Maven/AAR dependency resolver to avoid one Gradle
|
||||
call was not a good trade against "smallest honest route" (the
|
||||
standard this file already applied to `kotlinc`) -- so
|
||||
`app/shellApp/build.gradle.kts` gained one task,
|
||||
`printRuntimeClasspathJars`, which asks the `releaseRuntimeClasspath`
|
||||
configuration for its artifacts through an `ArtifactView` requesting
|
||||
the `android-classes-jar` attribute (the same post-AAR-transform
|
||||
view AGP's own dexing task consumes, so an AAR is already unpacked
|
||||
to a plain `.jar` by the time the xtask sees it) and writes their
|
||||
absolute paths, one per line, to
|
||||
`app/shellApp/build/xtask/runtime-classpath.txt`. `cargo xtask apk`
|
||||
runs `./gradlew :shellApp:printRuntimeClasspathJars` once (a few
|
||||
seconds, mostly UP-TO-DATE on a warm Gradle daemon), reads that file,
|
||||
and hands every jar in it to `d8` as an ordinary program input --
|
||||
`:link`'s `classes.jar` among them, per the paragraph above. Nothing
|
||||
past that one call touches Gradle. **What this trades away**: the
|
||||
pipeline is not Gradle-free end to end, only Gradle-free for the
|
||||
part that was actually expensive (assembling and dexing the app's
|
||||
own code, which the earlier options -- kotlinc, or a hand-rolled
|
||||
resolver -- were the two ways to avoid entirely). Recorded here
|
||||
rather than left implicit, matching how the `kotlinc` compromise
|
||||
above is recorded.
|
||||
|
||||
*The rest of the pipeline, in order (`apk.rs`):* `cargo ndk -t
|
||||
arm64-v8a -t x86_64 -P 26 -o app/shellApp/src/main/jniLibs/ build
|
||||
--release -p android-shell` (both ABIs by default -- real phone and
|
||||
this machine's emulator -- `--abi` overrides; always `--release`
|
||||
for the native library regardless of the APK's signing variant, for
|
||||
the reason E1 already established: a debug build's Vulkan
|
||||
object-labelling segfaults this emulator's driver, and there is no
|
||||
reason for a signing choice to make this crate's `.so` bigger).
|
||||
`javac -cp android.jar` compiles `MainActivity.java`,
|
||||
`NotificationService.java` and a freshly generated `PinnedCa.java`
|
||||
(same template as the Gradle `generatePinnedCa` task, same
|
||||
opening-quotes-adjacent-to-`"""` rule from AGENTS.md's "Things that
|
||||
have bitten") into one `classes.jar` (`jar cf` -- `d8` rejects a
|
||||
bare directory of `.class` files outright, "Unsupported source file
|
||||
type", discovered by trying it). `d8 --release --min-api 24 --lib
|
||||
android.jar` dexes that jar plus every classpath jar from the
|
||||
paragraph above into one `classes.dex` (no multidex needed at this
|
||||
size). `aapt2 link` compiles `app/shellApp/src/main/AndroidManifest.xml`
|
||||
into the base APK's `resources.arsc` -- the checked-in manifest has
|
||||
no `package` attribute (Gradle injects one from `android.namespace`
|
||||
during a manifest merge this pipeline doesn't run), so `apk.rs`
|
||||
writes a copy with `package="com.example.aiapp.shell"` spliced in
|
||||
rather than editing the source manifest, and refuses to run at all
|
||||
if the source ever gains one of its own (a version-drift guard
|
||||
cheaper than a real merge). `--min-sdk-version`/`--target-sdk-version`/
|
||||
`--version-code`/`--version-name` are passed on the command line for
|
||||
the same reason -- the raw manifest carries none of them, Gradle's
|
||||
`defaultConfig` normally does. `jar uf` (not a hand-rolled zip
|
||||
writer -- `jar` ships with the JDK this pipeline already needs)
|
||||
merges `classes.dex` and a staged `lib/<abi>/libandroid_shell.so`
|
||||
tree into the base APK (cargo-ndk's `-o` writes
|
||||
`jniLibs/<abi>/*.so`, matching the Gradle source-set layout it was
|
||||
pointed at; Android's own zip convention wants `lib/<abi>/*.so` at
|
||||
the archive root, hence the staging copy rather than an in-place
|
||||
rename). `zipalign -f -p 4` then `apksigner sign` finish it, signed
|
||||
with `~/.config/ai-app/release.jks` by default or
|
||||
`~/.android/debug.keystore` under `--debug`. The signed APK is
|
||||
copied to `xtask/build/outputs/apk/<mode>/ai-app-shell-<mode>.apk`
|
||||
as a final step -- a Gradle-shaped path (`*/build/outputs/apk/*/*.apk`)
|
||||
chosen so Dev Updater's fixed-pattern APK discovery
|
||||
(`~/repos/dev-updater/server/src/discover.rs`'s `APK_PATTERNS`,
|
||||
which has no per-component path override) finds it without any
|
||||
change on that side; the working files above it stay under
|
||||
`target/xtask/apk/`, an ordinary build-cache location (gitignored,
|
||||
along with `xtask/target/`).
|
||||
|
||||
*Wired into `.dev-updater.ron`*: a second `Apk` component, `shell`,
|
||||
`build: "cargo xtask apk"`, `modes: ["release", "debug"]`, no `cwd`
|
||||
(defaults to the checkout root, which both the `cargo xtask` alias
|
||||
and the publish path above need -- `.cargo/config.toml`'s alias
|
||||
resolves its `--manifest-path` relative to the *invoking* working
|
||||
directory, not to where the config file lives, which is what ruled
|
||||
out giving this component its own `cwd`). Dev Updater's `ByMode`
|
||||
appends the chosen mode word as the command's last argument
|
||||
(`build-apk.sh`'s own interface, per that component's comment), so
|
||||
`main.rs` accepts bare `release`/`debug` as well as `--release`/
|
||||
`--debug` for typing by hand. The existing `app` component
|
||||
(`build-apk.sh`, Gradle) is untouched.
|
||||
|
||||
*Verification.* `cargo fmt -- --check` and `cargo clippy
|
||||
--all-targets` clean, zero warnings, for `xtask` (host target --
|
||||
nothing in it is Android-specific; it *runs* `cargo ndk`, it isn't
|
||||
cross-compiled itself). `./run-tests.sh`: 127 `server` + 88
|
||||
`client-core` tests, unaffected, still passing. `apksigner verify
|
||||
--print-certs` on the xtask's release output confirms a V3 signer
|
||||
with `CN=ai-app` -- the same key `build-apk.sh` generates.
|
||||
|
||||
*The two pass-condition proofs*, both on this checkout's own AVD
|
||||
(`ai-app-2`, GPU host, brought up and torn down within this
|
||||
session):
|
||||
- **Installs over the Gradle-built one.** Built the Gradle release
|
||||
variant first (`AI_APP_KEYSTORE=~/.config/ai-app/release.jks
|
||||
AI_APP_KEYSTORE_PASSWORD=$(cat
|
||||
~/.config/ai-app/release.jks.password) ./gradlew
|
||||
:shellApp:assembleRelease` -- needed its own signing block added
|
||||
to `app/shellApp/build.gradle.kts`, copied from `androidApp`'s,
|
||||
since `shellApp` had none before this), installed it fresh
|
||||
(`adb uninstall com.example.aiapp.shell` first -- an older debug
|
||||
install from E3 testing was signed with a different key and
|
||||
`install -r` over it fails loudly with
|
||||
`INSTALL_FAILED_UPDATE_INCOMPATIBLE`, which is the correct,
|
||||
expected failure for a mismatched key rather than a bug), then
|
||||
`adb install -r xtask/build/outputs/apk/release/ai-app-shell-release.apk`:
|
||||
**`Success`**.
|
||||
- **The notification service starts.** Enrolled via
|
||||
`adb shell "am start -a android.intent.action.VIEW -d
|
||||
'aiappshell://enroll?host=10.0.2.2&port=<sandbox port>&token=<token>'"`,
|
||||
force-stopped the app, then re-launched it once (enrollment calls
|
||||
`sync()` from `MainActivity.onCreate`). `adb logcat` shows
|
||||
`ActivityManager: Background started FGS: Allowed ... intent:
|
||||
... cmp=com.example.aiapp.shell/.NotificationService`, immediately
|
||||
followed by `android-shell: jni::vm::java_vm: Attached thread
|
||||
ai-app-notifications`, a real TLS handshake to the sandbox's
|
||||
`10.0.2.2:<port>`, and `Response { status: 200 ... }` on
|
||||
`/notifications`. Pressed home, spawned a sandbox session and sent
|
||||
it `/question Should E5 proceed?`; `adb shell dumpsys notification
|
||||
--noredact` then shows a live `NotificationRecord` for
|
||||
`com.example.aiapp.shell`, `channel=sessions`, `tag=<session id>`
|
||||
-- posted while the app held no visible activity, the same bar
|
||||
E3's own proof cleared.
|
||||
|
||||
*Left undone, honestly.* No attempt to shrink the dex (R8/minify is
|
||||
off, matching `shellApp`'s existing `isMinifyEnabled = false`, so
|
||||
the APK carries the full unshrunk `androidx`/Kotlin-stdlib/coroutines
|
||||
graph -- about 5.2 MB signed with both ABIs, most of it native
|
||||
libraries and that dependency graph rather than this project's own
|
||||
code). No `--abi arm64-v8a`-only real-device install was attempted
|
||||
this session (no physical phone reachable from here); the emulator
|
||||
proof above is `x86_64` plus a cross-compiled but unexercised
|
||||
`arm64-v8a` `.so` in the same APK. Multidex is unneeded at today's
|
||||
size but nothing in `dex()` checks for the 64k-method ceiling should
|
||||
the dependency graph grow.
|
||||
|
||||
### The iris track
|
||||
|
||||
|
||||
Reference in new issue
Block a user