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;