ios: framework embedding in .app + -F search paths

- `-F <dir>` CLI flag adds Apple framework search paths (parallel to `-L`).
- `TargetConfig.framework_paths` flows into the iOS link line (`-F<dir>`).
- iOS link adds `-Wl,-rpath,@executable_path/Frameworks` so embedded
  frameworks resolve at runtime.
- `createBundle` now takes the framework list; for each one it locates
  `<name>.framework` in the `-F` paths and `cp -R`s it into
  `<bundle>.app/Frameworks/`.
- `c_import.compileCToObjects` forwards `-target`/`-isysroot` to clang so
  `#c_import` works under cross-compile (was using host clang implicitly).
- iOS SDK is auto-discovered once at startup and shared by both the C
  compile and the link paths.
- `SX_DEBUG_LINK=1` prints the resolved link argv.
- `library/modules/sdl3.sx`: drop `#library "SDL3"` — linking is now
  per-target (build.sx handles `-lSDL3` on macOS, `-framework SDL3` on iOS).
This commit is contained in:
agra
2026-05-17 14:38:58 +03:00
parent dc8529e3ea
commit e8bd40f710
4 changed files with 105 additions and 28 deletions

View File

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

View File

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

View File

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

View File

@@ -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 `<bundle>.app/Frameworks/<Name>.framework/<Name>` 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 `<name>.framework` in one of `framework_paths` and copy it into
/// `<dest_dir>/<name>.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 (`<TEAM>.*`) to the concrete bundle ID
/// (`<TEAM>.<bundle_id>`). Without this substitution the device installer