mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-24 05:40:15 +00:00
build: fat static archive and ubsan fix for external linkers (#12217)
## Summary > [!IMPORTANT] > Stacked on #12214. Review that first. (i am targeting `main` so here you will see the full changeset, including 12214 Two changes that make the static libghostty archive consumable by external linkers (MSVC link.exe, .NET NativeAOT, Rust, Go, etc.): **Fat static archive on all platforms** The static archive previously only bundled vendored deps on macOS (via libtool). On Windows and Linux the archive contained only the Zig-compiled code, requiring consumers to find and link freetype, harfbuzz, glslang, spirv-cross, simdutf, oniguruma, etc. separately. Now all platforms produce a single fat archive: - macOS: libtool (unchanged) - Windows: zig ar qcL --format=coff (MSVC's lib.exe can't read Zig-produced GNU-format archives, so we use the bundled LLVM archiver) - Linux: ar -M with MRI scripts (same approach as libghostty-vt) **MSVC ubsan suppression for C deps** Zig's ubsan runtime can't be bundled on Windows (LNK4229), leaving __ubsan_handle_* symbols unresolved. freetype, glslang, spirv-cross, and highway already suppress ubsan. This adds MSVC-conditional suppression to seven more: harfbuzz, libpng, dcimgui, wuffs, oniguruma, zlib, and stb. Gated on abi == .msvc so ubsan coverage is preserved on Linux/macOS. ## Test plan - [x] zig build produces a fat ghostty-static.lib (~230MB) with ~200 object files - [x] MSVC's lib /LIST can read the archive - [x] .NET NativeAOT consumer resolves all symbols (0 unresolved) - [x] Linux/macOS builds unaffected (ubsan remains enabled)
This commit is contained in:
@@ -60,6 +60,12 @@ pub fn build(b: *std.Build) !void {
|
||||
"-DIMGUI_USE_WCHAR32=1",
|
||||
"-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1",
|
||||
});
|
||||
if (target.result.abi == .msvc) {
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-fno-sanitize=undefined",
|
||||
"-fno-sanitize-trap=undefined",
|
||||
});
|
||||
}
|
||||
if (freetype) try flags.appendSlice(b.allocator, &.{
|
||||
"-DIMGUI_ENABLE_FREETYPE=1",
|
||||
});
|
||||
|
||||
@@ -123,6 +123,15 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-DHAVE_STDBOOL_H",
|
||||
});
|
||||
// Disable ubsan for MSVC: Zig's ubsan runtime cannot be bundled
|
||||
// on Windows (LNK4229), leaving __ubsan_handle_* unresolved when
|
||||
// the static archive is consumed by an external linker.
|
||||
if (target.result.abi == .msvc) {
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-fno-sanitize=undefined",
|
||||
"-fno-sanitize-trap=undefined",
|
||||
});
|
||||
}
|
||||
if (target.result.os.tag != .windows) {
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-DHAVE_UNISTD_H",
|
||||
|
||||
@@ -54,6 +54,12 @@ pub fn build(b: *std.Build) !void {
|
||||
"-DPNG_INTEL_SSE_OPT=0",
|
||||
"-DPNG_MIPS_MSA_OPT=0",
|
||||
});
|
||||
if (target.result.abi == .msvc) {
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-fno-sanitize=undefined",
|
||||
"-fno-sanitize-trap=undefined",
|
||||
});
|
||||
}
|
||||
|
||||
lib.addCSourceFiles(.{
|
||||
.root = upstream.path(""),
|
||||
|
||||
@@ -103,6 +103,12 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
|
||||
|
||||
var flags: std.ArrayList([]const u8) = .empty;
|
||||
defer flags.deinit(b.allocator);
|
||||
if (target.result.abi == .msvc) {
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-fno-sanitize=undefined",
|
||||
"-fno-sanitize-trap=undefined",
|
||||
});
|
||||
}
|
||||
lib.addCSourceFiles(.{
|
||||
.root = upstream.path(""),
|
||||
.flags = flags.items,
|
||||
|
||||
@@ -20,6 +20,10 @@ pub fn build(b: *std.Build) !void {
|
||||
var flags: std.ArrayList([]const u8) = .empty;
|
||||
defer flags.deinit(b.allocator);
|
||||
try flags.append(b.allocator, "-DWUFFS_IMPLEMENTATION");
|
||||
if (target.result.abi == .msvc) {
|
||||
try flags.append(b.allocator, "-fno-sanitize=undefined");
|
||||
try flags.append(b.allocator, "-fno-sanitize-trap=undefined");
|
||||
}
|
||||
inline for (@import("src/c.zig").defines) |key| {
|
||||
try flags.append(b.allocator, "-D" ++ key);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ pub fn build(b: *std.Build) !void {
|
||||
"-DHAVE_STDINT_H",
|
||||
"-DHAVE_STDDEF_H",
|
||||
});
|
||||
if (target.result.abi == .msvc) {
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
"-fno-sanitize=undefined",
|
||||
"-fno-sanitize-trap=undefined",
|
||||
});
|
||||
}
|
||||
if (target.result.os.tag != .windows) {
|
||||
try flags.append(b.allocator, "-DZ_HAVE_UNISTD_H");
|
||||
}
|
||||
|
||||
47
src/build/CombineArchivesStep.zig
Normal file
47
src/build/CombineArchivesStep.zig
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Combines multiple static archives into a single fat archive.
|
||||
//! Uses libtool on Darwin and a cross-platform MRI-script build tool
|
||||
//! on all other platforms (including Windows).
|
||||
const std = @import("std");
|
||||
const LibtoolStep = @import("LibtoolStep.zig");
|
||||
|
||||
/// Combine multiple static archives into a single fat archive.
|
||||
///
|
||||
/// `name` identifies the library (e.g. "ghostty-internal", "ghostty-vt").
|
||||
/// Output uses a `-fat` suffix to distinguish the combined archive from
|
||||
/// the single-library archive in the build cache.
|
||||
pub fn create(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
name: []const u8,
|
||||
sources: []const std.Build.LazyPath,
|
||||
) struct { step: *std.Build.Step, output: std.Build.LazyPath } {
|
||||
if (target.result.os.tag.isDarwin()) {
|
||||
const libtool = LibtoolStep.create(b, .{
|
||||
.name = name,
|
||||
.out_name = b.fmt("lib{s}-fat.a", .{name}),
|
||||
.sources = @constCast(sources),
|
||||
});
|
||||
return .{ .step = libtool.step, .output = libtool.output };
|
||||
}
|
||||
|
||||
// On non-Darwin, use a build tool that generates an MRI script and
|
||||
// pipes it to `zig ar -M`. This works on all platforms including
|
||||
// Windows (the previous /bin/sh approach did not).
|
||||
const tool = b.addExecutable(.{
|
||||
.name = "combine_archives",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/build/combine_archives.zig"),
|
||||
.target = b.graph.host,
|
||||
}),
|
||||
});
|
||||
const run = b.addRunArtifact(tool);
|
||||
run.addArg(b.graph.zig_exe);
|
||||
const out_name = if (target.result.os.tag == .windows)
|
||||
b.fmt("{s}-fat.lib", .{name})
|
||||
else
|
||||
b.fmt("lib{s}-fat.a", .{name});
|
||||
const output = run.addOutputFileArg(out_name);
|
||||
for (sources) |source| run.addFileArg(source);
|
||||
|
||||
return .{ .step = &run.step, .output = output };
|
||||
}
|
||||
@@ -2,9 +2,9 @@ const GhosttyLib = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const RunStep = std.Build.Step.Run;
|
||||
const CombineArchivesStep = @import("CombineArchivesStep.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const LibtoolStep = @import("LibtoolStep.zig");
|
||||
const LipoStep = @import("LipoStep.zig");
|
||||
|
||||
/// The step that generates the file.
|
||||
@@ -48,29 +48,18 @@ pub fn initStatic(
|
||||
}
|
||||
|
||||
// Add our dependencies. Get the list of all static deps so we can
|
||||
// build a combined archive if necessary.
|
||||
// build a combined archive.
|
||||
var lib_list = try deps.add(lib);
|
||||
try lib_list.append(b.allocator, lib.getEmittedBin());
|
||||
|
||||
if (!deps.config.target.result.os.tag.isDarwin()) return .{
|
||||
.step = &lib.step,
|
||||
.output = lib.getEmittedBin(),
|
||||
.dsym = null,
|
||||
.pkg_config = null,
|
||||
.pkg_config_static = null,
|
||||
};
|
||||
|
||||
// Create a static lib that contains all our dependencies.
|
||||
const libtool = LibtoolStep.create(b, .{
|
||||
.name = "ghostty",
|
||||
.out_name = "libghostty-fat.a",
|
||||
.sources = lib_list.items,
|
||||
});
|
||||
libtool.step.dependOn(&lib.step);
|
||||
// Combine all archives into a single fat static library so
|
||||
// consumers only need to link one file.
|
||||
const combined = CombineArchivesStep.create(b, deps.config.target, "ghostty-internal", lib_list.items);
|
||||
combined.step.dependOn(&lib.step);
|
||||
|
||||
return .{
|
||||
.step = libtool.step,
|
||||
.output = libtool.output,
|
||||
.step = combined.step,
|
||||
.output = combined.output,
|
||||
|
||||
// Static libraries cannot have dSYMs because they aren't linked.
|
||||
.dsym = null,
|
||||
|
||||
@@ -4,9 +4,9 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const RunStep = std.Build.Step.Run;
|
||||
const CombineArchivesStep = @import("CombineArchivesStep.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const GhosttyZig = @import("GhosttyZig.zig");
|
||||
const LibtoolStep = @import("LibtoolStep.zig");
|
||||
const LipoStep = @import("LipoStep.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const XCFrameworkStep = @import("XCFrameworkStep.zig");
|
||||
@@ -287,7 +287,7 @@ fn initLib(
|
||||
try sources.append(b.allocator, lib.getEmittedBin());
|
||||
try sources.appendSlice(b.allocator, zig.simd_libs.items);
|
||||
|
||||
const combined = combineArchives(b, target, sources.items);
|
||||
const combined = CombineArchivesStep.create(b, target, "ghostty-vt", sources.items);
|
||||
combined.step.dependOn(&lib.step);
|
||||
|
||||
return .{
|
||||
@@ -312,40 +312,6 @@ fn initLib(
|
||||
};
|
||||
}
|
||||
|
||||
/// Combine multiple static archives into a single fat archive.
|
||||
/// Uses libtool on Darwin and ar MRI scripts on other platforms.
|
||||
fn combineArchives(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
sources: []const std.Build.LazyPath,
|
||||
) struct { step: *std.Build.Step, output: std.Build.LazyPath } {
|
||||
if (target.result.os.tag.isDarwin()) {
|
||||
const libtool = LibtoolStep.create(b, .{
|
||||
.name = "ghostty-vt",
|
||||
.out_name = "libghostty-vt.a",
|
||||
.sources = @constCast(sources),
|
||||
});
|
||||
return .{ .step = libtool.step, .output = libtool.output };
|
||||
}
|
||||
|
||||
// On non-Darwin, use a build tool that generates an MRI script and
|
||||
// pipes it to `zig ar -M`. This works on all platforms including
|
||||
// Windows (the previous /bin/sh approach did not).
|
||||
const tool = b.addExecutable(.{
|
||||
.name = "combine_archives",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/build/combine_archives.zig"),
|
||||
.target = b.graph.host,
|
||||
}),
|
||||
});
|
||||
const run = b.addRunArtifact(tool);
|
||||
run.addArg(b.graph.zig_exe);
|
||||
const output = run.addOutputFileArg("libghostty-vt.a");
|
||||
for (sources) |source| run.addFileArg(source);
|
||||
|
||||
return .{ .step = &run.step, .output = output };
|
||||
}
|
||||
|
||||
/// Returns the Libs.private value for the pkg-config file.
|
||||
/// Vendored C++ dependencies are built in no-libcxx mode so consumers
|
||||
/// don't need libc++. System-provided simdutf still requires it.
|
||||
|
||||
@@ -412,7 +412,16 @@ pub fn add(
|
||||
// C files
|
||||
step.linkLibC();
|
||||
step.addIncludePath(b.path("src/stb"));
|
||||
step.addCSourceFiles(.{ .files = &.{"src/stb/stb.c"} });
|
||||
// Disable ubsan for MSVC: Zig's ubsan runtime cannot be bundled
|
||||
// on Windows (LNK4229), leaving __ubsan_handle_* unresolved when
|
||||
// the static archive is consumed by an external linker.
|
||||
step.addCSourceFiles(.{
|
||||
.files = &.{"src/stb/stb.c"},
|
||||
.flags = if (step.rootModuleTarget().abi == .msvc)
|
||||
&.{ "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined" }
|
||||
else
|
||||
&.{},
|
||||
});
|
||||
if (step.rootModuleTarget().os.tag == .linux) {
|
||||
step.addIncludePath(b.path("src/apprt/gtk"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user