bundling: Android APK pipeline moved into sx; android.sx state-on-plat
Week 7 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
plus the android.sx refactor + three sx-compiler fixes hit along the way
to get chess on Pixel 7 Pro responding to touch end-to-end.
library/modules/platform/bundle.sx now covers the Android APK shape
alongside macOS / iOS-sim / iOS-device. `android_bundle_main` discovers
the SDK ($ANDROID_HOME / $ANDROID_SDK_ROOT / $HOME/Library/Android/sdk),
picks the highest-versioned build-tools + platforms via
`process.run("ls .. | sort -V | tail -1")`, stages
`<apk>.stage/lib/arm64-v8a/<libfoo.so>`, synthesizes
AndroidManifest.xml (NativeActivity vs `#jni_main` Activity branch),
writes each `#jni_main` decl's Java source under
`<stage>/java/<pkg>/<Cls>.java`, runs javac --release 11 + d8 to
produce classes.dex, aapt2-links the unaligned APK, appends lib/ +
classes.dex + each registered asset tree via zip, zipalign + ensure
debug keystore via keytool + apksigner sign.
Compiler-side accessors (src/ir/compiler_hooks.zig + library/modules/compiler.sx):
- is_android predicate.
- set_manifest_path / manifest_path + set_keystore_path / keystore_path.
- jni_main_count / jni_main_foreign_path_at(i) /
jni_main_java_source_at(i) surface the `#jni_main` emissions that
the Zig createApk previously consumed directly.
- main.zig wires manifest_path, keystore_path, and the per-decl
(foreign_path, java_source) parallel slices into BuildConfig before
invoking the post-link callback.
CLI `--apk <path>` keeps working as a transitional alias: it now feeds
bundle_path so the existing auto-`post_link_module = "platform.bundle"`
shim fires the same way as `--bundle`. main.zig no longer calls
target.createApk directly.
Deletions in src/target.zig: createApk, compileJniMainSources,
buildJniMainManifest, buildAndroidManifest, ensureDebugKeystore,
libNameFromSoBasename, plus helpers splitForeignPath / discoverJavac /
discoverAndroidSdk / findHighestSubdir / runProcess / runProcessIn
(~400 lines). git grep returns only the obituary comment.
library/modules/platform/android.sx refactor (chess Android dependency):
- Module-level globals retired (g_app_window, g_egl_*, g_viewport_*,
g_dpi_scale, g_should_stop, g_render_thread*, g_user_main_fn,
g_touch_*) → AndroidPlatform struct fields.
- All sx_android_* helpers take `plat: *AndroidPlatform` as first arg.
Render thread receives plat via pthread_create's arg.
- New `logical_w: f32 = 0.0` field. Consumers set it before init() to
define the design width in points; `recompute_scale` derives
`dpi_scale = pixel_w / logical_w` (or 1.0 if unset). Called on
init / set_viewport / egl_init. drain_touches divides incoming
physical pixel coords by dpi_scale so chess sees logical-space
positions matching its layout. Touch lands on the right squares.
Three sx-compiler bugs hit + fixed along the way:
1. Top-level `inline if OS == .X { decls }` body decls were silently
dropped because scanDecls/lowerDecls had no .if_expr arm. New
`flattenComptimeConditionals` pre-pass in src/imports.zig
(threaded via ComptimeContext from core.zig) hoists matching arms
recursively. Regression at examples/124-inline-if-hoist-toplevel.sx.
2. Parser rejected `#import` / `#framework` inside inline-if bodies
because parseStmt in src/parser.zig only had arms for `#insert`.
Added the missing arms. Regression at
examples/123-inline-if-import-in-body.sx (landed earlier).
3. JNI `Call<T>Method` switches in src/ir/emit_llvm.zig (instance /
nonvirtual / static) were missing `.f32` rows — jfloat returns
(e.g. MotionEvent.getX/getY) fell into the silent-undef else arm.
Chess's sx_android_push_touch(plat, getAction(), getX(), getY())
delivered garbage f32 coords to the touch ring, so taps landed
nowhere recognisable. Added `.f32 => Jni.Call{Static,Nonvirtual,}FloatMethod`
rows to all three switches; lifted unsupported-type detection
from emit_llvm into lowerForeignMethodCall with proper
source-spanned diagnostics (`isJniReturnTypeSupported`). Regressions
at examples/ffi-jni-call-10-jfloat-return.sx,
examples/ffi-jni-class-09-multi-float-args.sx,
examples/ffi-jni-call-11-unsupported-return-diag.sx.
Stale-snapshot drift in tests/expected/ffi-objc-call-03-selector-sharing.ir
and ffi-objc-call-06-sret-return.ir picks up the new BuildOptions
accessor extern decls (is_android, set_manifest_path,
set_keystore_path, jni_main_count, jni_main_foreign_path_at,
jni_main_java_source_at). Verified diff is dead-decl-only.
Chess on Pixel 7 Pro: tap on e2 white pawn -> yellow selection +
green dots on legal e3/e4 targets; tap on e4 -> board updates with
1. e4, "Black to move" + "1. e4" in info panel.
zig build && zig build test && bash tests/run_examples.sh -> 145/145
green. bash tests/cross_compile.sh -> 7/7 green.
This commit is contained in:
@@ -43,6 +43,10 @@ bundle_main :: () -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if opts.is_android() {
|
||||
return android_bundle_main(opts, binary, bundle, bid);
|
||||
}
|
||||
|
||||
// Device builds without a real identity will be rejected by the
|
||||
// device, so fail fast with a clear hint — matches what the legacy
|
||||
// Zig path did at the top of createBundle.
|
||||
@@ -517,3 +521,604 @@ codesign :: (bundle: string, identity: string, ent_path: string) -> bool {
|
||||
out("error: codesign spawn failed\n");
|
||||
false;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Android APK pipeline.
|
||||
//
|
||||
// Same shape as the legacy Zig `createApk`:
|
||||
// 1. Discover SDK root + highest build-tools / platforms version.
|
||||
// 2. Stage `<apk>.stage/lib/arm64-v8a/<libfoo.so>`.
|
||||
// 3. Use the user-supplied AndroidManifest.xml or synthesize one
|
||||
// (NativeActivity shape when no `#jni_main` decl; Activity-bound
|
||||
// shape pointing at the user's `#jni_main` class otherwise).
|
||||
// 4. For each `#jni_main` decl: write `<stage>/java/<pkg>/<Cls>.java`,
|
||||
// compile via `javac --release 11 -classpath android.jar`, then
|
||||
// dex via `d8 --release --lib android.jar --output <stage>`.
|
||||
// 5. `aapt2 link -I android.jar --manifest <m> -o <apk>.unaligned`.
|
||||
// 6. `zip <unaligned> lib/` (from stage cwd) + `zip classes.dex` if
|
||||
// a dex was produced + zip each registered asset dir.
|
||||
// 7. `zipalign -f 4 <unaligned> <aligned>`.
|
||||
// 8. Ensure debug keystore (via `keytool`) at $HOME/.android or
|
||||
// `set_keystore_path()` override.
|
||||
// 9. `apksigner sign --ks ... --out <apk> <aligned>`.
|
||||
// =====================================================================
|
||||
|
||||
// Resolve a relative path against the current working directory at call
|
||||
// time, so it survives a later `cd` into a stage dir. Absolute paths
|
||||
// (leading `/`) are returned unchanged. Empty input is preserved.
|
||||
absolutify :: (path: string) -> string {
|
||||
if path.len == 0 { return path; }
|
||||
if path[0] == 47 { return path; }
|
||||
if r := run(str_to_cstr("pwd")) {
|
||||
if r.exit_code != 0 { return path; }
|
||||
cwd := r.stdout;
|
||||
// Strip trailing newline that `pwd` emits.
|
||||
if cwd.len > 0 {
|
||||
if cwd[cwd.len - 1] == 10 { cwd = substr(cwd, 0, cwd.len - 1); }
|
||||
}
|
||||
if cwd.len == 0 { return path; }
|
||||
return path_join(cwd, path);
|
||||
}
|
||||
path;
|
||||
}
|
||||
|
||||
android_bundle_main :: (opts: BuildOptions, binary: string, apk_path: string, bundle_id: string) -> bool {
|
||||
// The bundler `cd`s into the stage dir for `zip` steps, so any
|
||||
// relative path the caller gave us would resolve against the wrong
|
||||
// cwd. Pin everything to absolute paths up front.
|
||||
apk_path = absolutify(apk_path);
|
||||
binary = absolutify(binary);
|
||||
|
||||
sdk := discover_android_sdk();
|
||||
if sdk.len == 0 {
|
||||
out("error: cannot locate Android SDK \xe2\x80\x94 set $ANDROID_HOME\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
build_tools := find_highest_subdir(path_join(sdk, "build-tools"));
|
||||
if build_tools.len == 0 {
|
||||
out("error: no build-tools under ");
|
||||
out(sdk);
|
||||
out("/build-tools\n");
|
||||
return false;
|
||||
}
|
||||
platform_dir := find_highest_subdir(path_join(sdk, "platforms"));
|
||||
if platform_dir.len == 0 {
|
||||
out("error: no platforms under ");
|
||||
out(sdk);
|
||||
out("/platforms\n");
|
||||
return false;
|
||||
}
|
||||
android_jar := path_join(platform_dir, "android.jar");
|
||||
|
||||
aapt2_path := path_join(build_tools, "aapt2");
|
||||
zipalign_path := path_join(build_tools, "zipalign");
|
||||
apksigner_path := path_join(build_tools, "apksigner");
|
||||
d8_path := path_join(build_tools, "d8");
|
||||
|
||||
// Staging dir alongside the apk output.
|
||||
stage := concat(apk_path, ".stage");
|
||||
lib_dir := path_join(stage, "lib/arm64-v8a");
|
||||
|
||||
// Clean previous stage. `rm -rf` via shell until fs.sx grows
|
||||
// `delete_dir_all`.
|
||||
rm_cmd := concat("rm -rf \"", stage);
|
||||
rm_cmd = concat(rm_cmd, "\"");
|
||||
if r := run(str_to_cstr(rm_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: apk: failed to clean stage dir\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !create_dir_all(str_to_cstr(lib_dir)) {
|
||||
out("error: apk: cannot create stage lib dir\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// libsxhello.so must literally start with "lib" for Android's
|
||||
// loader. The user's -o path already does (build_options enforces
|
||||
// it). Copy by basename into the staging lib dir.
|
||||
so_basename := basename(binary);
|
||||
so_dest := path_join(lib_dir, so_basename);
|
||||
if !copy_file(str_to_cstr(binary), str_to_cstr(so_dest)) {
|
||||
out("error: apk: failed to copy .so into stage\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Manifest: user-supplied or auto-generated.
|
||||
manifest := opts.manifest_path();
|
||||
manifest_used := "";
|
||||
lib_name := lib_name_from_so_basename(so_basename);
|
||||
if manifest.len > 0 {
|
||||
manifest_used = manifest;
|
||||
} else {
|
||||
generated_xml := build_android_manifest(opts, bundle_id, lib_name);
|
||||
generated_path := path_join(stage, "AndroidManifest.xml");
|
||||
if !write_file(str_to_cstr(generated_path), generated_xml) {
|
||||
out("error: apk: failed to write AndroidManifest.xml\n");
|
||||
return false;
|
||||
}
|
||||
manifest_used = generated_path;
|
||||
}
|
||||
|
||||
// Compile each `#jni_main` decl's Java source.
|
||||
jm_count := opts.jni_main_count();
|
||||
if jm_count > 0 {
|
||||
if !compile_jni_main_sources(opts, stage, android_jar, d8_path) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// aapt2 link → unaligned apk with manifest + resources.
|
||||
unaligned := concat(apk_path, ".unaligned");
|
||||
aapt_cmd := concat("\"", aapt2_path);
|
||||
aapt_cmd = concat(aapt_cmd, "\" link -I \"");
|
||||
aapt_cmd = concat(aapt_cmd, android_jar);
|
||||
aapt_cmd = concat(aapt_cmd, "\" --manifest \"");
|
||||
aapt_cmd = concat(aapt_cmd, manifest_used);
|
||||
aapt_cmd = concat(aapt_cmd, "\" -o \"");
|
||||
aapt_cmd = concat(aapt_cmd, unaligned);
|
||||
aapt_cmd = concat(aapt_cmd, "\" 2>&1");
|
||||
if r := run(str_to_cstr(aapt_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: aapt2 link failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
out("error: aapt2 spawn failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Append lib/ tree. Using the `zip` command rather than re-encoding
|
||||
// the APK from scratch because aapt2 doesn't include arbitrary
|
||||
// directories and zip is on every macOS/Linux host by default.
|
||||
// Need to cd into stage so the relative `lib/` path is preserved
|
||||
// in the zip archive.
|
||||
if !run_in_dir(stage, concat("zip -q -r \"", concat(unaligned, "\" lib/"))) {
|
||||
return false;
|
||||
}
|
||||
if jm_count > 0 {
|
||||
if !run_in_dir(stage, concat("zip -q \"", concat(unaligned, "\" classes.dex"))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Asset dirs go in at their `dest` path inside the APK. The Zig
|
||||
// path used a hardcoded `assets/` walk; the sx form respects every
|
||||
// `add_asset_dir(src, dest)` pair the user registered.
|
||||
asset_count := opts.asset_dir_count();
|
||||
j : s64 = 0;
|
||||
while j < asset_count {
|
||||
src := opts.asset_dir_src_at(j);
|
||||
dest := opts.asset_dir_dest_at(j);
|
||||
if !zip_asset_dir(src, dest, unaligned) {
|
||||
return false;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
// zipalign → aligned apk.
|
||||
aligned := concat(apk_path, ".aligned");
|
||||
align_cmd := concat("\"", zipalign_path);
|
||||
align_cmd = concat(align_cmd, "\" -f 4 \"");
|
||||
align_cmd = concat(align_cmd, unaligned);
|
||||
align_cmd = concat(align_cmd, "\" \"");
|
||||
align_cmd = concat(align_cmd, aligned);
|
||||
align_cmd = concat(align_cmd, "\" 2>&1");
|
||||
if r := run(str_to_cstr(align_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: zipalign failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
out("error: zipalign spawn failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug keystore (auto-generated on first use) + apksigner.
|
||||
keystore := opts.keystore_path();
|
||||
if keystore.len == 0 {
|
||||
if home := env("HOME") {
|
||||
keystore = path_join(home, ".android/debug.keystore");
|
||||
} else {
|
||||
out("error: apk: cannot locate $HOME for default keystore\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !ensure_debug_keystore(keystore) {
|
||||
return false;
|
||||
}
|
||||
sign_cmd := concat("\"", apksigner_path);
|
||||
sign_cmd = concat(sign_cmd, "\" sign --ks \"");
|
||||
sign_cmd = concat(sign_cmd, keystore);
|
||||
sign_cmd = concat(sign_cmd, "\" --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey --out \"");
|
||||
sign_cmd = concat(sign_cmd, apk_path);
|
||||
sign_cmd = concat(sign_cmd, "\" \"");
|
||||
sign_cmd = concat(sign_cmd, aligned);
|
||||
sign_cmd = concat(sign_cmd, "\" 2>&1");
|
||||
if r := run(str_to_cstr(sign_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: apksigner failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
out("error: apksigner spawn failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up intermediates (keep stage/ in case users want to
|
||||
// inspect it).
|
||||
delete_file(str_to_cstr(unaligned));
|
||||
delete_file(str_to_cstr(aligned));
|
||||
run(str_to_cstr(concat("rm -rf \"", concat(stage, "\""))));
|
||||
|
||||
out("apk: ");
|
||||
out(apk_path);
|
||||
out("\n");
|
||||
true;
|
||||
}
|
||||
|
||||
// ── Android helpers ──────────────────────────────────────────────────
|
||||
|
||||
// Run `cmd` under a `cd <dir> && ...` shell wrapping. process.run
|
||||
// doesn't have a cwd arg in Phase 1A, so we compose it via the shell.
|
||||
// Output is folded via `2>&1` so failures hand the user one stream.
|
||||
run_in_dir :: (dir: string, cmd: string) -> bool {
|
||||
wrapped := concat("cd \"", dir);
|
||||
wrapped = concat(wrapped, "\" && ");
|
||||
wrapped = concat(wrapped, cmd);
|
||||
wrapped = concat(wrapped, " 2>&1");
|
||||
if r := run(str_to_cstr(wrapped)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: ");
|
||||
out(cmd);
|
||||
out(" failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
out("error: shell spawn failed\n");
|
||||
false;
|
||||
}
|
||||
|
||||
// Discover the Android SDK root. Honors $ANDROID_HOME /
|
||||
// $ANDROID_SDK_ROOT, otherwise picks the default install location on
|
||||
// macOS ($HOME/Library/Android/sdk).
|
||||
discover_android_sdk :: () -> string {
|
||||
if h := env("ANDROID_HOME") { return h; }
|
||||
if h := env("ANDROID_SDK_ROOT") { return h; }
|
||||
if home := env("HOME") {
|
||||
candidate := path_join(home, "Library/Android/sdk");
|
||||
if exists(str_to_cstr(candidate)) { return candidate; }
|
||||
}
|
||||
"";
|
||||
}
|
||||
|
||||
// Pick the lexicographically-highest subdir of `parent`. Equivalent to
|
||||
// `ls -1 <parent> | sort -V | tail -1`. Returns the full path or "".
|
||||
find_highest_subdir :: (parent: string) -> string {
|
||||
cmd := concat("ls -1 \"", parent);
|
||||
cmd = concat(cmd, "\" 2>/dev/null | sort -V | tail -1");
|
||||
if r := run(str_to_cstr(cmd)) {
|
||||
if r.exit_code != 0 { return ""; }
|
||||
name := r.stdout;
|
||||
// Strip trailing whitespace.
|
||||
while name.len > 0 {
|
||||
last := name[name.len - 1];
|
||||
if last == 10 { name = substr(name, 0, name.len - 1); }
|
||||
else if last == 13 { name = substr(name, 0, name.len - 1); }
|
||||
else if last == 32 { name = substr(name, 0, name.len - 1); }
|
||||
else if last == 9 { name = substr(name, 0, name.len - 1); }
|
||||
else { break; }
|
||||
}
|
||||
if name.len == 0 { return ""; }
|
||||
return path_join(parent, name);
|
||||
}
|
||||
"";
|
||||
}
|
||||
|
||||
// `libfoo.so` → `foo`. Android's `android.app.lib_name` meta-data
|
||||
// wants the trimmed name; the loader prepends `lib` and appends `.so`
|
||||
// at runtime.
|
||||
lib_name_from_so_basename :: (basename: string) -> string {
|
||||
name := basename;
|
||||
if name.len > 3 {
|
||||
if name[0] == 108 { // 'l'
|
||||
if name[1] == 105 { // 'i'
|
||||
if name[2] == 98 { // 'b'
|
||||
name = substr(name, 3, name.len - 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if name.len > 3 {
|
||||
last3 := name.len - 3;
|
||||
if name[last3] == 46 { // '.'
|
||||
if name[last3 + 1] == 115 { // 's'
|
||||
if name[last3 + 2] == 111 { // 'o'
|
||||
name = substr(name, 0, last3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
name;
|
||||
}
|
||||
|
||||
// AndroidManifest.xml synthesizer. When the program declares a
|
||||
// `#jni_main` class, the manifest points its `<activity
|
||||
// android:name>` at the user's class and flips
|
||||
// `android:hasCode="true"` so Android loads the bundled classes.dex.
|
||||
// Otherwise it falls back to the legacy NativeActivity shape with an
|
||||
// `android.app.lib_name` meta-data entry pointing at the .so.
|
||||
build_android_manifest :: (opts: BuildOptions, package: string, lib_name: string) -> string {
|
||||
pkg_esc := xml_escape(package);
|
||||
lib_esc := xml_escape(lib_name);
|
||||
if opts.jni_main_count() > 0 {
|
||||
// First `#jni_main` decl drives the Activity. The foreign_path
|
||||
// uses `/` separators; Java fully-qualified class names use
|
||||
// `.` so we rewrite.
|
||||
foreign := opts.jni_main_foreign_path_at(0);
|
||||
cls := slash_to_dot(foreign);
|
||||
cls_esc := xml_escape(cls);
|
||||
return format(#string MANIFEST
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="{}"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
<application android:label="{}" android:hasCode="true">
|
||||
<activity
|
||||
android:name="{}"
|
||||
android:exported="true"
|
||||
android:label="{}"
|
||||
android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
MANIFEST, pkg_esc, lib_esc, cls_esc, lib_esc);
|
||||
}
|
||||
// NativeActivity fallback — the .so provides ANativeActivity_onCreate.
|
||||
format(#string MANIFEST
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="{}"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
<application android:label="{}" android:hasCode="false">
|
||||
<activity
|
||||
android:name="android.app.NativeActivity"
|
||||
android:exported="true"
|
||||
android:label="{}"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize">
|
||||
<meta-data android:name="android.app.lib_name" android:value="{}" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
MANIFEST, pkg_esc, lib_esc, lib_esc, lib_esc);
|
||||
}
|
||||
|
||||
// `co/swipelab/sxchess/SxApp` → `co.swipelab.sxchess.SxApp`.
|
||||
slash_to_dot :: (path: string) -> string {
|
||||
buf := cstring(path.len);
|
||||
i := 0;
|
||||
while i < path.len {
|
||||
c := path[i];
|
||||
buf[i] = if c == 47 then 46 else c; // 47 = '/', 46 = '.'
|
||||
i += 1;
|
||||
}
|
||||
buf;
|
||||
}
|
||||
|
||||
// Last `/`-separated component of a forward-slash path (used to split
|
||||
// JNI foreign paths into pkg + class). `co/swipelab/Foo` → `Foo`.
|
||||
// `Foo` → `Foo`. `dir_part` returns the part before the last slash
|
||||
// (or "" if none).
|
||||
last_slash_component :: (path: string) -> string {
|
||||
i := path.len;
|
||||
while i > 0 {
|
||||
if path[i - 1] == 47 { return substr(path, i, path.len - i); }
|
||||
i -= 1;
|
||||
}
|
||||
path;
|
||||
}
|
||||
|
||||
dir_part :: (path: string) -> string {
|
||||
i := path.len;
|
||||
while i > 0 {
|
||||
if path[i - 1] == 47 { return substr(path, 0, i - 1); }
|
||||
i -= 1;
|
||||
}
|
||||
"";
|
||||
}
|
||||
|
||||
// Write each `#jni_main` decl's `.java` source, then compile to
|
||||
// classes via `javac --release 11 -classpath <android.jar>`, then dex
|
||||
// the resulting class files via `d8 --release --lib <android.jar>
|
||||
// --output <stage>` so `<stage>/classes.dex` lands where the
|
||||
// orchestrator can zip it into the APK.
|
||||
compile_jni_main_sources :: (opts: BuildOptions, stage: string, android_jar: string, d8_path: string) -> bool {
|
||||
java_root := path_join(stage, "java");
|
||||
classes_root := path_join(stage, "classes");
|
||||
if !create_dir_all(str_to_cstr(java_root)) {
|
||||
out("error: apk: cannot create java root\n");
|
||||
return false;
|
||||
}
|
||||
if !create_dir_all(str_to_cstr(classes_root)) {
|
||||
out("error: apk: cannot create classes root\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
javac := discover_javac();
|
||||
if javac.len == 0 {
|
||||
out("error: javac not on PATH and $JAVA_HOME unset \xe2\x80\x94 install a JDK (Android Studio bundles one at $ANDROID_STUDIO/Contents/jre)\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compose javac + d8 arg lists by walking jni_main_decls. Each
|
||||
// decl: write `<java_root>/<pkg>/<Cls>.java`, append java path to
|
||||
// javac argv + class path to d8 argv.
|
||||
javac_files := "";
|
||||
d8_files := "";
|
||||
count := opts.jni_main_count();
|
||||
i : s64 = 0;
|
||||
while i < count {
|
||||
foreign := opts.jni_main_foreign_path_at(i);
|
||||
java_source := opts.jni_main_java_source_at(i);
|
||||
pkg := dir_part(foreign);
|
||||
cls := last_slash_component(foreign);
|
||||
|
||||
pkg_dir := if pkg.len > 0 then path_join(java_root, pkg) else java_root;
|
||||
if !create_dir_all(str_to_cstr(pkg_dir)) {
|
||||
out("error: apk: cannot create java pkg dir\n");
|
||||
return false;
|
||||
}
|
||||
java_path := path_join(pkg_dir, concat(cls, ".java"));
|
||||
if !write_file(str_to_cstr(java_path), java_source) {
|
||||
out("error: apk: cannot write .java for ");
|
||||
out(foreign);
|
||||
out("\n");
|
||||
return false;
|
||||
}
|
||||
if javac_files.len > 0 { javac_files = concat(javac_files, " "); }
|
||||
javac_files = concat(javac_files, concat("\"", concat(java_path, "\"")));
|
||||
|
||||
class_subpath := if pkg.len > 0 then path_join(pkg, concat(cls, ".class")) else concat(cls, ".class");
|
||||
class_path := path_join(classes_root, class_subpath);
|
||||
if d8_files.len > 0 { d8_files = concat(d8_files, " "); }
|
||||
d8_files = concat(d8_files, concat("\"", concat(class_path, "\"")));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
javac_cmd := concat("\"", javac);
|
||||
javac_cmd = concat(javac_cmd, "\" -d \"");
|
||||
javac_cmd = concat(javac_cmd, classes_root);
|
||||
javac_cmd = concat(javac_cmd, "\" -classpath \"");
|
||||
javac_cmd = concat(javac_cmd, android_jar);
|
||||
javac_cmd = concat(javac_cmd, "\" --release 11 ");
|
||||
javac_cmd = concat(javac_cmd, javac_files);
|
||||
javac_cmd = concat(javac_cmd, " 2>&1");
|
||||
if r := run(str_to_cstr(javac_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: javac failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
out("error: javac spawn failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
d8_cmd := concat("\"", d8_path);
|
||||
d8_cmd = concat(d8_cmd, "\" --release --lib \"");
|
||||
d8_cmd = concat(d8_cmd, android_jar);
|
||||
d8_cmd = concat(d8_cmd, "\" --output \"");
|
||||
d8_cmd = concat(d8_cmd, stage);
|
||||
d8_cmd = concat(d8_cmd, "\" ");
|
||||
d8_cmd = concat(d8_cmd, d8_files);
|
||||
d8_cmd = concat(d8_cmd, " 2>&1");
|
||||
if r := run(str_to_cstr(d8_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: d8 failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
out("error: d8 spawn failed\n");
|
||||
return false;
|
||||
}
|
||||
true;
|
||||
}
|
||||
|
||||
// Locate `javac`. Honors `$JAVA_HOME/bin/javac` first (Android
|
||||
// Studio's bundled JDK sets this on macOS), then falls back to a PATH
|
||||
// lookup via `command -v`.
|
||||
discover_javac :: () -> string {
|
||||
if jh := env("JAVA_HOME") {
|
||||
cand := path_join(jh, "bin/javac");
|
||||
if exists(str_to_cstr(cand)) { return cand; }
|
||||
}
|
||||
if path := find_executable("javac") { return path; }
|
||||
"";
|
||||
}
|
||||
|
||||
// Zip the contents of `<src>` into the APK at `<dest>/`. Uses a
|
||||
// staging copy under `.sx-tmp/apk-assets/<dest>` so we can run `zip
|
||||
// -r` from a temporary cwd that produces clean entries (no `../`
|
||||
// noise). Missing src is treated as "nothing to do" so projects can
|
||||
// register optional asset trees.
|
||||
zip_asset_dir :: (src: string, dest: string, apk: string) -> bool {
|
||||
if !exists(str_to_cstr(src)) { return true; }
|
||||
asset_root := str_to_cstr(".sx-tmp/apk-assets");
|
||||
create_dir_all(asset_root);
|
||||
rm_cmd := concat("rm -rf .sx-tmp/apk-assets/", dest);
|
||||
run(str_to_cstr(rm_cmd));
|
||||
parent := if dir_part(dest).len > 0 then concat(".sx-tmp/apk-assets/", dir_part(dest)) else ".sx-tmp/apk-assets";
|
||||
if !create_dir_all(str_to_cstr(parent)) {
|
||||
out("error: apk: cannot create asset stage dir\n");
|
||||
return false;
|
||||
}
|
||||
cp_cmd := concat("cp -R \"", src);
|
||||
cp_cmd = concat(cp_cmd, "\" \".sx-tmp/apk-assets/");
|
||||
cp_cmd = concat(cp_cmd, dest);
|
||||
cp_cmd = concat(cp_cmd, "\" 2>&1");
|
||||
if r := run(str_to_cstr(cp_cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: cp -R asset dir failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
out("error: cp -R asset dir spawn failed\n");
|
||||
return false;
|
||||
}
|
||||
// Make apk path absolute-ish for the cd shell wrapping. The user
|
||||
// typically gives a relative path; resolve via $(pwd) into an
|
||||
// absolute one so `cd .sx-tmp/apk-assets && zip <apk>` still
|
||||
// references the right file.
|
||||
abs_apk := if apk.len > 0 then (if apk[0] == 47 then apk else concat("$(pwd)/", apk)) else apk;
|
||||
zip_cmd := concat("zip -q -r \"", abs_apk);
|
||||
zip_cmd = concat(zip_cmd, "\" \"");
|
||||
zip_cmd = concat(zip_cmd, dest);
|
||||
zip_cmd = concat(zip_cmd, "\"");
|
||||
if !run_in_dir(".sx-tmp/apk-assets", zip_cmd) { return false; }
|
||||
true;
|
||||
}
|
||||
|
||||
// Generate the Android debug keystore on first use. The defaults
|
||||
// match what Android Studio creates: alias `androiddebugkey`, password
|
||||
// `android` for both store and key, RSA-2048, 10000-day validity.
|
||||
ensure_debug_keystore :: (keystore_path: string) -> bool {
|
||||
if exists(str_to_cstr(keystore_path)) { return true; }
|
||||
// mkdir -p the parent dir if needed.
|
||||
parent := dir_part(keystore_path);
|
||||
if parent.len > 0 {
|
||||
create_dir_all(str_to_cstr(parent));
|
||||
}
|
||||
cmd := concat("keytool -genkeypair -keystore \"", keystore_path);
|
||||
cmd = concat(cmd, "\" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname \"CN=Android Debug,O=Android,C=US\" 2>&1");
|
||||
if r := run(str_to_cstr(cmd)) {
|
||||
if r.exit_code != 0 {
|
||||
out("error: keytool failed:\n");
|
||||
out(r.stdout);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
out("error: keytool spawn failed\n");
|
||||
false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user