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:
415
src/target.zig
415
src/target.zig
@@ -3,9 +3,11 @@ const llvm = @import("llvm_api.zig");
|
||||
const c = llvm.c;
|
||||
|
||||
/// One `#jni_main #jni_class("...")` declaration's Java-source emission.
|
||||
/// Populated by lowering and consumed by `createApk` to write a `.java`
|
||||
/// file under `<stage>/java/`, compile it via `javac`, and bundle the
|
||||
/// resulting `classes.dex` into the APK.
|
||||
/// Populated by lowering and surfaced to the sx Android bundler in
|
||||
/// `library/modules/platform/bundle.sx` via `BuildConfig.jni_main_*`,
|
||||
/// which writes a `.java` file under `<stage>/java/<pkg>/<Cls>.java`,
|
||||
/// compiles via `javac`, dexes via `d8`, and bundles the resulting
|
||||
/// `classes.dex` into the APK.
|
||||
pub const JniMainEmission = struct {
|
||||
/// foreign_path of the source decl (e.g. "co/swipelab/sxmain/SxApp").
|
||||
/// Splits into package + class name for `<stage>/java/<pkg>/<Class>.java`.
|
||||
@@ -233,405 +235,16 @@ pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
|
||||
return if (result >= 0 and result <= 255) @intCast(result) else 1;
|
||||
}
|
||||
|
||||
/// Discover the Android SDK root. Honors $ANDROID_HOME / $ANDROID_SDK_ROOT,
|
||||
/// otherwise picks the default install location on macOS. Caller owns slice.
|
||||
pub fn discoverAndroidSdk(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {
|
||||
if (std.c.getenv("ANDROID_HOME")) |env| {
|
||||
return try allocator.dupe(u8, std.mem.span(env));
|
||||
}
|
||||
if (std.c.getenv("ANDROID_SDK_ROOT")) |env| {
|
||||
return try allocator.dupe(u8, std.mem.span(env));
|
||||
}
|
||||
const home_env = std.c.getenv("HOME") orelse {
|
||||
std.debug.print("error: cannot locate Android SDK — set $ANDROID_HOME\n", .{});
|
||||
return error.SdkNotFound;
|
||||
};
|
||||
const home = std.mem.span(home_env);
|
||||
const sdk = try std.fmt.allocPrint(allocator, "{s}/Library/Android/sdk", .{home});
|
||||
var dir = std.Io.Dir.openDir(.cwd(), io, sdk, .{}) catch {
|
||||
std.debug.print("error: no Android SDK at {s} — install via Android Studio or set $ANDROID_HOME\n", .{sdk});
|
||||
return error.SdkNotFound;
|
||||
};
|
||||
dir.close(io);
|
||||
return sdk;
|
||||
}
|
||||
// Android APK bundling (createApk, compileJniMainSources,
|
||||
// buildAndroidManifest, buildJniMainManifest, ensureDebugKeystore,
|
||||
// libNameFromSoBasename + helpers) has moved to
|
||||
// `library/modules/platform/bundle.sx`. `src/main.zig` invokes it
|
||||
// post-link via the BuildOptions callback registered from sx code.
|
||||
// `--apk <path>` on the CLI is a transitional alias that feeds
|
||||
// `bundle_path` so the auto-fallback to `platform.bundle.bundle_main`
|
||||
// fires; programs that opt in via `set_post_link_callback` reach the
|
||||
// sx bundler directly.
|
||||
|
||||
/// Pick the lexicographically-highest subdir of `<sdk>/<subdir>` (matches the
|
||||
/// "newest version" convention for `build-tools/<version>` and
|
||||
/// `platforms/android-<api>`). Caller owns the joined slice.
|
||||
fn findHighestSubdir(allocator: std.mem.Allocator, io: std.Io, root: []const u8, subdir: []const u8) ![]const u8 {
|
||||
const parent = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ root, subdir });
|
||||
var dir = std.Io.Dir.openDir(.cwd(), io, parent, .{ .iterate = true }) catch {
|
||||
std.debug.print("error: no {s} under {s}\n", .{ subdir, root });
|
||||
return error.SdkNotFound;
|
||||
};
|
||||
defer dir.close(io);
|
||||
var best: ?[]const u8 = null;
|
||||
var it = dir.iterate();
|
||||
while (it.next(io) catch null) |entry| {
|
||||
if (entry.kind != .directory) continue;
|
||||
if (best == null or std.mem.order(u8, entry.name, best.?) == .gt) {
|
||||
best = try allocator.dupe(u8, entry.name);
|
||||
}
|
||||
}
|
||||
const name = best orelse {
|
||||
std.debug.print("error: no versions under {s}\n", .{parent});
|
||||
return error.SdkNotFound;
|
||||
};
|
||||
return try std.fmt.allocPrint(allocator, "{s}/{s}", .{ parent, name });
|
||||
}
|
||||
|
||||
/// Write each `JniMainEmission`'s `.java` source under `<stage>/java/<pkg>/`,
|
||||
/// invoke `javac` to compile to `<stage>/classes/`, then `d8` to produce
|
||||
/// `<stage>/classes.dex`. The caller bundles `classes.dex` into the APK.
|
||||
///
|
||||
/// `javac` is discovered via `$JAVA_HOME/bin/javac` first, then via PATH; if
|
||||
/// neither resolves, an error is reported pointing at the missing tool. The
|
||||
/// `--release 11` target keeps the emitted class file version low enough for
|
||||
/// every shipping d8 to consume without surprise.
|
||||
fn compileJniMainSources(
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
stage: []const u8,
|
||||
emissions: []const JniMainEmission,
|
||||
android_jar: []const u8,
|
||||
d8: []const u8,
|
||||
) !void {
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
const java_root = try std.fmt.allocPrint(allocator, "{s}/java", .{stage});
|
||||
const classes_root = try std.fmt.allocPrint(allocator, "{s}/classes", .{stage});
|
||||
try cwd.createDirPath(io, java_root);
|
||||
try cwd.createDirPath(io, classes_root);
|
||||
|
||||
var java_paths = std.ArrayList([]const u8).empty;
|
||||
var class_paths = std.ArrayList([]const u8).empty;
|
||||
for (emissions) |em| {
|
||||
const split = splitForeignPath(em.foreign_path);
|
||||
const pkg_dir = if (split.pkg.len > 0)
|
||||
try std.fmt.allocPrint(allocator, "{s}/{s}", .{ java_root, split.pkg })
|
||||
else
|
||||
try allocator.dupe(u8, java_root);
|
||||
try cwd.createDirPath(io, pkg_dir);
|
||||
|
||||
const java_path = try std.fmt.allocPrint(allocator, "{s}/{s}.java", .{ pkg_dir, split.cls });
|
||||
try cwd.writeFile(io, .{ .sub_path = java_path, .data = em.java_source });
|
||||
try java_paths.append(allocator, java_path);
|
||||
|
||||
const class_path = if (split.pkg.len > 0)
|
||||
try std.fmt.allocPrint(allocator, "{s}/{s}/{s}.class", .{ classes_root, split.pkg, split.cls })
|
||||
else
|
||||
try std.fmt.allocPrint(allocator, "{s}/{s}.class", .{ classes_root, split.cls });
|
||||
try class_paths.append(allocator, class_path);
|
||||
}
|
||||
|
||||
const javac = try discoverJavac(allocator, io);
|
||||
|
||||
var javac_argv = std.ArrayList([]const u8).empty;
|
||||
try javac_argv.appendSlice(allocator, &.{
|
||||
javac, "-d", classes_root,
|
||||
"-classpath", android_jar,
|
||||
"--release", "11",
|
||||
});
|
||||
for (java_paths.items) |p| try javac_argv.append(allocator, p);
|
||||
try runProcess(allocator, io, try javac_argv.toOwnedSlice(allocator));
|
||||
|
||||
var d8_argv = std.ArrayList([]const u8).empty;
|
||||
try d8_argv.appendSlice(allocator, &.{
|
||||
d8, "--release",
|
||||
"--lib", android_jar,
|
||||
"--output", stage,
|
||||
});
|
||||
for (class_paths.items) |p| try d8_argv.append(allocator, p);
|
||||
try runProcess(allocator, io, try d8_argv.toOwnedSlice(allocator));
|
||||
}
|
||||
|
||||
/// Split a JNI foreign path like `co/swipelab/sxmain/SxApp` into
|
||||
/// `{ pkg = "co/swipelab/sxmain", cls = "SxApp" }`. A path with no `/` is
|
||||
/// the default Java package (`{ pkg = "", cls = path }`).
|
||||
const PathParts = struct { pkg: []const u8, cls: []const u8 };
|
||||
fn splitForeignPath(foreign_path: []const u8) PathParts {
|
||||
const last_slash = std.mem.lastIndexOfScalar(u8, foreign_path, '/') orelse {
|
||||
return .{ .pkg = "", .cls = foreign_path };
|
||||
};
|
||||
return .{
|
||||
.pkg = foreign_path[0..last_slash],
|
||||
.cls = foreign_path[last_slash + 1 ..],
|
||||
};
|
||||
}
|
||||
|
||||
/// Locate `javac`. Honors `$JAVA_HOME/bin/javac` first (the Android Studio
|
||||
/// JDK install on macOS sets this), then falls back to PATH lookup via
|
||||
/// `which`. Returns an absolute path so subsequent `runProcess` calls work
|
||||
/// regardless of the CWD passed via `runProcessIn`.
|
||||
fn discoverJavac(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {
|
||||
if (std.c.getenv("JAVA_HOME")) |env| {
|
||||
const home = std.mem.span(env);
|
||||
const candidate = try std.fmt.allocPrint(allocator, "{s}/bin/javac", .{home});
|
||||
if (std.Io.Dir.cwd().statFile(io, candidate, .{})) |_| {
|
||||
return candidate;
|
||||
} else |_| {
|
||||
allocator.free(candidate);
|
||||
}
|
||||
}
|
||||
const which = std.process.run(allocator, io, .{ .argv = &.{ "/usr/bin/which", "javac" } }) catch |e| {
|
||||
std.debug.print("error: failed to locate javac via PATH: {}\n", .{e});
|
||||
return error.JavacNotFound;
|
||||
};
|
||||
defer allocator.free(which.stderr);
|
||||
errdefer allocator.free(which.stdout);
|
||||
if (which.term != .exited or which.term.exited != 0) {
|
||||
std.debug.print("error: javac not on PATH and $JAVA_HOME unset \u{2014} install a JDK (Android Studio bundles one at $ANDROID_STUDIO/Contents/jre)\n", .{});
|
||||
allocator.free(which.stdout);
|
||||
return error.JavacNotFound;
|
||||
}
|
||||
const trimmed = std.mem.trimEnd(u8, which.stdout, " \t\r\n");
|
||||
const out = try allocator.dupe(u8, trimmed);
|
||||
allocator.free(which.stdout);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Wrap a linked Android `.so` into a debug-signed APK. Steps:
|
||||
/// 1. Place the .so under `lib/arm64-v8a/` in a staging directory.
|
||||
/// 2. Generate (or copy) AndroidManifest.xml.
|
||||
/// 3. (Optional) Compile `#jni_main` Java sources to classes.dex.
|
||||
/// 4. aapt2 link → empty APK with resources/manifest.
|
||||
/// 5. Append the lib/ tree via `zip`.
|
||||
/// 6. (Optional) Append classes.dex if step 3 produced one.
|
||||
/// 7. zipalign → aligned APK.
|
||||
/// 8. apksigner → final signed APK at `target_config.apk_path`.
|
||||
pub fn createApk(allocator: std.mem.Allocator, io: std.Io, so_path: []const u8, target_config: TargetConfig, jni_main_decls: []const JniMainEmission) !void {
|
||||
const apk_path = target_config.apk_path orelse return error.NoApkPath;
|
||||
const bundle_id = target_config.bundle_id orelse {
|
||||
std.debug.print("error: --apk requires --bundle-id (e.g. co.swipelab.myapp)\n", .{});
|
||||
return error.MissingBundleId;
|
||||
};
|
||||
|
||||
const sdk_root = try discoverAndroidSdk(allocator, io);
|
||||
const build_tools = try findHighestSubdir(allocator, io, sdk_root, "build-tools");
|
||||
const platform_dir = try findHighestSubdir(allocator, io, sdk_root, "platforms");
|
||||
const android_jar = try std.fmt.allocPrint(allocator, "{s}/android.jar", .{platform_dir});
|
||||
|
||||
const aapt2 = try std.fmt.allocPrint(allocator, "{s}/aapt2", .{build_tools});
|
||||
const zipalign = try std.fmt.allocPrint(allocator, "{s}/zipalign", .{build_tools});
|
||||
const apksigner = try std.fmt.allocPrint(allocator, "{s}/apksigner", .{build_tools});
|
||||
const d8 = try std.fmt.allocPrint(allocator, "{s}/d8", .{build_tools});
|
||||
|
||||
// Staging dir alongside the apk output.
|
||||
const stage = try std.fmt.allocPrint(allocator, "{s}.stage", .{apk_path});
|
||||
const lib_dir = try std.fmt.allocPrint(allocator, "{s}/lib/arm64-v8a", .{stage});
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
cwd.deleteTree(io, stage) catch {};
|
||||
try cwd.createDirPath(io, lib_dir);
|
||||
|
||||
// libsxhello.so must literally start with "lib" for Android's loader.
|
||||
// The user's -o path already does (e.g. lib/.../libsxhello.so). We copy
|
||||
// by basename into the staging lib dir.
|
||||
const so_basename = std.fs.path.basename(so_path);
|
||||
const so_dest = try std.fs.path.join(allocator, &.{ lib_dir, so_basename });
|
||||
cwd.copyFile(so_path, cwd, so_dest, io, .{}) catch return error.ApkStageFailed;
|
||||
|
||||
// Manifest: either user-supplied or auto-generated. When a `#jni_main`
|
||||
// class is declared, the auto-generated manifest points its
|
||||
// `<activity android:name="...">` at the user's class and flips
|
||||
// `android:hasCode="true"` so Android loads the bundled classes.dex.
|
||||
// Otherwise we fall back to the legacy NativeActivity shape.
|
||||
const manifest_path = if (target_config.manifest_path) |mp|
|
||||
try allocator.dupe(u8, mp)
|
||||
else blk: {
|
||||
const generated = try std.fmt.allocPrint(allocator, "{s}/AndroidManifest.xml", .{stage});
|
||||
const lib_name = libNameFromSoBasename(so_basename);
|
||||
const manifest_xml = if (jni_main_decls.len > 0)
|
||||
try buildJniMainManifest(allocator, bundle_id, lib_name, jni_main_decls[0].foreign_path)
|
||||
else
|
||||
try buildAndroidManifest(allocator, bundle_id, lib_name);
|
||||
try cwd.writeFile(io, .{ .sub_path = generated, .data = manifest_xml });
|
||||
break :blk generated;
|
||||
};
|
||||
|
||||
// `#jni_main #jni_class("...")` decls: write .java files, compile with
|
||||
// javac, produce classes.dex via d8. Slice 2 of the #jni_main pipeline:
|
||||
// the .dex is bundled but the manifest still points at NativeActivity,
|
||||
// so the .dex is not yet referenced at runtime (slice 3 wires it).
|
||||
const has_dex = jni_main_decls.len > 0;
|
||||
if (has_dex) {
|
||||
try compileJniMainSources(allocator, io, stage, jni_main_decls, android_jar, d8);
|
||||
}
|
||||
|
||||
// aapt2 link → unaligned apk with manifest + resources (none for now).
|
||||
const unaligned = try std.fmt.allocPrint(allocator, "{s}.unaligned", .{apk_path});
|
||||
try runProcess(allocator, io, &.{
|
||||
aapt2, "link",
|
||||
"-I", android_jar,
|
||||
"--manifest", manifest_path,
|
||||
"-o", unaligned,
|
||||
});
|
||||
|
||||
// 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.
|
||||
try runProcessIn(allocator, io, stage, &.{ "zip", "-q", "-r", unaligned, "lib/" });
|
||||
|
||||
if (has_dex) {
|
||||
try runProcessIn(allocator, io, stage, &.{ "zip", "-q", unaligned, "classes.dex" });
|
||||
}
|
||||
|
||||
// Bundle the project's `./assets/` directory (if present) at the APK's
|
||||
// top level so AAssetManager_open(path) at runtime can read them.
|
||||
// Resolves relative to the user's CWD at invocation time — matches the
|
||||
// convention chess uses (assets/ next to main.sx).
|
||||
if (std.Io.Dir.openDir(.cwd(), io, "assets", .{})) |dir_handle| {
|
||||
var dh = dir_handle;
|
||||
dh.close(io);
|
||||
try runProcess(allocator, io, &.{ "zip", "-q", "-r", unaligned, "assets/" });
|
||||
} else |_| {}
|
||||
|
||||
// zipalign → aligned apk.
|
||||
const aligned = try std.fmt.allocPrint(allocator, "{s}.aligned", .{apk_path});
|
||||
try runProcess(allocator, io, &.{ zipalign, "-f", "4", unaligned, aligned });
|
||||
|
||||
// apksigner → final signed apk at apk_path.
|
||||
const keystore = target_config.keystore_path orelse blk: {
|
||||
const home_env = std.c.getenv("HOME") orelse return error.NoHomeDir;
|
||||
break :blk try std.fmt.allocPrint(allocator, "{s}/.android/debug.keystore", .{std.mem.span(home_env)});
|
||||
};
|
||||
// Generate debug keystore on first use (keytool defaults match Android's).
|
||||
try ensureDebugKeystore(allocator, io, keystore);
|
||||
try runProcess(allocator, io, &.{
|
||||
apksigner, "sign",
|
||||
"--ks", keystore,
|
||||
"--ks-pass", "pass:android",
|
||||
"--key-pass", "pass:android",
|
||||
"--ks-key-alias", "androiddebugkey",
|
||||
"--out", apk_path,
|
||||
aligned,
|
||||
});
|
||||
|
||||
// Clean up intermediate files; keep stage/ in case users want to inspect.
|
||||
cwd.deleteFile(io, unaligned) catch {};
|
||||
cwd.deleteFile(io, aligned) catch {};
|
||||
cwd.deleteTree(io, stage) catch {};
|
||||
}
|
||||
|
||||
/// `libfoo.so` → `foo` (Android's `android.app.lib_name` meta-data wants the
|
||||
/// trimmed name; the loader prepends `lib` and appends `.so`).
|
||||
fn libNameFromSoBasename(basename: []const u8) []const u8 {
|
||||
var name = basename;
|
||||
if (std.mem.startsWith(u8, name, "lib")) name = name[3..];
|
||||
if (std.mem.endsWith(u8, name, ".so")) name = name[0 .. name.len - 3];
|
||||
return name;
|
||||
}
|
||||
|
||||
/// Manifest for a `#jni_main` Activity: `<activity android:name>` points
|
||||
/// at the user's class, `android:hasCode="true"` so the bundled
|
||||
/// classes.dex is loaded, and the `android.app.lib_name` meta-data is
|
||||
/// dropped (that's a NativeActivity-only mechanism — Java-driven
|
||||
/// Activities load the .so via `System.loadLibrary` from a static
|
||||
/// initializer the Java emitter will synthesize once slice R.3 lands).
|
||||
fn buildJniMainManifest(allocator: std.mem.Allocator, package: []const u8, lib_name: []const u8, foreign_path: []const u8) ![]const u8 {
|
||||
var class_name = std.ArrayList(u8).empty;
|
||||
for (foreign_path) |ch| {
|
||||
try class_name.append(allocator, if (ch == '/') '.' else ch);
|
||||
}
|
||||
const activity_name = try class_name.toOwnedSlice(allocator);
|
||||
// `Theme.DeviceDefault.NoActionBar.Fullscreen` removes both the
|
||||
// ActionBar title (the "sxchess" strip) and the status bar — sx-rendered
|
||||
// apps own the whole window. Consumers wanting a different theme will
|
||||
// ship their own manifest via `--manifest`.
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<?xml version="1.0" encoding="utf-8"?>
|
||||
\\<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
\\ package="{s}"
|
||||
\\ android:versionCode="1"
|
||||
\\ android:versionName="1.0">
|
||||
\\ <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
\\ <application android:label="{s}" android:hasCode="true">
|
||||
\\ <activity
|
||||
\\ android:name="{s}"
|
||||
\\ android:exported="true"
|
||||
\\ android:label="{s}"
|
||||
\\ 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>
|
||||
\\
|
||||
, .{ package, lib_name, activity_name, lib_name });
|
||||
}
|
||||
|
||||
fn buildAndroidManifest(allocator: std.mem.Allocator, package: []const u8, lib_name: []const u8) ![]const u8 {
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<?xml version="1.0" encoding="utf-8"?>
|
||||
\\<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
\\ package="{s}"
|
||||
\\ android:versionCode="1"
|
||||
\\ android:versionName="1.0">
|
||||
\\ <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
\\ <application android:label="{s}" android:hasCode="false">
|
||||
\\ <activity
|
||||
\\ android:name="android.app.NativeActivity"
|
||||
\\ android:exported="true"
|
||||
\\ android:label="{s}"
|
||||
\\ android:configChanges="orientation|keyboardHidden|screenSize">
|
||||
\\ <meta-data android:name="android.app.lib_name" android:value="{s}" />
|
||||
\\ <intent-filter>
|
||||
\\ <action android:name="android.intent.action.MAIN" />
|
||||
\\ <category android:name="android.intent.category.LAUNCHER" />
|
||||
\\ </intent-filter>
|
||||
\\ </activity>
|
||||
\\ </application>
|
||||
\\</manifest>
|
||||
\\
|
||||
, .{ package, lib_name, lib_name, lib_name });
|
||||
}
|
||||
|
||||
fn ensureDebugKeystore(allocator: std.mem.Allocator, io: std.Io, keystore_path: []const u8) !void {
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
if (cwd.statFile(io, keystore_path, .{})) |_| {
|
||||
return;
|
||||
} else |_| {}
|
||||
if (std.fs.path.dirname(keystore_path)) |dir| {
|
||||
cwd.createDirPath(io, dir) catch {};
|
||||
}
|
||||
try runProcess(allocator, io, &.{
|
||||
"keytool",
|
||||
"-genkeypair",
|
||||
"-keystore", keystore_path,
|
||||
"-storepass", "android",
|
||||
"-alias", "androiddebugkey",
|
||||
"-keypass", "android",
|
||||
"-keyalg", "RSA",
|
||||
"-keysize", "2048",
|
||||
"-validity", "10000",
|
||||
"-dname", "CN=Android Debug,O=Android,C=US",
|
||||
});
|
||||
}
|
||||
|
||||
fn runProcess(allocator: std.mem.Allocator, io: std.Io, argv: []const []const u8) !void {
|
||||
return runProcessIn(allocator, io, null, argv);
|
||||
}
|
||||
|
||||
fn runProcessIn(allocator: std.mem.Allocator, io: std.Io, work_dir: ?[]const u8, argv: []const []const u8) !void {
|
||||
if (std.c.getenv("SX_DEBUG_APK") != null) {
|
||||
std.debug.print("[sx] apk:", .{});
|
||||
for (argv) |a| std.debug.print(" {s}", .{a});
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
const cwd_opt: std.process.Child.Cwd = if (work_dir) |wd| .{ .path = wd } else .inherit;
|
||||
const result = std.process.run(allocator, io, .{ .argv = argv, .cwd = cwd_opt }) catch |e| {
|
||||
std.debug.print("error: failed to spawn {s}: {}\n", .{ argv[0], e });
|
||||
return error.ApkStepFailed;
|
||||
};
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
if (result.term != .exited or result.term.exited != 0) {
|
||||
std.debug.print("error: {s} failed:\n{s}\n{s}\n", .{ argv[0], result.stdout, result.stderr });
|
||||
return error.ApkStepFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Discover the Android NDK root. Honors $ANDROID_NDK_HOME / $ANDROID_NDK_ROOT,
|
||||
/// otherwise picks the highest-versioned NDK under $HOME/Library/Android/sdk/ndk
|
||||
|
||||
Reference in New Issue
Block a user