Files
sx/src/main.zig
agra 2461218111 ffi #jni_main R.2: drop native_app_glue from Android link when #jni_main present
`target.link` now takes a `has_jni_main: bool` parameter (passed by
main.zig from `comp.getJniMainEmissions().len > 0`). When set:

  - native_app_glue.c is not compiled — no `.glue.o` produced.
  - `-u ANativeActivity_onCreate` is not added to the link argv.
  - The Java-driven Activity is the entry; the .so just provides JNI
    impls, bound at load time via the `JNI_OnLoad` slice R.3 will
    synthesize.

Legacy NativeActivity builds (no `#jni_main` decl) are unchanged: glue
is still compiled and `ANativeActivity_onCreate` still retained.

Verified end-to-end:
  - #jni_main .so: `llvm-nm -D` shows neither `ANativeActivity_onCreate`
    nor `android_main` (correct — Java side drives entry).
  - Legacy .so (99-android-egl-clear): both symbols still exported.

131 host / 4 cross / zig build test all green.
2026-05-20 14:59:49 +03:00

857 lines
36 KiB
Zig

const std = @import("std");
const sx = @import("sx");
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
const io = init.io;
const args = try init.minimal.args.toSlice(allocator);
// Stdlib discovered from binary location (or $SX_STDLIB_PATH override).
// Empty slice on hosts where discovery fails — imports fall back to CWD.
const stdlib_paths = sx.imports.discoverStdlibPaths(allocator) catch &[_][]const u8{};
if (args.len < 2) {
printUsage();
return;
}
const command = args[1];
// LSP subcommand doesn't need a file argument
if (std.mem.eql(u8, command, "lsp")) {
runLsp(allocator, io, stdlib_paths);
return;
}
// Parse flags and positional arguments
var input_path: ?[]const u8 = null;
var target_config = sx.target.TargetConfig{};
var lib_paths = std.ArrayList([]const u8).empty;
var framework_paths = std.ArrayList([]const u8).empty;
var link_flags = std.ArrayList([]const u8).empty;
var show_timing: bool = false;
var explicit_opt: bool = false;
var enable_cache: bool = false;
var i: usize = 2;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, arg, "--target")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --target requires a value\n", .{}); return; }
const raw = args[i];
// Shorthand aliases for common targets
const expanded = if (std.mem.eql(u8, raw, "wasm") or std.mem.eql(u8, raw, "wasm32") or std.mem.eql(u8, raw, "emscripten"))
"wasm32-unknown-emscripten"
else if (std.mem.eql(u8, raw, "wasm64"))
"wasm64-unknown-emscripten"
else if (std.mem.eql(u8, raw, "macos") or std.mem.eql(u8, raw, "macos-arm"))
"aarch64-apple-macos"
else if (std.mem.eql(u8, raw, "macos-x86"))
"x86_64-apple-macos"
else if (std.mem.eql(u8, raw, "linux") or std.mem.eql(u8, raw, "linux-x86"))
"x86_64-unknown-linux-gnu"
else if (std.mem.eql(u8, raw, "linux-arm"))
"aarch64-unknown-linux-gnu"
else if (std.mem.eql(u8, raw, "windows"))
"x86_64-windows-msvc"
else if (std.mem.eql(u8, raw, "ios") or std.mem.eql(u8, raw, "ios-arm"))
"arm64-apple-ios14.0"
else if (std.mem.eql(u8, raw, "ios-sim") or std.mem.eql(u8, raw, "ios-sim-arm"))
"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;
} else if (std.mem.eql(u8, arg, "--cpu")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --cpu requires a value\n", .{}); return; }
target_config.cpu = (try allocator.dupeZ(u8, args[i])).ptr;
} else if (std.mem.eql(u8, arg, "--opt")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --opt requires a value\n", .{}); return; }
target_config.opt_level = parseOptLevel(args[i]) orelse {
std.debug.print("error: invalid --opt value '{s}' (expected: none/0, less/1, default/2, aggressive/3)\n", .{args[i]});
return;
};
explicit_opt = true;
} else if (std.mem.eql(u8, arg, "-o")) {
i += 1;
if (i >= args.len) { std.debug.print("error: -o requires a value\n", .{}); return; }
target_config.output_path = args[i];
} else if (std.mem.eql(u8, arg, "--linker")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --linker requires a value\n", .{}); return; }
target_config.linker = args[i];
} else if (std.mem.eql(u8, arg, "--sysroot")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --sysroot requires a value\n", .{}); return; }
target_config.sysroot = args[i];
} else if (std.mem.eql(u8, arg, "--bundle")) {
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; }
target_config.bundle_id = args[i];
} else if (std.mem.eql(u8, arg, "--codesign-identity")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --codesign-identity requires a value\n", .{}); return; }
target_config.codesign_identity = args[i];
} else if (std.mem.eql(u8, arg, "--provisioning-profile")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --provisioning-profile requires a path\n", .{}); return; }
target_config.provisioning_profile = args[i];
} else if (std.mem.eql(u8, arg, "--entitlements")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --entitlements requires a path\n", .{}); return; }
target_config.entitlements_path = args[i];
} else if (std.mem.eql(u8, arg, "--time")) {
show_timing = true;
} else if (std.mem.eql(u8, arg, "--cache")) {
enable_cache = true;
} else if (std.mem.startsWith(u8, arg, "-L")) {
if (arg.len > 2) {
try lib_paths.append(allocator, arg[2..]);
} else {
i += 1;
if (i >= args.len) { std.debug.print("error: -L requires a value\n", .{}); return; }
try lib_paths.append(allocator, args[i]);
}
} else if (std.mem.startsWith(u8, arg, "-F")) {
if (arg.len > 2) {
try framework_paths.append(allocator, arg[2..]);
} else {
i += 1;
if (i >= args.len) { std.debug.print("error: -F requires a value\n", .{}); return; }
try framework_paths.append(allocator, args[i]);
}
} else if (std.mem.eql(u8, arg, "--lflags")) {
i += 1;
if (i >= args.len) { std.debug.print("error: --lflags requires a value\n", .{}); return; }
try link_flags.append(allocator, args[i]);
} else if (!std.mem.startsWith(u8, arg, "-")) {
input_path = arg;
} else {
std.debug.print("error: unknown flag '{s}'\n", .{arg});
return;
}
}
target_config.lib_paths = try lib_paths.toOwnedSlice(allocator);
target_config.framework_paths = try framework_paths.toOwnedSlice(allocator);
target_config.extra_link_flags = try link_flags.toOwnedSlice(allocator);
// Auto-discover iOS SDK once so both the C compile path and the link
// path see the same sysroot. Honors any explicit --sysroot.
if (target_config.isIOS() and target_config.sysroot == null) {
const sdk_name: []const u8 = if (target_config.isIOSSimulator()) "iphonesimulator" else "iphoneos";
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;
};
if (std.mem.eql(u8, command, "build")) {
const output_name = target_config.output_path orelse blk: {
const base = deriveOutputName(path);
if (target_config.isEmscripten()) {
break :blk try std.fmt.allocPrint(allocator, "{s}.html", .{base});
}
break :blk base;
};
compile(allocator, io, path, output_name, target_config, show_timing, enable_cache, stdlib_paths) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "ir")) {
emitIR(allocator, io, path, target_config, stdlib_paths) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "ir-dump")) {
dumpSxIR(allocator, io, path, stdlib_paths) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "asm")) {
emitAsm(allocator, io, path, target_config, stdlib_paths) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "run")) {
if (target_config.isWasm()) {
std.debug.print("error: 'run' is not supported for wasm targets. Use 'build' instead.\n", .{});
return;
}
// Default to -O0 for run (faster compile) unless user explicitly set --opt
if (!explicit_opt) target_config.opt_level = .none;
var timer = Timing.init(io, show_timing);
// Phase A: read + parse + resolveImports (for cache key)
timer.mark();
const source = readSource(allocator, io, path) catch std.process.exit(1);
timer.record("read");
var comp = sx.core.Compilation.init(allocator, io, path, source, target_config, stdlib_paths);
defer comp.deinit();
timer.mark();
comp.parse() catch { comp.renderErrors(); std.process.exit(1); };
timer.record("parse");
timer.mark();
comp.resolveImports() catch { comp.renderErrors(); std.process.exit(1); };
timer.record("imports");
// Cache check — use .o files (precompiled object, skip IR compilation in JIT)
// Disable caching for files with top-level #run (side effects lost on cache hit)
const root = comp.resolved_root orelse comp.root orelse return;
const use_cache = enable_cache and !hasTopLevelRun(root);
const key = computeCacheKey(source, &comp.import_sources, target_config);
const cache_obj = cachePath(allocator, key, "o") catch std.process.exit(1);
timer.mark();
const obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef = blk: {
if (use_cache) {
// Try loading cached .o from disk
var buf: sx.llvm_api.c.LLVMMemoryBufferRef = null;
var err_msg: [*c]u8 = null;
if (sx.llvm_api.c.LLVMCreateMemoryBufferWithContentsOfFile(cache_obj.ptr, &buf, &err_msg) == 0) {
timer.record("cache");
break :blk buf;
}
if (err_msg != null) sx.llvm_api.c.LLVMDisposeMessage(err_msg);
}
// Cache MISS — codegen + emit .o to memory (verify skipped: JIT catches errors)
comp.generateCode() catch { comp.renderErrors(); std.process.exit(1); };
timer.record("codegen");
timer.mark();
comp.ir_emitter.?.verifyWithMessage() catch std.process.exit(1);
const buf = comp.ir_emitter.?.emitObjectToMemory() catch std.process.exit(1);
timer.record("emit");
// Save .o to cache (extract data before JIT takes ownership)
if (use_cache) {
saveObjectToCache(buf, io, cache_obj);
}
break :blk buf;
};
// Compile C sources natively and dlopen before JIT
timer.mark();
var c_handle = compileCForJIT(allocator, io, &comp) catch { comp.renderErrors(); std.process.exit(1); };
defer c_handle.unload(io);
timer.record("c-import");
// dlopen #library dependencies so JIT can resolve foreign symbols
const libs = extractLibraries(allocator, root) catch std.process.exit(1);
var lib_handles = std.ArrayList(*anyopaque).empty;
defer {
for (lib_handles.items) |h| _ = std.c.dlclose(h);
}
for (libs) |lib_name| {
if (loadLibrary(allocator, lib_name, target_config.lib_paths)) |handle| {
lib_handles.append(allocator, handle) catch {};
} else {
const e = std.c.dlerror();
if (e) |msg| std.debug.print("warning: could not load library '{s}': {s}\n", .{ lib_name, std.mem.span(msg) });
}
}
// JIT from precompiled object (relocation only, no IR compilation)
sx.llvm_api.initNativeTarget();
timer.mark();
const exit_code = sx.target.runJITFromObject(obj_buf) catch {
// JIT failed — fall back to AOT
timer.record("jit-fail");
runAOT(allocator, io, path, target_config, &timer, enable_cache, stdlib_paths) catch std.process.exit(1);
timer.printAll();
return;
};
timer.record("jit");
timer.printAll();
if (exit_code != 0) std.process.exit(exit_code);
} else {
printUsage();
}
}
/// Compile C sources from #import c blocks and dlopen them for JIT.
fn compileCForJIT(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation) !sx.c_import.CImportHandle {
const c_infos = try comp.collectCImportSources();
if (c_infos.len == 0) return .{ .allocator = allocator };
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos, comp.target_config);
return try sx.c_import.loadCObjectsForJIT(allocator, io, obj_bufs);
}
/// Compile C sources from #import c blocks to .o files for linking.
fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation, tmp_dir: []const u8) ![]const []const u8 {
const c_infos = try comp.collectCImportSources();
if (c_infos.len == 0) return &.{};
// For Emscripten targets, use emcc to cross-compile C sources
if (comp.target_config.isEmscripten()) {
return try sx.c_import.compileCWithEmcc(allocator, io, c_infos, comp.target_config, tmp_dir);
}
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos, comp.target_config);
return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs, tmp_dir);
}
fn parseOptLevel(s: []const u8) ?sx.target.TargetConfig.OptLevel {
if (std.mem.eql(u8, s, "none") or std.mem.eql(u8, s, "0")) return .none;
if (std.mem.eql(u8, s, "less") or std.mem.eql(u8, s, "1")) return .less;
if (std.mem.eql(u8, s, "default") or std.mem.eql(u8, s, "2")) return .default;
if (std.mem.eql(u8, s, "aggressive") or std.mem.eql(u8, s, "3")) return .aggressive;
return null;
}
fn printUsage() void {
std.debug.print(
\\Usage: sx <command> [options] <file.sx>
\\
\\Commands:
\\ run Build and run immediately
\\ build Build binary in current directory
\\ ir Print LLVM IR to stdout
\\ asm Emit assembly (.s) file
\\ lsp Start language server (LSP)
\\
\\Options:
\\ --target <target> Target triple or shorthand: wasm, macos, linux, windows, ios, ios-sim (default: host)
\\ --cpu <name> CPU name (default: generic)
\\ --opt <level> Optimization: none/0, less/1, default/2, aggressive/3
\\ -o <path> Output path
\\ -L <path> Library search path (repeatable)
\\ --linker <cmd> Linker command (default: cc)
\\ --sysroot <path> Sysroot for cross-compilation
\\ --lflags <flag> Extra linker flag (repeatable, e.g. --lflags -sUSE_SDL=2)
\\ --bundle <Name.app> Wrap the binary in an iOS/macOS .app bundle (after linking)
\\ --bundle-id <id> CFBundleIdentifier (required with --bundle)
\\ --codesign-identity <name> Codesigning identity (e.g. "Apple Development: ...")
\\ --provisioning-profile <path> .mobileprovision to embed (required for device)
\\ --entitlements <path> Entitlements plist (auto-extracted from profile if omitted)
\\ --cache Enable build caching
\\ --time Show compilation timing breakdown
\\
, .{});
}
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;
const stdin_file = std.Io.File.stdin();
const stdout_file = std.Io.File.stdout();
var read_buf: [4096]u8 = undefined;
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, stdlib_paths);
while (true) {
const msg = transport.readMessage() catch |err| {
if (err == error.EndOfStream) break;
std.debug.print("lsp: read error: {}\n", .{err});
break;
};
const keep_going = server.handleMessage(msg);
if (!keep_going) break;
}
}
fn deriveOutputName(input_path: []const u8) []const u8 {
// Get basename (strip directory)
var start: usize = 0;
for (input_path, 0..) |ch, idx| {
if (ch == '/' or ch == '\\') start = idx + 1;
}
const basename = input_path[start..];
// Strip .sx extension
if (std.mem.endsWith(u8, basename, ".sx")) {
return basename[0 .. basename.len - 3];
}
return basename;
}
fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 {
const source_bytes = std.Io.Dir.readFileAlloc(.cwd(), io, input_path, allocator, .limited(10 * 1024 * 1024)) catch |err| {
std.debug.print("error: cannot read '{s}': {}\n", .{ input_path, err });
return error.CompileError;
};
return try allocator.dupeZ(u8, source_bytes);
}
fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, stdlib_paths: []const []const u8) !sx.core.Compilation {
timer.mark();
const source = try readSource(allocator, io, input_path);
timer.record("read");
var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config, stdlib_paths);
errdefer comp.deinit();
timer.mark();
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
timer.record("parse");
timer.mark();
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
timer.record("imports");
timer.mark();
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
timer.record("codegen");
timer.mark();
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
timer.record("verify");
return comp;
}
fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, stdlib_paths: []const []const u8) !void {
const source = try readSource(allocator, io, input_path);
var comp = sx.core.Compilation.init(allocator, io, input_path, source, .{}, stdlib_paths);
defer comp.deinit();
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
var ir_module = comp.lowerToIR() catch { comp.renderErrors(); return error.CompileError; };
defer ir_module.deinit();
var aw = std.Io.Writer.Allocating.init(allocator);
sx.ir.printModule(&ir_module, &aw.writer) catch return error.CompileError;
var result = aw.writer.toArrayList();
defer result.deinit(allocator);
std.debug.print("{s}", .{result.items});
}
fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, stdlib_paths: []const []const u8) !void {
var timer = Timing.init(io, false);
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer, stdlib_paths);
defer comp.deinit();
comp.ir_emitter.?.printIR();
}
fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, stdlib_paths: []const []const u8) !void {
var timer = Timing.init(io, false);
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer, stdlib_paths);
defer comp.deinit();
const asm_path = target_config.output_path orelse blk: {
const name = deriveOutputName(input_path);
break :blk try std.fmt.allocPrint(allocator, "{s}.s", .{name});
};
const asm_path_z = try allocator.dupeZ(u8, asm_path);
comp.ir_emitter.?.emitAssembly(asm_path_z.ptr) catch return error.CompileError;
std.debug.print("emitted: {s}\n", .{asm_path});
}
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, show_timing: bool, enable_cache: bool, stdlib_paths: []const []const u8) !void {
var timer = Timing.init(io, show_timing);
try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, enable_cache, stdlib_paths);
timer.printAll();
}
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool, stdlib_paths: []const []const u8) !void {
// Phase A: read + parse + resolveImports (fast: ~0.5ms)
timer.mark();
const source = try readSource(allocator, io, input_path);
timer.record("read");
var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config, stdlib_paths);
defer comp.deinit();
timer.mark();
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
timer.record("parse");
timer.mark();
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
timer.record("imports");
// Extract library + framework names from AST (needed for linking regardless of cache)
const root = comp.resolved_root orelse comp.root orelse return error.CompileError;
const libs = try extractLibraries(allocator, root);
var fws = try extractFrameworks(allocator, root);
// Create temp directory for build artifacts
const tmp_dir: []const u8 = ".sx-tmp";
std.Io.Dir.createDirPath(.cwd(), io, tmp_dir) catch {};
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}/main.o", .{tmp_dir}, 0);
// Cache: compute key and check for cached binary/.o
const key = computeCacheKey(source, &comp.import_sources, target_config);
const cache_obj = try cachePath(allocator, key, "o");
const cache_bin = try cachePath(allocator, key, "bin");
// Level 1: Try cached binary (skip everything — no codegen, no link)
if (enable_cache) bin_cache: {
std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache;
timer.record("cache");
return;
}
// Level 2: Try cached .o (skip codegen+emit, still need link)
const used_obj_cache = blk: {
if (!enable_cache) break :blk false;
std.Io.Dir.copyFile(.cwd(), cache_obj, .cwd(), obj_path, io, .{}) catch break :blk false;
break :blk true;
};
if (used_obj_cache) {
timer.record("cache");
} else {
// Cache MISS — full codegen + emit
timer.mark();
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
timer.record("codegen");
timer.mark();
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
timer.record("verify");
timer.mark();
comp.ir_emitter.?.emitObject(obj_path.ptr) catch return error.CompileError;
timer.record("emit");
// Save .o to cache
if (enable_cache) {
std.Io.Dir.copyFile(.cwd(), obj_path, .cwd(), cache_obj, io, .{ .make_path = true }) catch {};
}
}
// Compile C sources from #import c blocks to .o files
timer.mark();
const c_obj_paths = compileCForBuild(allocator, io, &comp, tmp_dir) catch {
std.debug.print("error: C import compilation failed\n", .{});
return error.CompileError;
};
timer.record("c-import");
// Merge build config (from #run blocks) with CLI config
var merged_config = target_config;
const build_flags = comp.getBuildLinkFlags();
const build_fws = comp.getBuildFrameworks();
if (build_fws.len > 0) {
var merged_fws: std.ArrayList([]const u8) = .empty;
for (fws) |f| try merged_fws.append(allocator, f);
for (build_fws) |f| try merged_fws.append(allocator, f);
// 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);
for (build_flags) |f| try all_flags.append(allocator, f);
merged_config.extra_link_flags = try all_flags.toOwnedSlice(allocator);
}
// Override output path from #run if set (and no explicit -o was given on CLI)
const final_output = if (target_config.output_path == null)
(comp.getBuildOutputPath() orelse output_path)
else
output_path;
// Override WASM shell template from #run if set
if (comp.getBuildWasmShell()) |shell| {
merged_config.wasm_shell_path = shell;
}
// Ensure output directory exists
if (std.mem.lastIndexOfScalar(u8, final_output, '/')) |sep| {
if (sep > 0) {
std.Io.Dir.createDirPath(.cwd(), io, final_output[0..sep]) catch {};
}
}
// Link (sx .o + C .o files)
timer.mark();
sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, fws, merged_config, comp.getJniMainEmissions().len > 0) catch {
std.debug.print("error: linking failed\n", .{});
return error.CompileError;
};
timer.record("link");
// Wrap into a .app bundle if requested (iOS/macOS).
if (merged_config.bundle_path) |bp| {
timer.mark();
sx.target.createBundle(allocator, io, final_output, merged_config, fws) catch std.process.exit(1);
timer.record("bundle");
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, comp.getJniMainEmissions()) 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);
}
// Save linked binary to cache
if (enable_cache) {
std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {};
}
std.debug.print("compiled: {s}\n", .{final_output});
// Clean up temp directory and all build artifacts
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
const shell_tmp = std.fmt.allocPrint(allocator, "{s}.shell.html", .{obj_path}) catch null;
if (shell_tmp) |sp| std.Io.Dir.deleteFile(.cwd(), io, sp) catch {};
for (c_obj_paths) |cop| {
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {};
}
std.Io.Dir.deleteDir(.cwd(), io, tmp_dir) catch {};
}
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool, stdlib_paths: []const []const u8) !void {
const tmp_bin = if (comptime @import("builtin").os.tag == .windows) "sx_run_tmp.exe" else "/tmp/sx_run_tmp";
try compileWithTimer(allocator, io, input_path, tmp_bin, target_config, timer, enable_cache, stdlib_paths);
defer {
std.Io.Dir.deleteFile(.cwd(), io, tmp_bin) catch {};
}
timer.mark();
var child = std.process.spawn(io, .{
.argv = &.{tmp_bin},
}) catch {
std.debug.print("error: failed to run program\n", .{});
return error.CompileError;
};
const term = child.wait(io) catch {
std.debug.print("error: program execution failed\n", .{});
return error.CompileError;
};
timer.record("exec");
switch (term) {
.exited => |code| if (code != 0) std.process.exit(code),
.signal => std.process.exit(1),
.stopped, .unknown => std.process.exit(1),
}
}
// --- Cache helpers ---
fn computeCacheKey(source: [:0]const u8, import_sources: *const std.StringHashMap([:0]const u8), target_config: sx.target.TargetConfig) u64 {
const Wyhash = std.hash.Wyhash;
var key = Wyhash.hash(0, source);
// XOR import hashes for order independence (HashMap iteration is non-deterministic)
var import_hash: u64 = 0;
var it = import_sources.iterator();
while (it.next()) |entry| {
var h = Wyhash.hash(0, entry.key_ptr.*);
h = Wyhash.hash(h, entry.value_ptr.*);
import_hash ^= h;
}
key = Wyhash.hash(key, std.mem.asBytes(&import_hash));
// Hash target config fields that affect codegen
if (target_config.triple) |t| key = Wyhash.hash(key, std.mem.span(t));
if (target_config.cpu) |cp| key = Wyhash.hash(key, std.mem.span(cp));
if (target_config.features) |f| key = Wyhash.hash(key, std.mem.span(f));
key = Wyhash.hash(key, std.mem.asBytes(&target_config.opt_level));
return key;
}
fn cachePath(allocator: std.mem.Allocator, key: u64, ext: []const u8) ![:0]const u8 {
return try std.fmt.allocPrintSentinel(allocator, ".sx-cache/{x:0>16}.{s}", .{ key, ext }, 0);
}
fn saveObjectToCache(obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef, io: std.Io, cache_path: [:0]const u8) void {
const c_api = sx.llvm_api.c;
const start = c_api.LLVMGetBufferStart(obj_buf);
const size = c_api.LLVMGetBufferSize(obj_buf);
if (start == null or size == 0) return;
const data = @as([*]const u8, @ptrCast(start))[0..size];
// Write to temp file, then copy to cache (make_path creates .sx-cache/ if needed)
std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = ".sx-cache-tmp", .data = data }) catch return;
std.Io.Dir.copyFile(.cwd(), ".sx-cache-tmp", .cwd(), cache_path, io, .{ .make_path = true }) catch {};
std.Io.Dir.deleteFile(.cwd(), io, ".sx-cache-tmp") catch {};
}
fn hasTopLevelRun(root: *const sx.ast.Node) bool {
for (root.data.root.decls) |decl| {
if (decl.data == .comptime_expr) return true;
}
return false;
}
fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 {
var libs = std.ArrayList([]const u8).empty;
var seen = std.StringHashMap(void).init(allocator);
const addLib = struct {
fn f(l: *std.ArrayList([]const u8), s: *std.StringHashMap(void), a: std.mem.Allocator, name: []const u8) !void {
if (s.contains(name)) return;
try s.put(name, {});
try l.append(a, name);
}
}.f;
for (root.data.root.decls) |decl| {
switch (decl.data) {
.library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name),
.namespace_decl => |ns| {
for (ns.decls) |nd| {
switch (nd.data) {
.library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name),
else => {},
}
}
},
else => {},
}
}
return try libs.toOwnedSlice(allocator);
}
fn extractFrameworks(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 {
var fws = std.ArrayList([]const u8).empty;
var seen = std.StringHashMap(void).init(allocator);
const addFw = struct {
fn f(l: *std.ArrayList([]const u8), s: *std.StringHashMap(void), a: std.mem.Allocator, name: []const u8) !void {
if (s.contains(name)) return;
try s.put(name, {});
try l.append(a, name);
}
}.f;
for (root.data.root.decls) |decl| {
switch (decl.data) {
.framework_decl => |fd| try addFw(&fws, &seen, allocator, fd.name),
.namespace_decl => |ns| {
for (ns.decls) |nd| {
switch (nd.data) {
.framework_decl => |fd| try addFw(&fws, &seen, allocator, fd.name),
else => {},
}
}
},
else => {},
}
}
return try fws.toOwnedSlice(allocator);
}
/// Try to dlopen a library by name, searching user paths, host paths, and common naming conventions.
fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_paths: []const []const u8) ?*anyopaque {
const is_macos = comptime @import("builtin").os.tag == .macos;
const suffixes: []const []const u8 = if (is_macos) &.{ ".dylib", ".so" } else &.{ ".so", ".dylib" };
// Search paths: user-supplied first, then host defaults
const search_paths = comptime blk: {
var paths: []const []const u8 = &.{};
for (sx.target.host_lib_paths) |p| {
paths = paths ++ .{p};
}
break :blk paths;
};
// Try each path with each suffix
const all_paths = [_][]const []const u8{ user_lib_paths, search_paths };
for (&all_paths) |paths| {
for (paths) |dir| {
for (suffixes) |sfx| {
const full = std.fmt.allocPrintSentinel(allocator, "{s}/lib{s}{s}", .{ dir, lib_name, sfx }, 0) catch continue;
if (std.c.dlopen(full.ptr, .{ .NOW = true })) |h| return h;
}
}
}
// Fallback: bare name (let dlopen search its default paths)
for (suffixes) |sfx| {
const bare = std.fmt.allocPrintSentinel(allocator, "lib{s}{s}", .{ lib_name, sfx }, 0) catch continue;
if (std.c.dlopen(bare.ptr, .{ .NOW = true })) |h| return h;
}
return null;
}
// Simple timing helper — records stage durations and prints a summary table.
const Timing = struct {
const max_entries = 16;
enabled: bool,
io: std.Io,
names: [max_entries][]const u8,
durations_ns: [max_entries]u64,
count: usize,
last: ?std.Io.Timestamp,
fn init(io: std.Io, enabled: bool) Timing {
return .{
.enabled = enabled,
.io = io,
.names = undefined,
.durations_ns = undefined,
.count = 0,
.last = if (enabled) std.Io.Timestamp.now(io, .awake) else null,
};
}
fn mark(self: *Timing) void {
if (self.enabled) self.last = std.Io.Timestamp.now(self.io, .awake);
}
fn record(self: *Timing, name: []const u8) void {
if (!self.enabled) return;
const now = std.Io.Timestamp.now(self.io, .awake);
const elapsed_ns: u64 = if (self.last) |prev| @intCast(prev.durationTo(now).nanoseconds) else 0;
if (self.count < max_entries) {
self.names[self.count] = name;
self.durations_ns[self.count] = elapsed_ns;
self.count += 1;
}
self.last = now;
}
fn printAll(self: *const Timing) void {
if (!self.enabled or self.count == 0) return;
var total_ns: u64 = 0;
for (self.durations_ns[0..self.count]) |d| total_ns += d;
std.debug.print("\n--- timing ---\n", .{});
for (0..self.count) |idx| {
const ms = @as(f64, @floatFromInt(self.durations_ns[idx])) / 1_000_000.0;
std.debug.print(" {s:<10} {d:>7.1} ms\n", .{ self.names[idx], ms });
}
const total_ms = @as(f64, @floatFromInt(total_ns)) / 1_000_000.0;
std.debug.print(" {s:<10} {d:>7.1} ms\n", .{ "total", total_ms });
}
};