build: add pkg-config static linking support and fat archives to libghostty (#12096)

The libghostty-vt pkg-config file was missing Libs.private, so
pkg-config --libs --static returned the same flags as the shared case,
omitting the C++ standard library needed by the SIMD code.

Additionally, the static archive did not bundle the vendored SIMD
dependencies (simdutf, highway, utfcpp), leaving consumers with
unresolved symbols when linking. If we're choosing to vendor (no -fsys)
then we should produce a fat static archive that includes them. If
`-fsys` is used, then we should not bundle them and instead reference
them via Requires.private, letting pkg-config chain to their own .pc
files.

Add Libs.private with the C++ runtime (-lc++ on Darwin, -lstdc++ on
Linux) and Requires.private for any SIMD deps provided via system
integration. When SIMD deps are vendored (the default), produce a fat
static archive that bundles them using libtool on Darwin and ar on
Linux. When they come from the system (-fsys=), reference them via
Requires.private instead, letting pkg-config chain to their own .pc
files.
This commit is contained in:
Mitchell Hashimoto
2026-04-04 06:56:19 -07:00
committed by GitHub
4 changed files with 230 additions and 10 deletions

View File

@@ -108,6 +108,7 @@ jobs:
- test-i18n
- test-fuzz-libghostty
- test-lib-vt
- test-lib-vt-pkgconfig
- test-macos
- test-windows
- pinact
@@ -327,6 +328,116 @@ jobs:
ls -la zig-out/lib/
ls -la zig-out/include/ghostty/
test-lib-vt-pkgconfig:
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build libghostty-vt
run: nix develop -c zig build -Demit-lib-vt
- name: Verify pkg-config file
run: |
export PKG_CONFIG_PATH="$PWD/zig-out/share/pkgconfig"
pkg-config --validate libghostty-vt
echo "Cflags: $(pkg-config --cflags libghostty-vt)"
echo "Libs: $(pkg-config --libs libghostty-vt)"
echo "Static: $(pkg-config --libs --static libghostty-vt)"
# Libs.private must include the C++ standard library
pkg-config --libs --static libghostty-vt | grep -q -- '-lc++'
- name: Verify static archive contains SIMD deps
run: |
nm -g zig-out/lib/libghostty-vt.a | grep -q ' T .*simdutf'
nm -g zig-out/lib/libghostty-vt.a | grep -q ' T .*3hwy'
- name: Write test program
run: |
cat > /tmp/test_libghostty_vt.c << 'TESTEOF'
#include <ghostty/vt.h>
#include <stdio.h>
int main(void) {
bool simd = false;
GhosttyResult r = ghostty_build_info(GHOSTTY_BUILD_INFO_SIMD, &simd);
if (r != GHOSTTY_SUCCESS) return 1;
printf("SIMD: %s\n", simd ? "yes" : "no");
return 0;
}
TESTEOF
- name: Test shared link via pkg-config
run: |
export PKG_CONFIG_PATH="$PWD/zig-out/share/pkgconfig"
nix develop -c cc -o /tmp/test_shared /tmp/test_libghostty_vt.c \
$(pkg-config --cflags --libs libghostty-vt) \
-Wl,-rpath,"$PWD/zig-out/lib"
/tmp/test_shared
- name: Test static link via pkg-config
run: |
export PKG_CONFIG_PATH="$PWD/zig-out/share/pkgconfig"
# The static library is compiled with LLVM libc++ (not GNU
# libstdc++), so linking requires a libc++-compatible toolchain.
# zig cc, clang, or gcc with libc++-dev installed all work.
nix develop -c zig cc -o /tmp/test_static /tmp/test_libghostty_vt.c \
$(pkg-config --cflags libghostty-vt) \
"$PWD/zig-out/lib/libghostty-vt.a" \
$(pkg-config --libs-only-l --static libghostty-vt | sed 's/-lghostty-vt//')
/tmp/test_static
# Verify it's truly statically linked (no libghostty-vt.so dependency)
! ldd /tmp/test_static 2>/dev/null | grep -q libghostty-vt
# Test system integration: rebuild with -Dsystem-simdutf=true so
# simdutf comes from the system instead of being vendored. This
# verifies the .pc file uses Requires.private for system deps and
# the fat archive only bundles the remaining vendored deps.
- name: Rebuild with system simdutf
run: |
rm -rf zig-out
nix develop -c zig build -Demit-lib-vt -fsys=simdutf
- name: Verify pkg-config with system simdutf
run: |
export PKG_CONFIG_PATH="$PWD/zig-out/share/pkgconfig"
pc_content=$(cat zig-out/share/pkgconfig/libghostty-vt.pc)
echo "$pc_content"
# Requires.private must reference simdutf
echo "$pc_content" | grep -q 'Requires.private:.*simdutf'
# Requires.private must NOT reference libhwy (still vendored)
! echo "$pc_content" | grep -q 'Requires.private:.*libhwy'
- name: Verify archive with system simdutf
run: |
# simdutf symbols must NOT be defined (comes from system)
! nm -g zig-out/lib/libghostty-vt.a | grep -q ' T .*simdutf'
# highway symbols must still be defined (vendored)
nm -g zig-out/lib/libghostty-vt.a | grep -q ' T .*3hwy'
build-flatpak:
strategy:
fail-fast: false

View File

@@ -151,15 +151,15 @@ add_dependencies(ghostty-vt zig_build_lib_vt)
# Static
#
# When linking the static library, consumers must also link its transitive
# dependencies. By default (with SIMD enabled), these are:
# - libc
# - libc++ (or libstdc++ on Linux)
# - highway
# - simdutf
# On Linux and macOS, the static library is a fat archive that bundles
# the vendored SIMD dependencies (highway, simdutf, utfcpp). Consumers
# only need to link libc and libc++ (LLVM's C++ runtime, not GNU
# libstdc++). Use zig cc, clang, or any toolchain with libc++ support.
#
# Building with -Dsimd=false removes the C++ / highway / simdutf
# dependencies, leaving only libc.
# On Windows, the SIMD dependencies are not bundled and must be linked
# separately.
#
# Building with -Dsimd=false removes all runtime dependencies.
add_library(ghostty-vt-static STATIC IMPORTED GLOBAL)
set_target_properties(ghostty-vt-static PROPERTIES
IMPORTED_LOCATION "${GHOSTTY_VT_STATIC_LIBRARY}"

View File

@@ -5,6 +5,8 @@ const builtin = @import("builtin");
const assert = std.debug.assert;
const RunStep = std.Build.Step.Run;
const GhosttyZig = @import("GhosttyZig.zig");
const LibtoolStep = @import("LibtoolStep.zig");
const SharedDeps = @import("SharedDeps.zig");
/// The step that generates the file.
step: *std.Build.Step,
@@ -185,9 +187,35 @@ fn initLib(
\\Version: 0.1.0
\\Cflags: -I${{includedir}}
\\Libs: -L${{libdir}} -lghostty-vt
, .{b.install_prefix}));
\\Libs.private: {s}
\\Requires.private: {s}
, .{ b.install_prefix, libsPrivate(zig), requiresPrivate(b) }));
};
// For static libraries with vendored SIMD dependencies, combine
// all archives into a single fat archive so consumers only need
// to link one file. Skip on Windows where ar/libtool aren't available.
if (kind == .static and
zig.simd_libs.items.len > 0 and
target.result.os.tag != .windows)
{
var sources: SharedDeps.LazyPathList = .empty;
try sources.append(b.allocator, lib.getEmittedBin());
try sources.appendSlice(b.allocator, zig.simd_libs.items);
const combined = combineArchives(b, target, sources.items);
combined.step.dependOn(&lib.step);
return .{
.step = combined.step,
.artifact = &b.addInstallArtifact(lib, .{}).step,
.kind = kind,
.output = combined.output,
.dsym = dsymutil,
.pkg_config = pc,
};
}
return .{
.step = &lib.step,
.artifact = &b.addInstallArtifact(lib, .{}).step,
@@ -198,6 +226,74 @@ 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 an MRI script with ar -M to combine archives
// directly without extracting. This avoids issues with ar x
// producing full-path member names and read-only permissions.
const run = RunStep.create(b, "combine-archives ghostty-vt");
run.addArgs(&.{
"/bin/sh", "-c",
\\set -e
\\out="$1"; shift
\\script="CREATE $out"
\\for a in "$@"; do
\\ script="$script
\\ADDLIB $a"
\\done
\\script="$script
\\SAVE
\\END"
\\echo "$script" | ar -M
,
"_",
});
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.
/// This includes the C++ standard library needed by SIMD code.
///
/// Zig compiles C++ code with LLVM's libc++ (not GNU libstdc++),
/// so consumers linking the static library need a libc++-compatible
/// toolchain: `zig cc`, `clang`, or GCC with `-lc++` installed.
fn libsPrivate(
zig: *const GhosttyZig,
) []const u8 {
return if (zig.vt_c.link_libcpp orelse false) "-lc++" else "";
}
/// Returns the Requires.private value for the pkg-config file.
/// When SIMD dependencies are provided by the system (via
/// -Dsystem-integration), we reference their pkg-config names so
/// that downstream consumers pick them up transitively.
fn requiresPrivate(b: *std.Build) []const u8 {
const system_simdutf = b.systemIntegrationOption("simdutf", .{});
const system_highway = b.systemIntegrationOption("highway", .{ .default = false });
if (system_simdutf and system_highway) return "simdutf, libhwy";
if (system_simdutf) return "simdutf";
if (system_highway) return "libhwy";
return "";
}
pub fn install(
self: *const GhosttyLibVt,
step: *std.Build.Step,

View File

@@ -11,6 +11,12 @@ const TerminalBuildOptions = @import("../terminal/build_options.zig").Options;
vt: *std.Build.Module,
vt_c: *std.Build.Module,
/// Static library paths for vendored SIMD dependencies. Populated
/// only when the dependencies are built from source (not provided
/// by the system via -Dsystem-integration). Used to produce a
/// combined static archive for downstream consumers.
simd_libs: SharedDeps.LazyPathList,
pub fn init(
b: *std.Build,
cfg: *const Config,
@@ -24,6 +30,8 @@ pub fn init(
// conditionally do this.
vt_options.oniguruma = false;
var simd_libs: SharedDeps.LazyPathList = .empty;
return .{
.vt = try initVt(
"ghostty-vt",
@@ -31,6 +39,7 @@ pub fn init(
cfg,
deps,
vt_options,
null,
),
.vt_c = try initVt(
@@ -43,7 +52,10 @@ pub fn init(
dup.c_abi = true;
break :options dup;
},
&simd_libs,
),
.simd_libs = simd_libs,
};
}
@@ -53,6 +65,7 @@ fn initVt(
cfg: *const Config,
deps: *const SharedDeps,
vt_options: TerminalBuildOptions,
simd_libs: ?*SharedDeps.LazyPathList,
) !*std.Build.Module {
// General build options
const general_options = b.addOptions();
@@ -82,7 +95,7 @@ fn initVt(
// If SIMD is enabled, add all our SIMD dependencies.
if (cfg.simd) {
try SharedDeps.addSimd(b, vt, null);
try SharedDeps.addSimd(b, vt, simd_libs);
}
return vt;