const std = @import("std"); const Allocator = std.mem.Allocator; const interp_mod = @import("interp.zig"); const Value = interp_mod.Value; const Interpreter = interp_mod.Interpreter; const inst = @import("inst.zig"); const FuncId = inst.FuncId; // ── BuildConfig ───────────────────────────────────────────────────────── // Mutable build configuration accumulated by #run blocks via #compiler methods. /// `(src_dir, dest_in_bundle)` pair recorded by /// `BuildOptions.add_asset_dir(src, dest)`. The sx bundler walks the /// list and recursively copies each `src` directory into the bundle /// at the relative `dest` path (e.g. `("assets", "assets")` copies /// `./assets/` to `/assets/`). Android's Week-7 APK path will /// zip the same pairs into the unaligned APK. pub const AssetDir = struct { src: []const u8, dest: []const u8, }; pub const BuildConfig = struct { link_flags: std.ArrayList([]const u8) = .empty, frameworks: std.ArrayList([]const u8) = .empty, asset_dirs: std.ArrayList(AssetDir) = .empty, output_path: ?[]const u8 = null, wasm_shell_path: ?[]const u8 = null, /// Post-link callback registered via /// `BuildOptions.set_post_link_callback(fn)`. When set, the /// compiler re-enters the IR interpreter after `target.link()` /// and invokes this function with no args. A `false` return is /// treated as a build failure. post_link_callback_fn: ?FuncId = null, /// Alternative to `post_link_callback_fn`: the qualified name of /// a module whose `bundle_main` function should be called /// post-link. post_link_module: ?[]const u8 = null, /// Path of the freshly-linked binary, populated by `main.zig` /// right before the post-link callback runs. The sx-side bundler /// reads this via `binary_path()` to know what file to wrap. binary_path: ?[]const u8 = null, // Apple `.app` / Android `.apk` bundling parameters. Set either // by the sx-side `BuildOptions.set_bundle_*` methods (preferred) // or by main.zig from CLI flags (transitional fallback). The sx // bundler reads them via the matching accessor methods. bundle_path: ?[]const u8 = null, bundle_id: ?[]const u8 = null, codesign_identity: ?[]const u8 = null, provisioning_profile: ?[]const u8 = null, /// Target triple as supplied to `--target` (canonicalized). /// Populated by main.zig before the post-link callback runs so the /// sx bundler can switch on iOS vs. macOS vs. simulator. target_triple: ?[]const u8 = null, /// Frameworks the binary links against (`-framework` names) and /// the search paths to look them up in (`-F` directories), forwarded /// from the link step so the sx bundler can embed them into /// `/Frameworks/`. target_frameworks: []const []const u8 = &.{}, target_framework_paths: []const []const u8 = &.{}, /// User-supplied `AndroidManifest.xml` override (`--manifest ` /// or `BuildOptions.set_manifest_path("...")`). When null, the /// Android bundler synthesizes a default manifest. manifest_path: ?[]const u8 = null, /// User-supplied debug keystore path (`--keystore ` or /// `BuildOptions.set_keystore_path("...")`). When null, the Android /// bundler uses `$HOME/.android/debug.keystore` (auto-generated on /// first use via `keytool`). keystore_path: ?[]const u8 = null, /// `#jni_main #jni_class("path") { ... }` decls discovered during /// lowering, paired with their pre-rendered Java source. The /// Android bundler writes each entry to /// `/java//.java`, compiles via `javac` + `d8`, /// and bundles the resulting `classes.dex` into the APK. Slices /// reference compiler-owned memory that outlives the post-link /// callback. jni_main_foreign_paths: []const []const u8 = &.{}, jni_main_java_sources: []const []const u8 = &.{}, pub fn deinit(self: *BuildConfig, alloc: Allocator) void { self.link_flags.deinit(alloc); self.frameworks.deinit(alloc); self.asset_dirs.deinit(alloc); } }; // ── Hook system ───────────────────────────────────────────────────────── pub const HookError = error{ CannotEvalComptime, TypeError, }; /// Hook function signature. Receives the interpreter (for heap/string access), /// resolved argument values, and the mutable build config. pub const HookFn = *const fn ( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value; pub const Registry = struct { hooks: std.StringHashMap(HookFn), pub fn init(alloc: Allocator) Registry { return .{ .hooks = std.StringHashMap(HookFn).init(alloc) }; } pub fn deinit(self: *Registry) void { self.hooks.deinit(); } pub fn get(self: *const Registry, name: []const u8) ?HookFn { return self.hooks.get(name); } /// Register all built-in compiler hooks. pub fn registerDefaults(self: *Registry) void { self.hooks.put("build_options", &hookBuildOptions) catch {}; self.hooks.put("BuildOptions.add_link_flag", &hookAddLinkFlag) catch {}; self.hooks.put("BuildOptions.add_framework", &hookAddFramework) catch {}; self.hooks.put("BuildOptions.add_asset_dir", &hookAddAssetDir) catch {}; self.hooks.put("BuildOptions.asset_dir_count", &hookAssetDirCount) catch {}; self.hooks.put("BuildOptions.asset_dir_src_at", &hookAssetDirSrcAt) catch {}; self.hooks.put("BuildOptions.asset_dir_dest_at", &hookAssetDirDestAt) catch {}; self.hooks.put("BuildOptions.set_output_path", &hookSetOutputPath) catch {}; self.hooks.put("BuildOptions.set_wasm_shell", &hookSetWasmShell) catch {}; self.hooks.put("BuildOptions.set_post_link_callback", &hookSetPostLinkCallback) catch {}; self.hooks.put("BuildOptions.set_post_link_module", &hookSetPostLinkModule) catch {}; self.hooks.put("BuildOptions.binary_path", &hookGetBinaryPath) catch {}; // Bundling setters self.hooks.put("BuildOptions.set_bundle_path", &hookSetBundlePath) catch {}; self.hooks.put("BuildOptions.set_bundle_id", &hookSetBundleId) catch {}; self.hooks.put("BuildOptions.set_codesign_identity", &hookSetCodesignIdentity) catch {}; self.hooks.put("BuildOptions.set_provisioning_profile", &hookSetProvisioningProfile) catch {}; // Bundling accessors self.hooks.put("BuildOptions.bundle_path", &hookGetBundlePath) catch {}; self.hooks.put("BuildOptions.bundle_id", &hookGetBundleId) catch {}; self.hooks.put("BuildOptions.codesign_identity", &hookGetCodesignIdentity) catch {}; self.hooks.put("BuildOptions.provisioning_profile", &hookGetProvisioningProfile) catch {}; // Target accessors — mirror TargetConfig.is{MacOS,IOS,IOSDevice,IOSSimulator,Android}() self.hooks.put("BuildOptions.target_triple", &hookGetTargetTriple) catch {}; self.hooks.put("BuildOptions.is_macos", &hookIsMacOS) catch {}; self.hooks.put("BuildOptions.is_ios", &hookIsIOS) catch {}; self.hooks.put("BuildOptions.is_ios_device", &hookIsIOSDevice) catch {}; self.hooks.put("BuildOptions.is_ios_simulator", &hookIsIOSSimulator) catch {}; self.hooks.put("BuildOptions.is_android", &hookIsAndroid) catch {}; // Android-specific setters + accessors self.hooks.put("BuildOptions.set_manifest_path", &hookSetManifestPath) catch {}; self.hooks.put("BuildOptions.manifest_path", &hookGetManifestPath) catch {}; self.hooks.put("BuildOptions.set_keystore_path", &hookSetKeystorePath) catch {}; self.hooks.put("BuildOptions.keystore_path", &hookGetKeystorePath) catch {}; // #jni_main class emissions, exposed by index so bundle.sx can iterate. self.hooks.put("BuildOptions.jni_main_count", &hookJniMainCount) catch {}; self.hooks.put("BuildOptions.jni_main_foreign_path_at", &hookJniMainForeignPathAt) catch {}; self.hooks.put("BuildOptions.jni_main_java_source_at", &hookJniMainJavaSourceAt) catch {}; // Framework list accessors (for `.app/Frameworks/` embedding) self.hooks.put("BuildOptions.framework_count", &hookFrameworkCount) catch {}; self.hooks.put("BuildOptions.framework_at", &hookFrameworkAt) catch {}; self.hooks.put("BuildOptions.framework_path_count", &hookFrameworkPathCount) catch {}; self.hooks.put("BuildOptions.framework_path_at", &hookFrameworkPathAt) catch {}; } }; // ── build_options() hook ──────────────────────────────────────────────── fn hookBuildOptions( _: *const Interpreter, _: []const Value, _: *BuildConfig, _: Allocator, ) HookError!Value { // build_options() returns a sentinel value; the real work happens // when methods like add_link_flag/set_output_path are called on it. return .void_val; } // ── BuildOptions hooks ────────────────────────────────────────────────── fn hookAddLinkFlag( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { // args: [self (BuildOptions value), flag_string] if (args.len < 2) return .void_val; const str_val = args[1]; if (str_val.asString(interp)) |s| { bc.link_flags.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime; } return .void_val; } fn hookAddFramework( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { // args: [self (BuildOptions value), framework_name] if (args.len < 2) return .void_val; const str_val = args[1]; if (str_val.asString(interp)) |s| { bc.frameworks.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime; } return .void_val; } fn hookAddAssetDir( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { // args: [self (BuildOptions value), src_path, dest_path_in_bundle] if (args.len < 3) return .void_val; const src = args[1].asString(interp) orelse return error.TypeError; const dest = args[2].asString(interp) orelse return error.TypeError; const src_dup = alloc.dupe(u8, src) catch return error.CannotEvalComptime; const dest_dup = alloc.dupe(u8, dest) catch return error.CannotEvalComptime; bc.asset_dirs.append(alloc, .{ .src = src_dup, .dest = dest_dup }) catch return error.CannotEvalComptime; return .void_val; } fn hookAssetDirCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .int = @intCast(bc.asset_dirs.items.len) }; } fn hookAssetDirSrcAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (args.len < 2) return Value{ .string = "" }; const idx = args[1].asInt() orelse return error.TypeError; if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len) return Value{ .string = "" }; return Value{ .string = bc.asset_dirs.items[@intCast(idx)].src }; } fn hookAssetDirDestAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (args.len < 2) return Value{ .string = "" }; const idx = args[1].asInt() orelse return error.TypeError; if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len) return Value{ .string = "" }; return Value{ .string = bc.asset_dirs.items[@intCast(idx)].dest }; } fn hookSetOutputPath( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { // args: [self (BuildOptions value), path_string] if (args.len < 2) return .void_val; const str_val = args[1]; if (str_val.asString(interp)) |s| { bc.output_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookSetWasmShell( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { // args: [self (BuildOptions value), path_string] if (args.len < 2) return .void_val; const str_val = args[1]; if (str_val.asString(interp)) |s| { bc.wasm_shell_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookSetPostLinkCallback( _: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator, ) HookError!Value { // args: [self (BuildOptions value), fn_value]. We accept a function // value (.func_ref) and stash the FuncId so `main.zig` can re-enter // the interpreter after linking. if (args.len < 2) return .void_val; switch (args[1]) { .func_ref => |id| bc.post_link_callback_fn = id, else => return error.TypeError, } return .void_val; } fn hookSetPostLinkModule( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.post_link_module = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } /// Read the linked-binary path that main.zig populated right before /// invoking the post-link callback. Returns the fat-string aggregate /// the interpreter normally hands out for sx `string` values. fn hookGetBinaryPath( interp: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator, ) HookError!Value { _ = interp; const path = bc.binary_path orelse ""; return Value{ .string = path }; } // ── Bundling setters & accessors ───────────────────────────────────── // Same pattern as set_output_path: take a string arg, dupe into the // long-lived allocator, store on BuildConfig. The companion accessor // reads back the same field; empty string when unset. fn hookSetBundlePath( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.bundle_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookSetBundleId( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.bundle_id = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookSetCodesignIdentity( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.codesign_identity = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookSetProvisioningProfile( interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator, ) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.provisioning_profile = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookGetBundlePath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.bundle_path orelse "" }; } fn hookGetBundleId(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.bundle_id orelse "" }; } fn hookGetCodesignIdentity(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.codesign_identity orelse "" }; } fn hookGetProvisioningProfile(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.provisioning_profile orelse "" }; } // ── Target accessors ────────────────────────────────────────────────── // These look at the target_triple that main.zig populates and answer // the same questions TargetConfig's helpers do for Zig callers. fn tripleContains(triple: ?[]const u8, needle: []const u8) bool { const t = triple orelse return false; return std.mem.indexOf(u8, t, needle) != null; } fn isIOSTriple(triple: ?[]const u8) bool { return tripleContains(triple, "apple-ios"); } fn hookGetTargetTriple(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.target_triple orelse "" }; } fn hookIsMacOS(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (isIOSTriple(bc.target_triple)) return Value{ .boolean = false }; const t = bc.target_triple orelse ""; const is_mac = std.mem.indexOf(u8, t, "apple-macosx") != null or std.mem.indexOf(u8, t, "apple-macos") != null or std.mem.indexOf(u8, t, "apple-darwin") != null; return Value{ .boolean = is_mac }; } fn hookIsIOS(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .boolean = isIOSTriple(bc.target_triple) }; } fn hookIsIOSDevice(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { const ios = isIOSTriple(bc.target_triple); const sim = tripleContains(bc.target_triple, "simulator"); return Value{ .boolean = ios and !sim }; } fn hookIsIOSSimulator(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { const ios = isIOSTriple(bc.target_triple); const sim = tripleContains(bc.target_triple, "simulator"); return Value{ .boolean = ios and sim }; } fn hookIsAndroid(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .boolean = tripleContains(bc.target_triple, "android") }; } // ── Android-specific bundling setters + accessors ───────────────────── fn hookSetManifestPath(interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.manifest_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookGetManifestPath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.manifest_path orelse "" }; } fn hookSetKeystorePath(interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator) HookError!Value { if (args.len < 2) return .void_val; if (args[1].asString(interp)) |s| { bc.keystore_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; } return .void_val; } fn hookGetKeystorePath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .string = bc.keystore_path orelse "" }; } // ── #jni_main emission accessors ────────────────────────────────────── // The Android bundler walks these as `0..jni_main_count()` and reads // each entry's `(foreign_path, java_source)` pair so it can write a // `.java` file per decl, compile via javac, and produce classes.dex // via d8 before zipping into the APK. fn hookJniMainCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return Value{ .int = @intCast(bc.jni_main_foreign_paths.len) }; } fn hookJniMainForeignPathAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (args.len < 2) return Value{ .string = "" }; const idx = args[1].asInt() orelse return error.TypeError; if (idx < 0 or @as(usize, @intCast(idx)) >= bc.jni_main_foreign_paths.len) return Value{ .string = "" }; return Value{ .string = bc.jni_main_foreign_paths[@intCast(idx)] }; } fn hookJniMainJavaSourceAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (args.len < 2) return Value{ .string = "" }; const idx = args[1].asInt() orelse return error.TypeError; if (idx < 0 or @as(usize, @intCast(idx)) >= bc.jni_main_java_sources.len) return Value{ .string = "" }; return Value{ .string = bc.jni_main_java_sources[@intCast(idx)] }; } // ── Framework list accessors ────────────────────────────────────────── // The Apple .app bundler in `library/modules/platform/bundle.sx` walks // the framework list to recursively copy each `.framework` // directory from the user's -F search paths into `/Frameworks/`. // Slice-of-string returns aren't natively expressible as a Value, so we // expose count + indexed lookups instead. fn intValue(n: i64) Value { return Value{ .int = n }; } fn hookFrameworkCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return intValue(@intCast(bc.target_frameworks.len)); } fn hookFrameworkAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (args.len < 2) return Value{ .string = "" }; const idx_i64 = args[1].asInt() orelse return error.TypeError; if (idx_i64 < 0 or @as(usize, @intCast(idx_i64)) >= bc.target_frameworks.len) { return Value{ .string = "" }; } return Value{ .string = bc.target_frameworks[@intCast(idx_i64)] }; } fn hookFrameworkPathCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { return intValue(@intCast(bc.target_framework_paths.len)); } fn hookFrameworkPathAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value { if (args.len < 2) return Value{ .string = "" }; const idx_i64 = args[1].asInt() orelse return error.TypeError; if (idx_i64 < 0 or @as(usize, @intCast(idx_i64)) >= bc.target_framework_paths.len) { return Value{ .string = "" }; } return Value{ .string = bc.target_framework_paths[@intCast(idx_i64)] }; }