diff --git a/library/modules/platform/bundle.sx b/library/modules/platform/bundle.sx index 2deeff3..0f7dee9 100644 --- a/library/modules/platform/bundle.sx +++ b/library/modules/platform/bundle.sx @@ -78,13 +78,26 @@ bundle_main :: () -> bool { return false; } - // Copy the linked binary into the bundle as ``. Flat - // layout (binary + Info.plist at bundle root) matches the legacy - // Zig path for every Apple target — the canonical macOS - // `Contents/MacOS/` layout is a follow-up. + // Apple .app layout: macOS goes through `Contents/{MacOS,Resources}`; + // iOS / iOS-sim lay everything flat at the bundle root. + is_mac := opts.is_macos(); + macos_dir := if is_mac then concat(bundle, "/Contents/MacOS") else bundle; + plist_dir := if is_mac then concat(bundle, "/Contents") else bundle; + asset_root := if is_mac then concat(bundle, "/Contents/Resources") else bundle; + if is_mac { + if !create_dir_all(str_to_cstr(macos_dir)) { + out("error: bundle: cannot create Contents/MacOS\n"); + return false; + } + if !create_dir_all(str_to_cstr(asset_root)) { + out("error: bundle: cannot create Contents/Resources\n"); + return false; + } + } + exe_name := basename(binary); binary_z := str_to_cstr(binary); - exe_dest := concat(bundle, "/"); + exe_dest := concat(macos_dir, "/"); exe_dest = concat(exe_dest, exe_name); exe_dest_z := str_to_cstr(exe_dest); if !copy_file(binary_z, exe_dest_z) { @@ -96,7 +109,7 @@ bundle_main :: () -> bool { // Write Info.plist. Per-target shape — iOS needs UIDeviceFamily + // UIApplicationSceneManifest + DTPlatformName, macOS doesn't. plist := build_info_plist(opts, exe_name, bid); - plist_path := concat(bundle, "/Info.plist"); + plist_path := concat(plist_dir, "/Info.plist"); plist_path_z := str_to_cstr(plist_path); if !write_file(plist_path_z, plist) { out("error: bundle: write Info.plist failed\n"); @@ -113,15 +126,15 @@ bundle_main :: () -> bool { } // Copy any user-registered asset directories into the bundle. - // Apple .app puts them at `//`. Android (Week 7) will - // zip them into the APK at the same relative path. Recursive copy - // shells out to `cp -R` until fs.sx grows `list_dir`. + // macOS: `/Contents/Resources//`. iOS: `//`. + // Android (Week 7) will zip them into the APK at the same relative path. + // Recursive copy shells out to `cp -R` until fs.sx grows `list_dir`. asset_count := opts.asset_dir_count(); j : s64 = 0; while j < asset_count { src := opts.asset_dir_src_at(j); dest := opts.asset_dir_dest_at(j); - if !copy_asset_dir(src, dest, bundle) { + if !copy_asset_dir(src, dest, asset_root) { out("error: bundle: failed to copy asset dir '"); out(src); out("'\n"); diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 5aae5d7..ae1f6b4 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -275,6 +275,11 @@ pub const LLVMEmitter = struct { // Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5). self.emitObjcSelectorInit(); + // Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at + // startup so relative asset paths work when Finder/`open` + // launches the binary with CWD=/. Non-bundled binaries no-op. + self.emitMacosBundleChdir(); + // Pass 3: Verify typeSizeBytes matches LLVM's ABI sizes self.verifySizes(); } @@ -387,6 +392,104 @@ pub const LLVMEmitter = struct { } } + /// On macOS, emit a startup helper that chdir's to the .app bundle's + /// `Contents/Resources` directory when the executable lives inside a + /// `.app/Contents/MacOS/` path. Lets relative asset paths like + /// `assets/foo.png` resolve correctly when Finder/`open` launches the + /// binary with CWD=/. + /// + /// Bundled binary: strstr finds the marker, chdir succeeds. + /// CLI binary / `sx run`: strstr returns null, the function no-ops. + /// + /// The call is injected at the very start of `main()` (matching the + /// pattern used for the Obj-C selector init) rather than registered + /// via `@llvm.global_ctors`, so the ORC JIT path runs it too without + /// special handling. + fn emitMacosBundleChdir(self: *LLVMEmitter) void { + if (!self.target_config.is_aot) return; + if (!self.target_config.isMacOS()) return; + + const ptr_ty = self.cached_ptr; + const i32_ty = self.cached_i32; + const i8_ty = self.cached_i8; + const void_ty = self.cached_void; + + // Declare libc externs (re-use if already declared). + var ns_params: [2]c.LLVMTypeRef = .{ ptr_ty, ptr_ty }; + const ns_ty = c.LLVMFunctionType(i32_ty, &ns_params, 2, 0); + var ns_fn = c.LLVMGetNamedFunction(self.llvm_module, "_NSGetExecutablePath"); + if (ns_fn == null) ns_fn = c.LLVMAddFunction(self.llvm_module, "_NSGetExecutablePath", ns_ty); + + var chdir_params: [1]c.LLVMTypeRef = .{ptr_ty}; + const chdir_ty = c.LLVMFunctionType(i32_ty, &chdir_params, 1, 0); + var chdir_fn = c.LLVMGetNamedFunction(self.llvm_module, "chdir"); + if (chdir_fn == null) chdir_fn = c.LLVMAddFunction(self.llvm_module, "chdir", chdir_ty); + + var ss_params: [2]c.LLVMTypeRef = .{ ptr_ty, ptr_ty }; + const ss_ty = c.LLVMFunctionType(ptr_ty, &ss_params, 2, 0); + var ss_fn = c.LLVMGetNamedFunction(self.llvm_module, "strstr"); + if (ss_fn == null) ss_fn = c.LLVMAddFunction(self.llvm_module, "strstr", ss_ty); + + var sc_params: [2]c.LLVMTypeRef = .{ ptr_ty, ptr_ty }; + const sc_ty = c.LLVMFunctionType(ptr_ty, &sc_params, 2, 0); + var sc_fn = c.LLVMGetNamedFunction(self.llvm_module, "strcpy"); + if (sc_fn == null) sc_fn = c.LLVMAddFunction(self.llvm_module, "strcpy", sc_ty); + + var no_params: [0]c.LLVMTypeRef = .{}; + const ctor_ty = c.LLVMFunctionType(void_ty, &no_params, 0, 0); + const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_macos_bundle_chdir", ctor_ty); + c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage); + + const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry"); + const found_bb = c.LLVMAppendBasicBlockInContext(self.context, ctor, "found"); + const done_bb = c.LLVMAppendBasicBlockInContext(self.context, ctor, "done"); + + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + + const buf_ty = c.LLVMArrayType2(i8_ty, 1024); + const buf = c.LLVMBuildAlloca(self.builder, buf_ty, "buf"); + const bufsize = c.LLVMBuildAlloca(self.builder, i32_ty, "bufsize"); + _ = c.LLVMBuildStore(self.builder, c.LLVMConstInt(i32_ty, 1024, 0), bufsize); + + var ns_args: [2]c.LLVMValueRef = .{ buf, bufsize }; + _ = c.LLVMBuildCall2(self.builder, ns_ty, ns_fn, &ns_args, 2, ""); + + const needle = self.emitCStringGlobal("/Contents/MacOS/", "__sx_macos_chdir_needle"); + const replacement = self.emitCStringGlobal("/Contents/Resources", "__sx_macos_chdir_replacement"); + + var ss_args: [2]c.LLVMValueRef = .{ buf, needle }; + const p = c.LLVMBuildCall2(self.builder, ss_ty, ss_fn, &ss_args, 2, "p"); + + const is_null = c.LLVMBuildIsNull(self.builder, p, "is_null"); + _ = c.LLVMBuildCondBr(self.builder, is_null, done_bb, found_bb); + + c.LLVMPositionBuilderAtEnd(self.builder, found_bb); + var sc_args: [2]c.LLVMValueRef = .{ p, replacement }; + _ = c.LLVMBuildCall2(self.builder, sc_ty, sc_fn, &sc_args, 2, ""); + var chdir_args: [1]c.LLVMValueRef = .{buf}; + _ = c.LLVMBuildCall2(self.builder, chdir_ty, chdir_fn, &chdir_args, 1, ""); + _ = c.LLVMBuildBr(self.builder, done_bb); + + c.LLVMPositionBuilderAtEnd(self.builder, done_bb); + _ = c.LLVMBuildRetVoid(self.builder); + + // Inject a call at the very start of main(). Matches the + // emitObjcSelectorInit pattern so the ORC JIT path picks it up + // without needing `@llvm.global_ctors` plumbing. + const main_fn = c.LLVMGetNamedFunction(self.llvm_module, "main"); + if (main_fn != null) { + const main_entry = c.LLVMGetEntryBasicBlock(main_fn); + const first_inst = c.LLVMGetFirstInstruction(main_entry); + if (first_inst != null) { + c.LLVMPositionBuilderBefore(self.builder, first_inst); + } else { + c.LLVMPositionBuilderAtEnd(self.builder, main_entry); + } + var no_args: [0]c.LLVMValueRef = .{}; + _ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, ""); + } + } + /// Return `{cls_slot, mid_slot}` global pair for the /// `(name, sig)` literal — created on first lookup, shared across /// later `#jni_call` sites with the same literal pair. Both diff --git a/src/main.zig b/src/main.zig index 4fef3c2..0bd85cd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,9 +46,9 @@ pub fn main(init: std.process.Init) !void { 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" + try macosTripleForArch(allocator, "aarch64") else if (std.mem.eql(u8, raw, "macos-x86")) - "x86_64-apple-macos" + try macosTripleForArch(allocator, "x86_64") 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")) @@ -182,6 +182,7 @@ pub fn main(init: std.process.Init) !void { }; if (std.mem.eql(u8, command, "build")) { + target_config.is_aot = true; const output_name = target_config.output_path orelse blk: { const base = deriveOutputName(path); if (target_config.isEmscripten()) { @@ -321,6 +322,24 @@ fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Com return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs, tmp_dir); } +/// Build an Apple Darwin triple for `arch` (e.g. "aarch64" / "x86_64") using +/// the host's OS version suffix from `LLVMGetDefaultTargetTriple()`. Without +/// the version suffix, the emitted object file carries no platform load +/// command and `ld` warns "no platform load command found ... assuming: macOS". +/// Falls back to "darwin" if the host triple doesn't start with darwin (e.g. +/// when sx is cross-built on Linux for macOS). +fn macosTripleForArch(allocator: std.mem.Allocator, arch: []const u8) ![]const u8 { + const host = sx.llvm_api.c.LLVMGetDefaultTargetTriple(); + defer sx.llvm_api.c.LLVMDisposeMessage(host); + const span = std.mem.span(host); + var it = std.mem.splitScalar(u8, span, '-'); + _ = it.next(); + _ = it.next(); + const os_part = it.next() orelse "darwin"; + const os_suffix = if (std.mem.startsWith(u8, os_part, "darwin")) os_part else "darwin"; + return std.fmt.allocPrint(allocator, "{s}-apple-{s}", .{ arch, os_suffix }); +} + 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; diff --git a/src/target.zig b/src/target.zig index 1554df5..68f2615 100644 --- a/src/target.zig +++ b/src/target.zig @@ -65,6 +65,12 @@ pub const TargetConfig = struct { /// Path to an entitlements plist. When null and `provisioning_profile` /// is set, the entitlements are auto-extracted from the profile. entitlements_path: ?[]const u8 = null, + /// True when emitting an ahead-of-time binary (`sx build`), false for + /// in-process JIT (`sx run`). Used by emit_llvm to gate code that only + /// makes sense for a standalone executable — e.g. the macOS bundle + /// `chdir` shouldn't run in JIT mode because it would mutate the host + /// sx process's CWD. + is_aot: bool = false, pub const OptLevel = enum { none,