authorgravatar for michael.dusan@gmail.comMichael Dusan <michael.dusan@gmail.com> 2021-01-11 19:52:21-05:00
committergravatar for michael.dusan@gmail.comMichael Dusan <michael.dusan@gmail.com> 2021-01-11 19:54:56-05:00
logf2be1fb23edc272c0a310a9ceb9cdff45e011d98
tree0278f7d9f05c2d87ba1b2d10ba6e2a1309931d56
parent56c03881ebd8617e6506bfad01bf9cfdd4d3df7e
signaturelock-open Commit is signed but in an unrecognized format.

macos: reimplement OS version detection

The macOS version is now obtained by parsing `SystemVersion.plist`. Test cases added for plist files that date back to '2005 Panther and up to the recent '2020 Big Sur 11.1 release of macOS. Thus we are now able to reliably identify 10.3...11.1 and higher. - drop use of kern.osproductversion sysctl - drop use of kern.osversion sysctl (fallback) - drop kern.osversion tests - add `lib.std.zig.system.detect()` - add minimalistic parser for `SystemVersion.plist` - add test cases for { 10.3, 10.3.9, 10.15.6, 11.0, 11.1 } closes #7569

3 files changed, 379 insertions(+), 455 deletions(-)

lib/std/target.zig+1-1
......@@ -260,7 +260,7 @@ pub const Target = struct {
260260 .macos => return .{
261261 .semver = .{
262262 .min = .{ .major = 10, .minor = 13 },
263 .max = .{ .major = 10, .minor = 15, .patch = 7 },
263 .max = .{ .major = 11, .minor = 1 },
264264 },
265265 },
266266 .ios => return .{
lib/std/zig/system.zig+1-32
......@@ -254,38 +254,7 @@ pub const NativeTargetInfo = struct {
254254 os.version_range.windows.min = detected_version;
255255 os.version_range.windows.max = detected_version;
256256 },
257 .macos => {
258 var scbuf: [32]u8 = undefined;
259 var size: usize = undefined;
260
261 // The osproductversion sysctl was introduced first with 10.13.4 High Sierra.
262 const key_osproductversion = "kern.osproductversion"; // eg. "10.15.4"
263 size = scbuf.len;
264 if (std.os.sysctlbynameZ(key_osproductversion, &scbuf, &size, null, 0)) |_| {
265 const string_version = scbuf[0 .. size - 1];
266 if (std.builtin.Version.parse(string_version)) |ver| {
267 os.version_range.semver.min = ver;
268 os.version_range.semver.max = ver;
269 } else |err| switch (err) {
270 error.Overflow => {},
271 error.InvalidCharacter => {},
272 error.InvalidVersion => {},
273 }
274 } else |err| switch (err) {
275 error.UnknownName => {
276 const key_osversion = "kern.osversion"; // eg. "19E287"
277 size = scbuf.len;
278 std.os.sysctlbynameZ(key_osversion, &scbuf, &size, null, 0) catch {
279 @panic("unable to detect macOS version: " ++ key_osversion);
280 };
281 if (macos.version_from_build(scbuf[0 .. size - 1])) |ver| {
282 os.version_range.semver.min = ver;
283 os.version_range.semver.max = ver;
284 } else |_| {}
285 },
286 else => @panic("unable to detect macOS version: " ++ key_osproductversion),
287 }
288 },
257 .macos => macos.detect(&os) catch {}, // valid to ignore any error and keep os defaults
289258 .freebsd => {
290259 var osreldate: u32 = undefined;
291260 var len: usize = undefined;
lib/std/zig/system/macos.zig+377-422
......@@ -6,453 +6,408 @@
66const std = @import("std");
77const assert = std.debug.assert;
88const mem = std.mem;
9const testing = std.testing;
910
10pub fn version_from_build(build: []const u8) !std.builtin.Version {
11 // build format:
12 // 19E287 (example)
13 // xxyzzz
11/// Detect macOS version.
12/// On error `os` will not be modified.
13pub fn detect(os: *std.Target.Os) !void {
14 // Drop use of osproductversion sysctl because:
15 // 1. only available 10.13.4 High Sierra and later
16 // 2. when used from a binary built against < SDK 11.0 it returns 10.16 and masks Big Sur 11.x version
1417 //
15 // major = 10
16 // minor = x - 4 = 19 - 4 = 15
17 // patch = ascii(y) - 'A' = 'E' - 'A' = 69 - 65 = 4
18 // answer: 10.15.4
18 // NEW APPROACH, STEP 1, parse file:
1919 //
20 // note: patch is typical but with some older releases (before 10.8) zzz is considered
20 // /System/Library/CoreServices/SystemVersion.plist
21 //
22 // NOTE: Historically `SystemVersion.plist` first appeared circa '2003
23 // with the release of Mac OS X 10.3.0 Panther.
24 //
25 // and if it contains a `10.16` value where the `16` is `>= 16` then it is a red herring
26 // and is discarded, and move on to next step. Otherwise we accept this is the
27 // canonical file for versioning.
28 //
29 // BACKGROUND: `10.(16+)` is not a proper version and does not have enough fidelity to
30 // indicate minor/point version of Big Sur and later. It is a context-sensitive result
31 // issued by the kernel for backwards compatibility purposes. Likely the kernel checks
32 // if the executable was linked against an SDK older than Big Sur.
33 //
34 // STEP 2, parse next file:
35 //
36 // /System/Library/CoreServices/.SystemVersionPlatform.plist
37 //
38 // NOTE: Historically `SystemVersionPlatform.plist` first appeared circa '2020
39 // with the release of macOS 11.0 Big Sur.
40 //
41 // Accessing the content via this path circumvents a context-sensitive result and
42 // yields a canonical Big Sur version.
43 //
44 // At this time there is no other known way for a < SDK 11.0 executable to obtain a
45 // canonical Big Sur version.
46 //
47 // This implementation uses a reasonably simplified approach to parse .plist file
48 // that while it is an xml document, we have good history on the file and its format
49 // such that I am comfortable with implementing a minimalistic parser.
50 // Things like string and general escapes are not supported.
51 const prefixSlash = "/System/Library/CoreServices/";
52 const format_failure = "macOS detect: failed to {s} '{s}': {}";
2153
22 // never return anything below 10.0.0
23 var result = std.builtin.Version{ .major = 10, .minor = 0, .patch = 0 };
54 const paths = [_][]const u8{
55 prefixSlash ++ "SystemVersion.plist",
56 prefixSlash ++ ".SystemVersionPlatform.plist",
57 };
58 for (paths) |path| {
59 // approx. 4 times historical file size
60 var buf: [2048]u8 = undefined;
2461
25 // parse format-x
26 var yindex: usize = 0;
27 {
28 while (yindex < build.len) : (yindex += 1) {
29 if (build[yindex] < '0' or build[yindex] > '9') break;
62 if (std.fs.cwd().readFile(path, &buf)) |bytes| {
63 if (parseSystemVersion(bytes)) |ver| {
64 // never return red herring
65 if (!(ver.major == 10 and ver.minor >= 16)) {
66 os.version_range.semver.min = ver;
67 os.version_range.semver.max = ver;
68 return;
69 }
70 continue;
71 } else |err| {
72 std.log.err(format_failure, .{ "parse", path, err });
73 return error.OSVersionDetectionFail;
74 }
75 } else |err| {
76 std.log.err(format_failure, .{ "read", path, err });
77 return error.OSVersionDetectionFail;
3078 }
31 if (yindex == 0) return result;
32 const x = std.fmt.parseUnsigned(u16, build[0..yindex], 10) catch return error.InvalidVersion;
33 if (x < 4) return result;
34 result.minor = x - 4;
3579 }
80 return error.OSVersionDetectionFail;
81}
3682
37 // parse format-y, format-z
38 {
39 // expect two more
40 if (build.len < yindex + 2) return error.InvalidVersion;
41 const y = build[yindex];
42 if (y < 'A') return error.InvalidVersion;
43 var zend = yindex + 1;
44 while (zend < build.len) {
45 if (build[zend] < '0' or build[zend] > '9') break;
46 zend += 1;
47 }
48 if (zend == yindex + 1) return error.InvalidVersion;
49 const z = std.fmt.parseUnsigned(u16, build[yindex + 1 .. zend], 10) catch return error.InvalidVersion;
50
51 result.patch = switch (result.minor) {
52 // TODO: compiler complains without explicit @as() coercion
53 0 => @as(u32, switch (y) { // Cheetah: 10.0
54 'K' => 0,
55 'L' => 1,
56 'P' => @as(u32, block: {
57 if (z < 13) break :block 2;
58 break :block 3;
59 }),
60 'Q' => 4,
61 else => return error.InvalidVersion,
62 }),
63 1 => @as(u32, switch (y) { // Puma: 10.1
64 'G' => 0,
65 'M' => 1,
66 'P' => 2,
67 'Q' => @as(u32, block: {
68 if (z < 125) break :block 3;
69 break :block 4;
70 }),
71 'S' => 5,
72 else => return error.InvalidVersion,
73 }),
74 2 => @as(u32, switch (y) { // Jaguar: 10.2
75 'C' => 0,
76 'D' => 1,
77 'F' => 2,
78 'G' => 3,
79 'I' => 4,
80 'L' => @as(u32, block: {
81 if (z < 60) break :block 5;
82 break :block 6;
83 }),
84 'R' => @as(u32, block: {
85 if (z < 73) break :block 7;
86 break :block 8;
87 }),
88 'S' => 8,
89 else => return error.InvalidVersion,
90 }),
91 3 => @as(u32, switch (y) { // Panther: 10.3
92 'B' => 0,
93 'C' => 1,
94 'D' => 2,
95 'F' => 3,
96 'H' => 4,
97 'M' => 5,
98 'R' => 6,
99 'S' => 7,
100 'U' => 8,
101 'W' => 9,
102 else => return error.InvalidVersion,
103 }),
104 4 => @as(u32, switch (y) { // Tiger: 10.4
105 'A' => 0,
106 'B' => 1,
107 'C',
108 'E',
109 => 2,
110 'F' => 3,
111 'G' => @as(u32, block: {
112 if (z >= 1454) break :block 5;
113 break :block 4;
114 }),
115 'H' => 5,
116 'I' => 6,
117 'J',
118 'K',
119 'N',
120 => 7,
121 'L' => 8,
122 'P' => 9,
123 'R' => 10,
124 'S' => 11,
125 else => return error.InvalidVersion,
126 }),
127 5 => @as(u32, switch (y) { // Leopard: 10.5
128 'A' => 0,
129 'B' => 1,
130 'C' => 2,
131 'D' => 3,
132 'E' => 4,
133 'F' => 5,
134 'G' => 6,
135 'J' => 7,
136 'L' => 8,
137 else => return error.InvalidVersion,
138 }),
139 6 => @as(u32, switch (y) { // Snow Leopard: 10.6
140 'A' => 0,
141 'B' => 1,
142 'C' => 2,
143 'D' => 3,
144 'F' => 4,
145 'H' => 5,
146 'J' => @as(u32, block: {
147 if (z < 869) break :block 6;
148 break :block 7;
149 }),
150 'K' => 8,
151 else => return error.InvalidVersion,
152 }),
153 7 => @as(u32, switch (y) { // Snow Leopard: 10.6
154 'A' => 0,
155 'B' => 1,
156 'C' => 2,
157 'D' => 3,
158 'E' => 4,
159 'G' => 5,
160 else => return error.InvalidVersion,
161 }),
162 else => y - 'A',
163 };
83fn parseSystemVersion(buf: []const u8) !std.builtin.Version {
84 var svt = SystemVersionTokenizer{ .bytes = buf };
85 try svt.skipUntilTag(.start, "dict");
86 while (true) {
87 try svt.skipUntilTag(.start, "key");
88 const content = try svt.expectContent();
89 try svt.skipUntilTag(.end, "key");
90 if (std.mem.eql(u8, content, "ProductVersion")) break;
16491 }
165 return result;
166}
92 try svt.skipUntilTag(.start, "string");
93 const ver = try svt.expectContent();
94 try svt.skipUntilTag(.end, "string");
16795
168test "version_from_build" {
169 // see https://en.wikipedia.org/wiki/MacOS_version_history#Releases
170 const known = [_][2][]const u8{
171 .{ "4K78", "10.0.0" },
172 .{ "4L13", "10.0.1" },
173 .{ "4P12", "10.0.2" },
174 .{ "4P13", "10.0.3" },
175 .{ "4Q12", "10.0.4" },
96 return std.builtin.Version.parse(ver);
97}
17698
177 .{ "5G64", "10.1.0" },
178 .{ "5M28", "10.1.1" },
179 .{ "5P48", "10.1.2" },
180 .{ "5Q45", "10.1.3" },
181 .{ "5Q125", "10.1.4" },
182 .{ "5S60", "10.1.5" },
99const SystemVersionTokenizer = struct {
100 bytes: []const u8,
101 index: usize = 0,
102 state: State = .begin,
183103
184 .{ "6C115", "10.2.0" },
185 .{ "6C115a", "10.2.0" },
186 .{ "6D52", "10.2.1" },
187 .{ "6F21", "10.2.2" },
188 .{ "6G30", "10.2.3" },
189 .{ "6G37", "10.2.3" },
190 .{ "6G50", "10.2.3" },
191 .{ "6I32", "10.2.4" },
192 .{ "6L29", "10.2.5" },
193 .{ "6L60", "10.2.6" },
194 .{ "6R65", "10.2.7" },
195 .{ "6R73", "10.2.8" },
196 .{ "6S90", "10.2.8" },
104 fn next(self: *@This()) !?Token {
105 var mark: usize = self.index;
106 var tag = Tag{};
107 var content: []const u8 = "";
197108
198 .{ "7B85", "10.3.0" },
199 .{ "7B86", "10.3.0" },
200 .{ "7C107", "10.3.1" },
201 .{ "7D24", "10.3.2" },
202 .{ "7D28", "10.3.2" },
203 .{ "7F44", "10.3.3" },
204 .{ "7H63", "10.3.4" },
205 .{ "7M34", "10.3.5" },
206 .{ "7R28", "10.3.6" },
207 .{ "7S215", "10.3.7" },
208 .{ "7U16", "10.3.8" },
209 .{ "7W98", "10.3.9" },
109 while (self.index < self.bytes.len) {
110 const char = self.bytes[self.index];
111 switch (self.state) {
112 .begin => switch (char) {
113 '<' => {
114 self.state = .tag0;
115 self.index += 1;
116 tag = Tag{};
117 mark = self.index;
118 },
119 '>' => {
120 return error.BadToken;
121 },
122 else => {
123 self.state = .content;
124 content = "";
125 mark = self.index;
126 },
127 },
128 .tag0 => switch (char) {
129 '<' => {
130 return error.BadToken;
131 },
132 '>' => {
133 self.state = .begin;
134 self.index += 1;
135 tag.name = self.bytes[mark..self.index];
136 return Token{ .tag = tag };
137 },
138 '"' => {
139 self.state = .tag_string;
140 self.index += 1;
141 },
142 '/' => {
143 self.state = .tag0_end_or_empty;
144 self.index += 1;
145 },
146 'A'...'Z', 'a'...'z' => {
147 self.state = .tagN;
148 tag.kind = .start;
149 self.index += 1;
150 },
151 else => {
152 self.state = .tagN;
153 self.index += 1;
154 },
155 },
156 .tag0_end_or_empty => switch (char) {
157 '<' => {
158 return error.BadToken;
159 },
160 '>' => {
161 self.state = .begin;
162 tag.kind = .empty;
163 tag.name = self.bytes[self.index..self.index];
164 self.index += 1;
165 return Token{ .tag = tag };
166 },
167 else => {
168 self.state = .tagN;
169 tag.kind = .end;
170 mark = self.index;
171 self.index += 1;
172 },
173 },
174 .tagN => switch (char) {
175 '<' => {
176 return error.BadToken;
177 },
178 '>' => {
179 self.state = .begin;
180 tag.name = self.bytes[mark..self.index];
181 self.index += 1;
182 return Token{ .tag = tag };
183 },
184 '"' => {
185 self.state = .tag_string;
186 self.index += 1;
187 },
188 '/' => {
189 self.state = .tagN_end;
190 tag.kind = .end;
191 self.index += 1;
192 },
193 else => {
194 self.index += 1;
195 },
196 },
197 .tagN_end => switch (char) {
198 '>' => {
199 self.state = .begin;
200 tag.name = self.bytes[mark..self.index];
201 self.index += 1;
202 return Token{ .tag = tag };
203 },
204 else => {
205 return error.BadToken;
206 },
207 },
208 .tag_string => switch (char) {
209 '"' => {
210 self.state = .tagN;
211 self.index += 1;
212 },
213 else => {
214 self.index += 1;
215 },
216 },
217 .content => switch (char) {
218 '<' => {
219 self.state = .tag0;
220 content = self.bytes[mark..self.index];
221 self.index += 1;
222 tag = Tag{};
223 mark = self.index;
224 return Token{ .content = content };
225 },
226 '>' => {
227 return error.BadToken;
228 },
229 else => {
230 self.index += 1;
231 },
232 },
233 }
234 }
210235
211 .{ "8A428", "10.4.0" },
212 .{ "8A432", "10.4.0" },
213 .{ "8B15", "10.4.1" },
214 .{ "8B17", "10.4.1" },
215 .{ "8C46", "10.4.2" },
216 .{ "8C47", "10.4.2" },
217 .{ "8E102", "10.4.2" },
218 .{ "8E45", "10.4.2" },
219 .{ "8E90", "10.4.2" },
220 .{ "8F46", "10.4.3" },
221 .{ "8G32", "10.4.4" },
222 .{ "8G1165", "10.4.4" },
223 .{ "8H14", "10.4.5" },
224 .{ "8G1454", "10.4.5" },
225 .{ "8I127", "10.4.6" },
226 .{ "8I1119", "10.4.6" },
227 .{ "8J135", "10.4.7" },
228 .{ "8J2135a", "10.4.7" },
229 .{ "8K1079", "10.4.7" },
230 .{ "8N5107", "10.4.7" },
231 .{ "8L127", "10.4.8" },
232 .{ "8L2127", "10.4.8" },
233 .{ "8P135", "10.4.9" },
234 .{ "8P2137", "10.4.9" },
235 .{ "8R218", "10.4.10" },
236 .{ "8R2218", "10.4.10" },
237 .{ "8R2232", "10.4.10" },
238 .{ "8S165", "10.4.11" },
239 .{ "8S2167", "10.4.11" },
236 return null;
237 }
240238
241 .{ "9A581", "10.5.0" },
242 .{ "9B18", "10.5.1" },
243 .{ "9C31", "10.5.2" },
244 .{ "9C7010", "10.5.2" },
245 .{ "9D34", "10.5.3" },
246 .{ "9E17", "10.5.4" },
247 .{ "9F33", "10.5.5" },
248 .{ "9G55", "10.5.6" },
249 .{ "9G66", "10.5.6" },
250 .{ "9J61", "10.5.7" },
251 .{ "9L30", "10.5.8" },
239 fn expectContent(self: *@This()) ![]const u8 {
240 if (try self.next()) |tok| {
241 switch (tok) {
242 .content => |content| {
243 return content;
244 },
245 else => {},
246 }
247 }
248 return error.UnexpectedToken;
249 }
252250
253 .{ "10A432", "10.6.0" },
254 .{ "10A433", "10.6.0" },
255 .{ "10B504", "10.6.1" },
256 .{ "10C540", "10.6.2" },
257 .{ "10D573", "10.6.3" },
258 .{ "10D575", "10.6.3" },
259 .{ "10D578", "10.6.3" },
260 .{ "10F569", "10.6.4" },
261 .{ "10H574", "10.6.5" },
262 .{ "10J567", "10.6.6" },
263 .{ "10J869", "10.6.7" },
264 .{ "10J3250", "10.6.7" },
265 .{ "10J4138", "10.6.7" },
266 .{ "10K540", "10.6.8" },
267 .{ "10K549", "10.6.8" },
251 fn skipUntilTag(self: *@This(), kind: Tag.Kind, name: []const u8) !void {
252 while (try self.next()) |tok| {
253 switch (tok) {
254 .tag => |tag| {
255 if (tag.kind == kind and std.mem.eql(u8, tag.name, name)) return;
256 },
257 else => {},
258 }
259 }
260 return error.TagNotFound;
261 }
268262
269 .{ "11A511", "10.7.0" },
270 .{ "11A511s", "10.7.0" },
271 .{ "11A2061", "10.7.0" },
272 .{ "11A2063", "10.7.0" },
273 .{ "11B26", "10.7.1" },
274 .{ "11B2118", "10.7.1" },
275 .{ "11C74", "10.7.2" },
276 .{ "11D50", "10.7.3" },
277 .{ "11E53", "10.7.4" },
278 .{ "11G56", "10.7.5" },
279 .{ "11G63", "10.7.5" },
263 const State = enum {
264 begin,
265 tag0,
266 tag0_end_or_empty,
267 tagN,
268 tagN_end,
269 tag_string,
270 content,
271 };
280272
281 .{ "12A269", "10.8.0" },
282 .{ "12B19", "10.8.1" },
283 .{ "12C54", "10.8.2" },
284 .{ "12C60", "10.8.2" },
285 .{ "12C2034", "10.8.2" },
286 .{ "12C3104", "10.8.2" },
287 .{ "12D78", "10.8.3" },
288 .{ "12E55", "10.8.4" },
289 .{ "12E3067", "10.8.4" },
290 .{ "12E4022", "10.8.4" },
291 .{ "12F37", "10.8.5" },
292 .{ "12F45", "10.8.5" },
293 .{ "12F2501", "10.8.5" },
294 .{ "12F2518", "10.8.5" },
295 .{ "12F2542", "10.8.5" },
296 .{ "12F2560", "10.8.5" },
273 const Token = union(enum) {
274 tag: Tag,
275 content: []const u8,
276 };
297277
298 .{ "13A603", "10.9.0" },
299 .{ "13B42", "10.9.1" },
300 .{ "13C64", "10.9.2" },
301 .{ "13C1021", "10.9.2" },
302 .{ "13D65", "10.9.3" },
303 .{ "13E28", "10.9.4" },
304 .{ "13F34", "10.9.5" },
305 .{ "13F1066", "10.9.5" },
306 .{ "13F1077", "10.9.5" },
307 .{ "13F1096", "10.9.5" },
308 .{ "13F1112", "10.9.5" },
309 .{ "13F1134", "10.9.5" },
310 .{ "13F1507", "10.9.5" },
311 .{ "13F1603", "10.9.5" },
312 .{ "13F1712", "10.9.5" },
313 .{ "13F1808", "10.9.5" },
314 .{ "13F1911", "10.9.5" },
278 const Tag = struct {
279 kind: Kind = .unknown,
280 name: []const u8 = "",
315281
316 .{ "14A389", "10.10.0" },
317 .{ "14B25", "10.10.1" },
318 .{ "14C109", "10.10.2" },
319 .{ "14C1510", "10.10.2" },
320 .{ "14C1514", "10.10.2" },
321 .{ "14C2043", "10.10.2" },
322 .{ "14C2513", "10.10.2" },
323 .{ "14D131", "10.10.3" },
324 .{ "14D136", "10.10.3" },
325 .{ "14E46", "10.10.4" },
326 .{ "14F27", "10.10.5" },
327 .{ "14F1021", "10.10.5" },
328 .{ "14F1505", "10.10.5" },
329 .{ "14F1509", "10.10.5" },
330 .{ "14F1605", "10.10.5" },
331 .{ "14F1713", "10.10.5" },
332 .{ "14F1808", "10.10.5" },
333 .{ "14F1909", "10.10.5" },
334 .{ "14F1912", "10.10.5" },
335 .{ "14F2009", "10.10.5" },
336 .{ "14F2109", "10.10.5" },
337 .{ "14F2315", "10.10.5" },
338 .{ "14F2411", "10.10.5" },
339 .{ "14F2511", "10.10.5" },
282 const Kind = enum { unknown, start, end, empty };
283 };
284};
340285
341 .{ "15A284", "10.11.0" },
342 .{ "15B42", "10.11.1" },
343 .{ "15C50", "10.11.2" },
344 .{ "15D21", "10.11.3" },
345 .{ "15E65", "10.11.4" },
346 .{ "15F34", "10.11.5" },
347 .{ "15G31", "10.11.6" },
348 .{ "15G1004", "10.11.6" },
349 .{ "15G1011", "10.11.6" },
350 .{ "15G1108", "10.11.6" },
351 .{ "15G1212", "10.11.6" },
352 .{ "15G1217", "10.11.6" },
353 .{ "15G1421", "10.11.6" },
354 .{ "15G1510", "10.11.6" },
355 .{ "15G1611", "10.11.6" },
356 .{ "15G17023", "10.11.6" },
357 .{ "15G18013", "10.11.6" },
358 .{ "15G19009", "10.11.6" },
359 .{ "15G20015", "10.11.6" },
360 .{ "15G21013", "10.11.6" },
361 .{ "15G22010", "10.11.6" },
286test "detect" {
287 const cases = .{
288 .{
289 \\<?xml version="1.0" encoding="UTF-8"?>
290 \\<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
291 \\<plist version="1.0">
292 \\<dict>
293 \\ <key>ProductBuildVersion</key>
294 \\ <string>7B85</string>
295 \\ <key>ProductCopyright</key>
296 \\ <string>Apple Computer, Inc. 1983-2003</string>
297 \\ <key>ProductName</key>
298 \\ <string>Mac OS X</string>
299 \\ <key>ProductUserVisibleVersion</key>
300 \\ <string>10.3</string>
301 \\ <key>ProductVersion</key>
302 \\ <string>10.3</string>
303 \\</dict>
304 \\</plist>
305 ,
306 .{ .major = 10, .minor = 3 },
307 },
308 .{
309 \\<?xml version="1.0" encoding="UTF-8"?>
310 \\<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
311 \\<plist version="1.0">
312 \\<dict>
313 \\ <key>ProductBuildVersion</key>
314 \\ <string>7W98</string>
315 \\ <key>ProductCopyright</key>
316 \\ <string>Apple Computer, Inc. 1983-2004</string>
317 \\ <key>ProductName</key>
318 \\ <string>Mac OS X</string>
319 \\ <key>ProductUserVisibleVersion</key>
320 \\ <string>10.3.9</string>
321 \\ <key>ProductVersion</key>
322 \\ <string>10.3.9</string>
323 \\</dict>
324 \\</plist>
325 ,
326 .{ .major = 10, .minor = 3, .patch = 9 },
327 },
328 .{
329 \\<?xml version="1.0" encoding="UTF-8"?>
330 \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
331 \\<plist version="1.0">
332 \\<dict>
333 \\ <key>ProductBuildVersion</key>
334 \\ <string>19G68</string>
335 \\ <key>ProductCopyright</key>
336 \\ <string>1983-2020 Apple Inc.</string>
337 \\ <key>ProductName</key>
338 \\ <string>Mac OS X</string>
339 \\ <key>ProductUserVisibleVersion</key>
340 \\ <string>10.15.6</string>
341 \\ <key>ProductVersion</key>
342 \\ <string>10.15.6</string>
343 \\ <key>iOSSupportVersion</key>
344 \\ <string>13.6</string>
345 \\</dict>
346 \\</plist>
347 ,
348 .{ .major = 10, .minor = 15, .patch = 6 },
349 },
350 .{
351 \\<?xml version="1.0" encoding="UTF-8"?>
352 \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
353 \\<plist version="1.0">
354 \\<dict>
355 \\ <key>ProductBuildVersion</key>
356 \\ <string>20A2408</string>
357 \\ <key>ProductCopyright</key>
358 \\ <string>1983-2020 Apple Inc.</string>
359 \\ <key>ProductName</key>
360 \\ <string>macOS</string>
361 \\ <key>ProductUserVisibleVersion</key>
362 \\ <string>11.0</string>
363 \\ <key>ProductVersion</key>
364 \\ <string>11.0</string>
365 \\ <key>iOSSupportVersion</key>
366 \\ <string>14.2</string>
367 \\</dict>
368 \\</plist>
369 ,
370 .{ .major = 11, .minor = 0 },
371 },
372 .{
373 \\<?xml version="1.0" encoding="UTF-8"?>
374 \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
375 \\<plist version="1.0">
376 \\<dict>
377 \\ <key>ProductBuildVersion</key>
378 \\ <string>20C63</string>
379 \\ <key>ProductCopyright</key>
380 \\ <string>1983-2020 Apple Inc.</string>
381 \\ <key>ProductName</key>
382 \\ <string>macOS</string>
383 \\ <key>ProductUserVisibleVersion</key>
384 \\ <string>11.1</string>
385 \\ <key>ProductVersion</key>
386 \\ <string>11.1</string>
387 \\ <key>iOSSupportVersion</key>
388 \\ <string>14.3</string>
389 \\</dict>
390 \\</plist>
391 ,
392 .{ .major = 11, .minor = 1 },
393 },
394 };
362395
363 .{ "16A323", "10.12.0" },
364 .{ "16B2555", "10.12.1" },
365 .{ "16B2657", "10.12.1" },
366 .{ "16C67", "10.12.2" },
367 .{ "16C68", "10.12.2" },
368 .{ "16D32", "10.12.3" },
369 .{ "16E195", "10.12.4" },
370 .{ "16F73", "10.12.5" },
371 .{ "16F2073", "10.12.5" },
372 .{ "16G29", "10.12.6" },
373 .{ "16G1036", "10.12.6" },
374 .{ "16G1114", "10.12.6" },
375 .{ "16G1212", "10.12.6" },
376 .{ "16G1314", "10.12.6" },
377 .{ "16G1408", "10.12.6" },
378 .{ "16G1510", "10.12.6" },
379 .{ "16G1618", "10.12.6" },
380 .{ "16G1710", "10.12.6" },
381 .{ "16G1815", "10.12.6" },
382 .{ "16G1917", "10.12.6" },
383 .{ "16G1918", "10.12.6" },
384 .{ "16G2016", "10.12.6" },
385 .{ "16G2127", "10.12.6" },
386 .{ "16G2128", "10.12.6" },
387 .{ "16G2136", "10.12.6" },
396 inline for (cases) |case| {
397 const ver0 = try parseSystemVersion(case[0]);
398 const ver1: std.builtin.Version = case[1];
399 try testVersionEquality(ver1, ver0);
400 }
401}
388402
389 .{ "17A365", "10.13.0" },
390 .{ "17A405", "10.13.0" },
391 .{ "17B48", "10.13.1" },
392 .{ "17B1002", "10.13.1" },
393 .{ "17B1003", "10.13.1" },
394 .{ "17C88", "10.13.2" },
395 .{ "17C89", "10.13.2" },
396 .{ "17C205", "10.13.2" },
397 .{ "17C2205", "10.13.2" },
398 .{ "17D47", "10.13.3" },
399 .{ "17D2047", "10.13.3" },
400 .{ "17D102", "10.13.3" },
401 .{ "17D2102", "10.13.3" },
402 .{ "17E199", "10.13.4" },
403 .{ "17E202", "10.13.4" },
404 .{ "17F77", "10.13.5" },
405 .{ "17G65", "10.13.6" },
406 .{ "17G2208", "10.13.6" },
407 .{ "17G3025", "10.13.6" },
408 .{ "17G4015", "10.13.6" },
409 .{ "17G5019", "10.13.6" },
410 .{ "17G6029", "10.13.6" },
411 .{ "17G6030", "10.13.6" },
412 .{ "17G7024", "10.13.6" },
413 .{ "17G8029", "10.13.6" },
414 .{ "17G8030", "10.13.6" },
415 .{ "17G8037", "10.13.6" },
416 .{ "17G9016", "10.13.6" },
417 .{ "17G10021", "10.13.6" },
418 .{ "17G11023", "10.13.6" },
419 .{ "17G12034", "10.13.6" },
403fn testVersionEquality(expected: std.builtin.Version, got: std.builtin.Version) !void {
404 var b_expected: [64]u8 = undefined;
405 const s_expected: []const u8 = try std.fmt.bufPrint(b_expected[0..], "{}", .{expected});
420406
421 .{ "18A391", "10.14.0" },
422 .{ "18B75", "10.14.1" },
423 .{ "18B2107", "10.14.1" },
424 .{ "18B3094", "10.14.1" },
425 .{ "18C54", "10.14.2" },
426 .{ "18D42", "10.14.3" },
427 .{ "18D43", "10.14.3" },
428 .{ "18D109", "10.14.3" },
429 .{ "18E226", "10.14.4" },
430 .{ "18E227", "10.14.4" },
431 .{ "18F132", "10.14.5" },
432 .{ "18G84", "10.14.6" },
433 .{ "18G87", "10.14.6" },
434 .{ "18G95", "10.14.6" },
435 .{ "18G103", "10.14.6" },
436 .{ "18G1012", "10.14.6" },
437 .{ "18G2022", "10.14.6" },
438 .{ "18G3020", "10.14.6" },
439 .{ "18G4032", "10.14.6" },
407 var b_got: [64]u8 = undefined;
408 const s_got: []const u8 = try std.fmt.bufPrint(b_got[0..], "{}", .{got});
440409
441 .{ "19A583", "10.15.0" },
442 .{ "19A602", "10.15.0" },
443 .{ "19A603", "10.15.0" },
444 .{ "19B88", "10.15.1" },
445 .{ "19C57", "10.15.2" },
446 .{ "19D76", "10.15.3" },
447 .{ "19E266", "10.15.4" },
448 .{ "19E287", "10.15.4" },
449 };
450 for (known) |pair| {
451 var buf: [32]u8 = undefined;
452 const ver = try version_from_build(pair[0]);
453 const sver = try std.fmt.bufPrint(buf[0..], "{d}.{d}.{d}", .{ ver.major, ver.minor, ver.patch });
454 std.testing.expect(std.mem.eql(u8, sver, pair[1]));
455 }
410 testing.expectEqualStrings(s_expected, s_got);
456411}
457412
458413/// Detect SDK path on Darwin.