android: forward NDK sysroot to embedded clang + skip auto #library/#framework

C imports (stb_image, stb_truetype) compiled via the embedded LLVM
clang library now resolve bionic headers on Android. main.zig auto-
fills target_config.sysroot with the NDK root (mirroring the iOS path
that auto-fills the iOS SDK path); c_import.zig derives the bionic
sysroot inside it and passes `--sysroot <ndk>/toolchains/llvm/prebuilt/<host>/sysroot`
to the embedded clang.

Android link branch in target.zig stops auto-appending entries from the
collected `#library` / `#framework` lists. Most `#library` directives
in the stdlib (`objc.sx`'s `objc :: #library "objc";`, frameworks set
by uikit.sx, etc.) describe Apple-specific intent that's nonsensical on
Android. Users opt into Android-side libs via `opts.add_link_flag(...)`
in build.sx — same shape as how the iOS branch already lists frameworks
in chess's configure_build.

Verified end-to-end: chess game compiles for Android, packages into a
debug-signed APK, installs and launches on Pixel 7 Pro. It crashes in
UIRenderer.init because raw GL calls run before EGL is up — same
architectural gap iOS bridges via the Metal GPU protocol. Next step
is a GLES3 GPU impl or a lazy-init in UIRenderer. 86/86 regression
tests + iOS-sim chess build clean.
This commit is contained in:
agra
2026-05-19 00:36:05 +03:00
parent 561ad03a7c
commit d8968ae093
3 changed files with 43 additions and 4 deletions

View File

@@ -10,6 +10,15 @@ pub const CSourceLocation = struct {
line: u32,
};
/// Derive the NDK sysroot path from the NDK root (which by convention
/// lives in `target_config.sysroot` on Android — see target.zig's
/// Android link branch + main.zig's auto-discovery). Returns a NUL-
/// terminated path suitable for clang's `--sysroot <path>` argv.
fn androidSysrootFromNdkRoot(allocator: std.mem.Allocator, ndk_root: []const u8) ![:0]u8 {
const host_tag: []const u8 = if (builtin.os.tag == .macos) "darwin-x86_64" else "linux-x86_64";
return try std.fmt.allocPrintSentinel(allocator, "{s}/toolchains/llvm/prebuilt/{s}/sysroot", .{ ndk_root, host_tag }, 0);
}
pub const CImportResult = struct {
fn_decls: []const *Node,
/// Source locations for each fn_decl (parallel array, same indices).
@@ -193,6 +202,18 @@ pub fn compileCToObjects(
try args_list.append(allocator, "-isysroot");
try args_list.append(allocator, (try allocator.dupeZ(u8, sr)).ptr);
}
// Android: route through the NDK sysroot so bionic headers resolve.
// The embedded clang library doesn't know how to be an Android cross-
// compiler on its own. `target_config.sysroot` holds the NDK root
// by convention (main.zig auto-fills it for --target android), so
// derive the headers/libs sysroot inside it.
if (target_config.isAndroid()) {
if (target_config.sysroot) |ndk_root| {
const sysroot = try androidSysrootFromNdkRoot(allocator, ndk_root);
try args_list.append(allocator, "--sysroot");
try args_list.append(allocator, sysroot.ptr);
}
}
for (info.includes) |inc| {
const dir = dirName(inc);
try args_list.append(allocator, (try allocPrintZ(allocator, "-I{s}", .{dir})).ptr);

View File

@@ -167,6 +167,15 @@ pub fn main(init: std.process.Init) !void {
target_config.sysroot = sx.target.discoverAppleSdk(allocator, io, sdk_name) catch null;
}
// Same idea for Android — the NDK root must be visible to BOTH the
// C-import compile path (so `--sysroot ndk/.../sysroot` finds bionic
// headers) and the link path. By convention, target_config.sysroot
// holds the NDK root on Android (target.zig's link branch + c_import.zig
// both read it). Honors any explicit --sysroot.
if (target_config.isAndroid() and target_config.sysroot == null) {
target_config.sysroot = sx.target.discoverAndroidNdk(allocator, io) catch null;
}
const path = input_path orelse {
printUsage();
return;
@@ -559,6 +568,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// Shadow the outer `fws` for the rest of the function by reassignment.
fws = try merged_fws.toOwnedSlice(allocator);
}
if (build_flags.len > 0) {
var all_flags: std.ArrayList([]const u8) = .empty;
for (target_config.extra_link_flags) |f| try all_flags.append(allocator, f);

View File

@@ -550,6 +550,14 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex
// owns `ANativeActivity_onCreate` and forwards into android_main on
// a dedicated thread. `-u ANativeActivity_onCreate` keeps the glue's
// symbol from being stripped (nothing in our .o references it).
//
// The `libraries` parameter (collected from `#library` directives)
// and `frameworks` parameter (Apple-only by definition) are
// intentionally ignored here. On Android, users opt into specific
// libs via `opts.add_link_flag("-l<name>")` in their build.sx —
// the platform-specific link surface should be expressed in build
// options rather than auto-inherited from every imported module
// (most of which assume Apple targets).
const ndk_root = if (target_config.sysroot) |sr|
try allocator.dupe(u8, sr)
else
@@ -587,11 +595,11 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex
for (target_config.lib_paths) |lp| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
}
for (libraries) |lib| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
}
// Default libs available on every Android runtime; linker drops
// unreferenced ones automatically.
// unreferenced ones automatically. `#library` directives are
// intentionally NOT auto-emitted here (most assume Apple targets);
// users opt in per-target via `opts.add_link_flag("-l...")` in
// their build.sx.
try argv.appendSlice(allocator, &.{ "-llog", "-landroid", "-lEGL", "-lGLESv3", "-lm", "-ldl" });
for (target_config.extra_link_flags) |flag| {
var it = std.mem.tokenizeScalar(u8, flag, ' ');