android target + APK pipeline; LSP imports honor stdlib paths
Android (toolchain):
--target android / --target android-arm64 → aarch64-linux-android21.
target.zig discovers $ANDROID_NDK_HOME (or scans
~/Library/Android/sdk/ndk/* for the newest), invokes the NDK clang
with -shared -fPIC and links libsxhello.so against -llog -landroid
-lEGL -lGLESv3 -lm -ldl. native_app_glue.c from the NDK is compiled
and linked alongside the sx .o so apps can use the conventional
android_main(struct android_app*) shape; -u ANativeActivity_onCreate
keeps glue's symbol live.
Android (APK):
--apk <out> wraps the .so into a debug-signed installable APK.
target.zig discovers the SDK at $ANDROID_HOME (or
~/Library/Android/sdk), picks the newest build-tools + platforms,
generates a NativeActivity AndroidManifest.xml from --bundle-id,
packages via aapt2 link, appends the lib/ tree, zipalign, then
apksigner against ~/.android/debug.keystore (auto-generated via
keytool on first use). One command end-to-end:
sx build --target android --apk out.apk \\
--bundle-id co.swipelab.foo main.sx
Verified on Pixel 7 Pro: install + launch reaches android_main.
Compiler (entry-point linkage):
Top-level fn defs default to LLVM internal linkage and are lazily
lowered (only `main` was eagerly lowered before). Added
isExportedEntryName() — a small allowlist for names the OS loader
calls: `main`, `android_main`, `ANativeActivity_onCreate`,
`JNI_OnLoad`. These get eagerly lowered AND keep external linkage,
so they actually land in .dynsym.
LSP (imports):
DocumentStore now takes the install-discovered stdlib_paths and
forwards them into resolveImportPath, mirroring the compiler. Before
this, every `#import "modules/..."` resolved through the stdlib path
failed silently inside the LSP and identifiers from those modules
showed as `undefined variable`. Repro on label.sx: 1 false positive
before, 0 after.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))) |_| {
|
||||
|
||||
30
src/main.zig
30
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);
|
||||
|
||||
328
src/target.zig
328
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 `<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 });
|
||||
}
|
||||
|
||||
/// 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,
|
||||
\\<?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
|
||||
/// (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 <sdk_name> --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";
|
||||
|
||||
Reference in New Issue
Block a user