diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3f1d876..e72d0b6 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -20,6 +20,16 @@ const Function = inst_mod.Function; const Module = mod_mod.Module; const Builder = mod_mod.Builder; +/// Names that must keep external LLVM linkage because the OS loader (not +/// sx code) is the caller. Without this they'd default to internal and +/// either DCE away or stay hidden from the dynamic symbol table. +fn isExportedEntryName(name: []const u8) bool { + return std.mem.eql(u8, name, "main") or + std.mem.eql(u8, name, "android_main") or + std.mem.eql(u8, name, "ANativeActivity_onCreate") or + std.mem.eql(u8, name, "JNI_OnLoad"); +} + // ── Scope ─────────────────────────────────────────────────────────────── const Binding = struct { @@ -569,7 +579,7 @@ pub const Lowering = struct { switch (decl.data) { .const_decl => |cd| { if (cd.value.data == .fn_decl) { - if (std.mem.eql(u8, cd.name, "main")) { + if (isExportedEntryName(cd.name)) { self.lazyLowerFunction(cd.name); } } else if (cd.value.data == .comptime_expr) { @@ -577,7 +587,7 @@ pub const Lowering = struct { } }, .fn_decl => |fd| { - if (std.mem.eql(u8, fd.name, "main")) { + if (isExportedEntryName(fd.name)) { self.lazyLowerFunction(fd.name); } }, @@ -728,7 +738,7 @@ pub const Lowering = struct { return; } func.is_extern = false; // promote from extern stub to real function - func.linkage = if (std.mem.eql(u8, name, "main")) .external else .internal; + func.linkage = if (isExportedEntryName(name)) .external else .internal; if (fd.call_conv == .c) func.call_conv = .c; // Set inst_counter to param count (params occupy refs 0..N-1) std.debug.assert(func.params.len == fd.params.len); // AST and IR param counts must match @@ -836,8 +846,11 @@ pub const Lowering = struct { ); _ = func_id; - // Set linkage for main - if (std.mem.eql(u8, name, "main")) { + // Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly, + // matches C `static`). isExportedEntryName lists the names the OS + // loader calls — `main`, Android NativeActivity hooks — which must + // stay externally visible. + if (isExportedEntryName(name)) { self.builder.currentFunc().linkage = .external; } diff --git a/src/lsp/document.zig b/src/lsp/document.zig index 9709c12..9f23037 100644 --- a/src/lsp/document.zig +++ b/src/lsp/document.zig @@ -47,13 +47,19 @@ pub const DocumentStore = struct { io: std.Io, /// Workspace root path (from initialize). Used to absolutify CWD-relative import paths. root_path: []const u8 = "", + /// Install-discovered stdlib search paths. Mirrors the compiler's + /// `--lib-path` resolution so `#import "modules/std.sx"` etc. find the + /// shipped library files even when the workspace is something other + /// than the sx repo (e.g. /Users/agra/projects/game). + stdlib_paths: []const []const u8 = &.{}, /// All loaded documents keyed by resolved file path. by_path: std.StringHashMap(*Document), - pub fn init(allocator: std.mem.Allocator, io: std.Io) DocumentStore { + pub fn init(allocator: std.mem.Allocator, io: std.Io, stdlib_paths: []const []const u8) DocumentStore { return .{ .allocator = allocator, .io = io, + .stdlib_paths = stdlib_paths, .by_path = std.StringHashMap(*Document).init(allocator), }; } @@ -184,7 +190,7 @@ pub const DocumentStore = struct { for (root.data.root.decls) |decl| { if (decl.data != .import_decl) continue; const imp = decl.data.import_decl; - const resolved_path = try sx.imports.resolveImportPath(self.allocator, self.io, base_dir, imp.path, self.rootPathOpt(), &.{}); + const resolved_path = try sx.imports.resolveImportPath(self.allocator, self.io, base_dir, imp.path, self.rootPathOpt(), self.stdlib_paths); try import_list.append(self.allocator, .{ .ns = imp.name, .path = resolved_path, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 9912221..b51d2a7 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -24,13 +24,15 @@ pub const Server = struct { io: std.Io, shutdown_requested: bool = false, root_path: []const u8 = "", + stdlib_paths: []const []const u8 = &.{}, - pub fn init(allocator: std.mem.Allocator, transport: *Transport, io: std.Io) Server { + pub fn init(allocator: std.mem.Allocator, transport: *Transport, io: std.Io, stdlib_paths: []const []const u8) Server { return .{ .allocator = allocator, - .documents = DocumentStore.init(allocator, io), + .documents = DocumentStore.init(allocator, io, stdlib_paths), .transport = transport, .io = io, + .stdlib_paths = stdlib_paths, }; } @@ -271,7 +273,7 @@ pub const Server = struct { if (findImportPathAtOffset(doc.source, offset)) |import_path| { const base_dir = sx.imports.dirName(file_path); const rp: ?[]const u8 = if (self.root_path.len > 0) self.root_path else null; - const resolved = try sx.imports.resolveImportPath(self.allocator, self.io, base_dir, import_path, rp, &.{}); + const resolved = try sx.imports.resolveImportPath(self.allocator, self.io, base_dir, import_path, rp, self.stdlib_paths); // For directory imports, try to read as file first if (std.Io.Dir.readFileAlloc(.cwd(), self.io, resolved, self.allocator, .limited(10 * 1024 * 1024))) |_| { diff --git a/src/main.zig b/src/main.zig index 9ce318a..4470fff 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,7 +19,7 @@ pub fn main(init: std.process.Init) !void { // LSP subcommand doesn't need a file argument if (std.mem.eql(u8, command, "lsp")) { - runLsp(allocator, io); + runLsp(allocator, io, stdlib_paths); return; } @@ -61,6 +61,10 @@ pub fn main(init: std.process.Init) !void { "arm64-apple-ios14.0-simulator" else if (std.mem.eql(u8, raw, "ios-sim-x86")) "x86_64-apple-ios14.0-simulator" + else if (std.mem.eql(u8, raw, "android") or std.mem.eql(u8, raw, "android-arm64")) + "aarch64-linux-android21" + else if (std.mem.eql(u8, raw, "android-x86_64")) + "x86_64-linux-android21" else raw; target_config.triple = (try allocator.dupeZ(u8, expanded)).ptr; @@ -92,6 +96,18 @@ pub fn main(init: std.process.Init) !void { i += 1; if (i >= args.len) { std.debug.print("error: --bundle requires a path (e.g. MyApp.app)\n", .{}); return; } target_config.bundle_path = args[i]; + } else if (std.mem.eql(u8, arg, "--apk")) { + i += 1; + if (i >= args.len) { std.debug.print("error: --apk requires a path (e.g. out.apk)\n", .{}); return; } + target_config.apk_path = args[i]; + } else if (std.mem.eql(u8, arg, "--manifest")) { + i += 1; + if (i >= args.len) { std.debug.print("error: --manifest requires a path\n", .{}); return; } + target_config.manifest_path = args[i]; + } else if (std.mem.eql(u8, arg, "--keystore")) { + i += 1; + if (i >= args.len) { std.debug.print("error: --keystore requires a path\n", .{}); return; } + target_config.keystore_path = args[i]; } else if (std.mem.eql(u8, arg, "--bundle-id")) { i += 1; if (i >= args.len) { std.debug.print("error: --bundle-id requires a value (e.g. co.swipelab.myapp)\n", .{}); return; } @@ -335,7 +351,7 @@ fn printUsage() void { , .{}); } -fn runLsp(allocator: std.mem.Allocator, io: std.Io) void { +fn runLsp(allocator: std.mem.Allocator, io: std.Io, stdlib_paths: []const []const u8) void { const Transport = sx.lsp.transport.Transport; const Server = sx.lsp.server.Server; @@ -346,7 +362,7 @@ fn runLsp(allocator: std.mem.Allocator, io: std.Io) void { var stdin_reader = stdin_file.readerStreaming(io, &read_buf); var transport = Transport.init(allocator, io, &stdin_reader.interface, stdout_file); - var server = Server.init(allocator, &transport, io); + var server = Server.init(allocator, &transport, io, stdlib_paths); while (true) { const msg = transport.readMessage() catch |err| { @@ -583,6 +599,14 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons std.debug.print("bundled: {s}\n", .{bp}); } + // Wrap into an .apk if requested (Android). + if (merged_config.apk_path) |ap| { + timer.mark(); + sx.target.createApk(allocator, io, final_output, merged_config) catch std.process.exit(1); + timer.record("apk"); + std.debug.print("apk: {s}\n", .{ap}); + } + // Post-process wasm HTML: inject content hash for cache busting if (merged_config.isEmscripten() and std.mem.endsWith(u8, final_output, ".html")) { sx.target.postProcessWasmHtml(allocator, io, final_output); diff --git a/src/target.zig b/src/target.zig index be5661b..6be9750 100644 --- a/src/target.zig +++ b/src/target.zig @@ -30,8 +30,17 @@ pub const TargetConfig = struct { /// and ad-hoc signed for simulator runs. bundle_path: ?[]const u8 = null, /// CFBundleIdentifier for the bundle (e.g. "co.swipelab.sxhello"). - /// Required when `bundle_path` is set. + /// Required when `bundle_path` is set. On Android, doubles as the + /// AndroidManifest package="..." attribute. bundle_id: ?[]const u8 = null, + /// Path to a `.apk` file to produce (Android). When set, the linked + /// `.so` is wrapped into a debug-signed APK ready for `adb install`. + apk_path: ?[]const u8 = null, + /// Custom AndroidManifest.xml path. When null, a minimal NativeActivity + /// manifest is generated from `bundle_id`. + manifest_path: ?[]const u8 = null, + /// Debug keystore for APK signing. Defaults to `~/.android/debug.keystore`. + keystore_path: ?[]const u8 = null, /// Codesigning identity (e.g. `"Apple Development: Alex (TEAMID)"` or a /// SHA-1 fingerprint from `security find-identity -p codesigning`). /// When null, ad-hoc signs with `-` (sufficient for simulator, not device). @@ -110,11 +119,18 @@ pub const TargetConfig = struct { return self.isIOS() and !self.tripleContains("simulator"); } - /// Check if target triple indicates Linux. + /// Check if target triple indicates Linux (NOT Android — Android uses + /// "linux-android" too but isAndroid() must take precedence). pub fn isLinux(self: TargetConfig) bool { + if (self.isAndroid()) return false; return self.tripleContains("linux"); } + /// Check if target triple indicates Android (e.g. aarch64-linux-android21). + pub fn isAndroid(self: TargetConfig) bool { + return self.tripleContains("android"); + } + /// Check if target triple indicates Emscripten (contains "emscripten"). pub fn isEmscripten(self: TargetConfig) bool { return self.tripleContains("emscripten"); @@ -205,6 +221,259 @@ 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; +} + +/// Pick the lexicographically-highest subdir of `/` (matches the +/// "newest version" convention for `build-tools/` and +/// `platforms/android-`). 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 }); +} + +/// 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. aapt2 link → empty APK with resources/manifest. +/// 4. Append the lib/ tree via `zip`. +/// 5. zipalign → aligned APK. +/// 6. 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) !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}); + + // 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. + 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 = try buildAndroidManifest(allocator, bundle_id, lib_name); + try cwd.writeFile(io, .{ .sub_path = generated, .data = manifest_xml }); + break :blk generated; + }; + + // 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/" }); + + // 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; +} + +fn buildAndroidManifest(allocator: std.mem.Allocator, package: []const u8, lib_name: []const u8) ![]const u8 { + return std.fmt.allocPrint(allocator, + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + , .{ 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 +/// (the SDK Manager default install location on macOS). Caller owns the slice. +pub fn discoverAndroidNdk(allocator: std.mem.Allocator, io: std.Io) ![]const u8 { + if (std.c.getenv("ANDROID_NDK_HOME")) |env| { + return try allocator.dupe(u8, std.mem.span(env)); + } + if (std.c.getenv("ANDROID_NDK_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 NDK \u{2014} set $ANDROID_NDK_HOME\n", .{}); + return error.NdkNotFound; + }; + const home = std.mem.span(home_env); + const ndk_root = try std.fmt.allocPrint(allocator, "{s}/Library/Android/sdk/ndk", .{home}); + var dir = std.Io.Dir.openDir(.cwd(), io, ndk_root, .{ .iterate = true }) catch { + std.debug.print("error: no NDK at {s} \u{2014} install via Android Studio or set $ANDROID_NDK_HOME\n", .{ndk_root}); + return error.NdkNotFound; + }; + 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 version = best orelse { + std.debug.print("error: no NDK versions under {s}\n", .{ndk_root}); + return error.NdkNotFound; + }; + return try std.fmt.allocPrint(allocator, "{s}/{s}", .{ ndk_root, version }); +} + /// Run `xcrun --sdk --show-sdk-path` and return the trimmed path. /// Caller owns the returned slice. pub fn discoverAppleSdk(allocator: std.mem.Allocator, io: std.Io, sdk_name: []const u8) ![]const u8 { @@ -273,6 +542,61 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex var it = std.mem.tokenizeScalar(u8, flag, ' '); while (it.next()) |part| try argv.append(allocator, part); } + } else if (target_config.isAndroid()) { + // Android: NDK clang. Produces a shared library (.so) loaded by + // NativeActivity. native_app_glue.c (from the NDK) is compiled and + // linked alongside the sx code so apps can use the conventional + // `android_main(struct android_app*)` event-loop shape — the glue + // 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). + const ndk_root = if (target_config.sysroot) |sr| + try allocator.dupe(u8, sr) + else + try discoverAndroidNdk(allocator, io); + const host_tag: []const u8 = if (@import("builtin").os.tag == .macos) "darwin-x86_64" else "linux-x86_64"; + const clang = try std.fmt.allocPrint(allocator, "{s}/toolchains/llvm/prebuilt/{s}/bin/clang", .{ ndk_root, host_tag }); + + const glue_src = try std.fmt.allocPrint(allocator, "{s}/sources/android/native_app_glue/android_native_app_glue.c", .{ndk_root}); + const glue_obj = try std.fmt.allocPrint(allocator, "{s}.glue.o", .{output_obj}); + var glue_argv = std.ArrayList([]const u8).empty; + try glue_argv.appendSlice(allocator, &.{ clang, "-c", "-fPIC" }); + if (target_config.triple) |t| { + try glue_argv.append(allocator, "-target"); + try glue_argv.append(allocator, std.mem.span(t)); + } + try glue_argv.appendSlice(allocator, &.{ glue_src, "-o", glue_obj }); + const glue_slice = try glue_argv.toOwnedSlice(allocator); + var glue_child = std.process.spawn(io, .{ .argv = glue_slice }) catch return error.LinkError; + const glue_term = glue_child.wait(io) catch return error.LinkError; + if (glue_term != .exited or glue_term.exited != 0) return error.LinkError; + + try argv.append(allocator, clang); + if (target_config.triple) |t| { + try argv.append(allocator, "-target"); + try argv.append(allocator, std.mem.span(t)); + } + try argv.append(allocator, "-shared"); + try argv.append(allocator, "-fPIC"); + try argv.appendSlice(allocator, &.{ "-u", "ANativeActivity_onCreate" }); + try argv.append(allocator, output_obj); + try argv.append(allocator, glue_obj); + for (extra_objects) |eo| try argv.append(allocator, eo); + try argv.append(allocator, "-o"); + try argv.append(allocator, output_bin); + 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. + try argv.appendSlice(allocator, &.{ "-llog", "-landroid", "-lEGL", "-lGLESv3", "-lm", "-ldl" }); + for (target_config.extra_link_flags) |flag| { + var it = std.mem.tokenizeScalar(u8, flag, ' '); + while (it.next()) |part| try argv.append(allocator, part); + } } else if (target_config.isEmscripten()) { // Emscripten: use emcc as the linker/driver const linker = target_config.linker orelse "emcc";