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

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