ios + ir cleanup
- ios: --target ios/ios-sim shorthands, iOS SDK auto-discovery,
#framework directive + BuildOptions.add_framework hook,
.app bundle + Info.plist + codesign (ad-hoc and real),
--codesign-identity/--provisioning-profile/--entitlements flags,
modules/std/{objc,uikit}.sx, dynamic class registration,
typed objc_msgSend cast pattern, UIApplicationMain handoff,
UIWindow scene attach. Runs on iPhone hardware.
- ir: silent .s64 defaults → loud diagnostics,
resolveReturnType infers from body, sub-byte int sizes match LLVM,
tuple type interning includes names, compile errors exit 1
- issue-NNNN convention: resolved bugs rename to focused features
- 50 regression tests passing
This commit is contained in:
285
src/target.zig
285
src/target.zig
@@ -23,6 +23,23 @@ pub const TargetConfig = struct {
|
||||
extra_link_flags: []const []const u8 = &.{},
|
||||
/// Custom WASM shell template path (overrides the built-in template).
|
||||
wasm_shell_path: ?[]const u8 = null,
|
||||
/// Path to a `.app` bundle directory to produce (iOS/macOS). When set, the
|
||||
/// linker output is moved into the bundle alongside a generated Info.plist
|
||||
/// and ad-hoc signed for simulator runs.
|
||||
bundle_path: ?[]const u8 = null,
|
||||
/// CFBundleIdentifier for the bundle (e.g. "co.swipelab.sxhello").
|
||||
/// Required when `bundle_path` is set.
|
||||
bundle_id: ?[]const u8 = null,
|
||||
/// Codesigning identity (e.g. `"Apple Development: Alex (TEAMID)"` or a
|
||||
/// SHA-1 fingerprint from `security find-identity -p codesigning`).
|
||||
/// When null, ad-hoc signs with `-` (sufficient for simulator, not device).
|
||||
codesign_identity: ?[]const u8 = null,
|
||||
/// Path to a `.mobileprovision` to embed as `embedded.mobileprovision`.
|
||||
/// Required for real-device builds.
|
||||
provisioning_profile: ?[]const u8 = null,
|
||||
/// 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,
|
||||
|
||||
pub const OptLevel = enum {
|
||||
none,
|
||||
@@ -70,11 +87,27 @@ pub const TargetConfig = struct {
|
||||
return self.tripleHasPrefix("wasm64", "wasm64");
|
||||
}
|
||||
|
||||
/// Check if target triple indicates macOS/Darwin.
|
||||
/// Check if target triple indicates macOS/Darwin (does not match iOS).
|
||||
pub fn isMacOS(self: TargetConfig) bool {
|
||||
if (self.isIOS()) return false;
|
||||
return self.tripleContains("darwin") or self.tripleContains("macos");
|
||||
}
|
||||
|
||||
/// Check if target triple indicates iOS (device or simulator).
|
||||
pub fn isIOS(self: TargetConfig) bool {
|
||||
return self.tripleContains("-apple-ios");
|
||||
}
|
||||
|
||||
/// Check if target triple indicates the iOS Simulator.
|
||||
pub fn isIOSSimulator(self: TargetConfig) bool {
|
||||
return self.isIOS() and self.tripleContains("simulator");
|
||||
}
|
||||
|
||||
/// Check if target triple indicates a real iOS device (not Simulator).
|
||||
pub fn isIOSDevice(self: TargetConfig) bool {
|
||||
return self.isIOS() and !self.tripleContains("simulator");
|
||||
}
|
||||
|
||||
/// Check if target triple indicates Linux.
|
||||
pub fn isLinux(self: TargetConfig) bool {
|
||||
return self.tripleContains("linux");
|
||||
@@ -170,10 +203,70 @@ pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
|
||||
return if (result >= 0 and result <= 255) @intCast(result) else 1;
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void {
|
||||
/// Run `xcrun --sdk <sdk_name> --show-sdk-path` and return the trimmed path.
|
||||
/// Caller owns the returned slice.
|
||||
pub fn discoverAppleSdk(allocator: std.mem.Allocator, io: std.Io, sdk_name: []const u8) ![]const u8 {
|
||||
const r = std.process.run(allocator, io, .{
|
||||
.argv = &.{ "xcrun", "--sdk", sdk_name, "--show-sdk-path" },
|
||||
}) catch |e| {
|
||||
std.debug.print("error: failed to run xcrun: {} \u{2014} install Xcode Command Line Tools (xcode-select --install)\n", .{e});
|
||||
return error.SdkNotFound;
|
||||
};
|
||||
defer allocator.free(r.stderr);
|
||||
errdefer allocator.free(r.stdout);
|
||||
if (r.term != .exited or r.term.exited != 0) {
|
||||
std.debug.print("error: xcrun --sdk {s} --show-sdk-path failed\n", .{sdk_name});
|
||||
allocator.free(r.stdout);
|
||||
return error.SdkNotFound;
|
||||
}
|
||||
const trimmed = std.mem.trimEnd(u8, r.stdout, " \t\r\n");
|
||||
const out = try allocator.dupe(u8, trimmed);
|
||||
allocator.free(r.stdout);
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, frameworks: []const []const u8, target_config: TargetConfig) !void {
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
|
||||
if (target_config.isEmscripten()) {
|
||||
if (target_config.isIOS()) {
|
||||
// iOS: clang driver with -isysroot pointing at the iOS SDK.
|
||||
// -l libraries are generally wrong for iOS (Apple ships system code
|
||||
// as frameworks); user-declared #library still pass through.
|
||||
const linker = target_config.linker orelse "clang";
|
||||
try argv.append(allocator, linker);
|
||||
if (target_config.triple) |t| {
|
||||
try argv.append(allocator, "-target");
|
||||
try argv.append(allocator, std.mem.span(t));
|
||||
}
|
||||
const sdk_path = if (target_config.sysroot) |sr|
|
||||
try allocator.dupe(u8, sr)
|
||||
else blk: {
|
||||
const sdk_name: []const u8 = if (target_config.isIOSSimulator()) "iphonesimulator" else "iphoneos";
|
||||
break :blk try discoverAppleSdk(allocator, io, sdk_name);
|
||||
};
|
||||
try argv.append(allocator, "-isysroot");
|
||||
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);
|
||||
try argv.append(allocator, output_obj);
|
||||
try argv.append(allocator, "-o");
|
||||
try argv.append(allocator, output_bin);
|
||||
for (extra_objects) |eo| try argv.append(allocator, eo);
|
||||
for (target_config.lib_paths) |lp| {
|
||||
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
|
||||
}
|
||||
for (libraries) |lib| {
|
||||
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
|
||||
}
|
||||
for (frameworks) |fw| {
|
||||
try argv.append(allocator, "-framework");
|
||||
try argv.append(allocator, fw);
|
||||
}
|
||||
for (target_config.extra_link_flags) |flag| {
|
||||
var it = std.mem.tokenizeScalar(u8, flag, ' ');
|
||||
while (it.next()) |part| try argv.append(allocator, part);
|
||||
}
|
||||
} else if (target_config.isEmscripten()) {
|
||||
// Emscripten: use emcc as the linker/driver
|
||||
const linker = target_config.linker orelse "emcc";
|
||||
try argv.appendSlice(allocator, &.{ linker, output_obj, "-o", output_bin });
|
||||
@@ -252,6 +345,14 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex
|
||||
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
|
||||
}
|
||||
|
||||
// Frameworks: only meaningful on Apple targets; silently ignored elsewhere.
|
||||
if (target_config.isMacOS()) {
|
||||
for (frameworks) |fw| {
|
||||
try argv.append(allocator, "-framework");
|
||||
try argv.append(allocator, fw);
|
||||
}
|
||||
}
|
||||
|
||||
// Extra linker flags — split space-separated flags into individual argv entries.
|
||||
for (target_config.extra_link_flags) |flag| {
|
||||
var it = std.mem.tokenizeScalar(u8, flag, ' ');
|
||||
@@ -270,6 +371,184 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex
|
||||
if (result.exited != 0) return error.LinkError;
|
||||
}
|
||||
|
||||
/// Move `binary_path` into a freshly-created `<bundle_path>` directory,
|
||||
/// 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 {
|
||||
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", .{});
|
||||
return error.MissingBundleId;
|
||||
};
|
||||
|
||||
// Device builds without a real identity will be rejected by the device,
|
||||
// so fail fast with a clear hint.
|
||||
if (target_config.isIOSDevice() and target_config.codesign_identity == null) {
|
||||
std.debug.print("error: --target ios requires --codesign-identity (e.g. \"Apple Development: ...\") and --provisioning-profile <path>\n", .{});
|
||||
return error.MissingCodesignIdentity;
|
||||
}
|
||||
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
cwd.deleteTree(io, bundle_path) catch {};
|
||||
try cwd.createDirPath(io, bundle_path);
|
||||
|
||||
const exe_name = std.fs.path.basename(binary_path);
|
||||
const exe_dest = try std.fs.path.join(allocator, &.{ bundle_path, exe_name });
|
||||
cwd.rename(binary_path, cwd, exe_dest, io) catch return error.BundleMoveFailed;
|
||||
|
||||
// Info.plist
|
||||
const plist = try buildInfoPlist(allocator, exe_name, bundle_id, target_config);
|
||||
const plist_path = try std.fs.path.join(allocator, &.{ bundle_path, "Info.plist" });
|
||||
try cwd.writeFile(io, .{ .sub_path = plist_path, .data = plist });
|
||||
|
||||
// Embed provisioning profile if supplied. Required for device installs.
|
||||
if (target_config.provisioning_profile) |pp| {
|
||||
const profile_data = std.Io.Dir.readFileAlloc(.cwd(), io, pp, allocator, .limited(1 * 1024 * 1024)) catch {
|
||||
std.debug.print("error: cannot read provisioning profile: {s}\n", .{pp});
|
||||
return error.ProvisioningProfileNotFound;
|
||||
};
|
||||
const embedded_path = try std.fs.path.join(allocator, &.{ bundle_path, "embedded.mobileprovision" });
|
||||
try cwd.writeFile(io, .{ .sub_path = embedded_path, .data = profile_data });
|
||||
}
|
||||
|
||||
// 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: {
|
||||
if (target_config.provisioning_profile) |pp| {
|
||||
break :blk try extractEntitlements(allocator, io, pp, bundle_id);
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
try codesign(allocator, io, bundle_path, identity, ent_path);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// rejects the app with `MIInstallerErrorDomain error 13` /
|
||||
/// `0xe8008015 (A valid provisioning profile ... was not found)`.
|
||||
/// Writes the resolved entitlements to `.sx-tmp/entitlements.plist`.
|
||||
fn extractEntitlements(allocator: std.mem.Allocator, io: std.Io, profile_path: []const u8, bundle_id: []const u8) ![]const u8 {
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
cwd.createDirPath(io, ".sx-tmp") catch {};
|
||||
|
||||
const profile_plist_path = ".sx-tmp/profile.plist";
|
||||
const ent_path = ".sx-tmp/entitlements.plist";
|
||||
|
||||
// 1. security cms -D -i <profile> -o profile.plist (decode CMS to plist)
|
||||
const r1 = std.process.run(allocator, io, .{
|
||||
.argv = &.{ "security", "cms", "-D", "-i", profile_path, "-o", profile_plist_path },
|
||||
}) catch return error.SecurityCommandFailed;
|
||||
defer allocator.free(r1.stdout);
|
||||
defer allocator.free(r1.stderr);
|
||||
if (r1.term != .exited or r1.term.exited != 0) {
|
||||
std.debug.print("error: failed to decode provisioning profile: {s}\n", .{r1.stderr});
|
||||
return error.SecurityCommandFailed;
|
||||
}
|
||||
|
||||
// 2. plutil -extract Entitlements xml1 -o entitlements.plist profile.plist
|
||||
const r2 = std.process.run(allocator, io, .{
|
||||
.argv = &.{ "plutil", "-extract", "Entitlements", "xml1", "-o", ent_path, profile_plist_path },
|
||||
}) catch return error.PlutilCommandFailed;
|
||||
defer allocator.free(r2.stdout);
|
||||
defer allocator.free(r2.stderr);
|
||||
if (r2.term != .exited or r2.term.exited != 0) {
|
||||
std.debug.print("error: failed to extract entitlements: {s}\n", .{r2.stderr});
|
||||
return error.PlutilCommandFailed;
|
||||
}
|
||||
|
||||
// 3. Read the team identifier so we can resolve the wildcard. The profile
|
||||
// stores it as `ApplicationIdentifierPrefix.0` (an array). We use that
|
||||
// path because `com.apple.developer.team-identifier` would confuse
|
||||
// plutil — dots in plutil paths are interpreted as path separators.
|
||||
const r3 = std.process.run(allocator, io, .{
|
||||
.argv = &.{ "plutil", "-extract", "ApplicationIdentifierPrefix.0", "raw", "-o", "-", profile_plist_path },
|
||||
}) catch return error.PlutilCommandFailed;
|
||||
defer allocator.free(r3.stdout);
|
||||
defer allocator.free(r3.stderr);
|
||||
if (r3.term != .exited or r3.term.exited != 0) {
|
||||
std.debug.print("error: profile missing ApplicationIdentifierPrefix: {s}\n", .{r3.stderr});
|
||||
return error.PlutilCommandFailed;
|
||||
}
|
||||
const team = std.mem.trimEnd(u8, r3.stdout, " \t\r\n");
|
||||
const resolved_app_id = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ team, bundle_id });
|
||||
defer allocator.free(resolved_app_id);
|
||||
|
||||
// 4. plutil -replace application-identifier -string "<team>.<bundle_id>" entitlements.plist
|
||||
const r4 = std.process.run(allocator, io, .{
|
||||
.argv = &.{ "plutil", "-replace", "application-identifier", "-string", resolved_app_id, ent_path },
|
||||
}) catch return error.PlutilCommandFailed;
|
||||
defer allocator.free(r4.stdout);
|
||||
defer allocator.free(r4.stderr);
|
||||
if (r4.term != .exited or r4.term.exited != 0) {
|
||||
std.debug.print("error: failed to resolve application-identifier: {s}\n", .{r4.stderr});
|
||||
return error.PlutilCommandFailed;
|
||||
}
|
||||
|
||||
return try allocator.dupe(u8, ent_path);
|
||||
}
|
||||
|
||||
fn buildInfoPlist(allocator: std.mem.Allocator, exe_name: []const u8, bundle_id: []const u8, target_config: TargetConfig) ![]const u8 {
|
||||
const min_os: []const u8 = "14.0";
|
||||
const is_sim = target_config.isIOSSimulator();
|
||||
const platform_key: []const u8 = if (is_sim) "iPhoneSimulator" else "iPhoneOS";
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
\\<plist version="1.0">
|
||||
\\<dict>
|
||||
\\ <key>CFBundleIdentifier</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>CFBundleName</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>CFBundleExecutable</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>CFBundlePackageType</key>
|
||||
\\ <string>APPL</string>
|
||||
\\ <key>CFBundleVersion</key>
|
||||
\\ <string>1</string>
|
||||
\\ <key>CFBundleShortVersionString</key>
|
||||
\\ <string>0.1</string>
|
||||
\\ <key>MinimumOSVersion</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>UIDeviceFamily</key>
|
||||
\\ <array>
|
||||
\\ <integer>1</integer>
|
||||
\\ </array>
|
||||
\\ <key>LSRequiresIPhoneOS</key>
|
||||
\\ <true/>
|
||||
\\ <key>UILaunchScreen</key>
|
||||
\\ <dict/>
|
||||
\\ <key>DTPlatformName</key>
|
||||
\\ <string>{s}</string>
|
||||
\\</dict>
|
||||
\\</plist>
|
||||
\\
|
||||
, .{ bundle_id, exe_name, exe_name, min_os, platform_key });
|
||||
}
|
||||
|
||||
fn codesign(allocator: std.mem.Allocator, io: std.Io, bundle_path: []const u8, identity: []const u8, entitlements: ?[]const u8) !void {
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
defer argv.deinit(allocator);
|
||||
try argv.appendSlice(allocator, &.{ "codesign", "--force", "--sign", identity, "--timestamp=none" });
|
||||
if (entitlements) |ep| {
|
||||
try argv.appendSlice(allocator, &.{ "--entitlements", ep });
|
||||
}
|
||||
try argv.append(allocator, bundle_path);
|
||||
|
||||
const r = std.process.run(allocator, io, .{ .argv = argv.items }) catch |e| {
|
||||
std.debug.print("error: failed to run codesign: {}\n", .{e});
|
||||
return error.CodesignFailed;
|
||||
};
|
||||
defer allocator.free(r.stdout);
|
||||
defer allocator.free(r.stderr);
|
||||
if (r.term != .exited or r.term.exited != 0) {
|
||||
std.debug.print("codesign failed: {s}\n", .{r.stderr});
|
||||
return error.CodesignFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// After emcc produces HTML output, inject cache-busting hashes into the
|
||||
/// generated <script> tag and add Module.locateFile for .wasm/.data files.
|
||||
pub fn postProcessWasmHtml(allocator: std.mem.Allocator, io: std.Io, html_path: []const u8) void {
|
||||
|
||||
Reference in New Issue
Block a user