feat(dist): bundled-zig link backend for hermetic macOS/Linux/Windows builds
Drive a bundled `zig` as `zig cc` for the AOT link step, supplying lld + CRT + libc (musl/glibc/mingw) so `sx build` produces native binaries with no host toolchain. Default Linux output is static musl (portable-anywhere). - src/zig_backend.zig: discover zig ($SX_ZIG / bundled-next-to-exe / PATH); bundled-vs-PATH provenance gates auto-activation. - src/target.zig: selectZigLinker + emitZigLinkArgv + zigTargetTriple, dispatched before the per-OS branches; macOS/Linux/Windows in scope. - src/ir/emit_llvm.zig: LLVMNormalizeTargetTriple so vendor-less zig triples (e.g. x86_64-windows-gnu) parse to the correct OS/object format (COFF not ELF). - src/main.zig: --self-contained / --no-self-contained; linux-musl, linux-musl-arm, windows-gnu shorthands; de-vendor linux/linux-arm to match the corpus runner. - examples/1660: Windows Win32 print-42 + exit(0) via kernel32 (ir-only off-Windows). Auto-activates only for a bundled zig; a PATH-only zig engages under --self-contained, so native dev/CI builds are never silently rerouted. Docs: readme Cross-Compilation, design/bundled-zig-link-backend-design.md, current/PLAN-DIST.md.
This commit is contained in:
127
src/target.zig
127
src/target.zig
@@ -1,6 +1,8 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const llvm = @import("llvm_api.zig");
|
||||
const c = llvm.c;
|
||||
const zig_backend = @import("zig_backend.zig");
|
||||
|
||||
/// One `#jni_main #jni_class("...")` declaration's Java-source emission.
|
||||
/// Populated by lowering and surfaced to the sx Android bundler in
|
||||
@@ -78,6 +80,13 @@ pub const TargetConfig = struct {
|
||||
/// The object is kept at `.sx-tmp/main.o` (its link-time path, so the
|
||||
/// debug map resolves when lldb is run from the project root).
|
||||
emit_obj: bool = false,
|
||||
/// Self-contained link backend (bundled `zig cc`). `.auto` uses it when a
|
||||
/// `zig` is discoverable, the target is Linux, and no explicit `--linker`
|
||||
/// was given; `.on` forces it (error if no zig / non-Linux target); `.off`
|
||||
/// uses the system `cc`. See design/bundled-zig-link-backend-design.md.
|
||||
self_contained: SelfContained = .auto,
|
||||
|
||||
pub const SelfContained = enum { auto, on, off };
|
||||
|
||||
pub const OptLevel = enum {
|
||||
none,
|
||||
@@ -194,8 +203,119 @@ pub const TargetConfig = struct {
|
||||
pub fn getLinker(self: TargetConfig) []const u8 {
|
||||
return self.linker orelse "cc";
|
||||
}
|
||||
|
||||
/// True when this target is in scope for the bundled-`zig` backend:
|
||||
/// the three desktop OSes (macOS, Linux, Windows). iOS/Android/wasm keep
|
||||
/// their specialized toolchains.
|
||||
pub fn zigBackendInScope(self: TargetConfig) bool {
|
||||
return self.isMacOS() or self.isLinux() or self.isWindows();
|
||||
}
|
||||
|
||||
/// The zig `-target` for the bundled-zig link backend. sx triples already
|
||||
/// use zig's scheme, so this is pure pass-through; only the null
|
||||
/// (host-default) case synthesizes a portable triple from the host arch +
|
||||
/// host OS (musl on Linux for static portability, mingw on Windows).
|
||||
/// Caller owns the returned slice.
|
||||
pub fn zigTargetTriple(self: TargetConfig, allocator: std.mem.Allocator) ![]const u8 {
|
||||
if (self.triple) |t| return allocator.dupe(u8, std.mem.span(t));
|
||||
const arch: []const u8 = if (self.isAarch64()) "aarch64" else "x86_64";
|
||||
const os_abi: []const u8 = switch (builtin.os.tag) {
|
||||
.linux => "linux-musl",
|
||||
.macos => "macos-none",
|
||||
.windows => "windows-gnu",
|
||||
else => "linux-musl",
|
||||
};
|
||||
return std.fmt.allocPrint(allocator, "{s}-{s}", .{ arch, os_abi });
|
||||
}
|
||||
};
|
||||
|
||||
/// Decide whether the link step drives the bundled-`zig` backend
|
||||
/// (`zig cc -target …`) or the system linker. Returns the zig path to use,
|
||||
/// or null for the system linker. Errors loudly when a self-contained link is
|
||||
/// requested but cannot be satisfied — never silently falls back in that case.
|
||||
///
|
||||
/// Auto mode engages ONLY for a *bundled* zig (a real distribution): a
|
||||
/// PATH-only zig is a dev convenience and never hijacks a native build, so the
|
||||
/// dev/CI corpus keeps using the system toolchain. `--self-contained` forces
|
||||
/// the backend with either bundled or PATH zig.
|
||||
/// See design/bundled-zig-link-backend-design.md §5.5.
|
||||
fn selectZigLinker(allocator: std.mem.Allocator, tc: TargetConfig) !?[]const u8 {
|
||||
switch (tc.self_contained) {
|
||||
.off => return null,
|
||||
.on => {
|
||||
if (!tc.zigBackendInScope()) {
|
||||
std.debug.print("error: --self-contained supports macOS/Linux/Windows targets only\n", .{});
|
||||
return error.LinkError;
|
||||
}
|
||||
const found = zig_backend.discoverZig(allocator) orelse {
|
||||
std.debug.print("error: --self-contained requested but no usable `zig` was found (set $SX_ZIG or put zig on PATH)\n", .{});
|
||||
return error.LinkError;
|
||||
};
|
||||
return found.path;
|
||||
},
|
||||
.auto => {
|
||||
if (!tc.zigBackendInScope()) return null;
|
||||
if (tc.linker != null) return null; // explicit --linker wins
|
||||
const found = zig_backend.discoverZig(allocator) orelse return null;
|
||||
if (!found.bundled) return null; // PATH zig does not auto-engage
|
||||
return found.path;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `zig cc` link argv (shared across macOS/Linux/Windows). zig cc is
|
||||
/// a clang-compatible driver, so `-o`/`-L`/`-l`/`-framework`/extra objects all
|
||||
/// pass through. `-static` is added only for musl (the portable Linux path);
|
||||
/// macOS cannot static-link libSystem and Windows uses dynamic mingw.
|
||||
fn emitZigLinkArgv(
|
||||
argv: *std.ArrayList([]const u8),
|
||||
allocator: std.mem.Allocator,
|
||||
zig_path: []const u8,
|
||||
output_obj: []const u8,
|
||||
extra_objects: []const []const u8,
|
||||
output_bin: []const u8,
|
||||
libraries: []const []const u8,
|
||||
frameworks: []const []const u8,
|
||||
tc: TargetConfig,
|
||||
) !void {
|
||||
try argv.appendSlice(allocator, &.{ zig_path, "cc" });
|
||||
if (tc.isMacOS()) {
|
||||
// The object stays Mach-O (emitted from Apple's `apple-darwin` triple,
|
||||
// which LLVM needs), but zig's -target parser rejects that scheme — so
|
||||
// hand it zig's vendor-less `<arch>-macos`. No -static (libSystem can't
|
||||
// be statically linked). Cross-to-macOS needs an SDK (out of scope).
|
||||
const arch: []const u8 = if (tc.isAarch64()) "aarch64" else "x86_64";
|
||||
try argv.appendSlice(allocator, &.{ "-target", try std.fmt.allocPrint(allocator, "{s}-macos", .{arch}) });
|
||||
} else {
|
||||
const ztriple = try tc.zigTargetTriple(allocator);
|
||||
try argv.appendSlice(allocator, &.{ "-target", ztriple });
|
||||
if (std.mem.indexOf(u8, ztriple, "musl") != null) try argv.append(allocator, "-static");
|
||||
}
|
||||
try argv.appendSlice(allocator, &.{ output_obj, "-o", output_bin });
|
||||
for (extra_objects) |eo| try argv.append(allocator, eo);
|
||||
|
||||
if (tc.sysroot) |sr| {
|
||||
try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr}));
|
||||
}
|
||||
for (tc.lib_paths) |lp| {
|
||||
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
|
||||
}
|
||||
for (libraries) |lib| {
|
||||
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
|
||||
}
|
||||
// Frameworks are Apple-only; ignored on Linux/Windows.
|
||||
if (tc.isMacOS()) {
|
||||
for (frameworks) |fw| {
|
||||
try argv.append(allocator, "-framework");
|
||||
try argv.append(allocator, fw);
|
||||
}
|
||||
}
|
||||
for (tc.extra_link_flags) |flag| {
|
||||
var it = std.mem.tokenizeScalar(u8, flag, ' ');
|
||||
while (it.next()) |part| try argv.append(allocator, part);
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a precompiled object file in-process using LLVM's ORC JIT.
|
||||
/// Takes ownership of obj_buf. Returns the exit code from main().
|
||||
/// `priority_dylibs` are consulted for symbols BEFORE the process-wide
|
||||
@@ -345,7 +465,11 @@ pub fn discoverAppleSdk(allocator: std.mem.Allocator, io: std.Io, sdk_name: []co
|
||||
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, frameworks: []const []const u8, target_config: TargetConfig, has_jni_main: bool) !void {
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
|
||||
if (target_config.isIOS()) {
|
||||
if (try selectZigLinker(allocator, target_config)) |zig_path| {
|
||||
// Bundled-zig backend: macOS/Linux/Windows linked uniformly via
|
||||
// `zig cc`, which supplies lld + CRT + libc with no host toolchain.
|
||||
try emitZigLinkArgv(&argv, allocator, zig_path, output_obj, extra_objects, output_bin, libraries, frameworks, target_config);
|
||||
} else if (target_config.isIOS()) {
|
||||
// iOS: clang driver with -isysroot pointing at the iOS SDK.
|
||||
// -l libraries are generally wrong for iOS (Apple ships system code
|
||||
// as frameworks); user-declared #library still pass through.
|
||||
@@ -652,7 +776,6 @@ pub fn postProcessWasmHtml(allocator: std.mem.Allocator, io: std.Io, html_path:
|
||||
|
||||
/// Common library paths for the host OS, computed at comptime.
|
||||
pub const host_lib_paths = blk: {
|
||||
const builtin = @import("builtin");
|
||||
var paths: []const []const u8 = &.{};
|
||||
if (builtin.os.tag == .macos) {
|
||||
if (builtin.cpu.arch == .aarch64) {
|
||||
|
||||
Reference in New Issue
Block a user