diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2021-12-02 21:51:14 -0700 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2021-12-02 21:51:14 -0700 |
| commit | f3edff439e2dd8e4055d21507b352141cd5b1718 (patch) | |
| tree | 160b6397c84011c008c0a5671bbdbbcf41c4b5fd | |
| parent | 0cd87102221233c2885faccfcaaac297b8d3b656 (diff) | |
| download | zig-f3edff439e2dd8e4055d21507b352141cd5b1718.tar.gz zig-f3edff439e2dd8e4055d21507b352141cd5b1718.zip | |
improve detection of how to execute binaries on the host
`getExternalExecutor` is moved from `std.zig.CrossTarget` to
`std.zig.system.NativeTargetInfo.getExternalExecutor`.
The function also now communicates a bit more information about *why*
the host is unable to execute a binary. The CLI is updated to report
this information in a useful manner.
`getExternalExecutor` is also improved to detect such patterns as:
* x86_64 is able to execute x86 binaries
* aarch64 is able to execute arm binaries
* etc.
Added qemu-hexagon support to `getExternalExecutor`.
`std.Target.canExecBinaries` of is removed; callers should use the more
powerful `getExternalExecutor` instead.
Now that `zig test` tries to run the resulting binary no matter what,
this commit has a follow-up change to the build system and docgen to
utilize the `getExternalExecutor` function and pass `--test-no-exec`
in some cases to avoid getting the error.
Additionally:
* refactor: extract NativePaths and NativeTargetInfo into their own
files named after the structs.
* small improvement to langref to reduce the complexity of the `callconv`
expression in a couple examples.
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | doc/docgen.zig | 33 | ||||
| -rw-r--r-- | doc/langref.html.in | 18 | ||||
| -rw-r--r-- | lib/std/build.zig | 154 | ||||
| -rw-r--r-- | lib/std/target.zig | 21 | ||||
| -rw-r--r-- | lib/std/zig/CrossTarget.zig | 89 | ||||
| -rw-r--r-- | lib/std/zig/system.zig | 1012 | ||||
| -rw-r--r-- | lib/std/zig/system/NativePaths.zig | 205 | ||||
| -rw-r--r-- | lib/std/zig/system/NativeTargetInfo.zig | 937 | ||||
| -rw-r--r-- | src/main.zig | 120 | ||||
| -rw-r--r-- | src/test.zig | 13 |
11 files changed, 1388 insertions, 1216 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 68ff24246d..c7ebfdfafc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -540,6 +540,8 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/zig/render.zig" "${CMAKE_SOURCE_DIR}/lib/std/zig/string_literal.zig" "${CMAKE_SOURCE_DIR}/lib/std/zig/system.zig" + "${CMAKE_SOURCE_DIR}/lib/std/zig/system/NativePaths.zig" + "${CMAKE_SOURCE_DIR}/lib/std/zig/system/NativeTargetInfo.zig" "${CMAKE_SOURCE_DIR}/lib/std/zig/system/x86.zig" "${CMAKE_SOURCE_DIR}/lib/std/zig/tokenizer.zig" "${CMAKE_SOURCE_DIR}/src/Air.zig" diff --git a/doc/docgen.zig b/doc/docgen.zig index 08502f0b79..29cb8bda5f 100644 --- a/doc/docgen.zig +++ b/doc/docgen.zig @@ -1202,6 +1202,7 @@ fn genHtml( var env_map = try process.getEnvMap(allocator); try env_map.put("ZIG_DEBUG_COLOR", "1"); + const host = try std.zig.system.NativeTargetInfo.detect(allocator, .{}); const builtin_code = try getBuiltinCode(allocator, &env_map, zig_exe); for (toc.nodes) |node| { @@ -1424,13 +1425,17 @@ fn genHtml( var test_args = std.ArrayList([]const u8).init(allocator); defer test_args.deinit(); - try test_args.appendSlice(&[_][]const u8{ zig_exe, "test", tmp_source_file_name }); + try test_args.appendSlice(&[_][]const u8{ + zig_exe, "test", tmp_source_file_name, + }); try shell_out.print("$ zig test {s}.zig ", .{code.name}); switch (code.mode) { .Debug => {}, else => { - try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) }); + try test_args.appendSlice(&[_][]const u8{ + "-O", @tagName(code.mode), + }); try shell_out.print("-O {s} ", .{@tagName(code.mode)}); }, } @@ -1441,8 +1446,26 @@ fn genHtml( if (code.target_str) |triple| { try test_args.appendSlice(&[_][]const u8{ "-target", triple }); try shell_out.print("-target {s} ", .{triple}); + + const cross_target = try std.zig.CrossTarget.parse(.{ + .arch_os_abi = triple, + }); + const target_info = try std.zig.system.NativeTargetInfo.detect( + allocator, + cross_target, + ); + switch (host.getExternalExecutor(target_info, .{ + .link_libc = code.link_libc, + })) { + .native => {}, + else => { + try test_args.appendSlice(&[_][]const u8{"--test-no-exec"}); + try shell_out.writeAll("--test-no-exec"); + }, + } } - const result = exec(allocator, &env_map, test_args.items) catch return parseError(tokenizer, code.source_token, "test failed", .{}); + const result = exec(allocator, &env_map, test_args.items) catch + return parseError(tokenizer, code.source_token, "test failed", .{}); const escaped_stderr = try escapeHtml(allocator, result.stderr); const escaped_stdout = try escapeHtml(allocator, result.stdout); try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout }); @@ -1504,9 +1527,7 @@ fn genHtml( defer test_args.deinit(); try test_args.appendSlice(&[_][]const u8{ - zig_exe, - "test", - tmp_source_file_name, + zig_exe, "test", tmp_source_file_name, }); var mode_arg: []const u8 = ""; switch (code.mode) { diff --git a/doc/langref.html.in b/doc/langref.html.in index 117c533f40..db68f8eb08 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -4763,7 +4763,13 @@ test "noreturn" { <p>Another use case for {#syntax#}noreturn{#endsyntax#} is the {#syntax#}exit{#endsyntax#} function:</p> {#code_begin|test|noreturn_from_exit#} {#target_windows#} -pub extern "kernel32" fn ExitProcess(exit_code: c_uint) callconv(if (@import("builtin").target.cpu.arch == .i386) .Stdcall else .C) noreturn; +const std = @import("std"); +const builtin = @import("builtin"); +const native_arch = builtin.cpu.arch; +const expect = std.testing.expect; + +const WINAPI: std.builtin.CallingConvention = if (native_arch == .i386) .Stdcall else .C; +extern "kernel32" fn ExitProcess(exit_code: c_uint) callconv(WINAPI) noreturn; test "foo" { const value = bar() catch ExitProcess(1); @@ -4774,12 +4780,15 @@ fn bar() anyerror!u32 { return 1234; } -const expect = @import("std").testing.expect; {#code_end#} {#header_close#} + {#header_open|Functions#} {#code_begin|test|functions#} -const expect = @import("std").testing.expect; +const std = @import("std"); +const builtin = @import("builtin"); +const native_arch = builtin.cpu.arch; +const expect = std.testing.expect; // Functions are declared like this fn add(a: i8, b: i8) i8 { @@ -4798,7 +4807,8 @@ export fn sub(a: i8, b: i8) i8 { return a - b; } // at link time, when linking statically, or at runtime, when linking // dynamically. // The callconv specifier changes the calling convention of the function. -extern "kernel32" fn ExitProcess(exit_code: u32) callconv(if (@import("builtin").target.cpu.arch == .i386) .Stdcall else .C) noreturn; +const WINAPI: std.builtin.CallingConvention = if (native_arch == .i386) .Stdcall else .C; +extern "kernel32" fn ExitProcess(exit_code: u32) callconv(WINAPI) noreturn; extern "c" fn atan2(a: f64, b: f64) f64; // The @setCold builtin tells the optimizer that a function is rarely called. diff --git a/lib/std/build.zig b/lib/std/build.zig index 75e6db301e..c8ff1e4c87 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -16,6 +16,7 @@ const BufMap = std.BufMap; const fmt_lib = std.fmt; const File = std.fs.File; const CrossTarget = std.zig.CrossTarget; +const NativeTargetInfo = std.zig.system.NativeTargetInfo; pub const FmtStep = @import("build/FmtStep.zig"); pub const TranslateCStep = @import("build/TranslateCStep.zig"); @@ -86,6 +87,9 @@ pub const Builder = struct { /// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`. glibc_runtimes_dir: ?[]const u8 = null, + /// Information about the native target. Computed before build() is invoked. + host: NativeTargetInfo, + const PkgConfigError = error{ PkgConfigCrashed, PkgConfigFailed, @@ -159,6 +163,8 @@ pub const Builder = struct { const env_map = try allocator.create(BufMap); env_map.* = try process.getEnvMap(allocator); + const host = try NativeTargetInfo.detect(allocator, .{}); + const self = try allocator.create(Builder); self.* = Builder{ .zig_exe = zig_exe, @@ -204,6 +210,7 @@ pub const Builder = struct { .install_path = undefined, .vcpkg_root = VcpkgRoot{ .unattempted = {} }, .args = null, + .host = host, }; try self.top_level_steps.append(&self.install_tls); try self.top_level_steps.append(&self.uninstall_tls); @@ -1436,6 +1443,7 @@ pub const LibExeObjStep = struct { builder: *Builder, name: []const u8, target: CrossTarget = CrossTarget{}, + target_info: NativeTargetInfo, linker_script: ?FileSource = null, version_script: ?[]const u8 = null, out_filename: []const u8, @@ -1655,6 +1663,8 @@ pub const LibExeObjStep = struct { .output_lib_path_source = GeneratedFile{ .step = &self.step }, .output_h_path_source = GeneratedFile{ .step = &self.step }, .output_pdb_path_source = GeneratedFile{ .step = &self.step }, + + .target_info = undefined, // populated in computeOutFileNames }; self.computeOutFileNames(); if (root_src) |rs| rs.addStepDependencies(&self.step); @@ -1662,11 +1672,11 @@ pub const LibExeObjStep = struct { } fn computeOutFileNames(self: *LibExeObjStep) void { - const target_info = std.zig.system.NativeTargetInfo.detect( - self.builder.allocator, - self.target, - ) catch unreachable; - const target = target_info.target; + self.target_info = NativeTargetInfo.detect(self.builder.allocator, self.target) catch + unreachable; + + const target = self.target_info.target; + self.out_filename = std.zig.binNameAlloc(self.builder.allocator, .{ .root_name = self.name, .target = target, @@ -2526,74 +2536,80 @@ pub const LibExeObjStep = struct { try zig_args.append("--test-cmd-bin"); } } - } else switch (self.target.getExternalExecutor()) { - .native => {}, - .unavailable => { - try zig_args.append("--test-no-exec"); - }, - .rosetta => if (builder.enable_rosetta) { - try zig_args.append("--test-cmd-bin"); - } else { - try zig_args.append("--test-no-exec"); - }, - .qemu => |bin_name| ok: { - if (builder.enable_qemu) qemu: { - const need_cross_glibc = self.target.isGnuLibC() and self.is_linking_libc; - const glibc_dir_arg = if (need_cross_glibc) - builder.glibc_runtimes_dir orelse break :qemu - else - null; - try zig_args.append("--test-cmd"); - try zig_args.append(bin_name); - if (glibc_dir_arg) |dir| { - // TODO look into making this a call to `linuxTriple`. This - // needs the directory to be called "i686" rather than - // "i386" which is why we do it manually here. - const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}"; - const cpu_arch = self.target.getCpuArch(); - const os_tag = self.target.getOsTag(); - const abi = self.target.getAbi(); - const cpu_arch_name: []const u8 = if (cpu_arch == .i386) - "i686" + } else { + const need_cross_glibc = self.target.isGnuLibC() and self.is_linking_libc; + + switch (self.builder.host.getExternalExecutor(self.target_info, .{ + .qemu_fixes_dl = need_cross_glibc and builder.glibc_runtimes_dir != null, + .link_libc = self.is_linking_libc, + })) { + .native => {}, + .bad_dl, .bad_os_or_cpu => { + try zig_args.append("--test-no-exec"); + }, + .rosetta => if (builder.enable_rosetta) { + try zig_args.append("--test-cmd-bin"); + } else { + try zig_args.append("--test-no-exec"); + }, + .qemu => |bin_name| ok: { + if (builder.enable_qemu) qemu: { + const glibc_dir_arg = if (need_cross_glibc) + builder.glibc_runtimes_dir orelse break :qemu else - @tagName(cpu_arch); - const full_dir = try std.fmt.allocPrint(builder.allocator, fmt_str, .{ - dir, cpu_arch_name, @tagName(os_tag), @tagName(abi), - }); - + null; try zig_args.append("--test-cmd"); - try zig_args.append("-L"); - try zig_args.append("--test-cmd"); - try zig_args.append(full_dir); + try zig_args.append(bin_name); + if (glibc_dir_arg) |dir| { + // TODO look into making this a call to `linuxTriple`. This + // needs the directory to be called "i686" rather than + // "i386" which is why we do it manually here. + const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}"; + const cpu_arch = self.target.getCpuArch(); + const os_tag = self.target.getOsTag(); + const abi = self.target.getAbi(); + const cpu_arch_name: []const u8 = if (cpu_arch == .i386) + "i686" + else + @tagName(cpu_arch); + const full_dir = try std.fmt.allocPrint(builder.allocator, fmt_str, .{ + dir, cpu_arch_name, @tagName(os_tag), @tagName(abi), + }); + + try zig_args.append("--test-cmd"); + try zig_args.append("-L"); + try zig_args.append("--test-cmd"); + try zig_args.append(full_dir); + } + try zig_args.append("--test-cmd-bin"); + break :ok; } + try zig_args.append("--test-no-exec"); + }, + .wine => |bin_name| if (builder.enable_wine) { + try zig_args.append("--test-cmd"); + try zig_args.append(bin_name); try zig_args.append("--test-cmd-bin"); - break :ok; - } - try zig_args.append("--test-no-exec"); - }, - .wine => |bin_name| if (builder.enable_wine) { - try zig_args.append("--test-cmd"); - try zig_args.append(bin_name); - try zig_args.append("--test-cmd-bin"); - } else { - try zig_args.append("--test-no-exec"); - }, - .wasmtime => |bin_name| if (builder.enable_wasmtime) { - try zig_args.append("--test-cmd"); - try zig_args.append(bin_name); - try zig_args.append("--test-cmd"); - try zig_args.append("--dir=."); - try zig_args.append("--test-cmd-bin"); - } else { - try zig_args.append("--test-no-exec"); - }, - .darling => |bin_name| if (builder.enable_darling) { - try zig_args.append("--test-cmd"); - try zig_args.append(bin_name); - try zig_args.append("--test-cmd-bin"); - } else { - try zig_args.append("--test-no-exec"); - }, + } else { + try zig_args.append("--test-no-exec"); + }, + .wasmtime => |bin_name| if (builder.enable_wasmtime) { + try zig_args.append("--test-cmd"); + try zig_args.append(bin_name); + try zig_args.append("--test-cmd"); + try zig_args.append("--dir=."); + try zig_args.append("--test-cmd-bin"); + } else { + try zig_args.append("--test-no-exec"); + }, + .darling => |bin_name| if (builder.enable_darling) { + try zig_args.append("--test-cmd"); + try zig_args.append(bin_name); + try zig_args.append("--test-cmd-bin"); + } else { + try zig_args.append("--test-no-exec"); + }, + } } for (self.packages.items) |pkg| { diff --git a/lib/std/target.zig b/lib/std/target.zig index 3f44b19bc2..c6ee53157d 100644 --- a/lib/std/target.zig +++ b/lib/std/target.zig @@ -1689,27 +1689,6 @@ pub const Target = struct { } } - /// Return whether or not the given host target is capable of executing natively executables - /// of the other target. - pub fn canExecBinariesOf(host_target: Target, binary_target: Target) bool { - if (host_target.os.tag != binary_target.os.tag) - return false; - - if (host_target.cpu.arch == binary_target.cpu.arch) - return true; - - if (host_target.cpu.arch == .x86_64 and binary_target.cpu.arch == .i386) - return true; - - if (host_target.cpu.arch == .aarch64 and binary_target.cpu.arch == .arm) - return true; - - if (host_target.cpu.arch == .aarch64_be and binary_target.cpu.arch == .armeb) - return true; - - return false; - } - /// 0c spim little-endian MIPS 3000 family /// 1c 68000 Motorola MC68000 /// 2c 68020 Motorola MC68020 diff --git a/lib/std/zig/CrossTarget.zig b/lib/std/zig/CrossTarget.zig index 96213b1471..05567d7d09 100644 --- a/lib/std/zig/CrossTarget.zig +++ b/lib/std/zig/CrossTarget.zig @@ -610,95 +610,6 @@ pub fn vcpkgTriplet(self: CrossTarget, allocator: mem.Allocator, linkage: VcpkgL return std.fmt.allocPrint(allocator, "{s}-{s}{s}", .{ arch, os, static_suffix }); } -pub const Executor = union(enum) { - native, - rosetta, - qemu: []const u8, - wine: []const u8, - wasmtime: []const u8, - darling: []const u8, - unavailable, -}; - -/// Note that even a `CrossTarget` which returns `false` for `isNative` could still be natively executed. -/// For example `-target arm-native` running on an aarch64 host. -pub fn getExternalExecutor(self: CrossTarget) Executor { - const cpu_arch = self.getCpuArch(); - const os_tag = self.getOsTag(); - const os_match = os_tag == builtin.os.tag; - - // If the OS and CPU arch match, the binary can be considered native. - // TODO additionally match the CPU features. This `getExternalExecutor` function should - // be moved to std.Target and match any chosen target against the native target. - if (os_match and cpu_arch == builtin.cpu.arch) { - // However, we also need to verify that the dynamic linker path is valid. - if (self.os_tag == null) { - return .native; - } - // TODO here we call toTarget, a deprecated function, because of the above TODO about moving - // this code to std.Target. - const opt_dl = self.dynamic_linker.get() orelse self.toTarget().standardDynamicLinkerPath().get(); - if (opt_dl) |dl| blk: { - std.fs.cwd().access(dl, .{}) catch break :blk; - return .native; - } - } - // If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2 - // to emulate the foreign architecture. - if (os_match and os_tag == .macos and builtin.cpu.arch == .aarch64) { - return switch (cpu_arch) { - .x86_64 => .rosetta, - else => .unavailable, - }; - } - - // If the OS matches, we can use QEMU to emulate a foreign architecture. - if (os_match) { - return switch (cpu_arch) { - .aarch64 => Executor{ .qemu = "qemu-aarch64" }, - .aarch64_be => Executor{ .qemu = "qemu-aarch64_be" }, - .arm => Executor{ .qemu = "qemu-arm" }, - .armeb => Executor{ .qemu = "qemu-armeb" }, - .i386 => Executor{ .qemu = "qemu-i386" }, - .mips => Executor{ .qemu = "qemu-mips" }, - .mipsel => Executor{ .qemu = "qemu-mipsel" }, - .mips64 => Executor{ .qemu = "qemu-mips64" }, - .mips64el => Executor{ .qemu = "qemu-mips64el" }, - .powerpc => Executor{ .qemu = "qemu-ppc" }, - .powerpc64 => Executor{ .qemu = "qemu-ppc64" }, - .powerpc64le => Executor{ .qemu = "qemu-ppc64le" }, - .riscv32 => Executor{ .qemu = "qemu-riscv32" }, - .riscv64 => Executor{ .qemu = "qemu-riscv64" }, - .s390x => Executor{ .qemu = "qemu-s390x" }, - .sparc => Executor{ .qemu = "qemu-sparc" }, - .x86_64 => Executor{ .qemu = "qemu-x86_64" }, - else => return .unavailable, - }; - } - - switch (os_tag) { - .windows => switch (cpu_arch.ptrBitWidth()) { - 32 => return Executor{ .wine = "wine" }, - 64 => return Executor{ .wine = "wine64" }, - else => return .unavailable, - }, - .wasi => switch (cpu_arch.ptrBitWidth()) { - 32 => return Executor{ .wasmtime = "wasmtime" }, - else => return .unavailable, - }, - .macos => { - // TODO loosen this check once upstream adds QEMU-based emulation - // layer for non-host architectures: - // https://github.com/darlinghq/darling/issues/863 - if (cpu_arch != builtin.cpu.arch) { - return .unavailable; - } - return Executor{ .darling = "darling" }; - }, - else => return .unavailable, - } -} - pub fn isGnuLibC(self: CrossTarget) bool { return Target.isGnuLibC_os_tag_abi(self.getOsTag(), self.getAbi()); } diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 5ba0d8198c..3ec92c8274 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -1,1007 +1,15 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const elf = std.elf; -const mem = std.mem; -const fs = std.fs; -const Allocator = std.mem.Allocator; -const ArrayList = std.ArrayList; -const assert = std.debug.assert; -const process = std.process; -const Target = std.Target; -const CrossTarget = std.zig.CrossTarget; -const native_endian = builtin.cpu.arch.endian(); -const linux = @import("system/linux.zig"); +pub const NativePaths = @import("system/NativePaths.zig"); +pub const NativeTargetInfo = @import("system/NativeTargetInfo.zig"); + pub const windows = @import("system/windows.zig"); pub const darwin = @import("system/darwin.zig"); - -pub const NativePaths = struct { - include_dirs: ArrayList([:0]u8), - lib_dirs: ArrayList([:0]u8), - framework_dirs: ArrayList([:0]u8), - rpaths: ArrayList([:0]u8), - warnings: ArrayList([:0]u8), - - pub fn detect(allocator: Allocator, native_info: NativeTargetInfo) !NativePaths { - const native_target = native_info.target; - - var self: NativePaths = .{ - .include_dirs = ArrayList([:0]u8).init(allocator), - .lib_dirs = ArrayList([:0]u8).init(allocator), - .framework_dirs = ArrayList([:0]u8).init(allocator), - .rpaths = ArrayList([:0]u8).init(allocator), - .warnings = ArrayList([:0]u8).init(allocator), - }; - errdefer self.deinit(); - - var is_nix = false; - if (process.getEnvVarOwned(allocator, "NIX_CFLAGS_COMPILE")) |nix_cflags_compile| { - defer allocator.free(nix_cflags_compile); - - is_nix = true; - var it = mem.tokenize(u8, nix_cflags_compile, " "); - while (true) { - const word = it.next() orelse break; - if (mem.eql(u8, word, "-isystem")) { - const include_path = it.next() orelse { - try self.addWarning("Expected argument after -isystem in NIX_CFLAGS_COMPILE"); - break; - }; - try self.addIncludeDir(include_path); - } else { - if (mem.startsWith(u8, word, "-frandom-seed=")) { - continue; - } - try self.addWarningFmt("Unrecognized C flag from NIX_CFLAGS_COMPILE: {s}", .{word}); - } - } - } else |err| switch (err) { - error.InvalidUtf8 => {}, - error.EnvironmentVariableNotFound => {}, - error.OutOfMemory => |e| return e, - } - if (process.getEnvVarOwned(allocator, "NIX_LDFLAGS")) |nix_ldflags| { - defer allocator.free(nix_ldflags); - - is_nix = true; - var it = mem.tokenize(u8, nix_ldflags, " "); - while (true) { - const word = it.next() orelse break; - if (mem.eql(u8, word, "-rpath")) { - const rpath = it.next() orelse { - try self.addWarning("Expected argument after -rpath in NIX_LDFLAGS"); - break; - }; - try self.addRPath(rpath); - } else if (word.len > 2 and word[0] == '-' and word[1] == 'L') { - const lib_path = word[2..]; - try self.addLibDir(lib_path); - } else { - try self.addWarningFmt("Unrecognized C flag from NIX_LDFLAGS: {s}", .{word}); - break; - } - } - } else |err| switch (err) { - error.InvalidUtf8 => {}, - error.EnvironmentVariableNotFound => {}, - error.OutOfMemory => |e| return e, - } - if (is_nix) { - return self; - } - - if (comptime builtin.target.isDarwin()) { - try self.addIncludeDir("/usr/include"); - try self.addIncludeDir("/usr/local/include"); - - try self.addLibDir("/usr/lib"); - try self.addLibDir("/usr/local/lib"); - - try self.addFrameworkDir("/Library/Frameworks"); - try self.addFrameworkDir("/System/Library/Frameworks"); - - return self; - } - - if (comptime native_target.os.tag == .solaris) { - try self.addLibDir("/usr/lib/64"); - try self.addLibDir("/usr/local/lib/64"); - try self.addLibDir("/lib/64"); - - try self.addIncludeDir("/usr/include"); - try self.addIncludeDir("/usr/local/include"); - - return self; - } - - if (native_target.os.tag != .windows) { - const triple = try native_target.linuxTriple(allocator); - const qual = native_target.cpu.arch.ptrBitWidth(); - - // TODO: $ ld --verbose | grep SEARCH_DIR - // the output contains some paths that end with lib64, maybe include them too? - // TODO: what is the best possible order of things? - // TODO: some of these are suspect and should only be added on some systems. audit needed. - - try self.addIncludeDir("/usr/local/include"); - try self.addLibDirFmt("/usr/local/lib{d}", .{qual}); - try self.addLibDir("/usr/local/lib"); - - try self.addIncludeDirFmt("/usr/include/{s}", .{triple}); - try self.addLibDirFmt("/usr/lib/{s}", .{triple}); - - try self.addIncludeDir("/usr/include"); - try self.addLibDirFmt("/lib{d}", .{qual}); - try self.addLibDir("/lib"); - try self.addLibDirFmt("/usr/lib{d}", .{qual}); - try self.addLibDir("/usr/lib"); - - // example: on a 64-bit debian-based linux distro, with zlib installed from apt: - // zlib.h is in /usr/include (added above) - // libz.so.1 is in /lib/x86_64-linux-gnu (added here) - try self.addLibDirFmt("/lib/{s}", .{triple}); - } - - return self; - } - - pub fn deinit(self: *NativePaths) void { - deinitArray(&self.include_dirs); - deinitArray(&self.lib_dirs); - deinitArray(&self.framework_dirs); - deinitArray(&self.rpaths); - deinitArray(&self.warnings); - self.* = undefined; - } - - fn deinitArray(array: *ArrayList([:0]u8)) void { - for (array.items) |item| { - array.allocator.free(item); - } - array.deinit(); - } - - pub fn addIncludeDir(self: *NativePaths, s: []const u8) !void { - return self.appendArray(&self.include_dirs, s); - } - - pub fn addIncludeDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { - const item = try std.fmt.allocPrintZ(self.include_dirs.allocator, fmt, args); - errdefer self.include_dirs.allocator.free(item); - try self.include_dirs.append(item); - } - - pub fn addLibDir(self: *NativePaths, s: []const u8) !void { - return self.appendArray(&self.lib_dirs, s); - } - - pub fn addLibDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { - const item = try std.fmt.allocPrintZ(self.lib_dirs.allocator, fmt, args); - errdefer self.lib_dirs.allocator.free(item); - try self.lib_dirs.append(item); - } - - pub fn addWarning(self: *NativePaths, s: []const u8) !void { - return self.appendArray(&self.warnings, s); - } - - pub fn addFrameworkDir(self: *NativePaths, s: []const u8) !void { - return self.appendArray(&self.framework_dirs, s); - } - - pub fn addFrameworkDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { - const item = try std.fmt.allocPrintZ(self.framework_dirs.allocator, fmt, args); - errdefer self.framework_dirs.allocator.free(item); - try self.framework_dirs.append(item); - } - - pub fn addWarningFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { - const item = try std.fmt.allocPrintZ(self.warnings.allocator, fmt, args); - errdefer self.warnings.allocator.free(item); - try self.warnings.append(item); - } - - pub fn addRPath(self: *NativePaths, s: []const u8) !void { - return self.appendArray(&self.rpaths, s); - } - - fn appendArray(self: *NativePaths, array: *ArrayList([:0]u8), s: []const u8) !void { - _ = self; - const item = try array.allocator.dupeZ(u8, s); - errdefer array.allocator.free(item); - try array.append(item); - } -}; - -pub const NativeTargetInfo = struct { - target: Target, - - dynamic_linker: DynamicLinker = DynamicLinker{}, - - pub const DynamicLinker = Target.DynamicLinker; - - pub const DetectError = error{ - OutOfMemory, - FileSystem, - SystemResources, - SymLinkLoop, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - DeviceBusy, - OSVersionDetectionFail, - }; - - /// Given a `CrossTarget`, which specifies in detail which parts of the target should be detected - /// natively, which should be standard or default, and which are provided explicitly, this function - /// resolves the native components by detecting the native system, and then resolves standard/default parts - /// relative to that. - /// Any resources this function allocates are released before returning, and so there is no - /// deinitialization method. - /// TODO Remove the Allocator requirement from this function. - pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!NativeTargetInfo { - var os = cross_target.getOsTag().defaultVersionRange(cross_target.getCpuArch()); - if (cross_target.os_tag == null) { - switch (builtin.target.os.tag) { - .linux => { - const uts = std.os.uname(); - const release = mem.sliceTo(&uts.release, 0); - // The release field sometimes has a weird format, - // `Version.parse` will attempt to find some meaningful interpretation. - if (std.builtin.Version.parse(release)) |ver| { - os.version_range.linux.range.min = ver; - os.version_range.linux.range.max = ver; - } else |err| switch (err) { - error.Overflow => {}, - error.InvalidCharacter => {}, - error.InvalidVersion => {}, - } - }, - .solaris => { - const uts = std.os.uname(); - const release = mem.sliceTo(&uts.release, 0); - if (std.builtin.Version.parse(release)) |ver| { - os.version_range.semver.min = ver; - os.version_range.semver.max = ver; - } else |err| switch (err) { - error.Overflow => {}, - error.InvalidCharacter => {}, - error.InvalidVersion => {}, - } - }, - .windows => { - const detected_version = windows.detectRuntimeVersion(); - os.version_range.windows.min = detected_version; - os.version_range.windows.max = detected_version; - }, - .macos => try darwin.macos.detect(&os), - .freebsd, .netbsd, .dragonfly => { - const key = switch (builtin.target.os.tag) { - .freebsd => "kern.osreldate", - .netbsd, .dragonfly => "kern.osrevision", - else => unreachable, - }; - var value: u32 = undefined; - var len: usize = @sizeOf(@TypeOf(value)); - - std.os.sysctlbynameZ(key, &value, &len, null, 0) catch |err| switch (err) { - error.NameTooLong => unreachable, // constant, known good value - error.PermissionDenied => unreachable, // only when setting values, - error.SystemResources => unreachable, // memory already on the stack - error.UnknownName => unreachable, // constant, known good value - error.Unexpected => return error.OSVersionDetectionFail, - }; - - switch (builtin.target.os.tag) { - .freebsd => { - // https://www.freebsd.org/doc/en_US.ISO8859-1/books/porters-handbook/versions.html - // Major * 100,000 has been convention since FreeBSD 2.2 (1997) - // Minor * 1(0),000 summed has been convention since FreeBSD 2.2 (1997) - // e.g. 492101 = 4.11-STABLE = 4.(9+2) - const major = value / 100_000; - const minor1 = value % 100_000 / 10_000; // usually 0 since 5.1 - const minor2 = value % 10_000 / 1_000; // 0 before 5.1, minor version since - const patch = value % 1_000; - os.version_range.semver.min = .{ .major = major, .minor = minor1 + minor2, .patch = patch }; - os.version_range.semver.max = os.version_range.semver.min; - }, - .netbsd => { - // #define __NetBSD_Version__ MMmmrrpp00 - // - // M = major version - // m = minor version; a minor number of 99 indicates current. - // r = 0 (*) - // p = patchlevel - const major = value / 100_000_000; - const minor = value % 100_000_000 / 1_000_000; - const patch = value % 10_000 / 100; - os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch }; - os.version_range.semver.max = os.version_range.semver.min; - }, - .dragonfly => { - // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/cb2cde83771754aeef9bb3251ee48959138dec87/Makefile.inc1#L15-L17 - // flat base10 format: Mmmmpp - // M = major - // m = minor; odd-numbers indicate current dev branch - // p = patch - const major = value / 100_000; - const minor = value % 100_000 / 100; - const patch = value % 100; - os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch }; - os.version_range.semver.max = os.version_range.semver.min; - }, - else => unreachable, - } - }, - .openbsd => { - const mib: [2]c_int = [_]c_int{ - std.os.CTL.KERN, - std.os.KERN.OSRELEASE, - }; - var buf: [64]u8 = undefined; - var len: usize = buf.len; - - std.os.sysctl(&mib, &buf, &len, null, 0) catch |err| switch (err) { - error.NameTooLong => unreachable, // constant, known good value - error.PermissionDenied => unreachable, // only when setting values, - error.SystemResources => unreachable, // memory already on the stack - error.UnknownName => unreachable, // constant, known good value - error.Unexpected => return error.OSVersionDetectionFail, - }; - - if (std.builtin.Version.parse(buf[0 .. len - 1])) |ver| { - os.version_range.semver.min = ver; - os.version_range.semver.max = ver; - } else |_| { - return error.OSVersionDetectionFail; - } - }, - else => { - // Unimplemented, fall back to default version range. - }, - } - } - - if (cross_target.os_version_min) |min| switch (min) { - .none => {}, - .semver => |semver| switch (cross_target.getOsTag()) { - .linux => os.version_range.linux.range.min = semver, - else => os.version_range.semver.min = semver, - }, - .windows => |win_ver| os.version_range.windows.min = win_ver, - }; - - if (cross_target.os_version_max) |max| switch (max) { - .none => {}, - .semver => |semver| switch (cross_target.getOsTag()) { - .linux => os.version_range.linux.range.max = semver, - else => os.version_range.semver.max = semver, - }, - .windows => |win_ver| os.version_range.windows.max = win_ver, - }; - - if (cross_target.glibc_version) |glibc| { - assert(cross_target.isGnuLibC()); - os.version_range.linux.glibc = glibc; - } - - // Until https://github.com/ziglang/zig/issues/4592 is implemented (support detecting the - // native CPU architecture as being different than the current target), we use this: - const cpu_arch = cross_target.getCpuArch(); - - var cpu = switch (cross_target.cpu_model) { - .native => detectNativeCpuAndFeatures(cpu_arch, os, cross_target), - .baseline => Target.Cpu.baseline(cpu_arch), - .determined_by_cpu_arch => if (cross_target.cpu_arch == null) - detectNativeCpuAndFeatures(cpu_arch, os, cross_target) - else - Target.Cpu.baseline(cpu_arch), - .explicit => |model| model.toCpu(cpu_arch), - } orelse backup_cpu_detection: { - break :backup_cpu_detection Target.Cpu.baseline(cpu_arch); - }; - var result = try detectAbiAndDynamicLinker(allocator, cpu, os, cross_target); - // For x86, we need to populate some CPU feature flags depending on architecture - // and mode: - // * 16bit_mode => if the abi is code16 - // * 32bit_mode => if the arch is i386 - // However, the "mode" flags can be used as overrides, so if the user explicitly - // sets one of them, that takes precedence. - switch (cpu_arch) { - .i386 => { - if (!std.Target.x86.featureSetHasAny(cross_target.cpu_features_add, .{ - .@"16bit_mode", .@"32bit_mode", - })) { - switch (result.target.abi) { - .code16 => result.target.cpu.features.addFeature( - @enumToInt(std.Target.x86.Feature.@"16bit_mode"), - ), - else => result.target.cpu.features.addFeature( - @enumToInt(std.Target.x86.Feature.@"32bit_mode"), - ), - } - } - }, - .arm, .armeb => { - // XXX What do we do if the target has the noarm feature? - // What do we do if the user specifies +thumb_mode? - }, - .thumb, .thumbeb => { - result.target.cpu.features.addFeature( - @enumToInt(std.Target.arm.Feature.thumb_mode), - ); - }, - else => {}, - } - cross_target.updateCpuFeatures(&result.target.cpu.features); - return result; - } - - /// First we attempt to use the executable's own binary. If it is dynamically - /// linked, then it should answer both the C ABI question and the dynamic linker question. - /// If it is statically linked, then we try /usr/bin/env. If that does not provide the answer, then - /// we fall back to the defaults. - /// TODO Remove the Allocator requirement from this function. - fn detectAbiAndDynamicLinker( - allocator: Allocator, - cpu: Target.Cpu, - os: Target.Os, - cross_target: CrossTarget, - ) DetectError!NativeTargetInfo { - const native_target_has_ld = comptime builtin.target.hasDynamicLinker(); - const is_linux = builtin.target.os.tag == .linux; - const have_all_info = cross_target.dynamic_linker.get() != null and - cross_target.abi != null and (!is_linux or cross_target.abi.?.isGnu()); - const os_is_non_native = cross_target.os_tag != null; - if (!native_target_has_ld or have_all_info or os_is_non_native) { - return defaultAbiAndDynamicLinker(cpu, os, cross_target); - } - if (cross_target.abi) |abi| { - if (abi.isMusl()) { - // musl implies static linking. - return defaultAbiAndDynamicLinker(cpu, os, cross_target); - } - } - // 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(@enumToInt(Target.Abi.none) == 0); - const fields = std.meta.fields(Target.Abi)[1..]; - var array: [fields.len]Target.Abi = undefined; - inline for (fields) |field, 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; - - for (all_abis) |abi| { - // This may be a nonsensical parameter. We detect this with error.UnknownDynamicLinkerPath and - // skip adding it to `ld_info_list`. - const target: Target = .{ - .cpu = cpu, - .os = os, - .abi = abi, - }; - const ld = target.standardDynamicLinkerPath(); - 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]; - - // Best case scenario: the executable is dynamically linked, and we can iterate - // over our own shared objects and find a dynamic linker. - self_exe: { - const lib_paths = try std.process.getSelfExeSharedLibPaths(allocator); - defer { - for (lib_paths) |lib_path| { - allocator.free(lib_path); - } - allocator.free(lib_paths); - } - - var found_ld_info: LdInfo = undefined; - var found_ld_path: [:0]const u8 = undefined; - - // Look for dynamic linker. - // This is O(N^M) but typical case here is N=2 and M=10. - find_ld: for (lib_paths) |lib_path| { - for (ld_info_list) |ld_info| { - const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); - if (std.mem.endsWith(u8, lib_path, standard_ld_basename)) { - found_ld_info = ld_info; - found_ld_path = lib_path; - break :find_ld; - } - } - } else break :self_exe; - - // Look for glibc version. - var os_adjusted = os; - if (builtin.target.os.tag == .linux and found_ld_info.abi.isGnu() and - cross_target.glibc_version == null) - { - for (lib_paths) |lib_path| { - if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) { - os_adjusted.version_range.linux.glibc = glibcVerFromSO(lib_path) catch |err| switch (err) { - error.UnrecognizedGnuLibCFileName => continue, - error.InvalidGnuLibCVersion => continue, - error.GnuLibCVersionUnavailable => continue, - else => |e| return e, - }; - break; - } - } - } - - var result: NativeTargetInfo = .{ - .target = .{ - .cpu = cpu, - .os = os_adjusted, - .abi = cross_target.abi orelse found_ld_info.abi, - }, - .dynamic_linker = if (cross_target.dynamic_linker.get() == null) - DynamicLinker.init(found_ld_path) - else - cross_target.dynamic_linker, - }; - return result; - } - - const env_file = std.fs.openFileAbsoluteZ("/usr/bin/env", .{}) catch |err| switch (err) { - error.NoSpaceLeft => unreachable, - error.NameTooLong => unreachable, - error.PathAlreadyExists => unreachable, - error.SharingViolation => unreachable, - error.InvalidUtf8 => unreachable, - error.BadPathName => unreachable, - error.PipeBusy => unreachable, - error.FileLocksNotSupported => unreachable, - error.WouldBlock => unreachable, - - error.IsDir, - error.NotDir, - error.AccessDenied, - error.NoDevice, - error.FileNotFound, - error.FileTooBig, - error.Unexpected, - => return defaultAbiAndDynamicLinker(cpu, os, cross_target), - - else => |e| return e, - }; - defer env_file.close(); - - // If Zig is statically linked, such as via distributed binary static builds, the above - // trick won't work. The next thing we fall back to is the same thing, but for /usr/bin/env. - // Since that path is hard-coded into the shebang line of many portable scripts, it's a - // reasonably reliable path to check for. - return abiAndDynamicLinkerFromFile(env_file, cpu, os, ld_info_list, cross_target) catch |err| switch (err) { - error.FileSystem, - error.SystemResources, - error.SymLinkLoop, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - => |e| return e, - - error.UnableToReadElfFile, - error.InvalidElfClass, - error.InvalidElfVersion, - error.InvalidElfEndian, - error.InvalidElfFile, - error.InvalidElfMagic, - error.Unexpected, - error.UnexpectedEndOfFile, - error.NameTooLong, - // Finally, we fall back on the standard path. - => defaultAbiAndDynamicLinker(cpu, os, cross_target), - }; - } - - const glibc_so_basename = "libc.so.6"; - - fn glibcVerFromSO(so_path: [:0]const u8) !std.builtin.Version { - var link_buf: [std.os.PATH_MAX]u8 = undefined; - const link_name = std.os.readlinkZ(so_path.ptr, &link_buf) catch |err| switch (err) { - error.AccessDenied => return error.GnuLibCVersionUnavailable, - error.FileSystem => return error.FileSystem, - error.SymLinkLoop => return error.SymLinkLoop, - error.NameTooLong => unreachable, - error.NotLink => return error.GnuLibCVersionUnavailable, - error.FileNotFound => return error.GnuLibCVersionUnavailable, - error.SystemResources => return error.SystemResources, - error.NotDir => return error.GnuLibCVersionUnavailable, - error.Unexpected => return error.GnuLibCVersionUnavailable, - error.InvalidUtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.UnsupportedReparsePointType => unreachable, // Windows only - }; - return glibcVerFromLinkName(link_name); - } - - fn glibcVerFromLinkName(link_name: []const u8) !std.builtin.Version { - // example: "libc-2.3.4.so" - // example: "libc-2.27.so" - const prefix = "libc-"; - const suffix = ".so"; - if (!mem.startsWith(u8, link_name, prefix) or !mem.endsWith(u8, link_name, suffix)) { - return error.UnrecognizedGnuLibCFileName; - } - // chop off "libc-" and ".so" - const link_name_chopped = link_name[prefix.len .. link_name.len - suffix.len]; - return std.builtin.Version.parse(link_name_chopped) catch |err| switch (err) { - error.Overflow => return error.InvalidGnuLibCVersion, - error.InvalidCharacter => return error.InvalidGnuLibCVersion, - error.InvalidVersion => return error.InvalidGnuLibCVersion, - }; - } - - pub const AbiAndDynamicLinkerFromFileError = error{ - FileSystem, - SystemResources, - SymLinkLoop, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - UnableToReadElfFile, - InvalidElfClass, - InvalidElfVersion, - InvalidElfEndian, - InvalidElfFile, - InvalidElfMagic, - Unexpected, - UnexpectedEndOfFile, - NameTooLong, - }; - - pub fn abiAndDynamicLinkerFromFile( - file: fs.File, - cpu: Target.Cpu, - os: Target.Os, - ld_info_list: []const LdInfo, - cross_target: CrossTarget, - ) AbiAndDynamicLinkerFromFileError!NativeTargetInfo { - var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; - _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len); - const hdr32 = @ptrCast(*elf.Elf32_Ehdr, &hdr_buf); - const hdr64 = @ptrCast(*elf.Elf64_Ehdr, &hdr_buf); - if (!mem.eql(u8, hdr32.e_ident[0..4], "\x7fELF")) return error.InvalidElfMagic; - const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI_DATA]) { - elf.ELFDATA2LSB => .Little, - elf.ELFDATA2MSB => .Big, - else => return error.InvalidElfEndian, - }; - const need_bswap = elf_endian != native_endian; - if (hdr32.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; - - const is_64 = switch (hdr32.e_ident[elf.EI_CLASS]) { - elf.ELFCLASS32 => false, - elf.ELFCLASS64 => true, - else => return error.InvalidElfClass, - }; - var phoff = elfInt(is_64, need_bswap, hdr32.e_phoff, hdr64.e_phoff); - const phentsize = elfInt(is_64, need_bswap, hdr32.e_phentsize, hdr64.e_phentsize); - const phnum = elfInt(is_64, need_bswap, hdr32.e_phnum, hdr64.e_phnum); - - var result: NativeTargetInfo = .{ - .target = .{ - .cpu = cpu, - .os = os, - .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os), - }, - .dynamic_linker = cross_target.dynamic_linker, - }; - var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC - const look_for_ld = cross_target.dynamic_linker.get() == null; - - var ph_buf: [16 * @sizeOf(elf.Elf64_Phdr)]u8 align(@alignOf(elf.Elf64_Phdr)) = undefined; - if (phentsize > @sizeOf(elf.Elf64_Phdr)) return error.InvalidElfFile; - - var ph_i: u16 = 0; - while (ph_i < phnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const ph_reserve: usize = @sizeOf(elf.Elf64_Phdr) - @sizeOf(elf.Elf32_Phdr); - const ph_read_byte_len = try preadMin(file, ph_buf[0 .. ph_buf.len - ph_reserve], phoff, phentsize); - var ph_buf_i: usize = 0; - while (ph_buf_i < ph_read_byte_len and ph_i < phnum) : ({ - ph_i += 1; - phoff += phentsize; - ph_buf_i += phentsize; - }) { - const ph32 = @ptrCast(*elf.Elf32_Phdr, @alignCast(@alignOf(elf.Elf32_Phdr), &ph_buf[ph_buf_i])); - const ph64 = @ptrCast(*elf.Elf64_Phdr, @alignCast(@alignOf(elf.Elf64_Phdr), &ph_buf[ph_buf_i])); - const p_type = elfInt(is_64, need_bswap, ph32.p_type, ph64.p_type); - switch (p_type) { - elf.PT_INTERP => if (look_for_ld) { - const p_offset = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); - const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); - if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; - const filesz = @intCast(usize, p_filesz); - _ = try preadMin(file, result.dynamic_linker.buffer[0..filesz], p_offset, 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.max_byte = @intCast(u8, len - 1); - - // 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.target.abi = ld_info.abi; - break; - } - } - }, - // We only need this for detecting glibc version. - elf.PT_DYNAMIC => if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and - cross_target.glibc_version == null) - { - var dyn_off = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); - const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); - const dyn_size: usize = if (is_64) @sizeOf(elf.Elf64_Dyn) else @sizeOf(elf.Elf32_Dyn); - const dyn_num = p_filesz / dyn_size; - var dyn_buf: [16 * @sizeOf(elf.Elf64_Dyn)]u8 align(@alignOf(elf.Elf64_Dyn)) = undefined; - var dyn_i: usize = 0; - dyn: while (dyn_i < dyn_num) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const dyn_reserve: usize = @sizeOf(elf.Elf64_Dyn) - @sizeOf(elf.Elf32_Dyn); - const dyn_read_byte_len = try preadMin( - file, - dyn_buf[0 .. dyn_buf.len - dyn_reserve], - dyn_off, - dyn_size, - ); - var dyn_buf_i: usize = 0; - while (dyn_buf_i < dyn_read_byte_len and dyn_i < dyn_num) : ({ - dyn_i += 1; - dyn_off += dyn_size; - dyn_buf_i += dyn_size; - }) { - const dyn32 = @ptrCast( - *elf.Elf32_Dyn, - @alignCast(@alignOf(elf.Elf32_Dyn), &dyn_buf[dyn_buf_i]), - ); - const dyn64 = @ptrCast( - *elf.Elf64_Dyn, - @alignCast(@alignOf(elf.Elf64_Dyn), &dyn_buf[dyn_buf_i]), - ); - const tag = elfInt(is_64, need_bswap, dyn32.d_tag, dyn64.d_tag); - const val = elfInt(is_64, need_bswap, dyn32.d_val, dyn64.d_val); - if (tag == elf.DT_RUNPATH) { - rpath_offset = val; - break :dyn; - } - } - } - }, - else => continue, - } - } - } - - if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and cross_target.glibc_version == null) { - if (rpath_offset) |rpoff| { - const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); - - var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); - const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); - const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); - - var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; - if (sh_buf.len < shentsize) return error.InvalidElfFile; - - _ = try preadMin(file, &sh_buf, str_section_off, shentsize); - const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf)); - const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf)); - const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); - const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); - var strtab_buf: [4096:0]u8 = undefined; - const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len); - const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len); - const shstrtab = strtab_buf[0..shstrtab_read_len]; - - const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); - var sh_i: u16 = 0; - const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); - const sh_read_byte_len = try preadMin( - file, - sh_buf[0 .. sh_buf.len - sh_reserve], - shoff, - shentsize, - ); - var sh_buf_i: usize = 0; - while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ - sh_i += 1; - shoff += shentsize; - sh_buf_i += shentsize; - }) { - const sh32 = @ptrCast( - *elf.Elf32_Shdr, - @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]), - ); - const sh64 = @ptrCast( - *elf.Elf64_Shdr, - @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]), - ); - const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); - // TODO this pointer cast should not be necessary - const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0); - if (mem.eql(u8, sh_name, ".dynstr")) { - break :find_dyn_str .{ - .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), - .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), - }; - } - } - } else null; - - if (dynstr) |ds| { - const strtab_len = std.math.min(ds.size, strtab_buf.len); - const strtab_read_len = try preadMin(file, &strtab_buf, ds.offset, strtab_len); - const strtab = strtab_buf[0..strtab_read_len]; - // TODO this pointer cast should not be necessary - const rpoff_usize = std.math.cast(usize, rpoff) catch |err| switch (err) { - error.Overflow => return error.InvalidElfFile, - }; - const rpath_list = mem.sliceTo(std.meta.assumeSentinel(strtab[rpoff_usize..].ptr, 0), 0); - var it = mem.tokenize(u8, rpath_list, ":"); - while (it.next()) |rpath| { - var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, - error.BadPathName => unreachable, - error.DeviceBusy => unreachable, - - error.FileNotFound, - error.NotDir, - error.AccessDenied, - error.NoDevice, - => continue, - - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, - }; - defer dir.close(); - - var link_buf: [std.os.PATH_MAX]u8 = undefined; - const link_name = std.os.readlinkatZ( - dir.fd, - glibc_so_basename, - &link_buf, - ) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.UnsupportedReparsePointType => unreachable, // Windows only - - error.AccessDenied, - error.FileNotFound, - error.NotLink, - error.NotDir, - => continue, - - error.SystemResources, - error.FileSystem, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, - }; - result.target.os.version_range.linux.glibc = glibcVerFromLinkName( - link_name, - ) catch |err| switch (err) { - error.UnrecognizedGnuLibCFileName, - error.InvalidGnuLibCVersion, - => continue, - }; - break; - } - } - } - } - - return result; - } - - fn preadMin(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usize { - var i: usize = 0; - while (i < min_read_len) { - const len = file.pread(buf[i..], offset + i) catch |err| switch (err) { - error.OperationAborted => unreachable, // Windows-only - error.WouldBlock => unreachable, // Did not request blocking mode - error.NotOpenForReading => unreachable, - error.SystemResources => return error.SystemResources, - error.IsDir => return error.UnableToReadElfFile, - error.BrokenPipe => return error.UnableToReadElfFile, - error.Unseekable => return error.UnableToReadElfFile, - error.ConnectionResetByPeer => return error.UnableToReadElfFile, - error.ConnectionTimedOut => return error.UnableToReadElfFile, - error.Unexpected => return error.Unexpected, - error.InputOutput => return error.FileSystem, - error.AccessDenied => return error.Unexpected, - }; - if (len == 0) return error.UnexpectedEndOfFile; - i += len; - } - return i; - } - - fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, cross_target: CrossTarget) !NativeTargetInfo { - const target: Target = .{ - .cpu = cpu, - .os = os, - .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os), - }; - return NativeTargetInfo{ - .target = target, - .dynamic_linker = if (cross_target.dynamic_linker.get() == null) - target.standardDynamicLinkerPath() - else - cross_target.dynamic_linker, - }; - } - - pub const LdInfo = struct { - ld: DynamicLinker, - abi: Target.Abi, - }; - - pub fn elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) { - if (is_64) { - if (need_bswap) { - return @byteSwap(@TypeOf(int_64), int_64); - } else { - return int_64; - } - } else { - if (need_bswap) { - return @byteSwap(@TypeOf(int_32), int_32); - } else { - return int_32; - } - } - } - - fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, cross_target: CrossTarget) ?Target.Cpu { - // Here we switch on a comptime value rather than `cpu_arch`. This is valid because `cpu_arch`, - // although it is a runtime value, is guaranteed to be one of the architectures in the set - // of the respective switch prong. - switch (builtin.cpu.arch) { - .x86_64, .i386 => { - return @import("system/x86.zig").detectNativeCpuAndFeatures(cpu_arch, os, cross_target); - }, - else => {}, - } - - switch (builtin.os.tag) { - .linux => return linux.detectNativeCpuAndFeatures(), - .macos => return darwin.macos.detectNativeCpuAndFeatures(), - else => {}, - } - - // This architecture does not have CPU model & feature detection yet. - // See https://github.com/ziglang/zig/issues/4591 - return null; - } -}; +pub const linux = @import("system/linux.zig"); test { - _ = @import("system/darwin.zig"); - _ = @import("system/linux.zig"); + _ = NativePaths; + _ = NativeTargetInfo; + + _ = darwin; + _ = linux; + _ = windows; } diff --git a/lib/std/zig/system/NativePaths.zig b/lib/std/zig/system/NativePaths.zig new file mode 100644 index 0000000000..8e3e46e481 --- /dev/null +++ b/lib/std/zig/system/NativePaths.zig @@ -0,0 +1,205 @@ +const std = @import("../../std.zig"); +const builtin = @import("builtin"); +const ArrayList = std.ArrayList; +const Allocator = std.mem.Allocator; +const process = std.process; +const mem = std.mem; + +const NativePaths = @This(); +const NativeTargetInfo = std.zig.system.NativeTargetInfo; + +include_dirs: ArrayList([:0]u8), +lib_dirs: ArrayList([:0]u8), +framework_dirs: ArrayList([:0]u8), +rpaths: ArrayList([:0]u8), +warnings: ArrayList([:0]u8), + +pub fn detect(allocator: Allocator, native_info: NativeTargetInfo) !NativePaths { + const native_target = native_info.target; + + var self: NativePaths = .{ + .include_dirs = ArrayList([:0]u8).init(allocator), + .lib_dirs = ArrayList([:0]u8).init(allocator), + .framework_dirs = ArrayList([:0]u8).init(allocator), + .rpaths = ArrayList([:0]u8).init(allocator), + .warnings = ArrayList([:0]u8).init(allocator), + }; + errdefer self.deinit(); + + var is_nix = false; + if (process.getEnvVarOwned(allocator, "NIX_CFLAGS_COMPILE")) |nix_cflags_compile| { + defer allocator.free(nix_cflags_compile); + + is_nix = true; + var it = mem.tokenize(u8, nix_cflags_compile, " "); + while (true) { + const word = it.next() orelse break; + if (mem.eql(u8, word, "-isystem")) { + const include_path = it.next() orelse { + try self.addWarning("Expected argument after -isystem in NIX_CFLAGS_COMPILE"); + break; + }; + try self.addIncludeDir(include_path); + } else { + if (mem.startsWith(u8, word, "-frandom-seed=")) { + continue; + } + try self.addWarningFmt("Unrecognized C flag from NIX_CFLAGS_COMPILE: {s}", .{word}); + } + } + } else |err| switch (err) { + error.InvalidUtf8 => {}, + error.EnvironmentVariableNotFound => {}, + error.OutOfMemory => |e| return e, + } + if (process.getEnvVarOwned(allocator, "NIX_LDFLAGS")) |nix_ldflags| { + defer allocator.free(nix_ldflags); + + is_nix = true; + var it = mem.tokenize(u8, nix_ldflags, " "); + while (true) { + const word = it.next() orelse break; + if (mem.eql(u8, word, "-rpath")) { + const rpath = it.next() orelse { + try self.addWarning("Expected argument after -rpath in NIX_LDFLAGS"); + break; + }; + try self.addRPath(rpath); + } else if (word.len > 2 and word[0] == '-' and word[1] == 'L') { + const lib_path = word[2..]; + try self.addLibDir(lib_path); + } else { + try self.addWarningFmt("Unrecognized C flag from NIX_LDFLAGS: {s}", .{word}); + break; + } + } + } else |err| switch (err) { + error.InvalidUtf8 => {}, + error.EnvironmentVariableNotFound => {}, + error.OutOfMemory => |e| return e, + } + if (is_nix) { + return self; + } + + if (comptime builtin.target.isDarwin()) { + try self.addIncludeDir("/usr/include"); + try self.addIncludeDir("/usr/local/include"); + + try self.addLibDir("/usr/lib"); + try self.addLibDir("/usr/local/lib"); + + try self.addFrameworkDir("/Library/Frameworks"); + try self.addFrameworkDir("/System/Library/Frameworks"); + + return self; + } + + if (comptime native_target.os.tag == .solaris) { + try self.addLibDir("/usr/lib/64"); + try self.addLibDir("/usr/local/lib/64"); + try self.addLibDir("/lib/64"); + + try self.addIncludeDir("/usr/include"); + try self.addIncludeDir("/usr/local/include"); + + return self; + } + + if (native_target.os.tag != .windows) { + const triple = try native_target.linuxTriple(allocator); + const qual = native_target.cpu.arch.ptrBitWidth(); + + // TODO: $ ld --verbose | grep SEARCH_DIR + // the output contains some paths that end with lib64, maybe include them too? + // TODO: what is the best possible order of things? + // TODO: some of these are suspect and should only be added on some systems. audit needed. + + try self.addIncludeDir("/usr/local/include"); + try self.addLibDirFmt("/usr/local/lib{d}", .{qual}); + try self.addLibDir("/usr/local/lib"); + + try self.addIncludeDirFmt("/usr/include/{s}", .{triple}); + try self.addLibDirFmt("/usr/lib/{s}", .{triple}); + + try self.addIncludeDir("/usr/include"); + try self.addLibDirFmt("/lib{d}", .{qual}); + try self.addLibDir("/lib"); + try self.addLibDirFmt("/usr/lib{d}", .{qual}); + try self.addLibDir("/usr/lib"); + + // example: on a 64-bit debian-based linux distro, with zlib installed from apt: + // zlib.h is in /usr/include (added above) + // libz.so.1 is in /lib/x86_64-linux-gnu (added here) + try self.addLibDirFmt("/lib/{s}", .{triple}); + } + + return self; +} + +pub fn deinit(self: *NativePaths) void { + deinitArray(&self.include_dirs); + deinitArray(&self.lib_dirs); + deinitArray(&self.framework_dirs); + deinitArray(&self.rpaths); + deinitArray(&self.warnings); + self.* = undefined; +} + +fn deinitArray(array: *ArrayList([:0]u8)) void { + for (array.items) |item| { + array.allocator.free(item); + } + array.deinit(); +} + +pub fn addIncludeDir(self: *NativePaths, s: []const u8) !void { + return self.appendArray(&self.include_dirs, s); +} + +pub fn addIncludeDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { + const item = try std.fmt.allocPrintZ(self.include_dirs.allocator, fmt, args); + errdefer self.include_dirs.allocator.free(item); + try self.include_dirs.append(item); +} + +pub fn addLibDir(self: *NativePaths, s: []const u8) !void { + return self.appendArray(&self.lib_dirs, s); +} + +pub fn addLibDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { + const item = try std.fmt.allocPrintZ(self.lib_dirs.allocator, fmt, args); + errdefer self.lib_dirs.allocator.free(item); + try self.lib_dirs.append(item); +} + +pub fn addWarning(self: *NativePaths, s: []const u8) !void { + return self.appendArray(&self.warnings, s); +} + +pub fn addFrameworkDir(self: *NativePaths, s: []const u8) !void { + return self.appendArray(&self.framework_dirs, s); +} + +pub fn addFrameworkDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { + const item = try std.fmt.allocPrintZ(self.framework_dirs.allocator, fmt, args); + errdefer self.framework_dirs.allocator.free(item); + try self.framework_dirs.append(item); +} + +pub fn addWarningFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void { + const item = try std.fmt.allocPrintZ(self.warnings.allocator, fmt, args); + errdefer self.warnings.allocator.free(item); + try self.warnings.append(item); +} + +pub fn addRPath(self: *NativePaths, s: []const u8) !void { + return self.appendArray(&self.rpaths, s); +} + +fn appendArray(self: *NativePaths, array: *ArrayList([:0]u8), s: []const u8) !void { + _ = self; + const item = try array.allocator.dupeZ(u8, s); + errdefer array.allocator.free(item); + try array.append(item); +} diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig new file mode 100644 index 0000000000..8ae9a4c708 --- /dev/null +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -0,0 +1,937 @@ +const std = @import("../../std.zig"); +const builtin = @import("builtin"); +const mem = std.mem; +const assert = std.debug.assert; +const fs = std.fs; +const elf = std.elf; +const native_endian = builtin.cpu.arch.endian(); + +const NativeTargetInfo = @This(); +const Target = std.Target; +const Allocator = std.mem.Allocator; +const CrossTarget = std.zig.CrossTarget; +const windows = std.zig.system.windows; +const darwin = std.zig.system.darwin; +const linux = std.zig.system.linux; + +target: Target, +dynamic_linker: DynamicLinker = DynamicLinker{}, + +pub const DynamicLinker = Target.DynamicLinker; + +pub const DetectError = error{ + OutOfMemory, + FileSystem, + SystemResources, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + DeviceBusy, + OSVersionDetectionFail, +}; + +/// Given a `CrossTarget`, which specifies in detail which parts of the target should be detected +/// natively, which should be standard or default, and which are provided explicitly, this function +/// resolves the native components by detecting the native system, and then resolves standard/default parts +/// relative to that. +/// Any resources this function allocates are released before returning, and so there is no +/// deinitialization method. +/// TODO Remove the Allocator requirement from this function. +pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!NativeTargetInfo { + var os = cross_target.getOsTag().defaultVersionRange(cross_target.getCpuArch()); + if (cross_target.os_tag == null) { + switch (builtin.target.os.tag) { + .linux => { + const uts = std.os.uname(); + const release = mem.sliceTo(&uts.release, 0); + // The release field sometimes has a weird format, + // `Version.parse` will attempt to find some meaningful interpretation. + if (std.builtin.Version.parse(release)) |ver| { + os.version_range.linux.range.min = ver; + os.version_range.linux.range.max = ver; + } else |err| switch (err) { + error.Overflow => {}, + error.InvalidCharacter => {}, + error.InvalidVersion => {}, + } + }, + .solaris => { + const uts = std.os.uname(); + const release = mem.sliceTo(&uts.release, 0); + if (std.builtin.Version.parse(release)) |ver| { + os.version_range.semver.min = ver; + os.version_range.semver.max = ver; + } else |err| switch (err) { + error.Overflow => {}, + error.InvalidCharacter => {}, + error.InvalidVersion => {}, + } + }, + .windows => { + const detected_version = windows.detectRuntimeVersion(); + os.version_range.windows.min = detected_version; + os.version_range.windows.max = detected_version; + }, + .macos => try darwin.macos.detect(&os), + .freebsd, .netbsd, .dragonfly => { + const key = switch (builtin.target.os.tag) { + .freebsd => "kern.osreldate", + .netbsd, .dragonfly => "kern.osrevision", + else => unreachable, + }; + var value: u32 = undefined; + var len: usize = @sizeOf(@TypeOf(value)); + + std.os.sysctlbynameZ(key, &value, &len, null, 0) catch |err| switch (err) { + error.NameTooLong => unreachable, // constant, known good value + error.PermissionDenied => unreachable, // only when setting values, + error.SystemResources => unreachable, // memory already on the stack + error.UnknownName => unreachable, // constant, known good value + error.Unexpected => return error.OSVersionDetectionFail, + }; + + switch (builtin.target.os.tag) { + .freebsd => { + // https://www.freebsd.org/doc/en_US.ISO8859-1/books/porters-handbook/versions.html + // Major * 100,000 has been convention since FreeBSD 2.2 (1997) + // Minor * 1(0),000 summed has been convention since FreeBSD 2.2 (1997) + // e.g. 492101 = 4.11-STABLE = 4.(9+2) + const major = value / 100_000; + const minor1 = value % 100_000 / 10_000; // usually 0 since 5.1 + const minor2 = value % 10_000 / 1_000; // 0 before 5.1, minor version since + const patch = value % 1_000; + os.version_range.semver.min = .{ .major = major, .minor = minor1 + minor2, .patch = patch }; + os.version_range.semver.max = os.version_range.semver.min; + }, + .netbsd => { + // #define __NetBSD_Version__ MMmmrrpp00 + // + // M = major version + // m = minor version; a minor number of 99 indicates current. + // r = 0 (*) + // p = patchlevel + const major = value / 100_000_000; + const minor = value % 100_000_000 / 1_000_000; + const patch = value % 10_000 / 100; + os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch }; + os.version_range.semver.max = os.version_range.semver.min; + }, + .dragonfly => { + // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/cb2cde83771754aeef9bb3251ee48959138dec87/Makefile.inc1#L15-L17 + // flat base10 format: Mmmmpp + // M = major + // m = minor; odd-numbers indicate current dev branch + // p = patch + const major = value / 100_000; + const minor = value % 100_000 / 100; + const patch = value % 100; + os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch }; + os.version_range.semver.max = os.version_range.semver.min; + }, + else => unreachable, + } + }, + .openbsd => { + const mib: [2]c_int = [_]c_int{ + std.os.CTL.KERN, + std.os.KERN.OSRELEASE, + }; + var buf: [64]u8 = undefined; + var len: usize = buf.len; + + std.os.sysctl(&mib, &buf, &len, null, 0) catch |err| switch (err) { + error.NameTooLong => unreachable, // constant, known good value + error.PermissionDenied => unreachable, // only when setting values, + error.SystemResources => unreachable, // memory already on the stack + error.UnknownName => unreachable, // constant, known good value + error.Unexpected => return error.OSVersionDetectionFail, + }; + + if (std.builtin.Version.parse(buf[0 .. len - 1])) |ver| { + os.version_range.semver.min = ver; + os.version_range.semver.max = ver; + } else |_| { + return error.OSVersionDetectionFail; + } + }, + else => { + // Unimplemented, fall back to default version range. + }, + } + } + + if (cross_target.os_version_min) |min| switch (min) { + .none => {}, + .semver => |semver| switch (cross_target.getOsTag()) { + .linux => os.version_range.linux.range.min = semver, + else => os.version_range.semver.min = semver, + }, + .windows => |win_ver| os.version_range.windows.min = win_ver, + }; + + if (cross_target.os_version_max) |max| switch (max) { + .none => {}, + .semver => |semver| switch (cross_target.getOsTag()) { + .linux => os.version_range.linux.range.max = semver, + else => os.version_range.semver.max = semver, + }, + .windows => |win_ver| os.version_range.windows.max = win_ver, + }; + + if (cross_target.glibc_version) |glibc| { + assert(cross_target.isGnuLibC()); + os.version_range.linux.glibc = glibc; + } + + // Until https://github.com/ziglang/zig/issues/4592 is implemented (support detecting the + // native CPU architecture as being different than the current target), we use this: + const cpu_arch = cross_target.getCpuArch(); + + var cpu = switch (cross_target.cpu_model) { + .native => detectNativeCpuAndFeatures(cpu_arch, os, cross_target), + .baseline => Target.Cpu.baseline(cpu_arch), + .determined_by_cpu_arch => if (cross_target.cpu_arch == null) + detectNativeCpuAndFeatures(cpu_arch, os, cross_target) + else + Target.Cpu.baseline(cpu_arch), + .explicit => |model| model.toCpu(cpu_arch), + } orelse backup_cpu_detection: { + break :backup_cpu_detection Target.Cpu.baseline(cpu_arch); + }; + var result = try detectAbiAndDynamicLinker(allocator, cpu, os, cross_target); + // For x86, we need to populate some CPU feature flags depending on architecture + // and mode: + // * 16bit_mode => if the abi is code16 + // * 32bit_mode => if the arch is i386 + // However, the "mode" flags can be used as overrides, so if the user explicitly + // sets one of them, that takes precedence. + switch (cpu_arch) { + .i386 => { + if (!std.Target.x86.featureSetHasAny(cross_target.cpu_features_add, .{ + .@"16bit_mode", .@"32bit_mode", + })) { + switch (result.target.abi) { + .code16 => result.target.cpu.features.addFeature( + @enumToInt(std.Target.x86.Feature.@"16bit_mode"), + ), + else => result.target.cpu.features.addFeature( + @enumToInt(std.Target.x86.Feature.@"32bit_mode"), + ), + } + } + }, + .arm, .armeb => { + // XXX What do we do if the target has the noarm feature? + // What do we do if the user specifies +thumb_mode? + }, + .thumb, .thumbeb => { + result.target.cpu.features.addFeature( + @enumToInt(std.Target.arm.Feature.thumb_mode), + ); + }, + else => {}, + } + cross_target.updateCpuFeatures(&result.target.cpu.features); + return result; +} + +/// First we attempt to use the executable's own binary. If it is dynamically +/// linked, then it should answer both the C ABI question and the dynamic linker question. +/// If it is statically linked, then we try /usr/bin/env. If that does not provide the answer, then +/// we fall back to the defaults. +/// TODO Remove the Allocator requirement from this function. +fn detectAbiAndDynamicLinker( + allocator: Allocator, + cpu: Target.Cpu, + os: Target.Os, + cross_target: CrossTarget, +) DetectError!NativeTargetInfo { + const native_target_has_ld = comptime builtin.target.hasDynamicLinker(); + const is_linux = builtin.target.os.tag == .linux; + const have_all_info = cross_target.dynamic_linker.get() != null and + cross_target.abi != null and (!is_linux or cross_target.abi.?.isGnu()); + const os_is_non_native = cross_target.os_tag != null; + if (!native_target_has_ld or have_all_info or os_is_non_native) { + return defaultAbiAndDynamicLinker(cpu, os, cross_target); + } + if (cross_target.abi) |abi| { + if (abi.isMusl()) { + // musl implies static linking. + return defaultAbiAndDynamicLinker(cpu, os, cross_target); + } + } + // 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(@enumToInt(Target.Abi.none) == 0); + const fields = std.meta.fields(Target.Abi)[1..]; + var array: [fields.len]Target.Abi = undefined; + inline for (fields) |field, 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; + + for (all_abis) |abi| { + // This may be a nonsensical parameter. We detect this with error.UnknownDynamicLinkerPath and + // skip adding it to `ld_info_list`. + const target: Target = .{ + .cpu = cpu, + .os = os, + .abi = abi, + }; + const ld = target.standardDynamicLinkerPath(); + 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]; + + // Best case scenario: the executable is dynamically linked, and we can iterate + // over our own shared objects and find a dynamic linker. + self_exe: { + const lib_paths = try std.process.getSelfExeSharedLibPaths(allocator); + defer { + for (lib_paths) |lib_path| { + allocator.free(lib_path); + } + allocator.free(lib_paths); + } + + var found_ld_info: LdInfo = undefined; + var found_ld_path: [:0]const u8 = undefined; + + // Look for dynamic linker. + // This is O(N^M) but typical case here is N=2 and M=10. + find_ld: for (lib_paths) |lib_path| { + for (ld_info_list) |ld_info| { + const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); + if (std.mem.endsWith(u8, lib_path, standard_ld_basename)) { + found_ld_info = ld_info; + found_ld_path = lib_path; + break :find_ld; + } + } + } else break :self_exe; + + // Look for glibc version. + var os_adjusted = os; + if (builtin.target.os.tag == .linux and found_ld_info.abi.isGnu() and + cross_target.glibc_version == null) + { + for (lib_paths) |lib_path| { + if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) { + os_adjusted.version_range.linux.glibc = glibcVerFromSO(lib_path) catch |err| switch (err) { + error.UnrecognizedGnuLibCFileName => continue, + error.InvalidGnuLibCVersion => continue, + error.GnuLibCVersionUnavailable => continue, + else => |e| return e, + }; + break; + } + } + } + + var result: NativeTargetInfo = .{ + .target = .{ + .cpu = cpu, + .os = os_adjusted, + .abi = cross_target.abi orelse found_ld_info.abi, + }, + .dynamic_linker = if (cross_target.dynamic_linker.get() == null) + DynamicLinker.init(found_ld_path) + else + cross_target.dynamic_linker, + }; + return result; + } + + const env_file = std.fs.openFileAbsoluteZ("/usr/bin/env", .{}) catch |err| switch (err) { + error.NoSpaceLeft => unreachable, + error.NameTooLong => unreachable, + error.PathAlreadyExists => unreachable, + error.SharingViolation => unreachable, + error.InvalidUtf8 => unreachable, + error.BadPathName => unreachable, + error.PipeBusy => unreachable, + error.FileLocksNotSupported => unreachable, + error.WouldBlock => unreachable, + + error.IsDir, + error.NotDir, + error.AccessDenied, + error.NoDevice, + error.FileNotFound, + error.FileTooBig, + error.Unexpected, + => return defaultAbiAndDynamicLinker(cpu, os, cross_target), + + else => |e| return e, + }; + defer env_file.close(); + + // If Zig is statically linked, such as via distributed binary static builds, the above + // trick won't work. The next thing we fall back to is the same thing, but for /usr/bin/env. + // Since that path is hard-coded into the shebang line of many portable scripts, it's a + // reasonably reliable path to check for. + return abiAndDynamicLinkerFromFile(env_file, cpu, os, ld_info_list, cross_target) catch |err| switch (err) { + error.FileSystem, + error.SystemResources, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + => |e| return e, + + error.UnableToReadElfFile, + error.InvalidElfClass, + error.InvalidElfVersion, + error.InvalidElfEndian, + error.InvalidElfFile, + error.InvalidElfMagic, + error.Unexpected, + error.UnexpectedEndOfFile, + error.NameTooLong, + // Finally, we fall back on the standard path. + => defaultAbiAndDynamicLinker(cpu, os, cross_target), + }; +} + +const glibc_so_basename = "libc.so.6"; + +fn glibcVerFromSO(so_path: [:0]const u8) !std.builtin.Version { + var link_buf: [std.os.PATH_MAX]u8 = undefined; + const link_name = std.os.readlinkZ(so_path.ptr, &link_buf) catch |err| switch (err) { + error.AccessDenied => return error.GnuLibCVersionUnavailable, + error.FileSystem => return error.FileSystem, + error.SymLinkLoop => return error.SymLinkLoop, + error.NameTooLong => unreachable, + error.NotLink => return error.GnuLibCVersionUnavailable, + error.FileNotFound => return error.GnuLibCVersionUnavailable, + error.SystemResources => return error.SystemResources, + error.NotDir => return error.GnuLibCVersionUnavailable, + error.Unexpected => return error.GnuLibCVersionUnavailable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.UnsupportedReparsePointType => unreachable, // Windows only + }; + return glibcVerFromLinkName(link_name); +} + +fn glibcVerFromLinkName(link_name: []const u8) !std.builtin.Version { + // example: "libc-2.3.4.so" + // example: "libc-2.27.so" + const prefix = "libc-"; + const suffix = ".so"; + if (!mem.startsWith(u8, link_name, prefix) or !mem.endsWith(u8, link_name, suffix)) { + return error.UnrecognizedGnuLibCFileName; + } + // chop off "libc-" and ".so" + const link_name_chopped = link_name[prefix.len .. link_name.len - suffix.len]; + return std.builtin.Version.parse(link_name_chopped) catch |err| switch (err) { + error.Overflow => return error.InvalidGnuLibCVersion, + error.InvalidCharacter => return error.InvalidGnuLibCVersion, + error.InvalidVersion => return error.InvalidGnuLibCVersion, + }; +} + +pub const AbiAndDynamicLinkerFromFileError = error{ + FileSystem, + SystemResources, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + UnableToReadElfFile, + InvalidElfClass, + InvalidElfVersion, + InvalidElfEndian, + InvalidElfFile, + InvalidElfMagic, + Unexpected, + UnexpectedEndOfFile, + NameTooLong, +}; + +pub fn abiAndDynamicLinkerFromFile( + file: fs.File, + cpu: Target.Cpu, + os: Target.Os, + ld_info_list: []const LdInfo, + cross_target: CrossTarget, +) AbiAndDynamicLinkerFromFileError!NativeTargetInfo { + var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; + _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len); + const hdr32 = @ptrCast(*elf.Elf32_Ehdr, &hdr_buf); + const hdr64 = @ptrCast(*elf.Elf64_Ehdr, &hdr_buf); + if (!mem.eql(u8, hdr32.e_ident[0..4], "\x7fELF")) return error.InvalidElfMagic; + const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI_DATA]) { + elf.ELFDATA2LSB => .Little, + elf.ELFDATA2MSB => .Big, + else => return error.InvalidElfEndian, + }; + const need_bswap = elf_endian != native_endian; + if (hdr32.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; + + const is_64 = switch (hdr32.e_ident[elf.EI_CLASS]) { + elf.ELFCLASS32 => false, + elf.ELFCLASS64 => true, + else => return error.InvalidElfClass, + }; + var phoff = elfInt(is_64, need_bswap, hdr32.e_phoff, hdr64.e_phoff); + const phentsize = elfInt(is_64, need_bswap, hdr32.e_phentsize, hdr64.e_phentsize); + const phnum = elfInt(is_64, need_bswap, hdr32.e_phnum, hdr64.e_phnum); + + var result: NativeTargetInfo = .{ + .target = .{ + .cpu = cpu, + .os = os, + .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os), + }, + .dynamic_linker = cross_target.dynamic_linker, + }; + var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC + const look_for_ld = cross_target.dynamic_linker.get() == null; + + var ph_buf: [16 * @sizeOf(elf.Elf64_Phdr)]u8 align(@alignOf(elf.Elf64_Phdr)) = undefined; + if (phentsize > @sizeOf(elf.Elf64_Phdr)) return error.InvalidElfFile; + + var ph_i: u16 = 0; + while (ph_i < phnum) { + // Reserve some bytes so that we can deref the 64-bit struct fields + // even when the ELF file is 32-bits. + const ph_reserve: usize = @sizeOf(elf.Elf64_Phdr) - @sizeOf(elf.Elf32_Phdr); + const ph_read_byte_len = try preadMin(file, ph_buf[0 .. ph_buf.len - ph_reserve], phoff, phentsize); + var ph_buf_i: usize = 0; + while (ph_buf_i < ph_read_byte_len and ph_i < phnum) : ({ + ph_i += 1; + phoff += phentsize; + ph_buf_i += phentsize; + }) { + const ph32 = @ptrCast(*elf.Elf32_Phdr, @alignCast(@alignOf(elf.Elf32_Phdr), &ph_buf[ph_buf_i])); + const ph64 = @ptrCast(*elf.Elf64_Phdr, @alignCast(@alignOf(elf.Elf64_Phdr), &ph_buf[ph_buf_i])); + const p_type = elfInt(is_64, need_bswap, ph32.p_type, ph64.p_type); + switch (p_type) { + elf.PT_INTERP => if (look_for_ld) { + const p_offset = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); + const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); + if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; + const filesz = @intCast(usize, p_filesz); + _ = try preadMin(file, result.dynamic_linker.buffer[0..filesz], p_offset, 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.max_byte = @intCast(u8, len - 1); + + // 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.target.abi = ld_info.abi; + break; + } + } + }, + // We only need this for detecting glibc version. + elf.PT_DYNAMIC => if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and + cross_target.glibc_version == null) + { + var dyn_off = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); + const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); + const dyn_size: usize = if (is_64) @sizeOf(elf.Elf64_Dyn) else @sizeOf(elf.Elf32_Dyn); + const dyn_num = p_filesz / dyn_size; + var dyn_buf: [16 * @sizeOf(elf.Elf64_Dyn)]u8 align(@alignOf(elf.Elf64_Dyn)) = undefined; + var dyn_i: usize = 0; + dyn: while (dyn_i < dyn_num) { + // Reserve some bytes so that we can deref the 64-bit struct fields + // even when the ELF file is 32-bits. + const dyn_reserve: usize = @sizeOf(elf.Elf64_Dyn) - @sizeOf(elf.Elf32_Dyn); + const dyn_read_byte_len = try preadMin( + file, + dyn_buf[0 .. dyn_buf.len - dyn_reserve], + dyn_off, + dyn_size, + ); + var dyn_buf_i: usize = 0; + while (dyn_buf_i < dyn_read_byte_len and dyn_i < dyn_num) : ({ + dyn_i += 1; + dyn_off += dyn_size; + dyn_buf_i += dyn_size; + }) { + const dyn32 = @ptrCast( + *elf.Elf32_Dyn, + @alignCast(@alignOf(elf.Elf32_Dyn), &dyn_buf[dyn_buf_i]), + ); + const dyn64 = @ptrCast( + *elf.Elf64_Dyn, + @alignCast(@alignOf(elf.Elf64_Dyn), &dyn_buf[dyn_buf_i]), + ); + const tag = elfInt(is_64, need_bswap, dyn32.d_tag, dyn64.d_tag); + const val = elfInt(is_64, need_bswap, dyn32.d_val, dyn64.d_val); + if (tag == elf.DT_RUNPATH) { + rpath_offset = val; + break :dyn; + } + } + } + }, + else => continue, + } + } + } + + if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and cross_target.glibc_version == null) { + if (rpath_offset) |rpoff| { + const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); + + var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); + const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); + const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); + + var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; + if (sh_buf.len < shentsize) return error.InvalidElfFile; + + _ = try preadMin(file, &sh_buf, str_section_off, shentsize); + const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf)); + const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf)); + const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); + const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); + var strtab_buf: [4096:0]u8 = undefined; + const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len); + const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len); + const shstrtab = strtab_buf[0..shstrtab_read_len]; + + const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); + var sh_i: u16 = 0; + const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { + // Reserve some bytes so that we can deref the 64-bit struct fields + // even when the ELF file is 32-bits. + const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); + const sh_read_byte_len = try preadMin( + file, + sh_buf[0 .. sh_buf.len - sh_reserve], + shoff, + shentsize, + ); + var sh_buf_i: usize = 0; + while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ + sh_i += 1; + shoff += shentsize; + sh_buf_i += shentsize; + }) { + const sh32 = @ptrCast( + *elf.Elf32_Shdr, + @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]), + ); + const sh64 = @ptrCast( + *elf.Elf64_Shdr, + @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]), + ); + const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); + // TODO this pointer cast should not be necessary + const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0); + if (mem.eql(u8, sh_name, ".dynstr")) { + break :find_dyn_str .{ + .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), + .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), + }; + } + } + } else null; + + if (dynstr) |ds| { + const strtab_len = std.math.min(ds.size, strtab_buf.len); + const strtab_read_len = try preadMin(file, &strtab_buf, ds.offset, strtab_len); + const strtab = strtab_buf[0..strtab_read_len]; + // TODO this pointer cast should not be necessary + const rpoff_usize = std.math.cast(usize, rpoff) catch |err| switch (err) { + error.Overflow => return error.InvalidElfFile, + }; + const rpath_list = mem.sliceTo(std.meta.assumeSentinel(strtab[rpoff_usize..].ptr, 0), 0); + var it = mem.tokenize(u8, rpath_list, ":"); + while (it.next()) |rpath| { + var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, + error.BadPathName => unreachable, + error.DeviceBusy => unreachable, + + error.FileNotFound, + error.NotDir, + error.AccessDenied, + error.NoDevice, + => continue, + + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SystemResources, + error.SymLinkLoop, + error.Unexpected, + => |e| return e, + }; + defer dir.close(); + + var link_buf: [std.os.PATH_MAX]u8 = undefined; + const link_name = std.os.readlinkatZ( + dir.fd, + glibc_so_basename, + &link_buf, + ) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.UnsupportedReparsePointType => unreachable, // Windows only + + error.AccessDenied, + error.FileNotFound, + error.NotLink, + error.NotDir, + => continue, + + error.SystemResources, + error.FileSystem, + error.SymLinkLoop, + error.Unexpected, + => |e| return e, + }; + result.target.os.version_range.linux.glibc = glibcVerFromLinkName( + link_name, + ) catch |err| switch (err) { + error.UnrecognizedGnuLibCFileName, + error.InvalidGnuLibCVersion, + => continue, + }; + break; + } + } + } + } + + return result; +} + +fn preadMin(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usize { + var i: usize = 0; + while (i < min_read_len) { + const len = file.pread(buf[i..], offset + i) catch |err| switch (err) { + error.OperationAborted => unreachable, // Windows-only + error.WouldBlock => unreachable, // Did not request blocking mode + error.NotOpenForReading => unreachable, + error.SystemResources => return error.SystemResources, + error.IsDir => return error.UnableToReadElfFile, + error.BrokenPipe => return error.UnableToReadElfFile, + error.Unseekable => return error.UnableToReadElfFile, + error.ConnectionResetByPeer => return error.UnableToReadElfFile, + error.ConnectionTimedOut => return error.UnableToReadElfFile, + error.Unexpected => return error.Unexpected, + error.InputOutput => return error.FileSystem, + error.AccessDenied => return error.Unexpected, + }; + if (len == 0) return error.UnexpectedEndOfFile; + i += len; + } + return i; +} + +fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, cross_target: CrossTarget) !NativeTargetInfo { + const target: Target = .{ + .cpu = cpu, + .os = os, + .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os), + }; + return NativeTargetInfo{ + .target = target, + .dynamic_linker = if (cross_target.dynamic_linker.get() == null) + target.standardDynamicLinkerPath() + else + cross_target.dynamic_linker, + }; +} + +pub const LdInfo = struct { + ld: DynamicLinker, + abi: Target.Abi, +}; + +pub fn elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) { + if (is_64) { + if (need_bswap) { + return @byteSwap(@TypeOf(int_64), int_64); + } else { + return int_64; + } + } else { + if (need_bswap) { + return @byteSwap(@TypeOf(int_32), int_32); + } else { + return int_32; + } + } +} + +fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, cross_target: CrossTarget) ?Target.Cpu { + // Here we switch on a comptime value rather than `cpu_arch`. This is valid because `cpu_arch`, + // although it is a runtime value, is guaranteed to be one of the architectures in the set + // of the respective switch prong. + switch (builtin.cpu.arch) { + .x86_64, .i386 => { + return @import("x86.zig").detectNativeCpuAndFeatures(cpu_arch, os, cross_target); + }, + else => {}, + } + + switch (builtin.os.tag) { + .linux => return linux.detectNativeCpuAndFeatures(), + .macos => return darwin.macos.detectNativeCpuAndFeatures(), + else => {}, + } + + // This architecture does not have CPU model & feature detection yet. + // See https://github.com/ziglang/zig/issues/4591 + return null; +} + +pub const Executor = union(enum) { + native, + rosetta, + qemu: []const u8, + wine: []const u8, + wasmtime: []const u8, + darling: []const u8, + bad_dl: []const u8, + bad_os_or_cpu, +}; + +pub const GetExternalExecutorOptions = struct { + allow_darling: bool = true, + allow_qemu: bool = true, + allow_rosetta: bool = true, + allow_wasmtime: bool = true, + allow_wine: bool = true, + qemu_fixes_dl: bool = false, + link_libc: bool = false, +}; + +/// Return whether or not the given host target is capable of executing natively executables +/// of the other target. +pub fn getExternalExecutor( + host: NativeTargetInfo, + candidate: NativeTargetInfo, + options: GetExternalExecutorOptions, +) Executor { + const os_match = host.target.os.tag == candidate.target.os.tag; + const cpu_ok = cpu_ok: { + if (host.target.cpu.arch == candidate.target.cpu.arch) + break :cpu_ok true; + + if (host.target.cpu.arch == .x86_64 and candidate.target.cpu.arch == .i386) + break :cpu_ok true; + + if (host.target.cpu.arch == .aarch64 and candidate.target.cpu.arch == .arm) + break :cpu_ok true; + + if (host.target.cpu.arch == .aarch64_be and candidate.target.cpu.arch == .armeb) + break :cpu_ok true; + + // TODO additionally detect incompatible CPU features. + // Note that in some cases the OS kernel will emulate missing CPU features + // when an illegal instruction is encountered. + + break :cpu_ok false; + }; + + var bad_result: Executor = .bad_os_or_cpu; + + if (os_match and cpu_ok) native: { + if (options.link_libc) { + if (candidate.dynamic_linker.get()) |candidate_dl| { + fs.cwd().access(candidate_dl, .{}) catch { + bad_result = .{ .bad_dl = candidate_dl }; + break :native; + }; + } + } + return .native; + } + + // If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2 + // to emulate the foreign architecture. + if (options.allow_rosetta and os_match and + host.target.os.tag == .macos and host.target.cpu.arch == .aarch64) + { + switch (candidate.target.cpu.arch) { + .x86_64 => return .rosetta, + else => return bad_result, + } + } + + // If the OS matches, we can use QEMU to emulate a foreign architecture. + if (options.allow_qemu and os_match and (!cpu_ok or options.qemu_fixes_dl)) { + return switch (candidate.target.cpu.arch) { + .aarch64 => Executor{ .qemu = "qemu-aarch64" }, + .aarch64_be => Executor{ .qemu = "qemu-aarch64_be" }, + .arm => Executor{ .qemu = "qemu-arm" }, + .armeb => Executor{ .qemu = "qemu-armeb" }, + .hexagon => Executor{ .qemu = "qemu-hexagon" }, + .i386 => Executor{ .qemu = "qemu-i386" }, + .m68k => Executor{ .qemu = "qemu-m68k" }, + .mips => Executor{ .qemu = "qemu-mips" }, + .mipsel => Executor{ .qemu = "qemu-mipsel" }, + .mips64 => Executor{ .qemu = "qemu-mips64" }, + .mips64el => Executor{ .qemu = "qemu-mips64el" }, + .powerpc => Executor{ .qemu = "qemu-ppc" }, + .powerpc64 => Executor{ .qemu = "qemu-ppc64" }, + .powerpc64le => Executor{ .qemu = "qemu-ppc64le" }, + .riscv32 => Executor{ .qemu = "qemu-riscv32" }, + .riscv64 => Executor{ .qemu = "qemu-riscv64" }, + .s390x => Executor{ .qemu = "qemu-s390x" }, + .sparc => Executor{ .qemu = "qemu-sparc" }, + .x86_64 => Executor{ .qemu = "qemu-x86_64" }, + else => return bad_result, + }; + } + + switch (candidate.target.os.tag) { + .windows => { + if (options.allow_wine) { + switch (candidate.target.cpu.arch.ptrBitWidth()) { + 32 => return Executor{ .wine = "wine" }, + 64 => return Executor{ .wine = "wine64" }, + else => return bad_result, + } + } + return bad_result; + }, + .wasi => { + if (options.allow_wasmtime) { + switch (candidate.target.cpu.arch.ptrBitWidth()) { + 32 => return Executor{ .wasmtime = "wasmtime" }, + else => return bad_result, + } + } + return bad_result; + }, + .macos => { + if (options.allow_darling) { + // This check can be loosened once darling adds a QEMU-based emulation + // layer for non-host architectures: + // https://github.com/darlinghq/darling/issues/863 + if (candidate.target.cpu.arch != builtin.cpu.arch) { + return bad_result; + } + return Executor{ .darling = "darling" }; + } + return bad_result; + }, + else => return bad_result, + } +} diff --git a/src/main.zig b/src/main.zig index 3b5f10685f..54adf80fa6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2539,6 +2539,7 @@ fn buildOutputType( &comp_destroyed, all_args, runtime_args_start, + link_libc, ); } @@ -2611,6 +2612,7 @@ fn buildOutputType( &comp_destroyed, all_args, runtime_args_start, + link_libc, ); }, .update_and_run => { @@ -2636,6 +2638,7 @@ fn buildOutputType( &comp_destroyed, all_args, runtime_args_start, + link_libc, ); }, } @@ -2700,6 +2703,7 @@ fn runOrTest( comp_destroyed: *bool, all_args: []const []const u8, runtime_args_start: ?usize, + link_libc: bool, ) !void { const exe_loc = emit_bin_loc orelse return; const exe_directory = exe_loc.directory orelse comp.bin_file.options.emit.?.directory; @@ -2740,7 +2744,7 @@ fn runOrTest( if (std.process.can_execv and arg_mode == .run and !watch) { // execv releases the locks; no need to destroy the Compilation here. const err = std.process.execv(gpa, argv.items); - try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info); + try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc); const cmd = try argvCmd(arena, argv.items); fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); } else { @@ -2759,7 +2763,7 @@ fn runOrTest( } const term = child.spawnAndWait() catch |err| { - try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info); + try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc); const cmd = try argvCmd(arena, argv.items); fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd }); }; @@ -4662,34 +4666,108 @@ fn warnAboutForeignBinaries( arena: Allocator, arg_mode: ArgMode, target_info: std.zig.system.NativeTargetInfo, + link_libc: bool, ) !void { const host_cross_target: std.zig.CrossTarget = .{}; const host_target_info = try detectNativeTargetInfo(gpa, host_cross_target); - if (!host_target_info.target.canExecBinariesOf(target_info.target)) { - const host_name = try host_target_info.target.zigTriple(arena); - const foreign_name = try target_info.target.zigTriple(arena); - const tip_suffix = switch (arg_mode) { - .zig_test => ". Consider using --test-no-exec or --test-cmd", - else => "", - }; - warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}){s}", .{ - host_name, foreign_name, tip_suffix, - }); - return; - } - - if (target_info.dynamic_linker.get()) |foreign_dl| { - std.fs.cwd().access(foreign_dl, .{}) catch { + switch (host_target_info.getExternalExecutor(target_info, .{ .link_libc = link_libc })) { + .native => return, + .rosetta => { + const host_name = try host_target_info.target.zigTriple(arena); + const foreign_name = try target_info.target.zigTriple(arena); + warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}). Consider installing Rosetta.", .{ + host_name, foreign_name, + }); + }, + .qemu => |qemu| { + const host_name = try host_target_info.target.zigTriple(arena); + const foreign_name = try target_info.target.zigTriple(arena); + switch (arg_mode) { + .zig_test => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ + "to run the tests", + .{ host_name, foreign_name, qemu }, + ), + else => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '{s}' to run the binary", + .{ host_name, foreign_name, qemu }, + ), + } + }, + .wine => |wine| { + const host_name = try host_target_info.target.zigTriple(arena); + const foreign_name = try target_info.target.zigTriple(arena); + switch (arg_mode) { + .zig_test => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ + "to run the tests", + .{ host_name, foreign_name, wine }, + ), + else => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '{s}' to run the binary", + .{ host_name, foreign_name, wine }, + ), + } + }, + .wasmtime => |wasmtime| { + const host_name = try host_target_info.target.zigTriple(arena); + const foreign_name = try target_info.target.zigTriple(arena); + switch (arg_mode) { + .zig_test => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ + "to run the tests", + .{ host_name, foreign_name, wasmtime }, + ), + else => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '{s}' to run the binary", + .{ host_name, foreign_name, wasmtime }, + ), + } + }, + .darling => |darling| { + const host_name = try host_target_info.target.zigTriple(arena); + const foreign_name = try target_info.target.zigTriple(arena); + switch (arg_mode) { + .zig_test => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ + "to run the tests", + .{ host_name, foreign_name, darling }, + ), + else => warn( + "the host system ({s}) does not appear to be capable of executing binaries " ++ + "from the target ({s}). Consider using '{s}' to run the binary", + .{ host_name, foreign_name, darling }, + ), + } + }, + .bad_dl => |foreign_dl| { const host_dl = host_target_info.dynamic_linker.get() orelse "(none)"; const tip_suffix = switch (arg_mode) { - .zig_test => ", --test-no-exec, or --test-cmd", + .zig_test => ", '--test-no-exec', or '--test-cmd'", else => "", }; - warn("the host system does not appear to be capable of executing binaries from the target because the host dynamic linker is located at '{s}', while the target dynamic linker path is '{s}'. Consider using --dynamic-linker{s}", .{ + warn("the host system does not appear to be capable of executing binaries from the target because the host dynamic linker is '{s}', while the target dynamic linker is '{s}'. Consider using '--dynamic-linker'{s}", .{ host_dl, foreign_dl, tip_suffix, }); - return; - }; + }, + .bad_os_or_cpu => { + const host_name = try host_target_info.target.zigTriple(arena); + const foreign_name = try target_info.target.zigTriple(arena); + const tip_suffix = switch (arg_mode) { + .zig_test => ". Consider using '--test-no-exec' or '--test-cmd'", + else => "", + }; + warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}){s}", .{ + host_name, foreign_name, tip_suffix, + }); + }, } } diff --git a/src/test.zig b/src/test.zig index 08c33b419d..af0dbec24b 100644 --- a/src/test.zig +++ b/src/test.zig @@ -611,6 +611,8 @@ pub const TestContext = struct { } fn run(self: *TestContext) !void { + const host = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, .{}); + var progress = std.Progress{}; const root_node = try progress.start("compiler", self.cases.items.len); defer root_node.end(); @@ -669,6 +671,7 @@ pub const TestContext = struct { zig_lib_directory, &thread_pool, global_cache_directory, + host, ) catch |err| { fail_count += 1; print("test '{s}' failed: {s}\n\n", .{ case.name, @errorName(err) }); @@ -687,6 +690,7 @@ pub const TestContext = struct { zig_lib_directory: Compilation.Directory, thread_pool: *ThreadPool, global_cache_directory: Compilation.Directory, + host: std.zig.system.NativeTargetInfo, ) !void { const target_info = try std.zig.system.NativeTargetInfo.detect(allocator, case.target); const target = target_info.target; @@ -882,6 +886,7 @@ pub const TestContext = struct { .stage1 => true, else => null, }; + const link_libc = case.backend == .llvm; const comp = try Compilation.create(allocator, .{ .local_cache_directory = zig_cache_directory, .global_cache_directory = global_cache_directory, @@ -903,7 +908,7 @@ pub const TestContext = struct { .is_native_os = case.target.isNativeOs(), .is_native_abi = case.target.isNativeAbi(), .dynamic_linker = target_info.dynamic_linker.get(), - .link_libc = case.backend == .llvm, + .link_libc = link_libc, .use_llvm = use_llvm, .use_stage1 = use_stage1, .self_exe_path = std.testing.zig_exe_path, @@ -1113,7 +1118,7 @@ pub const TestContext = struct { // child process. const exe_path = try std.fmt.allocPrint(arena, "." ++ std.fs.path.sep_str ++ "{s}", .{bin_name}); if (case.object_format != null and case.object_format.? == .c) { - if (case.target.getExternalExecutor() != .native) { + if (host.getExternalExecutor(target_info, .{ .link_libc = true }) != .native) { // We wouldn't be able to run the compiled C code. return; // Pass test. } @@ -1129,9 +1134,9 @@ pub const TestContext = struct { "-lc", exe_path, }); - } else switch (case.target.getExternalExecutor()) { + } else switch (host.getExternalExecutor(target_info, .{ .link_libc = link_libc })) { .native => try argv.append(exe_path), - .unavailable => return, // Pass test. + .bad_dl, .bad_os_or_cpu => return, // Pass test. .rosetta => if (enable_rosetta) { try argv.append(exe_path); |
