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