diff --git a/library/modules/sdl3.sx b/library/modules/sdl3.sx index a4c9b1e..74ac60e 100644 --- a/library/modules/sdl3.sx +++ b/library/modules/sdl3.sx @@ -1,4 +1,4 @@ -sdl3 :: #library "SDL3"; +// SDL3 bindings. Linking is per-target (handled in build.sx): // SDL_InitFlags SDL_INIT_VIDEO :u32: 0x20; @@ -320,27 +320,27 @@ SDL_Event :: enum struct { tag: u32; _: u32; payload: [30]u32; } { } // Functions -SDL_Init :: (flags: u32) -> bool #foreign sdl3; -SDL_Quit :: () -> void #foreign sdl3; -SDL_CreateWindow :: (title: [:0]u8, w: s32, h: s32, flags: u64) -> *void #foreign sdl3; -SDL_DestroyWindow :: (window: *void) -> void #foreign sdl3; -SDL_GL_SetAttribute :: (attr: s32, value: s32) -> bool #foreign sdl3; -SDL_GL_CreateContext :: (window: *void) -> *void #foreign sdl3; -SDL_GL_DestroyContext :: (context: *void) -> bool #foreign sdl3; -SDL_GL_MakeCurrent :: (window: *void, context: *void) -> bool #foreign sdl3; -SDL_GL_SwapWindow :: (window: *void) -> bool #foreign sdl3; -SDL_GL_SetSwapInterval :: (interval: s32) -> bool #foreign sdl3; -SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign sdl3; -SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign sdl3; -SDL_AddEventWatch :: (filter: *void, userdata: *void) -> bool #foreign sdl3; -SDL_GetTicks :: () -> u64 #foreign sdl3; -SDL_GetPerformanceCounter :: () -> u64 #foreign sdl3; -SDL_GetPerformanceFrequency :: () -> u64 #foreign sdl3; -SDL_Delay :: (ms: u32) -> void #foreign sdl3; -SDL_GetWindowDisplayScale :: (window: *void) -> f32 #foreign sdl3; -SDL_GetWindowSize :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; -SDL_SetWindowSize :: (window: *void, w: s32, h: s32) -> bool #foreign sdl3; -SDL_GetWindowSizeInPixels :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; +SDL_Init :: (flags: u32) -> bool #foreign; +SDL_Quit :: () -> void #foreign; +SDL_CreateWindow :: (title: [:0]u8, w: s32, h: s32, flags: u64) -> *void #foreign; +SDL_DestroyWindow :: (window: *void) -> void #foreign; +SDL_GL_SetAttribute :: (attr: s32, value: s32) -> bool #foreign; +SDL_GL_CreateContext :: (window: *void) -> *void #foreign; +SDL_GL_DestroyContext :: (context: *void) -> bool #foreign; +SDL_GL_MakeCurrent :: (window: *void, context: *void) -> bool #foreign; +SDL_GL_SwapWindow :: (window: *void) -> bool #foreign; +SDL_GL_SetSwapInterval :: (interval: s32) -> bool #foreign; +SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign; +SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign; +SDL_AddEventWatch :: (filter: *void, userdata: *void) -> bool #foreign; +SDL_GetTicks :: () -> u64 #foreign; +SDL_GetPerformanceCounter :: () -> u64 #foreign; +SDL_GetPerformanceFrequency :: () -> u64 #foreign; +SDL_Delay :: (ms: u32) -> void #foreign; +SDL_GetWindowDisplayScale :: (window: *void) -> f32 #foreign; +SDL_GetWindowSize :: (window: *void, w: *s32, h: *s32) -> bool #foreign; +SDL_SetWindowSize :: (window: *void, w: s32, h: s32) -> bool #foreign; +SDL_GetWindowSizeInPixels :: (window: *void, w: *s32, h: *s32) -> bool #foreign; SDL_Rect :: struct { x: s32; @@ -349,5 +349,5 @@ SDL_Rect :: struct { h: s32; } -SDL_GetPrimaryDisplay :: () -> u32 #foreign sdl3; -SDL_GetDisplayUsableBounds :: (display_id: u32, rect: *SDL_Rect) -> bool #foreign sdl3; +SDL_GetPrimaryDisplay :: () -> u32 #foreign; +SDL_GetDisplayUsableBounds :: (display_id: u32, rect: *SDL_Rect) -> bool #foreign; diff --git a/src/c_import.zig b/src/c_import.zig index f7232b4..8e5ab3f 100644 --- a/src/c_import.zig +++ b/src/c_import.zig @@ -175,6 +175,7 @@ pub fn processCImport( pub fn compileCToObjects( allocator: std.mem.Allocator, infos: []const CImportInfo, + target_config: @import("target.zig").TargetConfig, ) ![]c.LLVMMemoryBufferRef { var obj_bufs = std.ArrayList(c.LLVMMemoryBufferRef).empty; @@ -183,6 +184,15 @@ pub fn compileCToObjects( // Build clang args: -I dirs, -D defines, raw flags var args_list = std.ArrayList([*c]const u8).empty; + // Cross-compile target: forward -target / -isysroot when set. + if (target_config.triple) |t| { + try args_list.append(allocator, "-target"); + try args_list.append(allocator, t); + } + if (target_config.sysroot) |sr| { + try args_list.append(allocator, "-isysroot"); + try args_list.append(allocator, (try allocator.dupeZ(u8, sr)).ptr); + } for (info.includes) |inc| { const dir = dirName(inc); try args_list.append(allocator, (try allocPrintZ(allocator, "-I{s}", .{dir})).ptr); diff --git a/src/main.zig b/src/main.zig index 8092c44..9ce318a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -27,6 +27,7 @@ pub fn main(init: std.process.Init) !void { 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; @@ -119,6 +120,14 @@ pub fn main(init: std.process.Init) !void { 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; } @@ -132,8 +141,16 @@ pub fn main(init: std.process.Init) !void { } 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; + } + const path = input_path orelse { printUsage(); return; @@ -261,7 +278,7 @@ fn compileCForJIT(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compi 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); + const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos, comp.target_config); return try sx.c_import.loadCObjectsForJIT(allocator, io, obj_bufs); } @@ -275,7 +292,7 @@ fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Com 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); + 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); } @@ -561,7 +578,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons // 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) catch std.process.exit(1); + 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}); } diff --git a/src/target.zig b/src/target.zig index 9775afd..122af20 100644 --- a/src/target.zig +++ b/src/target.zig @@ -13,6 +13,8 @@ pub const TargetConfig = struct { opt_level: OptLevel = .default, /// Library search paths (-L flags). lib_paths: []const []const u8 = &.{}, + /// Framework search paths (-F flags). Apple-only. + framework_paths: []const []const u8 = &.{}, /// Output path override. output_path: ?[]const u8 = null, /// Linker command (null = "cc" on Unix, "link.exe" on Windows). @@ -248,6 +250,8 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex try argv.append(allocator, sdk_path); const min_flag: []const u8 = if (target_config.isIOSSimulator()) "-mios-simulator-version-min=14.0" else "-mios-version-min=14.0"; try argv.append(allocator, min_flag); + // Embedded framework load path: bundle/Frameworks at runtime. + try argv.append(allocator, "-Wl,-rpath,@executable_path/Frameworks"); try argv.append(allocator, output_obj); try argv.append(allocator, "-o"); try argv.append(allocator, output_bin); @@ -255,6 +259,9 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex for (target_config.lib_paths) |lp| { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp})); } + for (target_config.framework_paths) |fp| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-F{s}", .{fp})); + } for (libraries) |lib| { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib})); } @@ -363,6 +370,11 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex } const argv_slice = try argv.toOwnedSlice(allocator); + if (std.c.getenv("SX_DEBUG_LINK") != null) { + std.debug.print("[sx] link argv:", .{}); + for (argv_slice) |a| std.debug.print(" {s}", .{a}); + std.debug.print("\n", .{}); + } var child = std.process.spawn(io, .{ .argv = argv_slice, }) catch return error.LinkError; @@ -375,7 +387,7 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex /// write a minimal Info.plist, and ad-hoc codesign for simulator runs. /// The executable inside the bundle is named after the basename of /// `binary_path` (also used as CFBundleExecutable). -pub fn createBundle(allocator: std.mem.Allocator, io: std.Io, binary_path: []const u8, target_config: TargetConfig) !void { +pub fn createBundle(allocator: std.mem.Allocator, io: std.Io, binary_path: []const u8, target_config: TargetConfig, frameworks: []const []const u8) !void { const bundle_path = target_config.bundle_path orelse return error.NoBundlePath; const bundle_id = target_config.bundle_id orelse { std.debug.print("error: --bundle requires --bundle-id (e.g. co.swipelab.app)\n", .{}); @@ -412,6 +424,18 @@ pub fn createBundle(allocator: std.mem.Allocator, io: std.Io, binary_path: []con try cwd.writeFile(io, .{ .sub_path = embedded_path, .data = profile_data }); } + // Embed any dynamic frameworks the binary links against. iOS apps load + // frameworks from `.app/Frameworks/.framework/` via + // the `@executable_path/Frameworks` rpath we set at link time. For each + // framework, look it up in `framework_paths` and copy the bundle in. + if (target_config.isIOS() and frameworks.len > 0) { + const fw_dir = try std.fs.path.join(allocator, &.{ bundle_path, "Frameworks" }); + try cwd.createDirPath(io, fw_dir); + for (frameworks) |fw| { + try embedFramework(allocator, io, fw, target_config.framework_paths, fw_dir); + } + } + // Codesign: real identity for device, ad-hoc otherwise. const identity: []const u8 = target_config.codesign_identity orelse "-"; const ent_path: ?[]const u8 = if (target_config.entitlements_path) |e| e else blk: { @@ -423,6 +447,32 @@ pub fn createBundle(allocator: std.mem.Allocator, io: std.Io, binary_path: []con try codesign(allocator, io, bundle_path, identity, ent_path); } +/// Find `.framework` in one of `framework_paths` and copy it into +/// `/.framework`. Shells out to `cp -R` because Zig's std +/// doesn't expose a recursive-copy primitive on `Io.Dir` yet. +fn embedFramework(allocator: std.mem.Allocator, io: std.Io, name: []const u8, framework_paths: []const []const u8, dest_dir: []const u8) !void { + const cwd = std.Io.Dir.cwd(); + const subdir = try std.fmt.allocPrint(allocator, "{s}.framework", .{name}); + for (framework_paths) |fp| { + const candidate = try std.fs.path.join(allocator, &.{ fp, subdir }); + if (cwd.openDir(io, candidate, .{})) |d| { + d.close(io); + const dest = try std.fs.path.join(allocator, &.{ dest_dir, subdir }); + const r = std.process.run(allocator, io, .{ + .argv = &.{ "cp", "-R", candidate, dest }, + }) catch return error.FrameworkCopyFailed; + defer allocator.free(r.stdout); + defer allocator.free(r.stderr); + if (r.term != .exited or r.term.exited != 0) { + std.debug.print("error: cp -R {s} -> {s} failed: {s}\n", .{ candidate, dest, r.stderr }); + return error.FrameworkCopyFailed; + } + return; + } else |_| {} + } + std.debug.print("warning: framework '{s}' not found in any -F path; runtime load will fail\n", .{name}); +} + /// Extract entitlements XML from a `.mobileprovision` and resolve the /// `application-identifier` wildcard (`.*`) to the concrete bundle ID /// (`.`). Without this substitution the device installer