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};