首页
/ Ghostty libghostty-vt 交叉编译实战:CMake 结合 zig cc 构建目标平台静态库

Ghostty libghostty-vt 交叉编译实战:CMake 结合 zig cc 构建目标平台静态库

2026-09-05 10:18:23作者:卓艾滢Kingsley

本文以 Ghostty 仓库中的官方示例 c-vt-cmake-cross 为主体,讲解如何在 CMake 项目中交叉编译 libghostty-vt:通过 ghostty_vt_add_target() 为指定的 Zig 目标三元组构建静态/动态库,并用 zig cc 作为 C/C++ 交叉编译器链接出目标平台的可执行文件。读完本篇,你将掌握"目标平台自动推导 → zig cc 编译器替换 → 交叉目标库构建 → 静态链接"的完整链路,以及每一步背后的 CMake 实现原理。

示例定位:为什么需要交叉编译

Ghostty 将终端核心能力抽离为一个独立的 C ABI 库 libghostty-vt,CMake 侧通过仓库根目录的 CMakeLists.txt 以 IMPORTED 目标的形式暴露它。对于下游项目,最直接的两种消费方式是:

但这两种方式构建的都是宿主平台的二进制。当你需要在 Linux 上产出 Windows(MinGW)程序,或在 Windows/macOS 上产出 Linux(glibc)程序时,原生 IMPORTED 目标不再适用——这正是 c-vt-cmake-cross 示例要解决的问题。它演示了仓库根 CMakeLists 中 ghostty_vt_add_target() 函数的完整用法,并配合 GhosttyZigCompiler.cmakezig cc 完成交叉链接。

README 的说明,示例会自动根据宿主选择目标平台:

宿主 交叉目标
Linux Windows (MinGW)
Windows Linux (glibc)
macOS Linux (glibc)

如需覆盖自动推导,可在命令行传入 -DZIG_TARGET=...

构建步骤(可直接复制)

以下是 README 中的原始构建命令,前提是本机安装了 cmake(>= 3.19)和 zig 工具链(zig build -Demit-lib-vt 底层仍由 Zig 构建系统驱动):

cd example/c-vt-cmake-cross
cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../..
cmake --build build
file build/c_vt_cmake_cross

几点说明:

  • -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../.. 让 CMake 的 FetchContent 直接复用当前仓库,而不是从上游 Git 仓库拉取;脱离仓库单独使用示例时去掉该参数即可。
  • 最后的 file build/c_vt_cmake_cross 用于验证产物确实是目标平台格式(例如在 Linux 上构建出的应为 PE32+ / Windows x86-64 可执行文件)。

交叉编译流程拆解:CMakeLists.txt 的三个关键环节

示例的 CMakeLists.txt 只有 60 行,但结构上严格遵循了交叉编译在 CMake 中的时序约束。可以把它拆成三段来看。

1. 在 project() 之前推导目标三元组

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(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()
endif()

这里有两个容易踩坑的细节,源码注释已明确点出:

  1. 架构探测的兜底CMAKE_HOST_SYSTEM_PROCESSORproject() 之前可能尚未赋值,所以回退到 uname -m。探测结果还会做归一化:AMD64/ARM64 统一映射为 x86_64/aarch64,与 Zig 三元组的命名习惯对齐。
  2. 宿主不支持时直接报错else 分支用 FATAL_ERROR 明确要求用户手动传 -DZIG_TARGET=...,而不是静默猜一个默认值——这与 GhosttyZigCompiler.cmake 头注释中"模块自包含、可直接拷入下游工程"的定位一致:脚本要能独立于 Ghostty 仓库工作,因此不做任何隐式假设。

2. 用 zig cc 替换 C/C++ 编译器

# 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)

这一步是整个示例的关键。交叉编译时宿主的 C 编译器(如 gnu 平台上的 Linux gcc)产出的代码属于宿主 ABI,无法链接出目标平台二进制,因此必须把 C/C++ 编译器整体换成 zig cc——Zig 内置的 C 编译器前端 + LLVM 后端,配合 -target 参数即为跨平台编译器。

GhosttyZigCompiler.cmakeghostty_zig_compiler() 的机制值得细看:

  • 构建目录下生成两个小的包装脚本(Unix 是 shell 脚本,Windows 是 .cmd),本质只是转发到 zig:

    #!/bin/sh
    exec "${ZIG}" cc -target x86_64-windows-gnu "$@"
    
  • 然后把 CMAKE_C_COMPILERCMAKE_CXX_COMPILER 指向这两个脚本,并置 CMAKE_C_COMPILER_FORCED / CMAKE_CXX_COMPILER_FORCED 为 TRUE,跳过 CMake 对"非标准编译器"的检测。

  • 按目标三元组设置 CMAKE_SYSTEM_NAMEwindowsWindowsCMAKE_EXECUTABLE_SUFFIX.exelinuxLinuxdarwin|macosDarwin

该模块头注释特别强调了一条时序约束:必须在 project() 之前调用,因为 CMake 只在 project() 时读取编译器变量,之后不会再重新探测;而 FetchContent_MakeAvailable 内部也会触发 project(),所以它无法通过 FetchContent 消费,只能 include 进来——下游项目把文件拷到自己的 cmake/ 目录即可。这解释了为什么示例仓库内直接 include(../../dist/cmake/GhosttyZigCompiler.cmake),而注释里说"下游项目会把它复制进自己的工程树"。

3. 构建交叉目标的 libghostty-vt 并链接

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)

