In the past, this function attempted to use the executable's own binary if it was dynamically linked to answer both the C ABI question and the dynamic linker question. However, this could be problematic on a system that uses a RUNPATH for the compiler binary, locking it to an older glibc version, while system binaries such as /usr/bin/env use a newer glibc version. The problem is that libc.so.6 glibc version will match that of the system while the dynamic linker will match that of the compiler binary. Executables with these versions mismatching will fail to run.
Therefore, this function works the same regardless of whether the compiler binary is
dynamically or statically linked. It inspects /usr/bin/env as an ELF file to find the
answer to these questions, or if there is a shebang line, then it chases the referenced
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
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) {
return defaultAbiAndDynamicLinker(cpu, os, query);
}
if (query.abi) |abi| {
if (abi.isMusl()) {
// musl implies static linking.
return defaultAbiAndDynamicLinker(cpu, os, query);
}
}
// 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
// and supported by Zig. But that means that we must detect the system ABI here rather than
// relying on `builtin.target`.
const all_abis = comptime blk: {
assert(@backingInt(Target.Abi.none) == 0);
const field_names = std.meta.fieldNames(Target.Abi)[1..];
var array: [field_names.len]Target.Abi = undefined;
for (field_names, 0..) |field_name, i| {
array[i] = @field(Target.Abi, field_name);
}
break :blk array;
};
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, abi);
// 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;
},
}
const ld_info_list = ld_info_list_buffer[0..ld_info_list_len];
var file_reader: Io.File.Reader = undefined;
// According to `man 2 execve`:
//
// The kernel imposes a maximum length on the text
// that follows the "#!" characters at the start of a script;
// characters beyond the limit are ignored.
// Before Linux 5.1, the limit is 127 characters.
// Since Linux 5.1, the limit is 255 characters.
//
// Tests show that bash and zsh consider 255 as total limit,
// *including* "#!" characters and ignoring newline.
// For safety, we set max length as 255 + \n (1).
const max_shebang_line_size = 256;
var file_reader_buffer: [4096]u8 = undefined;
comptime assert(file_reader_buffer.len >= max_shebang_line_size);
// Best case scenario: the executable is dynamically linked, and we can iterate
// over our own shared objects and find a dynamic linker.
const header = elf_file: {
// This block looks for a shebang line in "/usr/bin/env". If it finds
// one, then instead of using "/usr/bin/env" as the ELF file to examine,
// 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",
};
while (true) {
const file = Io.Dir.openFileAbsolute(io, file_name, .{}) catch |err| switch (err) {
error.NoSpaceLeft => return error.Unexpected,
error.NameTooLong => return error.Unexpected,
error.PathAlreadyExists => return error.Unexpected,
error.BadPathName => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
error.FileLocksUnsupported => return error.Unexpected,
error.FileBusy => return error.Unexpected, // opened without write permissions
error.AntivirusInterference => return error.Unexpected, // Windows-only error
error.IsDir,
error.NotDir,
error.AccessDenied,
error.PermissionDenied,
error.NoDevice,
error.FileNotFound,
error.NetworkNotFound,
error.FileTooBig,
error.Unexpected,
=> |e| if (e == error.FileNotFound and os.tag == .linux 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;
} else return error.UnableToOpenElfFile,
else => |e| return e,
};
var is_elf_file = false;
defer if (!is_elf_file) file.close(io);
file_reader = .init(file, io, &file_reader_buffer);
file_name = undefined; // it aliases file_reader_buffer
const header = elf.Header.read(&file_reader.interface) catch |hdr_err| switch (hdr_err) {
error.EndOfStream,
error.InvalidElfMagic,
=> {
const shebang_line = file_reader.interface.takeSentinel('\n') catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
// It's neither an ELF file nor file with shebang line.
error.EndOfStream, error.StreamTooLong => return error.UnhelpfulFile,
};
if (!mem.startsWith(u8, shebang_line, "#!")) return error.UnhelpfulFile;
// We detected shebang, now parse entire line.
// Trim leading "#!", spaces and tabs.
const trimmed_line = mem.trimStart(u8, shebang_line[2..], &.{ ' ', '\t' });
// This line can have:
// * Interpreter path only,
// * Interpreter path and arguments, all separated by space, tab or NUL character.
// And optionally newline at the end.
const path_maybe_args = mem.trimEnd(u8, trimmed_line, "\n");
// Separate path and args.
const path_end = mem.findAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len;
const unvalidated_path = path_maybe_args[0..path_end];
file_name = if (fs.path.isAbsolute(unvalidated_path)) unvalidated_path else return error.RelativeShebang;
continue;
},
error.InvalidElfVersion,
error.InvalidElfClass,
error.InvalidElfEndian,
=> return error.InvalidElfFile,
error.ReadFailed => return file_reader.err.?,
};
is_elf_file = true;
break :elf_file header;
}
};
defer file_reader.file.close(io);
return abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) {
error.FileSystem,
error.SystemResources,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
error.Canceled,
=> |e| return e,
error.ReadFailed => return file_reader.err.?,
else => |e| {
std.log.warn("encountered {t}; falling back to default ABI and dynamic linker", .{e});
return defaultAbiAndDynamicLinker(cpu, os, query);
},
};
}