authorgravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-09-17 11:54:50+02:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-09-17 15:27:52+02:00
logb61678c1b631f78c3a5da4eca2e77ae3577c1093
tree8c9139726a47759cb5435bee648a5dab16df9f8b
parent4d1c5d698fffe9684b72bcd72d60dfd758eb0ccd
signature Signed by SSH key SHA256:7B/LJ7bpR1eX8aCXSr4mtd5M45VMPKcx9zY8e95b5QM

std.Target.Query: rework native ABI detection a bit

Native detection of the libc version now only happens if the ABI component in the triple is native (= omitted in text form), rather than the OS component. So the ABI is either fully native or fully specified with no weird in-between. I think this much more closely matches people's mental model for this stuff; Matthew and I struggled to make sense of the current semantics for almost an hour, and ultimately agreed that it really shouldn't work the way it did prior to this patch. This is technically a breaking change because `native-native-gnu` no longer means "use glibc with the detected native glibc version", and this concept is no longer expressible. I don't consider this a great loss; I couldn't find any usage in the wild that would be broken by this, and I would argue it was more likely to be an unhelpful surprise. On the other hand, this patch makes `native-native-gnueabihf` on e.g. an `x86_64-linux-gnu` system use the default glibc version rather than the native one - which was a behavior I found bizarre. I think the ABI detection code is now also much easier to reason about. closes https://codeberg.org/ziglang/zig/issues/36863

2 files changed, 116 insertions(+), 135 deletions(-)

