1const std = @import("std");
2const builtin = @import("builtin");
3const assert = std.debug.assert;
4const mem = std.mem;
5const testing = std.testing;
6const os = std.os;
7
8const Target = std.Target;
9
10/// Detect macOS version.
11/// `target_os` is not modified in case of error.
12pub fn detect(target_os: *Target.Os) !void {
13    // Drop use of osproductversion sysctl because:
14    //   1. only available 10.13.4 High Sierra and later
15    //   2. when used from a binary built against < SDK 11.0 it returns 10.16 and masks Big Sur 11.x version
16    //
17    // NEW APPROACH, STEP 1, parse file:
18    //
19    //   /System/Library/CoreServices/SystemVersion.plist
20    //
21    // NOTE: Historically `SystemVersion.plist` first appeared circa '2003
22    // with the release of Mac OS X 10.3.0 Panther.
23    //
24    // and if it contains a `10.16` value where the `16` is `>= 16` then it is non-canonical,
25    // discarded, and we move on to next step. Otherwise we accept the version.
26    //
27    // BACKGROUND: `10.(16+)` is not a proper version and does not have enough fidelity to
28    // indicate minor/point version of Big Sur and later. It is a context-sensitive result
29    // issued by the kernel for backwards compatibility purposes. Likely the kernel checks
30    // if the executable was linked against an SDK older than Big Sur.
31    //
32    // STEP 2, parse next file:
33    //
34    //   /System/Library/CoreServices/.SystemVersionPlatform.plist
35    //
36    // NOTE: Historically `SystemVersionPlatform.plist` first appeared circa '2020
37    // with the release of macOS 11.0 Big Sur.
38    //
39    // Accessing the content via this path circumvents a context-sensitive result and
40    // yields a canonical Big Sur version.
41    //
42    // At this time there is no other known way for a < SDK 11.0 executable to obtain a
43    // canonical Big Sur version.
44    //
45    // This implementation uses a reasonably simplified approach to parse .plist file
46    // that while it is an xml document, we have good history on the file and its format
47    // such that I am comfortable with implementing a minimalistic parser.
48    // Things like string and general escapes are not supported.
49    const prefixSlash = "/System/Library/CoreServices/";
50    const paths = [_][]const u8{
51        prefixSlash ++ "SystemVersion.plist",
52        prefixSlash ++ ".SystemVersionPlatform.plist",
53    };
54    for (paths) |path| {
55        // approx. 4 times historical file size
56        var buf: [2048]u8 = undefined;
57
58        if (std.fs.cwd().readFile(path, &buf)) |bytes| {
59            if (parseSystemVersion(bytes)) |ver| {
60                // never return non-canonical `10.(16+)`
61                if (!(ver.major == 10 and ver.minor >= 16)) {
62                    target_os.version_range.semver.min = ver;
63                    target_os.version_range.semver.max = ver;
64                    return;
65                }
66                continue;
67            } else |_| {
68                return error.OSVersionDetectionFail;
69            }
70        } else |_| {
71            return error.OSVersionDetectionFail;
72        }
73    }
74    return error.OSVersionDetectionFail;
75}
76
77fn parseSystemVersion(buf: []const u8) !std.builtin.Version {
78    var svt = SystemVersionTokenizer{ .bytes = buf };
79    try svt.skipUntilTag(.start, "dict");
80    while (true) {
81        try svt.skipUntilTag(.start, "key");
82        const content = try svt.expectContent();
83        try svt.skipUntilTag(.end, "key");
84        if (std.mem.eql(u8, content, "ProductVersion")) break;
85    }
86    try svt.skipUntilTag(.start, "string");
87    const ver = try svt.expectContent();
88    try svt.skipUntilTag(.end, "string");
89
90    return std.builtin.Version.parse(ver);
91}
92
93const SystemVersionTokenizer = struct {
94    bytes: []const u8,
95    index: usize = 0,
96    state: State = .begin,
97
98    fn next(self: *@This()) !?Token {
99        var mark: usize = self.index;
100        var tag = Tag{};
101        var content: []const u8 = "";
102
103        while (self.index < self.bytes.len) {
104            const char = self.bytes[self.index];
105            switch (self.state) {
106                .begin => switch (char) {
107                    '<' => {
108                        self.state = .tag0;
109                        self.index += 1;
110                        tag = Tag{};
111                        mark = self.index;
112                    },
113                    '>' => {
114                        return error.BadToken;
115                    },
116                    else => {
117                        self.state = .content;
118                        content = "";
119                        mark = self.index;
120                    },
121                },
122                .tag0 => switch (char) {
123                    '<' => {
124                        return error.BadToken;
125                    },
126                    '>' => {
127                        self.state = .begin;
128                        self.index += 1;
129                        tag.name = self.bytes[mark..self.index];
130                        return Token{ .tag = tag };
131                    },
132                    '"' => {
133                        self.state = .tag_string;
134                        self.index += 1;
135                    },
136                    '/' => {
137                        self.state = .tag0_end_or_empty;
138                        self.index += 1;
139                    },
140                    'A'...'Z', 'a'...'z' => {
141                        self.state = .tagN;
142                        tag.kind = .start;
143                        self.index += 1;
144                    },
145                    else => {
146                        self.state = .tagN;
147                        self.index += 1;
148                    },
149                },
150                .tag0_end_or_empty => switch (char) {
151                    '<' => {
152                        return error.BadToken;
153                    },
154                    '>' => {
155                        self.state = .begin;
156                        tag.kind = .empty;
157                        tag.name = self.bytes[self.index..self.index];
158                        self.index += 1;
159                        return Token{ .tag = tag };
160                    },
161                    else => {
162                        self.state = .tagN;
163                        tag.kind = .end;
164                        mark = self.index;
165                        self.index += 1;
166                    },
167                },
168                .tagN => switch (char) {
169                    '<' => {
170                        return error.BadToken;
171                    },
172                    '>' => {
173                        self.state = .begin;
174                        tag.name = self.bytes[mark..self.index];
175                        self.index += 1;
176                        return Token{ .tag = tag };
177                    },
178                    '"' => {
179                        self.state = .tag_string;
180                        self.index += 1;
181                    },
182                    '/' => {
183                        self.state = .tagN_end;
184                        tag.kind = .end;
185                        self.index += 1;
186                    },
187                    else => {
188                        self.index += 1;
189                    },
190                },
191                .tagN_end => switch (char) {
192                    '>' => {
193                        self.state = .begin;
194                        tag.name = self.bytes[mark..self.index];
195                        self.index += 1;
196                        return Token{ .tag = tag };
197                    },
198                    else => {
199                        return error.BadToken;
200                    },
201                },
202                .tag_string => switch (char) {
203                    '"' => {
204                        self.state = .tagN;
205                        self.index += 1;
206                    },
207                    else => {
208                        self.index += 1;
209                    },
210                },
211                .content => switch (char) {
212                    '<' => {
213                        self.state = .tag0;
214                        content = self.bytes[mark..self.index];
215                        self.index += 1;
216                        tag = Tag{};
217                        mark = self.index;
218                        return Token{ .content = content };
219                    },
220                    '>' => {
221                        return error.BadToken;
222                    },
223                    else => {
224                        self.index += 1;
225                    },
226                },
227            }
228        }
229
230        return null;
231    }
232
233    fn expectContent(self: *@This()) ![]const u8 {
234        if (try self.next()) |tok| {
235            switch (tok) {
236                .content => |content| {
237                    return content;
238                },
239                else => {},
240            }
241        }
242        return error.UnexpectedToken;
243    }
244
245    fn skipUntilTag(self: *@This(), kind: Tag.Kind, name: []const u8) !void {
246        while (try self.next()) |tok| {
247            switch (tok) {
248                .tag => |tag| {
249                    if (tag.kind == kind and std.mem.eql(u8, tag.name, name)) return;
250                },
251                else => {},
252            }
253        }
254        return error.TagNotFound;
255    }
256
257    const State = enum {
258        begin,
259        tag0,
260        tag0_end_or_empty,
261        tagN,
262        tagN_end,
263        tag_string,
264        content,
265    };
266
267    const Token = union(enum) {
268        tag: Tag,
269        content: []const u8,
270    };
271
272    const Tag = struct {
273        kind: Kind = .unknown,
274        name: []const u8 = "",
275
276        const Kind = enum { unknown, start, end, empty };
277    };
278};
279
280test "detect" {
281    const cases = .{
282        .{
283            \\<?xml version="1.0" encoding="UTF-8"?>
284            \\<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
285            \\<plist version="1.0">
286            \\<dict>
287            \\    <key>ProductBuildVersion</key>
288            \\    <string>7B85</string>
289            \\    <key>ProductCopyright</key>
290            \\    <string>Apple Computer, Inc. 1983-2003</string>
291            \\    <key>ProductName</key>
292            \\    <string>Mac OS X</string>
293            \\    <key>ProductUserVisibleVersion</key>
294            \\    <string>10.3</string>
295            \\    <key>ProductVersion</key>
296            \\    <string>10.3</string>
297            \\</dict>
298            \\</plist>
299            ,
300            .{ .major = 10, .minor = 3 },
301        },
302        .{
303            \\<?xml version="1.0" encoding="UTF-8"?>
304            \\<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
305            \\<plist version="1.0">
306            \\<dict>
307            \\	<key>ProductBuildVersion</key>
308            \\	<string>7W98</string>
309            \\	<key>ProductCopyright</key>
310            \\	<string>Apple Computer, Inc. 1983-2004</string>
311            \\	<key>ProductName</key>
312            \\	<string>Mac OS X</string>
313            \\	<key>ProductUserVisibleVersion</key>
314            \\	<string>10.3.9</string>
315            \\	<key>ProductVersion</key>
316            \\	<string>10.3.9</string>
317            \\</dict>
318            \\</plist>
319            ,
320            .{ .major = 10, .minor = 3, .patch = 9 },
321        },
322        .{
323            \\<?xml version="1.0" encoding="UTF-8"?>
324            \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
325            \\<plist version="1.0">
326            \\<dict>
327            \\	<key>ProductBuildVersion</key>
328            \\	<string>19G68</string>
329            \\	<key>ProductCopyright</key>
330            \\	<string>1983-2020 Apple Inc.</string>
331            \\	<key>ProductName</key>
332            \\	<string>Mac OS X</string>
333            \\	<key>ProductUserVisibleVersion</key>
334            \\	<string>10.15.6</string>
335            \\	<key>ProductVersion</key>
336            \\	<string>10.15.6</string>
337            \\	<key>iOSSupportVersion</key>
338            \\	<string>13.6</string>
339            \\</dict>
340            \\</plist>
341            ,
342            .{ .major = 10, .minor = 15, .patch = 6 },
343        },
344        .{
345            \\<?xml version="1.0" encoding="UTF-8"?>
346            \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
347            \\<plist version="1.0">
348            \\<dict>
349            \\	<key>ProductBuildVersion</key>
350            \\	<string>20A2408</string>
351            \\	<key>ProductCopyright</key>
352            \\	<string>1983-2020 Apple Inc.</string>
353            \\	<key>ProductName</key>
354            \\	<string>macOS</string>
355            \\	<key>ProductUserVisibleVersion</key>
356            \\	<string>11.0</string>
357            \\	<key>ProductVersion</key>
358            \\	<string>11.0</string>
359            \\	<key>iOSSupportVersion</key>
360            \\	<string>14.2</string>
361            \\</dict>
362            \\</plist>
363            ,
364            .{ .major = 11, .minor = 0 },
365        },
366        .{
367            \\<?xml version="1.0" encoding="UTF-8"?>
368            \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
369            \\<plist version="1.0">
370            \\<dict>
371            \\	<key>ProductBuildVersion</key>
372            \\	<string>20C63</string>
373            \\	<key>ProductCopyright</key>
374            \\	<string>1983-2020 Apple Inc.</string>
375            \\	<key>ProductName</key>
376            \\	<string>macOS</string>
377            \\	<key>ProductUserVisibleVersion</key>
378            \\	<string>11.1</string>
379            \\	<key>ProductVersion</key>
380            \\	<string>11.1</string>
381            \\	<key>iOSSupportVersion</key>
382            \\	<string>14.3</string>
383            \\</dict>
384            \\</plist>
385            ,
386            .{ .major = 11, .minor = 1 },
387        },
388    };
389
390    inline for (cases) |case| {
391        const ver0 = try parseSystemVersion(case[0]);
392        const ver1: std.builtin.Version = case[1];
393        try testVersionEquality(ver1, ver0);
394    }
395}
396
397fn testVersionEquality(expected: std.builtin.Version, got: std.builtin.Version) !void {
398    var b_expected: [64]u8 = undefined;
399    const s_expected: []const u8 = try std.fmt.bufPrint(b_expected[0..], "{}", .{expected});
400
401    var b_got: [64]u8 = undefined;
402    const s_got: []const u8 = try std.fmt.bufPrint(b_got[0..], "{}", .{got});
403
404    try testing.expectEqualStrings(s_expected, s_got);
405}
406
407pub fn detectNativeCpuAndFeatures() ?Target.Cpu {
408    var cpu_family: std.c.CPUFAMILY = undefined;
409    var len: usize = @sizeOf(std.c.CPUFAMILY);
410    os.sysctlbynameZ("hw.cpufamily", &cpu_family, &len, null, 0) catch |err| switch (err) {
411        error.NameTooLong => unreachable, // constant, known good value
412        error.PermissionDenied => unreachable, // only when setting values,
413        error.SystemResources => unreachable, // memory already on the stack
414        error.UnknownName => unreachable, // constant, known good value
415        error.Unexpected => unreachable, // EFAULT: stack should be safe, EISDIR/ENOTDIR: constant, known good value
416    };
417
418    const current_arch = builtin.cpu.arch;
419    switch (current_arch) {
420        .aarch64, .aarch64_be, .aarch64_32 => {
421            const model = switch (cpu_family) {
422                .ARM_FIRESTORM_ICESTORM => &Target.aarch64.cpu.apple_a14,
423                .ARM_LIGHTNING_THUNDER => &Target.aarch64.cpu.apple_a13,
424                .ARM_VORTEX_TEMPEST => &Target.aarch64.cpu.apple_a12,
425                .ARM_MONSOON_MISTRAL => &Target.aarch64.cpu.apple_a11,
426                .ARM_HURRICANE => &Target.aarch64.cpu.apple_a10,
427                .ARM_TWISTER => &Target.aarch64.cpu.apple_a9,
428                .ARM_TYPHOON => &Target.aarch64.cpu.apple_a8,
429                .ARM_CYCLONE => &Target.aarch64.cpu.cyclone,
430                else => return null,
431            };
432
433            return Target.Cpu{
434                .arch = current_arch,
435                .model = model,
436                .features = model.features,
437            };
438        },
439        else => {},
440    }
441
442    return null;
443}
444