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