From f2e299fb46fee50a348a76165f18e5433fdb9945 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Apr 2026 21:01:54 -0700 Subject: [PATCH] cmake: add ghostty_vt_add_target() for cross-compilation Add a ghostty_vt_add_target() CMake function that lets downstream projects build libghostty-vt for a specific Zig target triple. The function encapsulates zig discovery, build-type-to-optimize mapping, the zig build invocation, and output path conventions so consumers do not need to duplicate any of that logic. It creates named IMPORTED targets (e.g. ghostty-vt-static-linux-amd64) that work alongside the existing native ghostty-vt and ghostty-vt-static targets. The build-type mapping is factored into a shared _GHOSTTY_ZIG_OPT_FLAG variable used by both the native build and the new function. The static library targets now propagate c++ as a link dependency on non-Windows platforms, fixing link failures when consumers use static linking with the default SIMD-enabled build. A new example/c-vt-cmake-cross/ demonstrates end-to-end cross- compilation using zig cc as the C compiler, auto-detecting a cross target based on the host OS. --- .github/workflows/test.yml | 5 + CMakeLists.txt | 159 +++++++++++++++++++++++- dist/cmake/GhosttyZigCompiler.cmake | 74 +++++++++++ dist/cmake/README.md | 45 ++++++- example/c-vt-cmake-cross/CMakeLists.txt | 59 +++++++++ example/c-vt-cmake-cross/README.md | 21 ++++ example/c-vt-cmake-cross/src/main.c | 52 ++++++++ src/build/GhosttyLibVt.zig | 34 ++--- src/build/combine_archives.zig | 54 ++++++++ 9 files changed, 472 insertions(+), 31 deletions(-) create mode 100644 dist/cmake/GhosttyZigCompiler.cmake create mode 100644 example/c-vt-cmake-cross/CMakeLists.txt create mode 100644 example/c-vt-cmake-cross/README.md create mode 100644 example/c-vt-cmake-cross/src/main.c create mode 100644 src/build/combine_archives.zig diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 114d7bee7..173c1cf04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -278,6 +278,11 @@ jobs: fail-fast: false matrix: dir: ${{ fromJSON(needs.list-examples.outputs.cmake) }} + exclude: + # Cross-compilation with zig cc requires a single-config + # generator (Makefiles/Ninja). The Windows CI uses Visual + # Studio which always uses MSVC and ignores CMAKE_C_COMPILER. + - dir: c-vt-cmake-cross name: Example ${{ matrix.dir }} (Windows) runs-on: namespace-profile-ghostty-windows timeout-minutes: 45 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ca6c3e48..e564f8811 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,8 +44,27 @@ # target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt) # shared # target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt-static) # static # -# See dist/cmake/README.md for more details and example/c-vt-cmake/ for a -# complete working example. +# Cross-compilation +# ------------------- +# +# For building libghostty-vt for a non-native Zig target (e.g. cross- +# compiling), use the ghostty_vt_add_target() function after FetchContent: +# +# FetchContent_MakeAvailable(ghostty) +# ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu) +# +# target_link_libraries(myapp PRIVATE ghostty-vt-static-linux-amd64) # static +# target_link_libraries(myapp PRIVATE ghostty-vt-linux-amd64) # shared +# +# This handles zig discovery, build-type-to-optimize mapping, and output +# path conventions internally. Extra flags can be forwarded with ZIG_FLAGS: +# +# ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu +# ZIG_FLAGS -Dsimd=false) +# +# See dist/cmake/README.md for more details, example/c-vt-cmake/ for a +# complete working example, and example/c-vt-cmake-cross/ for a cross- +# compilation example. cmake_minimum_required(VERSION 3.19) project(ghostty-vt VERSION 0.1.0 LANGUAGES C) @@ -54,15 +73,22 @@ project(ghostty-vt VERSION 0.1.0 LANGUAGES C) set(GHOSTTY_ZIG_BUILD_FLAGS "" CACHE STRING "Additional flags to pass to zig build") -# Map CMake build types to Zig optimization levels. +# Map CMake build types to Zig optimization levels. The result is stored in +# _GHOSTTY_ZIG_OPT_FLAG so both the native build and ghostty_vt_add_target() +# can reuse it without duplicating the mapping logic. +set(_GHOSTTY_ZIG_OPT_FLAG "") if(CMAKE_BUILD_TYPE) string(TOUPPER "${CMAKE_BUILD_TYPE}" _bt) if(_bt STREQUAL "RELEASE" OR _bt STREQUAL "MINSIZEREL" OR _bt STREQUAL "RELWITHDEBINFO") - list(APPEND GHOSTTY_ZIG_BUILD_FLAGS "-Doptimize=ReleaseFast") + set(_GHOSTTY_ZIG_OPT_FLAG "-Doptimize=ReleaseFast") endif() unset(_bt) endif() +if(_GHOSTTY_ZIG_OPT_FLAG) + list(APPEND GHOSTTY_ZIG_BUILD_FLAGS "${_GHOSTTY_ZIG_OPT_FLAG}") +endif() + # --- Find Zig ---------------------------------------------------------------- find_program(ZIG_EXECUTABLE zig REQUIRED) @@ -229,3 +255,128 @@ install( "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt" ) + +# --- Cross-compilation helper ------------------------------------------------ +# +# For downstream projects that need to build libghostty-vt for a specific +# Zig target triple. For native builds, use the IMPORTED targets above +# (ghostty-vt, ghostty-vt-static) directly. +# +# Usage (in a downstream CMakeLists.txt after FetchContent_MakeAvailable): +# +# ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu) +# +# Creates: +# ghostty-vt-static-linux-amd64 (IMPORTED STATIC library) +# ghostty-vt-linux-amd64 (IMPORTED SHARED library) +# +# Optional ZIG_FLAGS to pass additional flags to zig build: +# +# ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu +# ZIG_FLAGS -Dsimd=false) + +function(ghostty_vt_add_target) + cmake_parse_arguments(PARSE_ARGV 0 _GVT "" "NAME;ZIG_TARGET" "ZIG_FLAGS") + + if(NOT _GVT_NAME) + message(FATAL_ERROR "ghostty_vt_add_target: NAME is required") + endif() + if(NOT _GVT_ZIG_TARGET) + message(FATAL_ERROR "ghostty_vt_add_target: ZIG_TARGET is required") + endif() + + set(_src_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}") + set(_prefix "${CMAKE_CURRENT_BINARY_DIR}/ghostty-${_GVT_NAME}") + + # Build flags + set(_flags + -Demit-lib-vt + -Dtarget=${_GVT_ZIG_TARGET} + --prefix "${_prefix}" + ) + + # Default to ReleaseFast when no build type is set. Debug builds enable + # UBSan in zig, and the sanitizer runtime is not available for all + # cross-compilation targets. + if(_GHOSTTY_ZIG_OPT_FLAG) + list(APPEND _flags "${_GHOSTTY_ZIG_OPT_FLAG}") + else() + list(APPEND _flags "-Doptimize=ReleaseFast") + endif() + + if(_GVT_ZIG_FLAGS) + list(APPEND _flags ${_GVT_ZIG_FLAGS}) + endif() + + # Output paths + set(_include_dir "${_prefix}/include") + + if(_GVT_ZIG_TARGET MATCHES "windows") + set(_static_lib "${_prefix}/lib/ghostty-vt-static.lib") + set(_shared_lib "${_prefix}/bin/ghostty-vt.dll") + set(_implib "${_prefix}/lib/ghostty-vt.lib") + elseif(_GVT_ZIG_TARGET MATCHES "darwin|macos") + set(_static_lib "${_prefix}/lib/libghostty-vt.a") + set(_shared_lib "${_prefix}/lib/libghostty-vt.0.1.0.dylib") + else() + set(_static_lib "${_prefix}/lib/libghostty-vt.a") + set(_shared_lib "${_prefix}/lib/libghostty-vt.so.0.1.0") + endif() + + file(MAKE_DIRECTORY "${_include_dir}") + + # Custom command: invoke zig build + add_custom_command( + OUTPUT "${_static_lib}" "${_shared_lib}" + COMMAND "${ZIG_EXECUTABLE}" build ${_flags} + WORKING_DIRECTORY "${_src_dir}" + COMMENT "Building libghostty-vt for ${_GVT_ZIG_TARGET}..." + USES_TERMINAL + ) + + set(_build_target "zig_build_lib_vt_${_GVT_NAME}") + add_custom_target(${_build_target} ALL + DEPENDS "${_static_lib}" "${_shared_lib}" + ) + + # Static target + set(_static_target "ghostty-vt-static-${_GVT_NAME}") + add_library(${_static_target} STATIC IMPORTED GLOBAL) + set_target_properties(${_static_target} PROPERTIES + IMPORTED_LOCATION "${_static_lib}" + INTERFACE_INCLUDE_DIRECTORIES "${_include_dir}" + INTERFACE_COMPILE_DEFINITIONS "GHOSTTY_STATIC" + ) + if(_GVT_ZIG_TARGET MATCHES "windows") + set_target_properties(${_static_target} PROPERTIES + INTERFACE_LINK_LIBRARIES "c++;ntdll;kernel32" + ) + else() + set_target_properties(${_static_target} PROPERTIES + INTERFACE_LINK_LIBRARIES "c++" + ) + endif() + add_dependencies(${_static_target} ${_build_target}) + + # Shared target + set(_shared_target "ghostty-vt-${_GVT_NAME}") + add_library(${_shared_target} SHARED IMPORTED GLOBAL) + set_target_properties(${_shared_target} PROPERTIES + IMPORTED_LOCATION "${_shared_lib}" + INTERFACE_INCLUDE_DIRECTORIES "${_include_dir}" + ) + if(_GVT_ZIG_TARGET MATCHES "windows") + set_target_properties(${_shared_target} PROPERTIES + IMPORTED_IMPLIB "${_implib}" + ) + elseif(_GVT_ZIG_TARGET MATCHES "darwin|macos") + set_target_properties(${_shared_target} PROPERTIES + IMPORTED_SONAME "@rpath/libghostty-vt.0.dylib" + ) + else() + set_target_properties(${_shared_target} PROPERTIES + IMPORTED_SONAME "libghostty-vt.so.0" + ) + endif() + add_dependencies(${_shared_target} ${_build_target}) +endfunction() diff --git a/dist/cmake/GhosttyZigCompiler.cmake b/dist/cmake/GhosttyZigCompiler.cmake new file mode 100644 index 000000000..e8efa4e5e --- /dev/null +++ b/dist/cmake/GhosttyZigCompiler.cmake @@ -0,0 +1,74 @@ +# GhosttyZigCompiler.cmake — set up zig cc as a cross compiler +# +# Provides ghostty_zig_compiler() which configures zig cc / zig c++ as +# the C/CXX compiler for a given Zig target triple. It creates small +# wrapper scripts (shell on Unix, .cmd on Windows) and sets the +# following CMake variables in the caller's scope: +# +# CMAKE_C_COMPILER, CMAKE_CXX_COMPILER, +# CMAKE_C_COMPILER_FORCED, CMAKE_CXX_COMPILER_FORCED, +# CMAKE_SYSTEM_NAME, CMAKE_EXECUTABLE_SUFFIX (Windows only) +# +# This file is self-contained with no dependencies on the ghostty +# source tree. Copy it into your project and include it directly. +# It cannot be consumed via FetchContent because it must run before +# project(), but FetchContent_MakeAvailable triggers project() +# internally. +# +# Must be called BEFORE project() — CMake reads the compiler variables +# at project() time and won't re-detect after that. +# +# Usage: +# +# cmake_minimum_required(VERSION 3.19) +# +# include(cmake/GhosttyZigCompiler.cmake) +# ghostty_zig_compiler(ZIG_TARGET x86_64-linux-gnu) +# +# project(myapp LANGUAGES C CXX) +# +# FetchContent_MakeAvailable(ghostty) +# ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu) +# target_link_libraries(myapp PRIVATE ghostty-vt-static-linux-amd64) +# +# See example/c-vt-cmake-cross/ for a complete working example. + +include_guard(GLOBAL) + +function(ghostty_zig_compiler) + cmake_parse_arguments(PARSE_ARGV 0 _GZC "" "ZIG_TARGET" "") + + if(NOT _GZC_ZIG_TARGET) + message(FATAL_ERROR "ghostty_zig_compiler: ZIG_TARGET is required") + endif() + + find_program(_GZC_ZIG zig REQUIRED) + + if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(_cc "${CMAKE_CURRENT_BINARY_DIR}/zig-cc.cmd") + set(_cxx "${CMAKE_CURRENT_BINARY_DIR}/zig-cxx.cmd") + file(WRITE "${_cc}" "@\"${_GZC_ZIG}\" cc -target ${_GZC_ZIG_TARGET} %*\n") + file(WRITE "${_cxx}" "@\"${_GZC_ZIG}\" c++ -target ${_GZC_ZIG_TARGET} %*\n") + else() + set(_cc "${CMAKE_CURRENT_BINARY_DIR}/zig-cc") + set(_cxx "${CMAKE_CURRENT_BINARY_DIR}/zig-c++") + file(WRITE "${_cc}" "#!/bin/sh\nexec \"${_GZC_ZIG}\" cc -target ${_GZC_ZIG_TARGET} \"$@\"\n") + file(WRITE "${_cxx}" "#!/bin/sh\nexec \"${_GZC_ZIG}\" c++ -target ${_GZC_ZIG_TARGET} \"$@\"\n") + file(CHMOD "${_cc}" "${_cxx}" + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) + endif() + + set(CMAKE_C_COMPILER "${_cc}" PARENT_SCOPE) + set(CMAKE_CXX_COMPILER "${_cxx}" PARENT_SCOPE) + set(CMAKE_C_COMPILER_FORCED TRUE PARENT_SCOPE) + set(CMAKE_CXX_COMPILER_FORCED TRUE PARENT_SCOPE) + + if(_GZC_ZIG_TARGET MATCHES "windows") + set(CMAKE_SYSTEM_NAME Windows PARENT_SCOPE) + set(CMAKE_EXECUTABLE_SUFFIX ".exe" PARENT_SCOPE) + elseif(_GZC_ZIG_TARGET MATCHES "linux") + set(CMAKE_SYSTEM_NAME Linux PARENT_SCOPE) + elseif(_GZC_ZIG_TARGET MATCHES "darwin|macos") + set(CMAKE_SYSTEM_NAME Darwin PARENT_SCOPE) + endif() +endfunction() diff --git a/dist/cmake/README.md b/dist/cmake/README.md index ed8b86453..10cd477ce 100644 --- a/dist/cmake/README.md +++ b/dist/cmake/README.md @@ -57,11 +57,46 @@ add_executable(myapp main.c) target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt) ``` -## Files +## Cross-compilation -- `ghostty-vt-config.cmake.in` — template for the CMake package config - file installed alongside the library, enabling `find_package()` support. +For cross-compiling to a different Zig target triple, use +`ghostty_vt_add_target()` after `FetchContent_MakeAvailable`: -## Example +```cmake +FetchContent_MakeAvailable(ghostty) +ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu) -See `example/c-vt-cmake/` for a complete working example. +add_executable(myapp main.c) +target_link_libraries(myapp PRIVATE ghostty-vt-static-linux-amd64) +``` + +### Using zig cc as the C/CXX compiler + +When cross-compiling, the host C compiler can't link binaries for the +target platform. `GhosttyZigCompiler.cmake` provides +`ghostty_zig_compiler()` to set up `zig cc` as the C/CXX compiler for +the cross target. It creates wrapper scripts (shell on Unix, `.cmd` on +Windows) and configures `CMAKE_C_COMPILER`, `CMAKE_CXX_COMPILER`, and +`CMAKE_SYSTEM_NAME`. + +The module is self-contained — copy it into your project (e.g. to +`cmake/`) and include it directly. It cannot be consumed via +FetchContent because it must run before `project()`, but +`FetchContent_MakeAvailable` triggers `project()` internally: + +```cmake +cmake_minimum_required(VERSION 3.19) + +include(cmake/GhosttyZigCompiler.cmake) +ghostty_zig_compiler(ZIG_TARGET x86_64-linux-gnu) + +project(myapp LANGUAGES C CXX) + +FetchContent_MakeAvailable(ghostty) +ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu) + +add_executable(myapp main.c) +target_link_libraries(myapp PRIVATE ghostty-vt-static-linux-amd64) +``` + +See `example/c-vt-cmake-cross/` for a complete working example. diff --git a/example/c-vt-cmake-cross/CMakeLists.txt b/example/c-vt-cmake-cross/CMakeLists.txt new file mode 100644 index 000000000..f0cc36854 --- /dev/null +++ b/example/c-vt-cmake-cross/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.19) + +# --- Determine cross-compilation target before project() -------------------- +# +# We need to know the target before project() so we can set up zig cc as the +# C/C++ compiler for the cross target. + +# Pick a cross-compilation target: build for a different OS than the host. +# Can be overridden with -DZIG_TARGET=... on the command line. +if(NOT ZIG_TARGET) + # CMAKE_HOST_SYSTEM_PROCESSOR may not be set before project(), so + # fall back to `uname -m`. + if(CMAKE_HOST_SYSTEM_PROCESSOR) + set(_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}") + else() + execute_process(COMMAND uname -m OUTPUT_VARIABLE _arch OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + if(_arch MATCHES "^(x86_64|AMD64)$") + set(_arch "x86_64") + elseif(_arch MATCHES "^(aarch64|arm64|ARM64)$") + set(_arch "aarch64") + endif() + + if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + set(ZIG_TARGET "${_arch}-windows-gnu") + elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(ZIG_TARGET "${_arch}-linux-gnu") + elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") + set(ZIG_TARGET "${_arch}-linux-gnu") + else() + message(FATAL_ERROR + "Cannot derive ZIG_TARGET for ${CMAKE_HOST_SYSTEM_NAME}. " + "Pass -DZIG_TARGET=... manually.") + endif() + + message(STATUS "Cross-compiling for ZIG_TARGET: ${ZIG_TARGET}") +endif() + +# --- Set up zig cc as the cross compiler ------------------------------------ + +# GhosttyZigCompiler.cmake must be called before project(). +# Downstream projects would copy this file into their tree; here we +# include it directly from the repo. +include(../../dist/cmake/GhosttyZigCompiler.cmake) +ghostty_zig_compiler(ZIG_TARGET "${ZIG_TARGET}") + +project(c-vt-cmake-cross LANGUAGES C CXX) + +include(FetchContent) +FetchContent_Declare(ghostty + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git + GIT_TAG main +) +FetchContent_MakeAvailable(ghostty) + +ghostty_vt_add_target(NAME cross ZIG_TARGET "${ZIG_TARGET}") + +add_executable(c_vt_cmake_cross src/main.c) +target_link_libraries(c_vt_cmake_cross PRIVATE ghostty-vt-static-cross) diff --git a/example/c-vt-cmake-cross/README.md b/example/c-vt-cmake-cross/README.md new file mode 100644 index 000000000..e00a8cf3f --- /dev/null +++ b/example/c-vt-cmake-cross/README.md @@ -0,0 +1,21 @@ +# c-vt-cmake-cross + +Demonstrates using `ghostty_vt_add_target()` to cross-compile +libghostty-vt with static linking. The target OS is chosen automatically: + +| Host | Target | +| ------- | --------------- | +| Linux | Windows (MinGW) | +| Windows | Linux (glibc) | +| macOS | Linux (glibc) | + +Override with `-DZIG_TARGET=...` if needed. + +## Building + +```shell-session +cd example/c-vt-cmake-cross +cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../.. +cmake --build build +file build/c_vt_cmake_cross +``` diff --git a/example/c-vt-cmake-cross/src/main.c b/example/c-vt-cmake-cross/src/main.c new file mode 100644 index 000000000..992586451 --- /dev/null +++ b/example/c-vt-cmake-cross/src/main.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include + +int main() { + // Create a terminal with a small grid + GhosttyTerminal terminal; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + assert(result == GHOSTTY_SUCCESS); + + // Write some VT-encoded content into the terminal + const char *commands[] = { + "Hello from a \033[1mCMake\033[0m-built program!\r\n", + "Line 2: \033[4munderlined\033[0m text\r\n", + "Line 3: \033[31mred\033[0m \033[32mgreen\033[0m \033[34mblue\033[0m\r\n", + }; + for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { + ghostty_terminal_vt_write(terminal, (const uint8_t *)commands[i], + strlen(commands[i])); + } + + // Format the terminal contents as plain text + GhosttyFormatterTerminalOptions fmt_opts = + GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions); + fmt_opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; + fmt_opts.trim = true; + + GhosttyFormatter formatter; + result = ghostty_formatter_terminal_new(NULL, &formatter, terminal, fmt_opts); + assert(result == GHOSTTY_SUCCESS); + + uint8_t *buf = NULL; + size_t len = 0; + result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + printf("Plain text (%zu bytes):\n", len); + fwrite(buf, 1, len, stdout); + printf("\n"); + + ghostty_free(NULL, buf, len); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 32f2e10a7..4de1b0df9 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -279,10 +279,9 @@ fn initLib( // 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. + // to link one file. if (kind == .static and - zig.simd_libs.items.len > 0 and - target.result.os.tag != .windows) + zig.simd_libs.items.len > 0) { var sources: SharedDeps.LazyPathList = .empty; try sources.append(b.allocator, lib.getEmittedBin()); @@ -329,26 +328,17 @@ fn combineArchives( 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 - , - "_", + // 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); const output = run.addOutputFileArg("libghostty-vt.a"); for (sources) |source| run.addFileArg(source); diff --git a/src/build/combine_archives.zig b/src/build/combine_archives.zig new file mode 100644 index 000000000..04f2c0e49 --- /dev/null +++ b/src/build/combine_archives.zig @@ -0,0 +1,54 @@ +//! Build tool that combines multiple static archives into a single fat +//! archive using an MRI script piped to `zig ar -M`. +//! +//! MRI scripts require stdin piping (`ar -M < script`), which can't be +//! expressed as a single command in the zig build system's RunStep. The +//! previous approach used `/bin/sh -c` to do the piping, but that isn't +//! available on Windows. This tool handles both the script generation +//! and the piping in a single cross-platform executable. +//! +//! Usage: combine_archives [input2.a ...] + +const std = @import("std"); + +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + const alloc = gpa.allocator(); + + const args = try std.process.argsAlloc(alloc); + if (args.len < 3) { + std.log.err("usage: combine_archives ", .{}); + std.process.exit(1); + } + + const output_path = args[1]; + const inputs = args[2..]; + + // Build the MRI script. + var script: std.ArrayListUnmanaged(u8) = .empty; + try script.appendSlice(alloc, "CREATE "); + try script.appendSlice(alloc, output_path); + try script.append(alloc, '\n'); + for (inputs) |input| { + try script.appendSlice(alloc, "ADDLIB "); + try script.appendSlice(alloc, input); + try script.append(alloc, '\n'); + } + try script.appendSlice(alloc, "SAVE\nEND\n"); + + var child: std.process.Child = .init(&.{ "zig", "ar", "-M" }, alloc); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + try child.spawn(); + try child.stdin.?.writeAll(script.items); + child.stdin.?.close(); + child.stdin = null; + + const term = try child.wait(); + if (term.Exited != 0) { + std.log.err("zig ar -M exited with code {d}", .{term.Exited}); + std.process.exit(1); + } +}