lib/std/Target/Query.zig+36-43
......@@ -26,33 +26,37 @@ cpu_features_sub: Target.Cpu.Feature.Set = .empty,
2626/// `null` means native.
2727os_tag: ?Target.Os.Tag = null,
2828
29/// `null` means the default version range for `os_tag`. If `os_tag` is `null` (native)
30/// then `null` for this field means native.
29/// `null` means native when `os_tag` is native; otherwise, the default minimum version for
30/// `cpu_arch`, `os_tag`, and `abi`.
3131os_version_min: ?OsVersion = null,
3232
33/// When cross compiling, `null` means default (latest known OS version).
34/// When `os_tag` is native, `null` means equal to the native OS version.
33/// `null` means native when `os_tag` is native; otherwise, the latest known OS version.
3534os_version_max: ?OsVersion = null,
3635
37/// `null` means default when cross compiling, or native when `os_tag` is native.
38/// If `isGnu()` is `false`, this must be `null` and is ignored.
36/// `null` means native when `abi` is native; otherwise, the default minimum version for `cpu_arch`,
37/// `os_tag`, and `abi`.
38///
39/// Must be `null` when `abi` is `null` or `isGnu()` is `false`.
3940glibc_version: ?SemanticVersion = null,
4041
41/// `null` means default when cross compiling, or native when `os_tag` is native.
42/// If `isAndroid()` is `false`, this must be `null` and is ignored.
42/// `null` means native when `abi` is native; otherwise, the default minimum version for `cpu_arch`
43/// and `abi`.
44///
45/// Must be `null` when `abi` is `null` or `isAndroid()` is `false`.
4346android_api_level: ?u32 = null,
4447
45/// `null` means the native C ABI, if `os_tag` is native, otherwise it means the default C ABI.
48/// `null` means native if `os_tag` is native; otherwise, best-effort default for `cpu_arch` and
49/// `os_tag`.
4650abi: ?Target.Abi = null,
4751
48/// When `os_tag` is `null`, then `null` means native. Otherwise it means the standard path
49/// based on the `os_tag`. When `dynamic_linker` is a non-`null` empty string, no dynamic
50/// linker is used regardless of `os_tag`.
51dynamic_linker: ?Target.DynamicLinker = null,
52
53/// `null` means default for the cpu/arch/os combo.
52/// `null` means default for the `cpu_arch` and `os_tag` combination.
5453ofmt: ?Target.ObjectFormat = null,
5554
55/// `null` means native when `abi` is native; otherwise, the standard path based on `cpu_arch`,
56/// `os_tag`, and `abi`. A non-`null` empty string means no dynamic linker is used regardless of the
57/// values of other fields.
58dynamic_linker: ?Target.DynamicLinker = null,
59
5660pub const CpuModel = union(enum) {
5761 /// Always native
5862 native,
......@@ -175,12 +179,12 @@ pub const ParseOptions = struct {
175179 /// parsed CPU Architecture. If native, then this will be "native". Otherwise, it will be "baseline".
176180 cpu_features: ?[]const u8 = null,
177181
182 object_format: ?[]const u8 = null,
183
178184 /// Absolute path to dynamic linker, to override the default, which is either a natively
179185 /// detected path, or a standard path.
180186 dynamic_linker: ?[]const u8 = null,
181187
182 object_format: ?[]const u8 = null,
183
184188 /// If this is provided, the function will populate some information about parsing failures,
185189 /// so that user-friendly error messages can be delivered.
186190 diagnostics: ?*Diagnostics = null,
......@@ -381,12 +385,16 @@ pub fn isNativeCpu(self: Query) bool {
381385}
382386
383387pub fn isNativeOs(self: Query) bool {
384 return self.os_tag == null and self.os_version_min == null and self.os_version_max == null and
385 self.dynamic_linker == null and self.glibc_version == null and self.android_api_level == null;
388 return self.os_tag == null and
389 self.os_version_min == null and
390 self.os_version_max == null;
386391}
387392
388393pub fn isNativeAbi(self: Query) bool {
389 return self.os_tag == null and self.abi == null;
394 return self.os_tag == null and
395 self.abi == null and
396 self.glibc_version == null and
397 self.android_api_level == null;
390398}
391399
392400pub fn isNativeTriple(self: Query) bool {
......@@ -394,11 +402,13 @@ pub fn isNativeTriple(self: Query) bool {
394402}
395403
396404pub fn isNative(self: Query) bool {
397 return self.isNativeTriple() and self.ofmt == null;
405 return self.isNativeTriple() and
406 self.ofmt == null and
407 self.dynamic_linker == null;
398408}
399409
400410pub fn canDetectLibC(self: Query) bool {
401 if (self.isNativeOs()) return true;
411 if (self.isNativeOs() and self.isNativeAbi()) return true;
402412 if (self.os_tag) |os| {
403413 if (builtin.os.tag == .macos and os.isDarwin()) return true;
404414 if (os == .linux) {
......@@ -459,14 +469,14 @@ pub fn zigTriple(self: Query, gpa: Allocator) Allocator.Error![]u8 {
459469 }
460470
461471 if (self.glibc_version) |v| {
462 const name = if (self.abi) |abi| @tagName(abi) else "gnu";
472 const name = @tagName(self.abi.?);
463473 try result.ensureUnusedCapacity(gpa, name.len + 2);
464474 result.appendAssumeCapacity('-');
465475 result.appendSliceAssumeCapacity(name);
466476 result.appendAssumeCapacity('.');
467477 try formatVersion(v, gpa, &result);
468478 } else if (self.android_api_level) |lvl| {
469 const name = if (self.abi) |abi| @tagName(abi) else "android";
479 const name = @tagName(self.abi.?);
470480 try result.ensureUnusedCapacity(gpa, name.len + 2);
471481 result.appendAssumeCapacity('-');
472482 result.appendSliceAssumeCapacity(name);
......@@ -538,6 +548,7 @@ pub fn allocDescription(self: Query, allocator: Allocator) ![]u8 {
538548 return self.zigTriple(allocator);
539549}
540550
551/// Deprecated; set `glibc_version` directly. Will be removed in 0.18.0.
541552pub fn setGnuLibCVersion(self: *Query, major: u32, minor: u32, patch: u32) void {
542553 self.glibc_version = SemanticVersion{ .major = major, .minor = minor, .patch = patch };
543554}
......@@ -599,8 +610,8 @@ pub fn eql(a: Query, b: Query) bool {
599610 if (!versionEqualOpt(a.glibc_version, b.glibc_version)) return false;
600611 if (a.android_api_level != b.android_api_level) return false;
601612 if (a.abi != b.abi) return false;
602 if (!dynamicLinkerEqualOpt(a.dynamic_linker, b.dynamic_linker)) return false;
603613 if (a.ofmt != b.ofmt) return false;
614 if (!dynamicLinkerEqualOpt(a.dynamic_linker, b.dynamic_linker)) return false;
604615
605616 return true;
606617}
......@@ -620,24 +631,6 @@ fn dynamicLinkerEqualOpt(a: ?Target.DynamicLinker, b: ?Target.DynamicLinker) boo
620631test parse {
621632 const io = std.testing.io;
622633
623 if (builtin.target.isGnuLibC()) {
624 var query = try Query.parse(.{});
625 query.setGnuLibCVersion(2, 1, 1);
626
627 const text = try query.zigTriple(std.testing.allocator);
628 defer std.testing.allocator.free(text);
629
630 try std.testing.expectEqualSlices(u8, "native-native-gnu.2.1.1", text);
631 }
632 if (builtin.target.abi.isAndroid()) {
633 var query = try Query.parse(.{});
634 query.android_api_level = 30;
635
636 const text = try query.zigTriple(std.testing.allocator);
637 defer std.testing.allocator.free(text);
638
639 try std.testing.expectEqualSlices(u8, "native-native-android.30", text);
640 }
641634 {
642635 const query = try Query.parse(.{
643636 .arch_os_abi = "aarch64-linux",
lib/std/zig/system.zig+80-92
......@@ -419,6 +419,7 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target {
419419 var result = detectAbiAndDynamicLinker(io, cpu, os, query) catch |err| switch (err) {
420420 error.Canceled => |e| return e,
421421 error.Unexpected => |e| return e,
422 error.ApiLevelQueryFailed => |e| return e,
422423 error.WouldBlock => return error.Unexpected,
423424 error.ConnectionResetByPeer => return error.Unexpected,
424425 error.NotOpenForReading => return error.Unexpected,
......@@ -497,27 +498,6 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target {
497498 }
498499 }
499500
500 if (builtin.os.tag == .linux and result.isBionicLibC() and query.os_tag == null and query.android_api_level == null) {
501 result.os.version_range.linux.android = detectAndroidApiLevel(io) catch |err| return switch (err) {
502 error.InvalidWtf8,
503 error.InvalidBatchScriptArg,
504 => unreachable, // Windows-only
505 error.ApiLevelQueryFailed => |e| e,
506 else => blk: {
507 std.log.err("spawning or reading from getprop failed ({s})", .{@errorName(err)});
508 switch (err) {
509 error.SystemResources,
510 error.FileSystem,
511 error.ProcessFdQuotaExceeded,
512 error.SystemFdQuotaExceeded,
513 error.SymLinkLoop,
514 => |e| break :blk e,
515 else => break :blk error.ApiLevelQueryFailed,
516 }
517 },
518 };
519 }
520
521501 return result;
522502}
523503
......@@ -591,11 +571,13 @@ fn abiAndDynamicLinkerFromFile(
591571 ld_info_list: []const LdInfo,
592572 query: Target.Query,
593573) AbiAndDynamicLinkerFromFileError!Target {
574 assert(query.abi == null); // See the early exit in `detectAbiAndDynamicLinker`.
575
594576 const io = file_reader.io;
595577 var result: Target = .{
596578 .cpu = cpu,
597579 .os = os,
598 .abi = query.abi orelse Target.Abi.default(cpu.arch, os.tag),
580 .abi = Target.Abi.default(cpu.arch, os.tag),
599581 .ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch),
600582 .dynamic_linker = query.dynamic_linker orelse .none,
601583 };
......@@ -609,26 +591,25 @@ fn abiAndDynamicLinkerFromFile(
609591 .INTERP => {
610592 got_dyn_section = true;
611593
612 if (look_for_ld) {
613 const p_filesz = phdr.filesz;
614 if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong;
615 const filesz: usize = @intCast(p_filesz);
616 try file_reader.seekTo(phdr.offset);
617 try file_reader.interface.readSliceAll(result.dynamic_linker.buffer[0..filesz]);
618 // PT.INTERP includes a null byte in filesz.
619 const len = filesz - 1;
620 // dynamic_linker.max_byte is "max", not "len".
621 // We know it will fit in u8 because we check against dynamic_linker.buffer.len above.
622 result.dynamic_linker.len = @intCast(len);
623
624 // Use it to determine ABI.
625 const full_ld_path = result.dynamic_linker.buffer[0..len];
626 for (ld_info_list) |ld_info| {
627 const standard_ld_basename = fs.path.basename(ld_info.ld.get().?);
628 if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) {
629 result.abi = ld_info.abi;
630 break;
631 }
594 var interp: Target.DynamicLinker = .none;
595 const p_filesz = phdr.filesz;
596 if (p_filesz < 2) return error.InvalidElfFile;
597 if (p_filesz > interp.buffer.len) return error.NameTooLong;
598 const filesz: usize = @intCast(p_filesz);
599 try file_reader.seekTo(phdr.offset);
600 try file_reader.interface.readSliceAll(interp.buffer[0..filesz]);
601 // PT.INTERP includes a null byte in filesz.
602 const len = filesz - 1;
603 // dynamic_linker.max_byte is "max", not "len".
604 // We know it will fit in u8 because we check against dynamic_linker.buffer.len above.
605 interp.len = @intCast(len);
606
607 // Use it to determine ABI.
608 for (ld_info_list) |ld_info| {
609 if (std.mem.endsWith(u8, interp.get().?, fs.path.basename(ld_info.ld.get().?))) {
610 result.abi = ld_info.abi;
611 if (look_for_ld) result.dynamic_linker = interp;
612 break;
632613 }
633614 }
634615 },
......@@ -650,6 +631,10 @@ fn abiAndDynamicLinkerFromFile(
650631 };
651632 }
652633
634 if (look_for_ld and result.dynamic_linker.get() == null) {
635 result.dynamic_linker = .standard(cpu, os.tag, result.abi);
636 }
637
653638 if (!got_dyn_section) {
654639 return error.StaticElfFile;
655640 }
......@@ -951,17 +936,14 @@ fn glibcVerFromSoFile(file_reader: *Io.File.Reader) !std.SemanticVersion {
951936/// file recursively. If that does not provide the answer, then the function falls back to
952937/// defaults.
953938fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Target.Query) !Target {
954 const native_target_has_ld = comptime Target.DynamicLinker.kind(builtin.os.tag) != .none;
955 const is_linux = builtin.target.os.tag == .linux;
956 const is_illumos = builtin.target.os.tag == .illumos;
957 const is_darwin = builtin.target.os.tag.isDarwin();
958 const have_all_info = query.dynamic_linker != null and
959 query.abi != null and (!is_linux or query.abi.?.isGnu());
960 const os_is_non_native = query.os_tag != null;
961 // The illumos environment is always the same.
962 if (!native_target_has_ld or have_all_info or os_is_non_native or is_illumos or is_darwin) {
939 const native_os_uses_elf = comptime Target.ObjectFormat.default(builtin.target.os.tag, builtin.target.cpu.arch) == .elf;
940 const native_ld_needs_abi = comptime Target.DynamicLinker.kind(builtin.target.os.tag) == .arch_os_abi;
941 if (!native_os_uses_elf or !native_ld_needs_abi or query.os_tag != null or query.abi != null) {
963942 return defaultAbiAndDynamicLinker(cpu, os, query);
964943 }
944
945 assert(os.tag == builtin.target.os.tag);
946
965947 // The current target's ABI cannot be relied on for this. For example, we may build the zig
966948 // compiler for target riscv64-linux-musl and provide a tarball for users to download.
967949 // A user could then run that zig compiler on riscv64-linux-gnu. This use case is well-defined
......@@ -979,30 +961,17 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
979961 var ld_info_list_buffer: [all_abis.len]LdInfo = undefined;
980962 var ld_info_list_len: usize = 0;
981963
982 switch (Target.DynamicLinker.kind(os.tag)) {
983 // The OS has no dynamic linker. Leave the list empty and rely on `Abi.default()` to pick
984 // something sensible in `abiAndDynamicLinkerFromFile()`.
985 .none => {},
986 // The OS has a system-wide dynamic linker. Unfortunately, this implies that there's no
987 // useful ABI information that we can glean from it merely being present. That means the
988 // best we can do for this case (for now) is also `Abi.default()`.
989 .arch_os => {},
990 // The OS can have different dynamic linker paths depending on libc/ABI. In this case, we
991 // need to gather all the valid arch/OS/ABI combinations. `abiAndDynamicLinkerFromFile()`
992 // will then look for a dynamic linker with a matching path on the system and pick the ABI
993 // we associated it with here.
994 .arch_os_abi => for (all_abis) |abi| {
995 const ld = Target.DynamicLinker.standard(cpu, os.tag, abi);
996
997 // Does the generated target triple actually have a standard dynamic linker path?
998 if (ld.get() == null) continue;
999
1000 ld_info_list_buffer[ld_info_list_len] = .{
1001 .ld = ld,
1002 .abi = abi,
1003 };
1004 ld_info_list_len += 1;
1005 },
964 for (all_abis) |abi| {
965 const ld: Target.DynamicLinker = .standard(cpu, os.tag, abi);
966
967 // Does the generated target triple actually have a standard dynamic linker path?
968 if (ld.get() == null) continue;
969
970 ld_info_list_buffer[ld_info_list_len] = .{
971 .ld = ld,
972 .abi = abi,
973 };
974 ld_info_list_len += 1;
1006975 }
1007976
1008977 const ld_info_list = ld_info_list_buffer[0..ld_info_list_len];
......@@ -1031,13 +1000,9 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
10311000 // it uses the file it references instead, doing the same logic
10321001 // recursively in case it finds another shebang line.
10331002
1034 var file_name: []const u8 = switch (os.tag) {
1035 // Since /usr/bin/env is hard-coded into the shebang line of many
1036 // portable scripts, it's a reasonably reliable path to start with.
1037 else => "/usr/bin/env",
1038 // Haiku does not have a /usr root directory.
1039 .haiku => "/bin/env",
1040 };
1003 // Since /usr/bin/env is hard-coded into the shebang line of many
1004 // portable scripts, it's a reasonably reliable path to start with.
1005 var file_name: []const u8 = "/usr/bin/env";
10411006
10421007 while (true) {
10431008 const file = Io.Dir.openFileAbsolute(io, file_name, .{}) catch |err| switch (err) {
......@@ -1059,7 +1024,7 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
10591024 error.NetworkNotFound,
10601025 error.FileTooBig,
10611026 error.Unexpected,
1062 => |e| if (e == error.FileNotFound and os.tag == .linux and mem.eql(u8, file_name, "/usr/bin/env")) {
1027 => |e| if (builtin.target.os.tag == .linux and e == error.FileNotFound and mem.eql(u8, file_name, "/usr/bin/env")) {
10631028 // Android does not have a /usr directory, so try again
10641029 file_name = "/system/bin/env";
10651030 continue;
......@@ -1113,7 +1078,7 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
11131078 };
11141079 defer file_reader.file.close(io);
11151080
1116 return abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) {
1081 var result = abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) {
11171082 error.FileSystem,
11181083 error.SystemResources,
11191084 error.SymLinkLoop,
......@@ -1129,6 +1094,29 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
11291094 return defaultAbiAndDynamicLinker(cpu, os, query);
11301095 },
11311096 };
1097
1098 if (builtin.target.os.tag == .linux and result.isBionicLibC() and query.android_api_level == null) {
1099 result.os.version_range.linux.android = detectAndroidApiLevel(io) catch |err| return switch (err) {
1100 error.InvalidWtf8,
1101 error.InvalidBatchScriptArg,
1102 => unreachable, // Windows-only
1103 error.ApiLevelQueryFailed => |e| e,
1104 else => blk: {
1105 std.log.err("spawning or reading from getprop failed ({s})", .{@errorName(err)});
1106 switch (err) {
1107 error.SystemResources,
1108 error.FileSystem,
1109 error.ProcessFdQuotaExceeded,
1110 error.SystemFdQuotaExceeded,
1111 error.SymLinkLoop,
1112 => |e| break :blk e,
1113 else => break :blk error.ApiLevelQueryFailed,
1114 }
1115 },
1116 };
1117 }
1118
1119 return result;
11321120}
11331121
11341122fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, query: Target.Query) Target {
......@@ -1166,13 +1154,6 @@ fn detectAndroidApiLevel(io: Io) !u32 {
11661154 var stdout_buf: [92 + 1]u8 = undefined;
11671155 var reader = child.stdout.?.readerStreaming(io, &.{});
11681156 const n = try reader.interface.readSliceShort(&stdout_buf);
1169 const api_level = std.fmt.parseInt(u32, stdout_buf[0 .. n - 1], 10) catch |e| {
1170 std.log.err(
1171 "Could not parse API level, unexpected getprop output '{s}' ({s})",
1172 .{ stdout_buf[0 .. n - 1], @errorName(e) },
1173 );
1174 return error.ApiLevelQueryFailed;
1175 };
11761157
11771158 switch (try child.wait(io)) {
11781159 .exited => |code| if (code != 0) {
......@@ -1193,7 +1174,14 @@ fn detectAndroidApiLevel(io: Io) !u32 {
11931174 },
11941175 }
11951176
1196 return api_level;
1177 const result = mem.trimEnd(u8, stdout_buf[0..n], "\n");
1178 return std.fmt.parseInt(u32, result, 10) catch |e| {
1179 std.log.err(
1180 "Could not parse API level, unexpected getprop output '{s}' ({s})",
1181 .{ result, @errorName(e) },
1182 );
1183 return error.ApiLevelQueryFailed;
1184 };
11971185}
11981186
11991187test {