const std = @import("std"); const fs = std.fs; const mem = std.mem; const process = std.process; const assert = std.debug.assert; const tmpDir = std.testing.tmpDir; const fatal = std.process.fatal; const info = std.log.info; const Allocator = mem.Allocator; const OsTag = std.Target.Os.Tag; var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; const gpa = general_purpose_allocator.allocator(); const Arch = enum { aarch64, x86_64, }; const Abi = enum { none }; const OsVer = enum(u32) { catalina = 10, big_sur = 11, monterey = 12, ventura = 13, sonoma = 14, sequoia = 15, tahoe = 26, }; const Target = struct { arch: Arch, os: OsTag = .macos, os_ver: OsVer, abi: Abi = .none, fn name(self: Target, allocator: Allocator) ![]const u8 { return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ @tagName(self.arch), @tagName(self.os), @tagName(self.abi), }); } fn fullName(self: Target, allocator: Allocator) ![]const u8 { return std.fmt.allocPrint(allocator, "{s}-{s}.{d}-{s}", .{ @tagName(self.arch), @tagName(self.os), @intFromEnum(self.os_ver), @tagName(self.abi), }); } }; const headers_source_prefix: []const u8 = "headers"; const usage = \\fetch_them_macos_headers [options] [cc args] \\ \\Options: \\ --sysroot Path to macOS SDK \\ \\General Options: \\-h, --help Print this help and exit ; pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const allocator = arena.allocator(); const args = try std.process.argsAlloc(allocator); var argv = std.array_list.Managed([]const u8).init(allocator); var sysroot: ?[]const u8 = null; var args_iter = ArgsIterator{ .args = args[1..] }; while (args_iter.next()) |arg| { if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { return info(usage, .{}); } else if (mem.eql(u8, arg, "--sysroot")) { sysroot = args_iter.nextOrFatal(); } else try argv.append(arg); } var threaded: std.Io.Threaded = .init(gpa); defer threaded.deinit(); const io = threaded.io(); const sysroot_path = sysroot orelse blk: { const target = try std.zig.system.resolveTargetQuery(io, .{}); break :blk std.zig.system.darwin.getSdk(allocator, &target) orelse fatal("no SDK found; you can provide one explicitly with '--sysroot' flag", .{}); }; var sdk_dir = try std.fs.cwd().openDir(sysroot_path, .{}); defer sdk_dir.close(); const sdk_info = try sdk_dir.readFileAlloc("SDKSettings.json", allocator, .limited(std.math.maxInt(u32))); const parsed_json = try std.json.parseFromSlice(struct { DefaultProperties: struct { MACOSX_DEPLOYMENT_TARGET: []const u8 }, }, allocator, sdk_info, .{ .ignore_unknown_fields = true }); const version = Version.parse(parsed_json.value.DefaultProperties.MACOSX_DEPLOYMENT_TARGET) orelse fatal("don't know how to parse SDK version: {s}", .{ parsed_json.value.DefaultProperties.MACOSX_DEPLOYMENT_TARGET, }); const os_ver: OsVer = @enumFromInt(version.major); info("found SDK deployment target macOS {f} aka '{t}'", .{ version, os_ver }); var tmp = tmpDir(.{}); defer tmp.cleanup(); for (&[_]Arch{ .aarch64, .x86_64 }) |arch| { const target: Target = .{ .arch = arch, .os_ver = os_ver, }; try fetchTarget(allocator, argv.items, sysroot_path, target, version, tmp); } } fn fetchTarget( arena: Allocator, args: []const []const u8, sysroot: []const u8, target: Target, ver: Version, tmp: std.testing.TmpDir, ) !void { const tmp_filename = "macos-headers"; const headers_list_filename = "macos-headers.o.d"; const tmp_path = try tmp.dir.realpathAlloc(arena, "."); const tmp_file_path = try fs.path.join(arena, &[_][]const u8{ tmp_path, tmp_filename }); const headers_list_path = try fs.path.join(arena, &[_][]const u8{ tmp_path, headers_list_filename }); const macos_version = try std.fmt.allocPrint(arena, "-mmacosx-version-min={d}.{d}", .{ ver.major, ver.minor, }); var cc_argv = std.array_list.Managed([]const u8).init(arena); try cc_argv.appendSlice(&[_][]const u8{ "cc", "-arch", switch (target.arch) { .x86_64 => "x86_64", .aarch64 => "arm64", }, macos_version, "-isysroot", sysroot, "-iwithsysroot", "/usr/include", "-o", tmp_file_path, "macos-headers.c", "-MD", "-MV", "-MF", headers_list_path, }); try cc_argv.appendSlice(args); const res = try std.process.Child.run(.{ .allocator = arena, .argv = cc_argv.items, }); if (res.stderr.len != 0) { std.log.err("{s}", .{res.stderr}); } // Read in the contents of `macos-headers.o.d` const headers_list_file = try tmp.dir.openFile(headers_list_filename, .{}); defer headers_list_file.close(); var headers_dir = fs.cwd().openDir(headers_source_prefix, .{}) catch |err| switch (err) { error.FileNotFound, error.NotDir, => fatal("path '{s}' not found or not a directory. Did you accidentally delete it?", .{ headers_source_prefix, }), else => return err, }; defer headers_dir.close(); const dest_path = try target.fullName(arena); try headers_dir.deleteTree(dest_path); var dest_dir = try headers_dir.makeOpenPath(dest_path, .{}); var dirs = std.StringHashMap(fs.Dir).init(arena); try dirs.putNoClobber(".", dest_dir); var headers_list_file_reader = headers_list_file.reader(&.{}); const headers_list_str = try headers_list_file_reader.interface.allocRemaining(arena, .unlimited); const prefix = "/usr/include"; var it = mem.splitScalar(u8, headers_list_str, '\n'); while (it.next()) |line| { if (mem.lastIndexOf(u8, line, "clang") != null) continue; if (mem.lastIndexOf(u8, line, prefix[0..])) |idx| { const out_rel_path = line[idx + prefix.len + 1 ..]; const out_rel_path_stripped = mem.trim(u8, out_rel_path, " \\"); const dirname = fs.path.dirname(out_rel_path_stripped) orelse "."; const maybe_dir = try dirs.getOrPut(dirname); if (!maybe_dir.found_existing) { maybe_dir.value_ptr.* = try dest_dir.makeOpenPath(dirname, .{}); } const basename = fs.path.basename(out_rel_path_stripped); const line_stripped = mem.trim(u8, line, " \\"); const abs_dirname = fs.path.dirname(line_stripped).?; var orig_subdir = try fs.cwd().openDir(abs_dirname, .{}); defer orig_subdir.close(); try orig_subdir.copyFile(basename, maybe_dir.value_ptr.*, basename, .{}); } } var dir_it = dirs.iterator(); while (dir_it.next()) |entry| { entry.value_ptr.close(); } } const ArgsIterator = struct { args: []const []const u8, i: usize = 0, fn next(it: *@This()) ?[]const u8 { if (it.i >= it.args.len) { return null; } defer it.i += 1; return it.args[it.i]; } fn nextOrFatal(it: *@This()) []const u8 { const arg = it.next() orelse fatal("expected parameter after '{s}'", .{it.args[it.i - 1]}); return arg; } }; const Version = struct { major: u16, minor: u8, patch: u8, fn parse(raw: []const u8) ?Version { var parsed: [3]u16 = [_]u16{0} ** 3; var count: usize = 0; var it = std.mem.splitAny(u8, raw, "."); while (it.next()) |comp| { if (count >= 3) return null; parsed[count] = std.fmt.parseInt(u16, comp, 10) catch return null; count += 1; } if (count == 0) return null; const major = parsed[0]; const minor = std.math.cast(u8, parsed[1]) orelse return null; const patch = std.math.cast(u8, parsed[2]) orelse return null; return .{ .major = major, .minor = minor, .patch = patch }; } pub fn format( v: Version, writer: *std.Io.Writer, ) std.Io.Writer.Error!void { try writer.print("{d}.{d}.{d}", .{ v.major, v.minor, v.patch }); } };