From e157dd69c57a39f8d3a88b12e050e69830bfb1d5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Apr 2026 20:36:32 -0700 Subject: [PATCH] build: add pkg-config static linking support and fat archives to libghostty 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. --- .github/workflows/test.yml | 111 +++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 16 +++--- src/build/GhosttyLibVt.zig | 98 +++++++++++++++++++++++++++++++- src/build/GhosttyZig.zig | 15 ++++- 4 files changed, 230 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b60685c5e..9e292c078 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 + #include + 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 326f5b37a..1ca6c3e48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}" diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 10d988b38..3e1be4777 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -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, diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index aabc00d46..4901180d1 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -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;