Commit f2be1fb23e

Michael Dusan <michael.dusan@gmail.com>
2021-01-12 01:52:21
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
1 parent 56c0388
Changed files (3)
lib/std/zig/system/macos.zig
@@ -6,453 +6,408 @@
 const std = @import("std");
 const assert = std.debug.assert;
 const mem = std.mem;
+const testing = std.testing;
 
-pub fn version_from_build(build: []const u8) !std.builtin.Version {
-    // build format:
-    //   19E287 (example)
-    //   xxyzzz
+/// Detect macOS version.
+/// On error `os` will not be modified.
+pub fn detect(os: *std.Target.Os) !void {
+    // Drop use of osproductversion sysctl because:
+    //   1. only available 10.13.4 High Sierra and later
+    //   2. when used from a binary built against < SDK 11.0 it returns 10.16 and masks Big Sur 11.x version
     //
-    // major = 10
-    // minor = x - 4 = 19 - 4 = 15
-    // patch = ascii(y) - 'A' = 'E' - 'A' = 69 - 65 = 4
-    // answer: 10.15.4
+    // NEW APPROACH, STEP 1, parse file:
     //
-    // note: patch is typical but with some older releases (before 10.8) zzz is considered
+    //   /System/Library/CoreServices/SystemVersion.plist
+    //
+    // NOTE: Historically `SystemVersion.plist` first appeared circa '2003
+    // with the release of Mac OS X 10.3.0 Panther.
+    //
+    // and if it contains a `10.16` value where the `16` is `>= 16` then it is a red herring
+    // and is discarded, and move on to next step. Otherwise we accept this is the
+    // canonical file for versioning.
+    //
+    // BACKGROUND: `10.(16+)` is not a proper version and does not have enough fidelity to
+    // indicate minor/point version of Big Sur and later. It is a context-sensitive result
+    // issued by the kernel for backwards compatibility purposes. Likely the kernel checks
+    // if the executable was linked against an SDK older than Big Sur.
+    //
+    // STEP 2, parse next file:
+    //
+    //   /System/Library/CoreServices/.SystemVersionPlatform.plist
+    //
+    // NOTE: Historically `SystemVersionPlatform.plist` first appeared circa '2020
+    // with the release of macOS 11.0 Big Sur.
+    //
+    // Accessing the content via this path circumvents a context-sensitive result and
+    // yields a canonical Big Sur version.
+    //
+    // At this time there is no other known way for a < SDK 11.0 executable to obtain a
+    // canonical Big Sur version.
+    //
+    // This implementation uses a reasonably simplified approach to parse .plist file
+    // that while it is an xml document, we have good history on the file and its format
+    // such that I am comfortable with implementing a minimalistic parser.
+    // Things like string and general escapes are not supported.
+    const prefixSlash = "/System/Library/CoreServices/";
+    const format_failure = "macOS detect: failed to {s} '{s}': {}";
 
-    // never return anything below 10.0.0
-    var result = std.builtin.Version{ .major = 10, .minor = 0, .patch = 0 };
+    const paths = [_][]const u8{
+        prefixSlash ++ "SystemVersion.plist",
+        prefixSlash ++ ".SystemVersionPlatform.plist",
+    };
+    for (paths) |path| {
+        // approx. 4 times historical file size
+        var buf: [2048]u8 = undefined;
 
-    // parse format-x
-    var yindex: usize = 0;
-    {
-        while (yindex < build.len) : (yindex += 1) {
-            if (build[yindex] < '0' or build[yindex] > '9') break;
+        if (std.fs.cwd().readFile(path, &buf)) |bytes| {
+            if (parseSystemVersion(bytes)) |ver| {
+                // never return red herring
+                if (!(ver.major == 10 and ver.minor >= 16)) {
+                    os.version_range.semver.min = ver;
+                    os.version_range.semver.max = ver;
+                    return;
+                }
+                continue;
+            } else |err| {
+                std.log.err(format_failure, .{ "parse", path, err });
+                return error.OSVersionDetectionFail;
+            }
+        } else |err| {
+            std.log.err(format_failure, .{ "read", path, err });
+            return error.OSVersionDetectionFail;
         }
-        if (yindex == 0) return result;
-        const x = std.fmt.parseUnsigned(u16, build[0..yindex], 10) catch return error.InvalidVersion;
-        if (x < 4) return result;
-        result.minor = x - 4;
     }
+    return error.OSVersionDetectionFail;
+}
 
-    // parse format-y, format-z
-    {
-        // expect two more
-        if (build.len < yindex + 2) return error.InvalidVersion;
-        const y = build[yindex];
-        if (y < 'A') return error.InvalidVersion;
-        var zend = yindex + 1;
-        while (zend < build.len) {
-            if (build[zend] < '0' or build[zend] > '9') break;
-            zend += 1;
-        }
-        if (zend == yindex + 1) return error.InvalidVersion;
-        const z = std.fmt.parseUnsigned(u16, build[yindex + 1 .. zend], 10) catch return error.InvalidVersion;
-
-        result.patch = switch (result.minor) {
-            // TODO: compiler complains without explicit @as() coercion
-            0 => @as(u32, switch (y) { // Cheetah: 10.0
-                'K' => 0,
-                'L' => 1,
-                'P' => @as(u32, block: {
-                    if (z < 13) break :block 2;
-                    break :block 3;
-                }),
-                'Q' => 4,
-                else => return error.InvalidVersion,
-            }),
-            1 => @as(u32, switch (y) { // Puma: 10.1
-                'G' => 0,
-                'M' => 1,
-                'P' => 2,
-                'Q' => @as(u32, block: {
-                    if (z < 125) break :block 3;
-                    break :block 4;
-                }),
-                'S' => 5,
-                else => return error.InvalidVersion,
-            }),
-            2 => @as(u32, switch (y) { // Jaguar: 10.2
-                'C' => 0,
-                'D' => 1,
-                'F' => 2,
-                'G' => 3,
-                'I' => 4,
-                'L' => @as(u32, block: {
-                    if (z < 60) break :block 5;
-                    break :block 6;
-                }),
-                'R' => @as(u32, block: {
-                    if (z < 73) break :block 7;
-                    break :block 8;
-                }),
-                'S' => 8,
-                else => return error.InvalidVersion,
-            }),
-            3 => @as(u32, switch (y) { // Panther: 10.3
-                'B' => 0,
-                'C' => 1,
-                'D' => 2,
-                'F' => 3,
-                'H' => 4,
-                'M' => 5,
-                'R' => 6,
-                'S' => 7,
-                'U' => 8,
-                'W' => 9,
-                else => return error.InvalidVersion,
-            }),
-            4 => @as(u32, switch (y) { // Tiger: 10.4
-                'A' => 0,
-                'B' => 1,
-                'C',
-                'E',
-                => 2,
-                'F' => 3,
-                'G' => @as(u32, block: {
-                    if (z >= 1454) break :block 5;
-                    break :block 4;
-                }),
-                'H' => 5,
-                'I' => 6,
-                'J',
-                'K',
-                'N',
-                => 7,
-                'L' => 8,
-                'P' => 9,
-                'R' => 10,
-                'S' => 11,
-                else => return error.InvalidVersion,
-            }),
-            5 => @as(u32, switch (y) { // Leopard: 10.5
-                'A' => 0,
-                'B' => 1,
-                'C' => 2,
-                'D' => 3,
-                'E' => 4,
-                'F' => 5,
-                'G' => 6,
-                'J' => 7,
-                'L' => 8,
-                else => return error.InvalidVersion,
-            }),
-            6 => @as(u32, switch (y) { // Snow Leopard: 10.6
-                'A' => 0,
-                'B' => 1,
-                'C' => 2,
-                'D' => 3,
-                'F' => 4,
-                'H' => 5,
-                'J' => @as(u32, block: {
-                    if (z < 869) break :block 6;
-                    break :block 7;
-                }),
-                'K' => 8,
-                else => return error.InvalidVersion,
-            }),
-            7 => @as(u32, switch (y) { // Snow Leopard: 10.6
-                'A' => 0,
-                'B' => 1,
-                'C' => 2,
-                'D' => 3,
-                'E' => 4,
-                'G' => 5,
-                else => return error.InvalidVersion,
-            }),
-            else => y - 'A',
-        };
+fn parseSystemVersion(buf: []const u8) !std.builtin.Version {
+    var svt = SystemVersionTokenizer{ .bytes = buf };
+    try svt.skipUntilTag(.start, "dict");
+    while (true) {
+        try svt.skipUntilTag(.start, "key");
+        const content = try svt.expectContent();
+        try svt.skipUntilTag(.end, "key");
+        if (std.mem.eql(u8, content, "ProductVersion")) break;
     }
-    return result;
-}
+    try svt.skipUntilTag(.start, "string");
+    const ver = try svt.expectContent();
+    try svt.skipUntilTag(.end, "string");
 
-test "version_from_build" {
-    // see https://en.wikipedia.org/wiki/MacOS_version_history#Releases
-    const known = [_][2][]const u8{
-        .{ "4K78", "10.0.0" },
-        .{ "4L13", "10.0.1" },
-        .{ "4P12", "10.0.2" },
-        .{ "4P13", "10.0.3" },
-        .{ "4Q12", "10.0.4" },
+    return std.builtin.Version.parse(ver);
+}
 
-        .{ "5G64", "10.1.0" },
-        .{ "5M28", "10.1.1" },
-        .{ "5P48", "10.1.2" },
-        .{ "5Q45", "10.1.3" },
-        .{ "5Q125", "10.1.4" },
-        .{ "5S60", "10.1.5" },
+const SystemVersionTokenizer = struct {
+    bytes: []const u8,
+    index: usize = 0,
+    state: State = .begin,
 
-        .{ "6C115", "10.2.0" },
-        .{ "6C115a", "10.2.0" },
-        .{ "6D52", "10.2.1" },
-        .{ "6F21", "10.2.2" },
-        .{ "6G30", "10.2.3" },
-        .{ "6G37", "10.2.3" },
-        .{ "6G50", "10.2.3" },
-        .{ "6I32", "10.2.4" },
-        .{ "6L29", "10.2.5" },
-        .{ "6L60", "10.2.6" },
-        .{ "6R65", "10.2.7" },
-        .{ "6R73", "10.2.8" },
-        .{ "6S90", "10.2.8" },
+    fn next(self: *@This()) !?Token {
+        var mark: usize = self.index;
+        var tag = Tag{};
+        var content: []const u8 = "";
 
-        .{ "7B85", "10.3.0" },
-        .{ "7B86", "10.3.0" },
-        .{ "7C107", "10.3.1" },
-        .{ "7D24", "10.3.2" },
-        .{ "7D28", "10.3.2" },
-        .{ "7F44", "10.3.3" },
-        .{ "7H63", "10.3.4" },
-        .{ "7M34", "10.3.5" },
-        .{ "7R28", "10.3.6" },
-        .{ "7S215", "10.3.7" },
-        .{ "7U16", "10.3.8" },
-        .{ "7W98", "10.3.9" },
+        while (self.index < self.bytes.len) {
+            const char = self.bytes[self.index];
+            switch (self.state) {
+                .begin => switch (char) {
+                    '<' => {
+                        self.state = .tag0;
+                        self.index += 1;
+                        tag = Tag{};
+                        mark = self.index;
+                    },
+                    '>' => {
+                        return error.BadToken;
+                    },
+                    else => {
+                        self.state = .content;
+                        content = "";
+                        mark = self.index;
+                    },
+                },
+                .tag0 => switch (char) {
+                    '<' => {
+                        return error.BadToken;
+                    },
+                    '>' => {
+                        self.state = .begin;
+                        self.index += 1;
+                        tag.name = self.bytes[mark..self.index];
+                        return Token{ .tag = tag };
+                    },
+                    '"' => {
+                        self.state = .tag_string;
+                        self.index += 1;
+                    },
+                    '/' => {
+                        self.state = .tag0_end_or_empty;
+                        self.index += 1;
+                    },
+                    'A'...'Z', 'a'...'z' => {
+                        self.state = .tagN;
+                        tag.kind = .start;
+                        self.index += 1;
+                    },
+                    else => {
+                        self.state = .tagN;
+                        self.index += 1;
+                    },
+                },
+                .tag0_end_or_empty => switch (char) {
+                    '<' => {
+                        return error.BadToken;
+                    },
+                    '>' => {
+                        self.state = .begin;
+                        tag.kind = .empty;
+                        tag.name = self.bytes[self.index..self.index];
+                        self.index += 1;
+                        return Token{ .tag = tag };
+                    },
+                    else => {
+                        self.state = .tagN;
+                        tag.kind = .end;
+                        mark = self.index;
+                        self.index += 1;
+                    },
+                },
+                .tagN => switch (char) {
+                    '<' => {
+                        return error.BadToken;
+                    },
+                    '>' => {
+                        self.state = .begin;
+                        tag.name = self.bytes[mark..self.index];
+                        self.index += 1;
+                        return Token{ .tag = tag };
+                    },
+                    '"' => {
+                        self.state = .tag_string;
+                        self.index += 1;
+                    },
+                    '/' => {
+                        self.state = .tagN_end;
+                        tag.kind = .end;
+                        self.index += 1;
+                    },
+                    else => {
+                        self.index += 1;
+                    },
+                },
+                .tagN_end => switch (char) {
+                    '>' => {
+                        self.state = .begin;
+                        tag.name = self.bytes[mark..self.index];
+                        self.index += 1;
+                        return Token{ .tag = tag };
+                    },
+                    else => {
+                        return error.BadToken;
+                    },
+                },
+                .tag_string => switch (char) {
+                    '"' => {
+                        self.state = .tagN;
+                        self.index += 1;
+                    },
+                    else => {
+                        self.index += 1;
+                    },
+                },
+                .content => switch (char) {
+                    '<' => {
+                        self.state = .tag0;
+                        content = self.bytes[mark..self.index];
+                        self.index += 1;
+                        tag = Tag{};
+                        mark = self.index;
+                        return Token{ .content = content };
+                    },
+                    '>' => {
+                        return error.BadToken;
+                    },
+                    else => {
+                        self.index += 1;
+                    },
+                },
+            }
+        }
 
-        .{ "8A428", "10.4.0" },
-        .{ "8A432", "10.4.0" },
-        .{ "8B15", "10.4.1" },
-        .{ "8B17", "10.4.1" },
-        .{ "8C46", "10.4.2" },
-        .{ "8C47", "10.4.2" },
-        .{ "8E102", "10.4.2" },
-        .{ "8E45", "10.4.2" },
-        .{ "8E90", "10.4.2" },
-        .{ "8F46", "10.4.3" },
-        .{ "8G32", "10.4.4" },
-        .{ "8G1165", "10.4.4" },
-        .{ "8H14", "10.4.5" },
-        .{ "8G1454", "10.4.5" },
-        .{ "8I127", "10.4.6" },
-        .{ "8I1119", "10.4.6" },
-        .{ "8J135", "10.4.7" },
-        .{ "8J2135a", "10.4.7" },
-        .{ "8K1079", "10.4.7" },
-        .{ "8N5107", "10.4.7" },
-        .{ "8L127", "10.4.8" },
-        .{ "8L2127", "10.4.8" },
-        .{ "8P135", "10.4.9" },
-        .{ "8P2137", "10.4.9" },
-        .{ "8R218", "10.4.10" },
-        .{ "8R2218", "10.4.10" },
-        .{ "8R2232", "10.4.10" },
-        .{ "8S165", "10.4.11" },
-        .{ "8S2167", "10.4.11" },
+        return null;
+    }
 
-        .{ "9A581", "10.5.0" },
-        .{ "9B18", "10.5.1" },
-        .{ "9C31", "10.5.2" },
-        .{ "9C7010", "10.5.2" },
-        .{ "9D34", "10.5.3" },
-        .{ "9E17", "10.5.4" },
-        .{ "9F33", "10.5.5" },
-        .{ "9G55", "10.5.6" },
-        .{ "9G66", "10.5.6" },
-        .{ "9J61", "10.5.7" },
-        .{ "9L30", "10.5.8" },
+    fn expectContent(self: *@This()) ![]const u8 {
+        if (try self.next()) |tok| {
+            switch (tok) {
+                .content => |content| {
+                    return content;
+                },
+                else => {},
+            }
+        }
+        return error.UnexpectedToken;
+    }
 
-        .{ "10A432", "10.6.0" },
-        .{ "10A433", "10.6.0" },
-        .{ "10B504", "10.6.1" },
-        .{ "10C540", "10.6.2" },
-        .{ "10D573", "10.6.3" },
-        .{ "10D575", "10.6.3" },
-        .{ "10D578", "10.6.3" },
-        .{ "10F569", "10.6.4" },
-        .{ "10H574", "10.6.5" },
-        .{ "10J567", "10.6.6" },
-        .{ "10J869", "10.6.7" },
-        .{ "10J3250", "10.6.7" },
-        .{ "10J4138", "10.6.7" },
-        .{ "10K540", "10.6.8" },
-        .{ "10K549", "10.6.8" },
+    fn skipUntilTag(self: *@This(), kind: Tag.Kind, name: []const u8) !void {
+        while (try self.next()) |tok| {
+            switch (tok) {
+                .tag => |tag| {
+                    if (tag.kind == kind and std.mem.eql(u8, tag.name, name)) return;
+                },
+                else => {},
+            }
+        }
+        return error.TagNotFound;
+    }
 
-        .{ "11A511", "10.7.0" },
-        .{ "11A511s", "10.7.0" },
-        .{ "11A2061", "10.7.0" },
-        .{ "11A2063", "10.7.0" },
-        .{ "11B26", "10.7.1" },
-        .{ "11B2118", "10.7.1" },
-        .{ "11C74", "10.7.2" },
-        .{ "11D50", "10.7.3" },
-        .{ "11E53", "10.7.4" },
-        .{ "11G56", "10.7.5" },
-        .{ "11G63", "10.7.5" },
+    const State = enum {
+        begin,
+        tag0,
+        tag0_end_or_empty,
+        tagN,
+        tagN_end,
+        tag_string,
+        content,
+    };
 
-        .{ "12A269", "10.8.0" },
-        .{ "12B19", "10.8.1" },
-        .{ "12C54", "10.8.2" },
-        .{ "12C60", "10.8.2" },
-        .{ "12C2034", "10.8.2" },
-        .{ "12C3104", "10.8.2" },
-        .{ "12D78", "10.8.3" },
-        .{ "12E55", "10.8.4" },
-        .{ "12E3067", "10.8.4" },
-        .{ "12E4022", "10.8.4" },
-        .{ "12F37", "10.8.5" },
-        .{ "12F45", "10.8.5" },
-        .{ "12F2501", "10.8.5" },
-        .{ "12F2518", "10.8.5" },
-        .{ "12F2542", "10.8.5" },
-        .{ "12F2560", "10.8.5" },
+    const Token = union(enum) {
+        tag: Tag,
+        content: []const u8,
+    };
 
-        .{ "13A603", "10.9.0" },
-        .{ "13B42", "10.9.1" },
-        .{ "13C64", "10.9.2" },
-        .{ "13C1021", "10.9.2" },
-        .{ "13D65", "10.9.3" },
-        .{ "13E28", "10.9.4" },
-        .{ "13F34", "10.9.5" },
-        .{ "13F1066", "10.9.5" },
-        .{ "13F1077", "10.9.5" },
-        .{ "13F1096", "10.9.5" },
-        .{ "13F1112", "10.9.5" },
-        .{ "13F1134", "10.9.5" },
-        .{ "13F1507", "10.9.5" },
-        .{ "13F1603", "10.9.5" },
-        .{ "13F1712", "10.9.5" },
-        .{ "13F1808", "10.9.5" },
-        .{ "13F1911", "10.9.5" },
+    const Tag = struct {
+        kind: Kind = .unknown,
+        name: []const u8 = "",
 
-        .{ "14A389", "10.10.0" },
-        .{ "14B25", "10.10.1" },
-        .{ "14C109", "10.10.2" },
-        .{ "14C1510", "10.10.2" },
-        .{ "14C1514", "10.10.2" },
-        .{ "14C2043", "10.10.2" },
-        .{ "14C2513", "10.10.2" },
-        .{ "14D131", "10.10.3" },
-        .{ "14D136", "10.10.3" },
-        .{ "14E46", "10.10.4" },
-        .{ "14F27", "10.10.5" },
-        .{ "14F1021", "10.10.5" },
-        .{ "14F1505", "10.10.5" },
-        .{ "14F1509", "10.10.5" },
-        .{ "14F1605", "10.10.5" },
-        .{ "14F1713", "10.10.5" },
-        .{ "14F1808", "10.10.5" },
-        .{ "14F1909", "10.10.5" },
-        .{ "14F1912", "10.10.5" },
-        .{ "14F2009", "10.10.5" },
-        .{ "14F2109", "10.10.5" },
-        .{ "14F2315", "10.10.5" },
-        .{ "14F2411", "10.10.5" },
-        .{ "14F2511", "10.10.5" },
+        const Kind = enum { unknown, start, end, empty };
+    };
+};
 
-        .{ "15A284", "10.11.0" },
-        .{ "15B42", "10.11.1" },
-        .{ "15C50", "10.11.2" },
-        .{ "15D21", "10.11.3" },
-        .{ "15E65", "10.11.4" },
-        .{ "15F34", "10.11.5" },
-        .{ "15G31", "10.11.6" },
-        .{ "15G1004", "10.11.6" },
-        .{ "15G1011", "10.11.6" },
-        .{ "15G1108", "10.11.6" },
-        .{ "15G1212", "10.11.6" },
-        .{ "15G1217", "10.11.6" },
-        .{ "15G1421", "10.11.6" },
-        .{ "15G1510", "10.11.6" },
-        .{ "15G1611", "10.11.6" },
-        .{ "15G17023", "10.11.6" },
-        .{ "15G18013", "10.11.6" },
-        .{ "15G19009", "10.11.6" },
-        .{ "15G20015", "10.11.6" },
-        .{ "15G21013", "10.11.6" },
-        .{ "15G22010", "10.11.6" },
+test "detect" {
+    const cases = .{
+        .{
+            \\<?xml version="1.0" encoding="UTF-8"?>
+            \\<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+            \\<plist version="1.0">
+            \\<dict>
+            \\    <key>ProductBuildVersion</key>
+            \\    <string>7B85</string>
+            \\    <key>ProductCopyright</key>
+            \\    <string>Apple Computer, Inc. 1983-2003</string>
+            \\    <key>ProductName</key>
+            \\    <string>Mac OS X</string>
+            \\    <key>ProductUserVisibleVersion</key>
+            \\    <string>10.3</string>
+            \\    <key>ProductVersion</key>
+            \\    <string>10.3</string>
+            \\</dict>
+            \\</plist>
+            ,
+            .{ .major = 10, .minor = 3 },
+        },
+        .{
+            \\<?xml version="1.0" encoding="UTF-8"?>
+            \\<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+            \\<plist version="1.0">
+            \\<dict>
+            \\	<key>ProductBuildVersion</key>
+            \\	<string>7W98</string>
+            \\	<key>ProductCopyright</key>
+            \\	<string>Apple Computer, Inc. 1983-2004</string>
+            \\	<key>ProductName</key>
+            \\	<string>Mac OS X</string>
+            \\	<key>ProductUserVisibleVersion</key>
+            \\	<string>10.3.9</string>
+            \\	<key>ProductVersion</key>
+            \\	<string>10.3.9</string>
+            \\</dict>
+            \\</plist>
+            ,
+            .{ .major = 10, .minor = 3, .patch = 9 },
+        },
+        .{
+            \\<?xml version="1.0" encoding="UTF-8"?>
+            \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+            \\<plist version="1.0">
+            \\<dict>
+            \\	<key>ProductBuildVersion</key>
+            \\	<string>19G68</string>
+            \\	<key>ProductCopyright</key>
+            \\	<string>1983-2020 Apple Inc.</string>
+            \\	<key>ProductName</key>
+            \\	<string>Mac OS X</string>
+            \\	<key>ProductUserVisibleVersion</key>
+            \\	<string>10.15.6</string>
+            \\	<key>ProductVersion</key>
+            \\	<string>10.15.6</string>
+            \\	<key>iOSSupportVersion</key>
+            \\	<string>13.6</string>
+            \\</dict>
+            \\</plist>
+            ,
+            .{ .major = 10, .minor = 15, .patch = 6 },
+        },
+        .{
+            \\<?xml version="1.0" encoding="UTF-8"?>
+            \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+            \\<plist version="1.0">
+            \\<dict>
+            \\	<key>ProductBuildVersion</key>
+            \\	<string>20A2408</string>
+            \\	<key>ProductCopyright</key>
+            \\	<string>1983-2020 Apple Inc.</string>
+            \\	<key>ProductName</key>
+            \\	<string>macOS</string>
+            \\	<key>ProductUserVisibleVersion</key>
+            \\	<string>11.0</string>
+            \\	<key>ProductVersion</key>
+            \\	<string>11.0</string>
+            \\	<key>iOSSupportVersion</key>
+            \\	<string>14.2</string>
+            \\</dict>
+            \\</plist>
+            ,
+            .{ .major = 11, .minor = 0 },
+        },
+        .{
+            \\<?xml version="1.0" encoding="UTF-8"?>
+            \\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+            \\<plist version="1.0">
+            \\<dict>
+            \\	<key>ProductBuildVersion</key>
+            \\	<string>20C63</string>
+            \\	<key>ProductCopyright</key>
+            \\	<string>1983-2020 Apple Inc.</string>
+            \\	<key>ProductName</key>
+            \\	<string>macOS</string>
+            \\	<key>ProductUserVisibleVersion</key>
+            \\	<string>11.1</string>
+            \\	<key>ProductVersion</key>
+            \\	<string>11.1</string>
+            \\	<key>iOSSupportVersion</key>
+            \\	<string>14.3</string>
+            \\</dict>
+            \\</plist>
+            ,
+            .{ .major = 11, .minor = 1 },
+        },
+    };
 
-        .{ "16A323", "10.12.0" },
-        .{ "16B2555", "10.12.1" },
-        .{ "16B2657", "10.12.1" },
-        .{ "16C67", "10.12.2" },
-        .{ "16C68", "10.12.2" },
-        .{ "16D32", "10.12.3" },
-        .{ "16E195", "10.12.4" },
-        .{ "16F73", "10.12.5" },
-        .{ "16F2073", "10.12.5" },
-        .{ "16G29", "10.12.6" },
-        .{ "16G1036", "10.12.6" },
-        .{ "16G1114", "10.12.6" },
-        .{ "16G1212", "10.12.6" },
-        .{ "16G1314", "10.12.6" },
-        .{ "16G1408", "10.12.6" },
-        .{ "16G1510", "10.12.6" },
-        .{ "16G1618", "10.12.6" },
-        .{ "16G1710", "10.12.6" },
-        .{ "16G1815", "10.12.6" },
-        .{ "16G1917", "10.12.6" },
-        .{ "16G1918", "10.12.6" },
-        .{ "16G2016", "10.12.6" },
-        .{ "16G2127", "10.12.6" },
-        .{ "16G2128", "10.12.6" },
-        .{ "16G2136", "10.12.6" },
+    inline for (cases) |case| {
+        const ver0 = try parseSystemVersion(case[0]);
+        const ver1: std.builtin.Version = case[1];
+        try testVersionEquality(ver1, ver0);
+    }
+}
 
-        .{ "17A365", "10.13.0" },
-        .{ "17A405", "10.13.0" },
-        .{ "17B48", "10.13.1" },
-        .{ "17B1002", "10.13.1" },
-        .{ "17B1003", "10.13.1" },
-        .{ "17C88", "10.13.2" },
-        .{ "17C89", "10.13.2" },
-        .{ "17C205", "10.13.2" },
-        .{ "17C2205", "10.13.2" },
-        .{ "17D47", "10.13.3" },
-        .{ "17D2047", "10.13.3" },
-        .{ "17D102", "10.13.3" },
-        .{ "17D2102", "10.13.3" },
-        .{ "17E199", "10.13.4" },
-        .{ "17E202", "10.13.4" },
-        .{ "17F77", "10.13.5" },
-        .{ "17G65", "10.13.6" },
-        .{ "17G2208", "10.13.6" },
-        .{ "17G3025", "10.13.6" },
-        .{ "17G4015", "10.13.6" },
-        .{ "17G5019", "10.13.6" },
-        .{ "17G6029", "10.13.6" },
-        .{ "17G6030", "10.13.6" },
-        .{ "17G7024", "10.13.6" },
-        .{ "17G8029", "10.13.6" },
-        .{ "17G8030", "10.13.6" },
-        .{ "17G8037", "10.13.6" },
-        .{ "17G9016", "10.13.6" },
-        .{ "17G10021", "10.13.6" },
-        .{ "17G11023", "10.13.6" },
-        .{ "17G12034", "10.13.6" },
+fn testVersionEquality(expected: std.builtin.Version, got: std.builtin.Version) !void {
+    var b_expected: [64]u8 = undefined;
+    const s_expected: []const u8 = try std.fmt.bufPrint(b_expected[0..], "{}", .{expected});
 
-        .{ "18A391", "10.14.0" },
-        .{ "18B75", "10.14.1" },
-        .{ "18B2107", "10.14.1" },
-        .{ "18B3094", "10.14.1" },
-        .{ "18C54", "10.14.2" },
-        .{ "18D42", "10.14.3" },
-        .{ "18D43", "10.14.3" },
-        .{ "18D109", "10.14.3" },
-        .{ "18E226", "10.14.4" },
-        .{ "18E227", "10.14.4" },
-        .{ "18F132", "10.14.5" },
-        .{ "18G84", "10.14.6" },
-        .{ "18G87", "10.14.6" },
-        .{ "18G95", "10.14.6" },
-        .{ "18G103", "10.14.6" },
-        .{ "18G1012", "10.14.6" },
-        .{ "18G2022", "10.14.6" },
-        .{ "18G3020", "10.14.6" },
-        .{ "18G4032", "10.14.6" },
+    var b_got: [64]u8 = undefined;
+    const s_got: []const u8 = try std.fmt.bufPrint(b_got[0..], "{}", .{got});
 
-        .{ "19A583", "10.15.0" },
-        .{ "19A602", "10.15.0" },
-        .{ "19A603", "10.15.0" },
-        .{ "19B88", "10.15.1" },
-        .{ "19C57", "10.15.2" },
-        .{ "19D76", "10.15.3" },
-        .{ "19E266", "10.15.4" },
-        .{ "19E287", "10.15.4" },
-    };
-    for (known) |pair| {
-        var buf: [32]u8 = undefined;
-        const ver = try version_from_build(pair[0]);
-        const sver = try std.fmt.bufPrint(buf[0..], "{d}.{d}.{d}", .{ ver.major, ver.minor, ver.patch });
-        std.testing.expect(std.mem.eql(u8, sver, pair[1]));
-    }
+    testing.expectEqualStrings(s_expected, s_got);
 }
 
 /// Detect SDK path on Darwin.
lib/std/zig/system.zig
@@ -254,38 +254,7 @@ pub const NativeTargetInfo = struct {
                     os.version_range.windows.min = detected_version;
                     os.version_range.windows.max = detected_version;
                 },
-                .macos => {
-                    var scbuf: [32]u8 = undefined;
-                    var size: usize = undefined;
-
-                    // The osproductversion sysctl was introduced first with 10.13.4 High Sierra.
-                    const key_osproductversion = "kern.osproductversion"; // eg. "10.15.4"
-                    size = scbuf.len;
-                    if (std.os.sysctlbynameZ(key_osproductversion, &scbuf, &size, null, 0)) |_| {
-                        const string_version = scbuf[0 .. size - 1];
-                        if (std.builtin.Version.parse(string_version)) |ver| {
-                            os.version_range.semver.min = ver;
-                            os.version_range.semver.max = ver;
-                        } else |err| switch (err) {
-                            error.Overflow => {},
-                            error.InvalidCharacter => {},
-                            error.InvalidVersion => {},
-                        }
-                    } else |err| switch (err) {
-                        error.UnknownName => {
-                            const key_osversion = "kern.osversion"; // eg. "19E287"
-                            size = scbuf.len;
-                            std.os.sysctlbynameZ(key_osversion, &scbuf, &size, null, 0) catch {
-                                @panic("unable to detect macOS version: " ++ key_osversion);
-                            };
-                            if (macos.version_from_build(scbuf[0 .. size - 1])) |ver| {
-                                os.version_range.semver.min = ver;
-                                os.version_range.semver.max = ver;
-                            } else |_| {}
-                        },
-                        else => @panic("unable to detect macOS version: " ++ key_osproductversion),
-                    }
-                },
+                .macos => macos.detect(&os) catch {}, // valid to ignore any error and keep os defaults
                 .freebsd => {
                     var osreldate: u32 = undefined;
                     var len: usize = undefined;
lib/std/target.zig
@@ -260,7 +260,7 @@ pub const Target = struct {
                     .macos => return .{
                         .semver = .{
                             .min = .{ .major = 10, .minor = 13 },
-                            .max = .{ .major = 10, .minor = 15, .patch = 7 },
+                            .max = .{ .major = 11, .minor = 1 },
                         },
                     },
                     .ios => return .{