This commit is contained in:
agra
2026-05-23 15:41:12 +03:00
parent 4c6c29b299
commit 49b39ba07a
4 changed files with 153 additions and 12 deletions

View File

@@ -78,13 +78,26 @@ bundle_main :: () -> bool {
return false;
}
// Copy the linked binary into the bundle as `<exe_name>`. 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 `<bundle>/<dest>/`. 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: `<bundle>/Contents/Resources/<dest>/`. iOS: `<bundle>/<dest>/`.
// 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");

View File

@@ -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

View File

@@ -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;

View File

@@ -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,