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:
52
src/main.zig
52
src/main.zig
@@ -408,16 +408,17 @@ fn printInterpBailDiag(comp: *const sx.core.Compilation, label: []const u8, err:
|
||||
std.debug.print("error: {s} failed: {s}\n", .{ label, @errorName(err) });
|
||||
return;
|
||||
};
|
||||
const op_detail: []const u8 = if (sx.ir.Interpreter.last_bail_builtin) |b| b else op;
|
||||
if (sx.ir.Interpreter.last_bail_file) |file| {
|
||||
if (comp.import_sources.get(file)) |source| {
|
||||
const loc = sx.errors.SourceLoc.compute(source, sx.ir.Interpreter.last_bail_offset);
|
||||
std.debug.print("error: {s} failed: {s} (op={s}) at {s}:{d}:{d}\n", .{ label, @errorName(err), op, file, loc.line, loc.col });
|
||||
std.debug.print("error: {s} failed: {s} (op={s}/{s}) at {s}:{d}:{d}\n", .{ label, @errorName(err), op, op_detail, file, loc.line, loc.col });
|
||||
return;
|
||||
}
|
||||
std.debug.print("error: {s} failed: {s} (op={s}) at {s}:+{d}\n", .{ label, @errorName(err), op, file, sx.ir.Interpreter.last_bail_offset });
|
||||
std.debug.print("error: {s} failed: {s} (op={s}/{s}) at {s}:+{d}\n", .{ label, @errorName(err), op, op_detail, file, sx.ir.Interpreter.last_bail_offset });
|
||||
return;
|
||||
}
|
||||
std.debug.print("error: {s} failed: {s} (op={s})\n", .{ label, @errorName(err), op });
|
||||
std.debug.print("error: {s} failed: {s} (op={s}/{s})\n", .{ label, @errorName(err), op, op_detail });
|
||||
}
|
||||
|
||||
fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 {
|
||||
@@ -620,14 +621,6 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
};
|
||||
timer.record("link");
|
||||
|
||||
// Wrap into an .apk if requested (Android).
|
||||
if (merged_config.apk_path) |ap| {
|
||||
timer.mark();
|
||||
sx.target.createApk(allocator, io, final_output, merged_config, comp.getJniMainEmissions()) catch std.process.exit(1);
|
||||
timer.record("apk");
|
||||
std.debug.print("apk: {s}\n", .{ap});
|
||||
}
|
||||
|
||||
// Make the linked binary's path + bundling config visible to the
|
||||
// post-link callback via `BuildOptions.binary_path()`,
|
||||
// `BuildOptions.bundle_path()`, etc. CLI flags
|
||||
@@ -635,7 +628,11 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
// bundler doesn't need a separate code path.
|
||||
if (comp.ir_emitter) |*e| {
|
||||
e.build_config.binary_path = final_output;
|
||||
if (e.build_config.bundle_path == null) e.build_config.bundle_path = merged_config.bundle_path;
|
||||
// `--apk <path>` is a transitional alias for the bundle_path
|
||||
// → post_link_module = "platform.bundle" auto-fallback. The
|
||||
// sx Android bundler reads `bundle_path()` regardless of which
|
||||
// CLI flag the user typed.
|
||||
if (e.build_config.bundle_path == null) e.build_config.bundle_path = merged_config.bundle_path orelse merged_config.apk_path;
|
||||
if (e.build_config.bundle_id == null) e.build_config.bundle_id = merged_config.bundle_id;
|
||||
if (e.build_config.codesign_identity == null) e.build_config.codesign_identity = merged_config.codesign_identity;
|
||||
if (e.build_config.provisioning_profile == null) e.build_config.provisioning_profile = merged_config.provisioning_profile;
|
||||
@@ -646,6 +643,37 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
if (merged_config.triple) |t| e.build_config.target_triple = std.mem.span(t);
|
||||
e.build_config.target_frameworks = fws;
|
||||
e.build_config.target_framework_paths = merged_config.framework_paths;
|
||||
// Android-specific bundling state.
|
||||
if (e.build_config.manifest_path == null) e.build_config.manifest_path = merged_config.manifest_path;
|
||||
if (e.build_config.keystore_path == null) e.build_config.keystore_path = merged_config.keystore_path;
|
||||
// `#jni_main` decls flow from the compiler's lowering pass —
|
||||
// pre-rendered Java sources + the foreign_path for each. Build
|
||||
// two parallel slices since BuildConfig hooks return strings.
|
||||
const jni_decls = comp.getJniMainEmissions();
|
||||
if (jni_decls.len > 0) {
|
||||
// If the output path was set via `BuildOptions.set_output_path`
|
||||
// (i.e. from a #run block, not CLI -o), the Java sources were
|
||||
// rendered during lowering before we knew the .so basename and
|
||||
// they're missing the `static { System.loadLibrary(...); }`
|
||||
// block. Inject it now using the final resolved output.
|
||||
const lib_name: ?[]const u8 = blk: {
|
||||
const base = std.fs.path.basename(final_output);
|
||||
if (!std.mem.startsWith(u8, base, "lib")) break :blk null;
|
||||
if (!std.mem.endsWith(u8, base, ".so")) break :blk null;
|
||||
break :blk base[3 .. base.len - 3];
|
||||
};
|
||||
const fps = try allocator.alloc([]const u8, jni_decls.len);
|
||||
const srcs = try allocator.alloc([]const u8, jni_decls.len);
|
||||
for (jni_decls, 0..) |em, idx| {
|
||||
fps[idx] = em.foreign_path;
|
||||
srcs[idx] = if (lib_name) |ln|
|
||||
try sx.ir.jni_java_emit.injectLoadLibrary(allocator, em.java_source, ln)
|
||||
else
|
||||
em.java_source;
|
||||
}
|
||||
e.build_config.jni_main_foreign_paths = fps;
|
||||
e.build_config.jni_main_java_sources = srcs;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI `--bundle <path>` migration shim. The legacy Zig bundler
|
||||
|
||||
Reference in New Issue
Block a user