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:
150
src/imports.zig
150
src/imports.zig
@@ -5,6 +5,140 @@ const errors = @import("errors.zig");
|
||||
const c_import = @import("c_import.zig");
|
||||
const Node = ast.Node;
|
||||
|
||||
/// Comptime evaluation context for the inline-if hoisting pass below.
|
||||
/// Mirrors the values `injectComptimeConstants` will later push into the
|
||||
/// lowering's `comptime_constants` map (OS / ARCH / POINTER_SIZE), but
|
||||
/// derived directly from the build target so we can resolve top-level
|
||||
/// `inline if OS == .X { ... }` arms before imports + lowering run.
|
||||
pub const ComptimeContext = struct {
|
||||
/// Lowercase OS name matching the OperatingSystem enum tag
|
||||
/// (macos / linux / windows / wasm / ios / android / unknown).
|
||||
os: []const u8 = "unknown",
|
||||
/// Lowercase architecture name matching the Architecture enum tag
|
||||
/// (aarch64 / x86_64 / wasm32 / wasm64 / unknown).
|
||||
arch: []const u8 = "unknown",
|
||||
/// 4 for wasm32, 8 for every other target.
|
||||
pointer_size: i64 = 8,
|
||||
};
|
||||
|
||||
/// Top-level `inline if OS == .X { decls }` blocks are parsed as
|
||||
/// `if_expr` / `match_expr` nodes in `root.decls`, but the lowering
|
||||
/// pass only knows how to dispatch on `.fn_decl` / `.const_decl` /
|
||||
/// `.var_decl` / etc. at decl positions — an `if_expr` at the top
|
||||
/// level is silently dropped. Same story for `#import` decls inside an
|
||||
/// `inline if` body: they need to be surfaced to the top so import
|
||||
/// resolution sees them.
|
||||
///
|
||||
/// This pass walks `decls`, replaces every comptime conditional with
|
||||
/// the body of its taken arm (recursively flattened), and drops the
|
||||
/// rest. A condition we can't resolve at this stage is also dropped —
|
||||
/// the caller may want to surface that as a diagnostic later, but for
|
||||
/// the OS / ARCH / POINTER_SIZE patterns we cover here it shouldn't
|
||||
/// happen in practice.
|
||||
pub fn flattenComptimeConditionals(allocator: std.mem.Allocator, decls: []const *Node, ctx: ComptimeContext) std.mem.Allocator.Error![]const *Node {
|
||||
var out = std.ArrayList(*Node).empty;
|
||||
for (decls) |decl| {
|
||||
switch (decl.data) {
|
||||
.if_expr => |ie| {
|
||||
if (ie.is_comptime) {
|
||||
if (evalComptimeCondition(ie.condition, ctx)) |is_true| {
|
||||
const taken: ?*const Node = if (is_true) ie.then_branch else ie.else_branch;
|
||||
if (taken) |b| try appendBranchDecls(allocator, &out, b, ctx);
|
||||
continue;
|
||||
}
|
||||
// Couldn't evaluate — drop the whole conditional. This is
|
||||
// a conservative choice; future work may surface it as a
|
||||
// diagnostic. For OS / ARCH / POINTER_SIZE comparisons
|
||||
// the eval is total, so this shouldn't fire in practice.
|
||||
continue;
|
||||
}
|
||||
try out.append(allocator, decl);
|
||||
},
|
||||
.match_expr => |me| {
|
||||
if (me.is_comptime) {
|
||||
if (evalComptimeMatch(&me, ctx)) |body| {
|
||||
try appendBranchDecls(allocator, &out, body, ctx);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try out.append(allocator, decl);
|
||||
},
|
||||
else => try out.append(allocator, decl),
|
||||
}
|
||||
}
|
||||
return try out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn appendBranchDecls(allocator: std.mem.Allocator, out: *std.ArrayList(*Node), branch: *const Node, ctx: ComptimeContext) std.mem.Allocator.Error!void {
|
||||
const stmts: []const *Node = if (branch.data == .block)
|
||||
branch.data.block.stmts
|
||||
else
|
||||
&[_]*Node{@constCast(branch)};
|
||||
const recursed = try flattenComptimeConditionals(allocator, stmts, ctx);
|
||||
try out.appendSlice(allocator, recursed);
|
||||
}
|
||||
|
||||
fn evalComptimeCondition(node: *const Node, ctx: ComptimeContext) ?bool {
|
||||
if (node.data != .binary_op) return null;
|
||||
const bo = &node.data.binary_op;
|
||||
if (bo.op != .eq and bo.op != .neq) return null;
|
||||
const name = switch (bo.lhs.data) {
|
||||
.identifier => |id| id.name,
|
||||
else => return null,
|
||||
};
|
||||
if (std.mem.eql(u8, name, "OS") or std.mem.eql(u8, name, "ARCH")) {
|
||||
const variant = switch (bo.rhs.data) {
|
||||
.enum_literal => |el| el.name,
|
||||
else => return null,
|
||||
};
|
||||
const target = if (std.mem.eql(u8, name, "OS")) ctx.os else ctx.arch;
|
||||
const matches = std.mem.eql(u8, variant, target);
|
||||
return if (bo.op == .eq) matches else !matches;
|
||||
}
|
||||
if (std.mem.eql(u8, name, "POINTER_SIZE")) {
|
||||
const rhs_val: i64 = switch (bo.rhs.data) {
|
||||
.int_literal => |il| il.value,
|
||||
else => return null,
|
||||
};
|
||||
const matches = ctx.pointer_size == rhs_val;
|
||||
return if (bo.op == .eq) matches else !matches;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn evalComptimeMatch(me: *const ast.MatchExpr, ctx: ComptimeContext) ?*const Node {
|
||||
const name = switch (me.subject.data) {
|
||||
.identifier => |id| id.name,
|
||||
else => return null,
|
||||
};
|
||||
if (std.mem.eql(u8, name, "OS") or std.mem.eql(u8, name, "ARCH")) {
|
||||
const target = if (std.mem.eql(u8, name, "OS")) ctx.os else ctx.arch;
|
||||
for (me.arms) |arm| {
|
||||
const pattern = arm.pattern orelse continue;
|
||||
const variant = switch (pattern.data) {
|
||||
.enum_literal => |el| el.name,
|
||||
else => continue,
|
||||
};
|
||||
if (std.mem.eql(u8, variant, target)) return arm.body;
|
||||
}
|
||||
for (me.arms) |arm| if (arm.pattern == null) return arm.body;
|
||||
return null;
|
||||
}
|
||||
if (std.mem.eql(u8, name, "POINTER_SIZE")) {
|
||||
for (me.arms) |arm| {
|
||||
const pattern = arm.pattern orelse continue;
|
||||
const rhs_val: i64 = switch (pattern.data) {
|
||||
.int_literal => |il| il.value,
|
||||
else => continue,
|
||||
};
|
||||
if (ctx.pointer_size == rhs_val) return arm.body;
|
||||
}
|
||||
for (me.arms) |arm| if (arm.pattern == null) return arm.body;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn dirName(path: []const u8) []const u8 {
|
||||
var last_sep: usize = 0;
|
||||
var found = false;
|
||||
@@ -176,6 +310,7 @@ pub fn resolveImports(
|
||||
diagnostics: ?*errors.DiagnosticList,
|
||||
stdlib_paths: []const []const u8,
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
comptime_ctx: ComptimeContext,
|
||||
) !ResolvedModule {
|
||||
// Record this file's edge set so `param_impl_map` lookups can filter
|
||||
// candidates by what's been imported from where. Populated as each
|
||||
@@ -196,9 +331,15 @@ pub fn resolveImports(
|
||||
return mod;
|
||||
}
|
||||
|
||||
// Hoist top-level `inline if OS == .X { ... }` body decls (including
|
||||
// any `#import`s inside them) to the top level before resolution
|
||||
// proceeds. After this pass, the decl list contains no top-level
|
||||
// `if_expr` / `match_expr` nodes with `is_comptime = true`.
|
||||
const flat_decls = try flattenComptimeConditionals(allocator, root.data.root.decls, comptime_ctx);
|
||||
|
||||
var decl_list = std.ArrayList(*Node).empty;
|
||||
|
||||
for (root.data.root.decls) |decl| {
|
||||
for (flat_decls) |decl| {
|
||||
if (decl.data == .c_import_decl) {
|
||||
// Resolve `#source` / `#include` paths through the same chain
|
||||
// as `#import`: importing-file's directory → CWD → stdlib
|
||||
@@ -312,7 +453,7 @@ pub fn resolveImports(
|
||||
// Push onto chain before recursing, pop after
|
||||
try chain.put(resolved_path, {});
|
||||
const imp_dir = dirName(resolved_path);
|
||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph);
|
||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
|
||||
_ = chain.remove(resolved_path);
|
||||
|
||||
// Cache
|
||||
@@ -320,7 +461,7 @@ pub fn resolveImports(
|
||||
break :blk result;
|
||||
} else |_| {
|
||||
// File read failed — try as directory import
|
||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph) catch {
|
||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, comptime_ctx) catch {
|
||||
if (diagnostics) |diags| {
|
||||
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
|
||||
}
|
||||
@@ -354,6 +495,7 @@ fn resolveDirectoryImport(
|
||||
span: ast.Span,
|
||||
stdlib_paths: []const []const u8,
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
comptime_ctx: ComptimeContext,
|
||||
) anyerror!ResolvedModule {
|
||||
// Open the directory with iteration capability
|
||||
const dir = std.Io.Dir.openDir(.cwd(), io, dir_path, .{ .iterate = true }) catch {
|
||||
@@ -419,7 +561,7 @@ fn resolveDirectoryImport(
|
||||
};
|
||||
|
||||
try chain.put(file_path, {});
|
||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph);
|
||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
|
||||
_ = chain.remove(file_path);
|
||||
|
||||
try cache.put(file_path, result);
|
||||
|
||||
Reference in New Issue
Block a user