1const std = @import("std");
2const Io = std.Io;
3const Dir = std.Io.Dir;
4const mem = std.mem;
5const process = std.process;
6const assert = std.debug.assert;
7const fatal = std.process.fatal;
8const info = std.log.info;
9const Allocator = std.mem.Allocator;
10const OsTag = std.Target.Os.Tag;
11
12const Arch = enum {
13 aarch64,
14 x86_64,
15};
16
17const Abi = enum { none };
18
19const OsVer = enum(u32) {
20 catalina = 10,
21 big_sur = 11,
22 monterey = 12,
23 ventura = 13,
24 sonoma = 14,
25 sequoia = 15,
26 tahoe = 26,
27};
28
29const Target = struct {
30 arch: Arch,
31 os: OsTag = .macos,
32 os_ver: OsVer,
33 abi: Abi = .none,
34
35 fn name(self: Target, allocator: Allocator) ![]const u8 {
36 return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{
37 @tagName(self.arch),
38 @tagName(self.os),
39 @tagName(self.abi),
40 });
41 }
42
43 fn fullName(self: Target, allocator: Allocator) ![]const u8 {
44 return std.fmt.allocPrint(allocator, "{s}-{s}.{d}-{s}", .{
45 @tagName(self.arch),
46 @tagName(self.os),
47 @backingInt(self.os_ver),
48 @tagName(self.abi),
49 });
50 }
51};
52
53const headers_source_prefix: []const u8 = "headers";
54
55const usage =
56 \\fetch_them_macos_headers [options] [cc args]
57 \\
58 \\Options:
59 \\ --sysroot Path to macOS SDK
60 \\
61 \\General Options:
62 \\-h, --help Print this help and exit
63;
64
65pub fn main(init: std.process.Init) !void {
66 const io = init.io;
67 const arena = init.arena.allocator();
68 const args = try init.minimal.args.toSlice(arena);
69
70 var argv = std.array_list.Managed([]const u8).init(arena);
71 var sysroot: ?[]const u8 = null;
72
73 var args_iter = ArgsIterator{ .args = args[1..] };
74 while (args_iter.next()) |arg| {
75 if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) {
76 return info(usage, .{});
77 } else if (mem.eql(u8, arg, "--sysroot")) {
78 sysroot = args_iter.nextOrFatal();
79 } else try argv.append(arg);
80 }
81
82 const sysroot_path = sysroot orelse blk: {
83 const target = try std.zig.system.resolveTargetQuery(io, .{});
84 break :blk std.zig.system.darwin.getSdk(arena, io, &target) orelse
85 fatal("no SDK found; you can provide one explicitly with '--sysroot' flag", .{});
86 };
87
88 var sdk_dir = try Dir.cwd().openDir(io, sysroot_path, .{});
89 defer sdk_dir.close(io);
90 const sdk_info = try sdk_dir.readFileAlloc(io, "SDKSettings.json", arena, .limited(std.math.maxInt(u32)));
91
92 const parsed_json = try std.json.parseFromSlice(struct {
93 DefaultProperties: struct { MACOSX_DEPLOYMENT_TARGET: []const u8 },
94 }, arena, sdk_info, .{ .ignore_unknown_fields = true });
95
96 const version = Version.parse(parsed_json.value.DefaultProperties.MACOSX_DEPLOYMENT_TARGET) orelse
97 fatal("don't know how to parse SDK version: {s}", .{
98 parsed_json.value.DefaultProperties.MACOSX_DEPLOYMENT_TARGET,
99 });
100 const os_ver: OsVer = @fromBackingInt(@intCast(version.major));
101 info("found SDK deployment target macOS {f} aka '{t}'", .{ version, os_ver });
102
103 const tmp_dir: Io.Dir = .cwd();
104
105 for (&[_]Arch{ .aarch64, .x86_64 }) |arch| {
106 const target: Target = .{
107 .arch = arch,
108 .os_ver = os_ver,
109 };
110 try fetchTarget(arena, io, argv.items, sysroot_path, target, version, tmp_dir);
111 }
112}
113
114fn fetchTarget(
115 arena: Allocator,
116 io: Io,
117 args: []const []const u8,
118 sysroot: []const u8,
119 target: Target,
120 ver: Version,
121 tmp_dir: Io.Dir,
122) !void {
123 const tmp_filename = "macos-headers";
124 const headers_list_filename = "macos-headers.o.d";
125 const tmp_path = try tmp_dir.realPathFileAlloc(io, ".", arena);
126 const tmp_file_path = try Dir.path.join(arena, &[_][]const u8{ tmp_path, tmp_filename });
127 const headers_list_path = try Dir.path.join(arena, &[_][]const u8{ tmp_path, headers_list_filename });
128
129 const macos_version = try std.fmt.allocPrint(arena, "-mmacosx-version-min={d}.{d}", .{
130 ver.major,
131 ver.minor,
132 });
133
134 var cc_argv = std.array_list.Managed([]const u8).init(arena);
135 try cc_argv.appendSlice(&[_][]const u8{
136 "cc",
137 "-arch",
138 switch (target.arch) {
139 .x86_64 => "x86_64",
140 .aarch64 => "arm64",
141 },
142 macos_version,
143 "-isysroot",
144 sysroot,
145 "-iwithsysroot",
146 "/usr/include",
147 "-o",
148 tmp_file_path,
149 "macos-headers.c",
150 "-MD",
151 "-MV",
152 "-MF",
153 headers_list_path,
154 });
155 try cc_argv.appendSlice(args);
156
157 const res = try std.process.run(arena, io, .{ .argv = cc_argv.items });
158
159 if (res.stderr.len != 0) {
160 std.log.err("{s}", .{res.stderr});
161 }
162
163 // Read in the contents of `macos-headers.o.d`
164 const headers_list_file = try tmp_dir.openFile(io, headers_list_filename, .{});
165 defer headers_list_file.close(io);
166
167 var headers_dir = Dir.cwd().openDir(io, headers_source_prefix, .{}) catch |err| switch (err) {
168 error.FileNotFound,
169 error.NotDir,
170 => fatal("path '{s}' not found or not a directory. Did you accidentally delete it?", .{
171 headers_source_prefix,
172 }),
173 else => return err,
174 };
175 defer headers_dir.close(io);
176
177 const dest_path = try target.fullName(arena);
178 try headers_dir.deleteTree(io, dest_path);
179
180 var dest_dir = try headers_dir.createDirPathOpen(io, dest_path, .{});
181 var dirs = std.StringHashMap(Dir).init(arena);
182 try dirs.putNoClobber(".", dest_dir);
183
184 var headers_list_file_reader = headers_list_file.reader(io, &.{});
185 const headers_list_str = try headers_list_file_reader.interface.allocRemaining(arena, .unlimited);
186 const prefix = "/usr/include";
187
188 var it = mem.splitScalar(u8, headers_list_str, '\n');
189 while (it.next()) |line| {
190 if (mem.findLast(u8, line, "clang") != null) continue;
191 if (mem.findLast(u8, line, prefix[0..])) |idx| {
192 const out_rel_path = line[idx + prefix.len + 1 ..];
193 const out_rel_path_stripped = mem.trim(u8, out_rel_path, " \\");
194 const dirname = Dir.path.dirname(out_rel_path_stripped) orelse ".";
195 const maybe_dir = try dirs.getOrPut(dirname);
196 if (!maybe_dir.found_existing) {
197 maybe_dir.value_ptr.* = try dest_dir.createDirPathOpen(io, dirname, .{});
198 }
199 const basename = Dir.path.basename(out_rel_path_stripped);
200
201 const line_stripped = mem.trim(u8, line, " \\");
202 const abs_dirname = Dir.path.dirname(line_stripped).?;
203 var orig_subdir = try Dir.cwd().openDir(io, abs_dirname, .{});
204 defer orig_subdir.close(io);
205
206 try orig_subdir.copyFile(basename, maybe_dir.value_ptr.*, basename, io, .{});
207 }
208 }
209
210 var dir_it = dirs.iterator();
211 while (dir_it.next()) |entry| {
212 entry.value_ptr.close(io);
213 }
214}
215
216const ArgsIterator = struct {
217 args: []const []const u8,
218 i: usize = 0,
219
220 fn next(it: *@This()) ?[]const u8 {
221 if (it.i >= it.args.len) {
222 return null;
223 }
224 defer it.i += 1;
225 return it.args[it.i];
226 }
227
228 fn nextOrFatal(it: *@This()) []const u8 {
229 const arg = it.next() orelse fatal("expected parameter after '{s}'", .{it.args[it.i - 1]});
230 return arg;
231 }
232};
233
234const Version = struct {
235 major: u16,
236 minor: u8,
237 patch: u8,
238
239 fn parse(raw: []const u8) ?Version {
240 var parsed: [3]u16 = @splat(0);
241 var count: usize = 0;
242 var it = std.mem.splitAny(u8, raw, ".");
243 while (it.next()) |comp| {
244 if (count >= 3) return null;
245 parsed[count] = std.fmt.parseInt(u16, comp, 10) catch return null;
246 count += 1;
247 }
248 if (count == 0) return null;
249 const major = parsed[0];
250 const minor = std.math.cast(u8, parsed[1]) orelse return null;
251 const patch = std.math.cast(u8, parsed[2]) orelse return null;
252 return .{ .major = major, .minor = minor, .patch = patch };
253 }
254
255 pub fn format(
256 v: Version,
257 writer: *Io.Writer,
258 ) Io.Writer.Error!void {
259 try writer.print("{d}.{d}.{d}", .{ v.major, v.minor, v.patch });
260 }
261};