From b61678c1b631f78c3a5da4eca2e77ae3577c1093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Thu, 17 Sep 2026 11:54:50 +0200 Subject: [PATCH] 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 --- lib/std/Target/Query.zig | 79 +++++++++---------- lib/std/zig/system.zig | 166 ++++++++++++++++++--------------------- 2 files changed, 113 insertions(+), 132 deletions(-) diff --git a/lib/std/Target/Query.zig b/lib/std/Target/Query.zig index c1a533020b2de773e1c04544ff61a46eb865d583..b889354c5e68d15c1ddc86d825877cb68b8d4632 100644 --- a/lib/std/Target/Query.zig +++ b/lib/std/Target/Query.zig @@ -26,33 +26,37 @@ cpu_features_sub: Target.Cpu.Feature.Set = .empty, /// `null` means native. os_tag: ?Target.Os.Tag = null, -/// `null` means the default version range for `os_tag`. If `os_tag` is `null` (native) -/// then `null` for this field means native. +/// `null` means native when `os_tag` is native; otherwise, the default minimum version for +/// `cpu_arch`, `os_tag`, and `abi`. os_version_min: ?OsVersion = null, -/// When cross compiling, `null` means default (latest known OS version). -/// When `os_tag` is native, `null` means equal to the native OS version. +/// `null` means native when `os_tag` is native; otherwise, the latest known OS version. os_version_max: ?OsVersion = null, -/// `null` means default when cross compiling, or native when `os_tag` is native. -/// If `isGnu()` is `false`, this must be `null` and is ignored. +/// `null` means native when `abi` is native; otherwise, the default minimum version for `cpu_arch`, +/// `os_tag`, and `abi`. +/// +/// Must be `null` when `abi` is `null` or `isGnu()` is `false`. glibc_version: ?SemanticVersion = null, -/// `null` means default when cross compiling, or native when `os_tag` is native. -/// If `isAndroid()` is `false`, this must be `null` and is ignored. +/// `null` means native when `abi` is native; otherwise, the default minimum version for `cpu_arch` +/// and `abi`. +/// +/// Must be `null` when `abi` is `null` or `isAndroid()` is `false`. android_api_level: ?u32 = null, -/// `null` means the native C ABI, if `os_tag` is native, otherwise it means the default C ABI. +/// `null` means native if `os_tag` is native; otherwise, best-effort default for `cpu_arch` and +/// `os_tag`. abi: ?Target.Abi = null, -/// When `os_tag` is `null`, then `null` means native. Otherwise it means the standard path -/// based on the `os_tag`. When `dynamic_linker` is a non-`null` empty string, no dynamic -/// linker is used regardless of `os_tag`. -dynamic_linker: ?Target.DynamicLinker = null, - -/// `null` means default for the cpu/arch/os combo. +/// `null` means default for the `cpu_arch` and `os_tag` combination. ofmt: ?Target.ObjectFormat = null, +/// `null` means native when `abi` is native; otherwise, the standard path based on `cpu_arch`, +/// `os_tag`, and `abi`. A non-`null` empty string means no dynamic linker is used regardless of the +/// values of other fields. +dynamic_linker: ?Target.DynamicLinker = null, + pub const CpuModel = union(enum) { /// Always native native, @@ -175,12 +179,12 @@ pub const ParseOptions = struct { /// parsed CPU Architecture. If native, then this will be "native". Otherwise, it will be "baseline". cpu_features: ?[]const u8 = null, + object_format: ?[]const u8 = null, + /// Absolute path to dynamic linker, to override the default, which is either a natively /// detected path, or a standard path. dynamic_linker: ?[]const u8 = null, - object_format: ?[]const u8 = null, - /// If this is provided, the function will populate some information about parsing failures, /// so that user-friendly error messages can be delivered. diagnostics: ?*Diagnostics = null, @@ -381,12 +385,16 @@ pub fn isNativeCpu(self: Query) bool { } pub fn isNativeOs(self: Query) bool { - return self.os_tag == null and self.os_version_min == null and self.os_version_max == null and - self.dynamic_linker == null and self.glibc_version == null and self.android_api_level == null; + return self.os_tag == null and + self.os_version_min == null and + self.os_version_max == null; } pub fn isNativeAbi(self: Query) bool { - return self.os_tag == null and self.abi == null; + return self.os_tag == null and + self.abi == null and + self.glibc_version == null and + self.android_api_level == null; } pub fn isNativeTriple(self: Query) bool { @@ -394,11 +402,13 @@ pub fn isNativeTriple(self: Query) bool { } pub fn isNative(self: Query) bool { - return self.isNativeTriple() and self.ofmt == null; + return self.isNativeTriple() and + self.ofmt == null and + self.dynamic_linker == null; } pub fn canDetectLibC(self: Query) bool { - if (self.isNativeOs()) return true; + if (self.isNativeOs() and self.isNativeAbi()) return true; if (self.os_tag) |os| { if (builtin.os.tag == .macos and os.isDarwin()) return true; if (os == .linux) { @@ -459,14 +469,14 @@ pub fn zigTriple(self: Query, gpa: Allocator) Allocator.Error![]u8 { } if (self.glibc_version) |v| { - const name = if (self.abi) |abi| @tagName(abi) else "gnu"; + const name = @tagName(self.abi.?); try result.ensureUnusedCapacity(gpa, name.len + 2); result.appendAssumeCapacity('-'); result.appendSliceAssumeCapacity(name); result.appendAssumeCapacity('.'); try formatVersion(v, gpa, &result); } else if (self.android_api_level) |lvl| { - const name = if (self.abi) |abi| @tagName(abi) else "android"; + const name = @tagName(self.abi.?); try result.ensureUnusedCapacity(gpa, name.len + 2); result.appendAssumeCapacity('-'); result.appendSliceAssumeCapacity(name); @@ -538,6 +548,7 @@ pub fn allocDescription(self: Query, allocator: Allocator) ![]u8 { return self.zigTriple(allocator); } +/// Deprecated; set `glibc_version` directly. Will be removed in 0.18.0. pub fn setGnuLibCVersion(self: *Query, major: u32, minor: u32, patch: u32) void { self.glibc_version = SemanticVersion{ .major = major, .minor = minor, .patch = patch }; } @@ -599,8 +610,8 @@ pub fn eql(a: Query, b: Query) bool { if (!versionEqualOpt(a.glibc_version, b.glibc_version)) return false; if (a.android_api_level != b.android_api_level) return false; if (a.abi != b.abi) return false; - if (!dynamicLinkerEqualOpt(a.dynamic_linker, b.dynamic_linker)) return false; if (a.ofmt != b.ofmt) return false; + if (!dynamicLinkerEqualOpt(a.dynamic_linker, b.dynamic_linker)) return false; return true; } @@ -620,24 +631,6 @@ fn dynamicLinkerEqualOpt(a: ?Target.DynamicLinker, b: ?Target.DynamicLinker) boo test parse { const io = std.testing.io; - if (builtin.target.isGnuLibC()) { - var query = try Query.parse(.{}); - query.setGnuLibCVersion(2, 1, 1); - - const text = try query.zigTriple(std.testing.allocator); - defer std.testing.allocator.free(text); - - try std.testing.expectEqualSlices(u8, "native-native-gnu.2.1.1", text); - } - if (builtin.target.abi.isAndroid()) { - var query = try Query.parse(.{}); - query.android_api_level = 30; - - const text = try query.zigTriple(std.testing.allocator); - defer std.testing.allocator.free(text); - - try std.testing.expectEqualSlices(u8, "native-native-android.30", text); - } { const query = try Query.parse(.{ .arch_os_abi = "aarch64-linux", diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 5f9a5c68fe52aa4a9bed491c81adf4edfa12c149..857ae1d2ff1c35feac45eb5b2f40cca4986d74f3 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -419,6 +419,7 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { var result = detectAbiAndDynamicLinker(io, cpu, os, query) catch |err| switch (err) { error.Canceled => |e| return e, error.Unexpected => |e| return e, + error.ApiLevelQueryFailed => |e| return e, error.WouldBlock => return error.Unexpected, error.ConnectionResetByPeer => return error.Unexpected, error.NotOpenForReading => return error.Unexpected, @@ -497,27 +498,6 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { } } - if (builtin.os.tag == .linux and result.isBionicLibC() and query.os_tag == null and query.android_api_level == null) { - result.os.version_range.linux.android = detectAndroidApiLevel(io) catch |err| return switch (err) { - error.InvalidWtf8, - error.InvalidBatchScriptArg, - => unreachable, // Windows-only - error.ApiLevelQueryFailed => |e| e, - else => blk: { - std.log.err("spawning or reading from getprop failed ({s})", .{@errorName(err)}); - switch (err) { - error.SystemResources, - error.FileSystem, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SymLinkLoop, - => |e| break :blk e, - else => break :blk error.ApiLevelQueryFailed, - } - }, - }; - } - return result; } @@ -591,11 +571,13 @@ fn abiAndDynamicLinkerFromFile( ld_info_list: []const LdInfo, query: Target.Query, ) AbiAndDynamicLinkerFromFileError!Target { + assert(query.abi == null); // See the early exit in `detectAbiAndDynamicLinker`. + const io = file_reader.io; var result: Target = .{ .cpu = cpu, .os = os, - .abi = query.abi orelse Target.Abi.default(cpu.arch, os.tag), + .abi = Target.Abi.default(cpu.arch, os.tag), .ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch), .dynamic_linker = query.dynamic_linker orelse .none, }; @@ -609,26 +591,25 @@ fn abiAndDynamicLinkerFromFile( .INTERP => { got_dyn_section = true; - if (look_for_ld) { - const p_filesz = phdr.filesz; - if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; - const filesz: usize = @intCast(p_filesz); - try file_reader.seekTo(phdr.offset); - try file_reader.interface.readSliceAll(result.dynamic_linker.buffer[0..filesz]); - // PT.INTERP includes a null byte in filesz. - const len = filesz - 1; - // dynamic_linker.max_byte is "max", not "len". - // We know it will fit in u8 because we check against dynamic_linker.buffer.len above. - result.dynamic_linker.len = @intCast(len); + var interp: Target.DynamicLinker = .none; + const p_filesz = phdr.filesz; + if (p_filesz < 2) return error.InvalidElfFile; + if (p_filesz > interp.buffer.len) return error.NameTooLong; + const filesz: usize = @intCast(p_filesz); + try file_reader.seekTo(phdr.offset); + try file_reader.interface.readSliceAll(interp.buffer[0..filesz]); + // PT.INTERP includes a null byte in filesz. + const len = filesz - 1; + // dynamic_linker.max_byte is "max", not "len". + // We know it will fit in u8 because we check against dynamic_linker.buffer.len above. + interp.len = @intCast(len); - // Use it to determine ABI. - const full_ld_path = result.dynamic_linker.buffer[0..len]; - for (ld_info_list) |ld_info| { - const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); - if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) { - result.abi = ld_info.abi; - break; - } + // Use it to determine ABI. + for (ld_info_list) |ld_info| { + if (std.mem.endsWith(u8, interp.get().?, fs.path.basename(ld_info.ld.get().?))) { + result.abi = ld_info.abi; + if (look_for_ld) result.dynamic_linker = interp; + break; } } }, @@ -650,6 +631,10 @@ fn abiAndDynamicLinkerFromFile( }; } + if (look_for_ld and result.dynamic_linker.get() == null) { + result.dynamic_linker = .standard(cpu, os.tag, result.abi); + } + if (!got_dyn_section) { return error.StaticElfFile; } @@ -951,17 +936,14 @@ fn glibcVerFromSoFile(file_reader: *Io.File.Reader) !std.SemanticVersion { /// file recursively. If that does not provide the answer, then the function falls back to /// defaults. fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Target.Query) !Target { - const native_target_has_ld = comptime Target.DynamicLinker.kind(builtin.os.tag) != .none; - const is_linux = builtin.target.os.tag == .linux; - const is_illumos = builtin.target.os.tag == .illumos; - const is_darwin = builtin.target.os.tag.isDarwin(); - const have_all_info = query.dynamic_linker != null and - query.abi != null and (!is_linux or query.abi.?.isGnu()); - const os_is_non_native = query.os_tag != null; - // The illumos environment is always the same. - if (!native_target_has_ld or have_all_info or os_is_non_native or is_illumos or is_darwin) { + const native_os_uses_elf = comptime Target.ObjectFormat.default(builtin.target.os.tag, builtin.target.cpu.arch) == .elf; + const native_ld_needs_abi = comptime Target.DynamicLinker.kind(builtin.target.os.tag) == .arch_os_abi; + if (!native_os_uses_elf or !native_ld_needs_abi or query.os_tag != null or query.abi != null) { return defaultAbiAndDynamicLinker(cpu, os, query); } + + assert(os.tag == builtin.target.os.tag); + // The current target's ABI cannot be relied on for this. For example, we may build the zig // compiler for target riscv64-linux-musl and provide a tarball for users to download. // 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 var ld_info_list_buffer: [all_abis.len]LdInfo = undefined; var ld_info_list_len: usize = 0; - switch (Target.DynamicLinker.kind(os.tag)) { - // The OS has no dynamic linker. Leave the list empty and rely on `Abi.default()` to pick - // something sensible in `abiAndDynamicLinkerFromFile()`. - .none => {}, - // The OS has a system-wide dynamic linker. Unfortunately, this implies that there's no - // useful ABI information that we can glean from it merely being present. That means the - // best we can do for this case (for now) is also `Abi.default()`. - .arch_os => {}, - // The OS can have different dynamic linker paths depending on libc/ABI. In this case, we - // need to gather all the valid arch/OS/ABI combinations. `abiAndDynamicLinkerFromFile()` - // will then look for a dynamic linker with a matching path on the system and pick the ABI - // we associated it with here. - .arch_os_abi => for (all_abis) |abi| { - const ld = Target.DynamicLinker.standard(cpu, os.tag, abi); + for (all_abis) |abi| { + const ld: Target.DynamicLinker = .standard(cpu, os.tag, abi); - // Does the generated target triple actually have a standard dynamic linker path? - if (ld.get() == null) continue; + // Does the generated target triple actually have a standard dynamic linker path? + if (ld.get() == null) continue; - ld_info_list_buffer[ld_info_list_len] = .{ - .ld = ld, - .abi = abi, - }; - ld_info_list_len += 1; - }, + ld_info_list_buffer[ld_info_list_len] = .{ + .ld = ld, + .abi = abi, + }; + ld_info_list_len += 1; } 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 // it uses the file it references instead, doing the same logic // recursively in case it finds another shebang line. - var file_name: []const u8 = switch (os.tag) { - // Since /usr/bin/env is hard-coded into the shebang line of many - // portable scripts, it's a reasonably reliable path to start with. - else => "/usr/bin/env", - // Haiku does not have a /usr root directory. - .haiku => "/bin/env", - }; + // Since /usr/bin/env is hard-coded into the shebang line of many + // portable scripts, it's a reasonably reliable path to start with. + var file_name: []const u8 = "/usr/bin/env"; while (true) { 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 error.NetworkNotFound, error.FileTooBig, error.Unexpected, - => |e| if (e == error.FileNotFound and os.tag == .linux and mem.eql(u8, file_name, "/usr/bin/env")) { + => |e| if (builtin.target.os.tag == .linux and e == error.FileNotFound and mem.eql(u8, file_name, "/usr/bin/env")) { // Android does not have a /usr directory, so try again file_name = "/system/bin/env"; continue; @@ -1113,7 +1078,7 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ }; defer file_reader.file.close(io); - return abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) { + var result = abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) { error.FileSystem, error.SystemResources, error.SymLinkLoop, @@ -1129,6 +1094,29 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ return defaultAbiAndDynamicLinker(cpu, os, query); }, }; + + if (builtin.target.os.tag == .linux and result.isBionicLibC() and query.android_api_level == null) { + result.os.version_range.linux.android = detectAndroidApiLevel(io) catch |err| return switch (err) { + error.InvalidWtf8, + error.InvalidBatchScriptArg, + => unreachable, // Windows-only + error.ApiLevelQueryFailed => |e| e, + else => blk: { + std.log.err("spawning or reading from getprop failed ({s})", .{@errorName(err)}); + switch (err) { + error.SystemResources, + error.FileSystem, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SymLinkLoop, + => |e| break :blk e, + else => break :blk error.ApiLevelQueryFailed, + } + }, + }; + } + + return result; } fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, query: Target.Query) Target { @@ -1166,13 +1154,6 @@ fn detectAndroidApiLevel(io: Io) !u32 { var stdout_buf: [92 + 1]u8 = undefined; var reader = child.stdout.?.readerStreaming(io, &.{}); const n = try reader.interface.readSliceShort(&stdout_buf); - const api_level = std.fmt.parseInt(u32, stdout_buf[0 .. n - 1], 10) catch |e| { - std.log.err( - "Could not parse API level, unexpected getprop output '{s}' ({s})", - .{ stdout_buf[0 .. n - 1], @errorName(e) }, - ); - return error.ApiLevelQueryFailed; - }; switch (try child.wait(io)) { .exited => |code| if (code != 0) { @@ -1193,7 +1174,14 @@ fn detectAndroidApiLevel(io: Io) !u32 { }, } - return api_level; + const result = mem.trimEnd(u8, stdout_buf[0..n], "\n"); + return std.fmt.parseInt(u32, result, 10) catch |e| { + std.log.err( + "Could not parse API level, unexpected getprop output '{s}' ({s})", + .{ result, @errorName(e) }, + ); + return error.ApiLevelQueryFailed; + }; } test { -- 2.54.0