ghostty_vt_add_target() 定义在仓库根目录的 CMakeLists.txt 中,NAME cross 会生成两个 IMPORTED 目标:

  • ghostty-vt-static-cross:静态库(本示例链接的就是它);
  • ghostty-vt-cross:动态库。

从函数实现看,它做的事情是:

  1. 组装 zig build 参数:-Demit-lib-vt-Dtarget=${ZIG_TARGET}--prefix <构建目录>/ghostty-<NAME>
  2. 优化级别:如果 CMake 指定了 CMAKE_BUILD_TYPE=Release 等,映射为 -Doptimize=ReleaseFast未指定时也默认 ReleaseFast——源码注释解释了原因:Debug 模式会启用 UBSan,而 sanitizer 运行时并非所有交叉目标都可用;
  3. 按目标平台推断产物路径(Windows 为 ghostty-vt-static.lib/ghostty-vt.dll,Linux 为 libghostty-vt.a/libghostty-vt.so.0.1.0),注册 add_custom_command + add_custom_target
  4. 为静态目标追加 GHOSTTY_STATIC 编译定义;若目标是 Windows,还追加 INTERFACE_LINK_LIBRARIES "ntdll;kernel32",因为 Zig 标准库在 Windows 静态场景下使用了 NT API 函数,消费方需要自行链接(对应仓库根 CMakeLists.txt 中相同逻辑的注释说明)。

此外函数还支持 ZIG_FLAGS 追加参数,例如关闭 SIMD:

ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu
                      ZIG_FLAGS -Dsimd=false)

这与 c-vt-cmake-static 示例 中通过 GHOSTTY_ZIG_BUILD_FLAGS "-Dsimd=false" 达到的效果一致。从仓库根 CMakeLists 的注释可以看到原因:Linux/macOS 上的静态库会把 highway、simdutf 等 SIMD 依赖打包进 fat archive(消费方只需链接 libc),而 Windows 上不打包;-Dsimd=false 则可以彻底移除运行时依赖。交叉编译到目标平台时,这类依赖处理是静态链接场景下的核心考量。

更完整的接口说明可参考 dist/cmake/README.md 的 "Cross-compilation" 一节。

被交叉编译的应用程序本身

示例程序 src/main.cc-vt-cmakec-vt-cmake-static 示例同源,用于验证 C ABI 的可用性:

  1. ghostty_terminal_new(NULL, &terminal, 80, 24) 创建 80×24 的终端网格;
  2. ghostty_terminal_vt_write() 写入三行带 ANSI 序列的 VT 内容(粗体、下划线、红/绿/蓝着色);
  3. 通过 ghostty_formatter_terminal_new() 创建 GHOSTTY_FORMATTER_FORMAT_PLAINtrim = true 的格式化器,ghostty_formatter_format_alloc() 一次性产出纯文本并打印;
  4. 依次调用 ghostty_freeghostty_formatter_freeghostty_terminal_free 释放资源。

这段代码全部来自 C 头文件 include/ghostty/vt.h,不包含任何 Zig 依赖——这正是交叉编译能"干净"成立的前提:应用侧只需要头文件、zig cc 和一个静态库,其余终端状态机、格式化逻辑全部在 libghostty-vt 内部。产物用 file 命令检查即可确认平台归属,例如 Linux 宿主构建出的应是 PE32+ executable ... x86-64 (console),说明 C 代码与 libghostty-vt 静态库都按 Windows 目标产出并完成了链接。

小结:下游项目复现该流程的完整清单

综合本示例与仓库文档,将 libghostty-vt 交叉编译进你自己的 CMake 项目,需要四步:

  1. GhosttyZigCompiler.cmake 拷入工程(自包含、无仓库依赖),并在 project() 之前调用 ghostty_zig_compiler(ZIG_TARGET <triple>)
  2. FetchContent 引入 Ghostty 并 FetchContent_MakeAvailable
  3. 调用 ghostty_vt_add_target(NAME <name> ZIG_TARGET <triple>),按目标追加 ZIG_FLAGS(如 -Dsimd=false);
  4. 可执行目标链接 ghostty-vt-static-<name>(静态)或 ghostty-vt-<name>(动态,Windows 下走 DLL import library)。

该方案的适用前提是:构建机上有 zig 工具链(zig buildzig cc 均由 Zig 提供);CMake >= 3.19;且目标三元组是 Zig 支持的目标。它绕开了传统"安装整套 MinGW/sysroot 工具链"的交叉环境搭建——Zig 同时承担库构建(zig build -Demit-lib-vt -Dtarget=...)和 C 编译链接(zig cc -target ...)两个角色,是这条交叉编译链路能成立的根本原因。

登录后查看全文
热门项目推荐
相关项目推荐