From 3dee62f9046e0fcbc9841ccfe4a96d7f78fd90c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Mar 2026 10:21:31 -0700 Subject: [PATCH] build: add CMake support for libghostty-vt Add a top-level CMakeLists.txt that wraps `zig build lib-vt` so that CMake-based downstream projects can consume libghostty-vt without needing to interact with the Zig build system directly. A custom command triggers the zig build during `cmake --build`, and the resulting shared library is exposed as an IMPORTED target. Downstream projects can pull in the library via FetchContent, which fetches the source and builds it as part of their own CMake build, or via find_package after a manual install step. The package config template in dist/cmake/ sets up the ghostty-vt::ghostty-vt target with proper include paths and macOS rpath handling. A c-vt-cmake example demonstrates the FetchContent workflow, creating a terminal, writing VT sequences, and formatting the output as plain text. CI is updated to auto-discover and build CMake-based examples alongside the existing Zig-based ones. --- .github/workflows/test.yml | 56 +++++++-- .gitignore | 3 + CMakeLists.txt | 160 ++++++++++++++++++++++++++ dist/cmake/README.md | 67 +++++++++++ dist/cmake/ghostty-vt-config.cmake.in | 33 ++++++ example/.gitignore | 1 + example/c-vt-cmake/CMakeLists.txt | 12 ++ example/c-vt-cmake/README.md | 21 ++++ example/c-vt-cmake/src/main.c | 52 +++++++++ nix/devShell.nix | 2 + 10 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 dist/cmake/README.md create mode 100644 dist/cmake/ghostty-vt-config.cmake.in create mode 100644 example/c-vt-cmake/CMakeLists.txt create mode 100644 example/c-vt-cmake/README.md create mode 100644 example/c-vt-cmake/src/main.c diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbbfa8714..824953756 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,7 +85,8 @@ jobs: - skip - build-bench - build-dist - - build-examples + - build-examples-zig + - build-examples-cmake - build-flatpak - build-libghostty-vt - build-libghostty-vt-android @@ -172,22 +173,26 @@ jobs: needs: skip runs-on: namespace-profile-ghostty-xsm outputs: - dirs: ${{ steps.list.outputs.dirs }} + zig: ${{ steps.list.outputs.zig }} + cmake: ${{ steps.list.outputs.cmake }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - id: list name: List example directories run: | - dirs=$(ls example/*/build.zig.zon 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') - echo "$dirs" | jq . - echo "dirs=$dirs" >> "$GITHUB_OUTPUT" + zig=$(ls example/*/build.zig.zon 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "$zig" | jq . + echo "zig=$zig" >> "$GITHUB_OUTPUT" + cmake=$(ls example/*/CMakeLists.txt 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "$cmake" | jq . + echo "cmake=$cmake" >> "$GITHUB_OUTPUT" - build-examples: + build-examples-zig: strategy: fail-fast: false matrix: - dir: ${{ fromJSON(needs.list-examples.outputs.dirs) }} + dir: ${{ fromJSON(needs.list-examples.outputs.zig) }} name: Example ${{ matrix.dir }} runs-on: namespace-profile-ghostty-xsm needs: [test, list-examples] @@ -219,6 +224,43 @@ jobs: cd example/${{ matrix.dir }} nix develop -c zig build + build-examples-cmake: + strategy: + fail-fast: false + matrix: + dir: ${{ fromJSON(needs.list-examples.outputs.cmake) }} + name: Example ${{ matrix.dir }} + runs-on: namespace-profile-ghostty-xsm + needs: [test, list-examples] + 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@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build Example + run: | + cd example/${{ matrix.dir }} + nix develop -c cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=${{ github.workspace }} + nix develop -c cmake --build build + build-flatpak: strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index 74f3f85eb..699ac9a5f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ zig-cache/ .zig-cache/ zig-out/ +build-cmake/ +CMakeCache.txt +CMakeFiles/ /build.zig.zon.bak /result* /.nixos-test-history diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..4198bca36 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,160 @@ +# CMake wrapper for libghostty-vt +# +# This file delegates to `zig build lib-vt` to produce the shared library, +# headers, and pkg-config file. It exists so that CMake-based projects can +# consume libghostty-vt without interacting with the Zig build system +# directly. However, downstream users do still require `zig` on the PATH. +# Please consult the Ghostty docs for the required Zig version: +# +# https://ghostty.org/docs/install/build +# +# Building within the Ghostty repo +# --------------------------------- +# +# cmake -B build +# cmake --build build +# cmake --install build --prefix /usr/local +# +# Pass extra flags to the Zig build with GHOSTTY_ZIG_BUILD_FLAGS: +# +# cmake -B build -DGHOSTTY_ZIG_BUILD_FLAGS="-Demit-macos-app=false" +# +# Integrating into a downstream CMake project +# --------------------------------------------- +# +# Option 1 — FetchContent (recommended, no manual install step): +# +# include(FetchContent) +# FetchContent_Declare(ghostty +# GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git +# GIT_TAG main +# ) +# FetchContent_MakeAvailable(ghostty) +# +# target_link_libraries(myapp PRIVATE ghostty-vt) +# +# To use a local checkout instead of fetching: +# +# cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=/path/to/ghostty +# +# Option 2 — find_package (after installing to a prefix): +# +# find_package(ghostty-vt REQUIRED) +# target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt) +# +# See dist/cmake/README.md for more details and example/c-vt-cmake/ for a +# complete working example. + +cmake_minimum_required(VERSION 3.19) +project(ghostty-vt VERSION 0.1.0 LANGUAGES C) + +# --- Options ---------------------------------------------------------------- + +set(GHOSTTY_ZIG_BUILD_FLAGS "" CACHE STRING "Additional flags to pass to zig build") + +# --- Find Zig ---------------------------------------------------------------- + +find_program(ZIG_EXECUTABLE zig REQUIRED) +message(STATUS "Found zig: ${ZIG_EXECUTABLE}") + +# --- Build via zig build ----------------------------------------------------- + +# The zig build installs into zig-out/ relative to the source tree. +set(ZIG_OUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/zig-out") + +# The library file that zig build produces. +if(APPLE) + set(GHOSTTY_VT_LIBNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(GHOSTTY_VT_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt.0${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(GHOSTTY_VT_REALNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt.0.1.0${CMAKE_SHARED_LIBRARY_SUFFIX}") +elseif(WIN32) + set(GHOSTTY_VT_LIBNAME "ghostty-vt.dll") + set(GHOSTTY_VT_REALNAME "ghostty-vt.dll") +else() + set(GHOSTTY_VT_LIBNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(GHOSTTY_VT_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}.0") + set(GHOSTTY_VT_REALNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}.0.1.0") +endif() + +set(GHOSTTY_VT_LIBRARY "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_REALNAME}") + +# Ensure the output directories exist so CMake doesn't reject the +# INTERFACE_INCLUDE_DIRECTORIES before the zig build has run. +file(MAKE_DIRECTORY "${ZIG_OUT_DIR}/include") + +# Custom command: run zig build lib-vt +add_custom_command( + OUTPUT "${GHOSTTY_VT_LIBRARY}" + COMMAND "${ZIG_EXECUTABLE}" build lib-vt ${GHOSTTY_ZIG_BUILD_FLAGS} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building libghostty-vt via zig build..." + USES_TERMINAL +) + +add_custom_target(zig_build_lib_vt ALL DEPENDS "${GHOSTTY_VT_LIBRARY}") + +# --- IMPORTED library target -------------------------------------------------- + +add_library(ghostty-vt SHARED IMPORTED GLOBAL) +set_target_properties(ghostty-vt PROPERTIES + IMPORTED_LOCATION "${GHOSTTY_VT_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${ZIG_OUT_DIR}/include" +) +if(APPLE) + set_target_properties(ghostty-vt PROPERTIES + IMPORTED_SONAME "@rpath/${GHOSTTY_VT_SONAME}" + ) +elseif(NOT WIN32) + set_target_properties(ghostty-vt PROPERTIES + IMPORTED_SONAME "${GHOSTTY_VT_SONAME}" + ) +endif() + +add_dependencies(ghostty-vt zig_build_lib_vt) + +# --- Install ------------------------------------------------------------------ + +include(GNUInstallDirs) + +# Install the library +install(FILES "${GHOSTTY_VT_LIBRARY}" TYPE LIB) +if(NOT WIN32) + # Install symlinks + install(CODE " + execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink + \"${GHOSTTY_VT_REALNAME}\" + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${GHOSTTY_VT_SONAME}\") + execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink + \"${GHOSTTY_VT_SONAME}\" + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${GHOSTTY_VT_LIBNAME}\") + ") +endif() + +# Install headers +install(DIRECTORY "${ZIG_OUT_DIR}/include/ghostty" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + +# --- CMake package config for find_package() ---------------------------------- + +include(CMakePackageConfigHelpers) + +# Generate the config file +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/dist/cmake/ghostty-vt-config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt" +) + +# Generate the version file +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion +) + +# Install the config files +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt" +) diff --git a/dist/cmake/README.md b/dist/cmake/README.md new file mode 100644 index 000000000..a57d435c8 --- /dev/null +++ b/dist/cmake/README.md @@ -0,0 +1,67 @@ +# CMake Support for libghostty-vt + +The top-level `CMakeLists.txt` wraps the Zig build system so that CMake +projects can consume libghostty-vt without invoking `zig build` manually. +Running `cmake --build` triggers `zig build lib-vt` automatically. + +This means downstream projects do require a working Zig compiler on +`PATH` to build, but don't need to know any Zig-specific details. + +## Using FetchContent (recommended) + +Add the following to your project's `CMakeLists.txt`: + +```cmake +include(FetchContent) +FetchContent_Declare(ghostty + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git + GIT_TAG main +) +FetchContent_MakeAvailable(ghostty) + +add_executable(myapp main.c) +target_link_libraries(myapp PRIVATE ghostty-vt) +``` + +This fetches the Ghostty source, builds libghostty-vt via Zig during your +CMake build, and links it into your target. Headers are added to the +include path automatically. + +### Using a local checkout + +If you already have the Ghostty source checked out, skip the download by +pointing CMake at it: + +```shell-session +cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=/path/to/ghostty +cmake --build build +``` + +## Using find_package (install-based) + +Build and install libghostty-vt first: + +```shell-session +cd /path/to/ghostty +cmake -B build +cmake --build build +cmake --install build --prefix /usr/local +``` + +Then in your project: + +```cmake +find_package(ghostty-vt REQUIRED) + +add_executable(myapp main.c) +target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt) +``` + +## Files + +- `ghostty-vt-config.cmake.in` — template for the CMake package config + file installed alongside the library, enabling `find_package()` support. + +## Example + +See `example/c-vt-cmake/` for a complete working example. diff --git a/dist/cmake/ghostty-vt-config.cmake.in b/dist/cmake/ghostty-vt-config.cmake.in new file mode 100644 index 000000000..d67f99c0b --- /dev/null +++ b/dist/cmake/ghostty-vt-config.cmake.in @@ -0,0 +1,33 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +if(NOT TARGET ghostty-vt::ghostty-vt) + add_library(ghostty-vt::ghostty-vt SHARED IMPORTED) + + set(_ghostty_vt_libdir "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@") + + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_LOCATION "${_ghostty_vt_libdir}/@GHOSTTY_VT_REALNAME@" + INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@" + ) + + if(APPLE) + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_SONAME "@rpath/@GHOSTTY_VT_SONAME@" + INTERFACE_LINK_DIRECTORIES "${_ghostty_vt_libdir}" + ) + # Ensure consumers can find the @rpath dylib at runtime + set_property(TARGET ghostty-vt::ghostty-vt APPEND PROPERTY + INTERFACE_LINK_OPTIONS "LINKER:-rpath,${_ghostty_vt_libdir}" + ) + elseif(NOT WIN32) + set_target_properties(ghostty-vt::ghostty-vt PROPERTIES + IMPORTED_SONAME "@GHOSTTY_VT_SONAME@" + ) + endif() + + unset(_ghostty_vt_libdir) +endif() + +check_required_components(ghostty-vt) diff --git a/example/.gitignore b/example/.gitignore index 3fa248f4c..6f372bc4d 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules/ example.wasm* +build/ diff --git a/example/c-vt-cmake/CMakeLists.txt b/example/c-vt-cmake/CMakeLists.txt new file mode 100644 index 000000000..ff6e35bc1 --- /dev/null +++ b/example/c-vt-cmake/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.19) +project(c-vt-cmake LANGUAGES C) + +include(FetchContent) +FetchContent_Declare(ghostty + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git + GIT_TAG main +) +FetchContent_MakeAvailable(ghostty) + +add_executable(c_vt_cmake src/main.c) +target_link_libraries(c_vt_cmake PRIVATE ghostty-vt) diff --git a/example/c-vt-cmake/README.md b/example/c-vt-cmake/README.md new file mode 100644 index 000000000..d76ca946d --- /dev/null +++ b/example/c-vt-cmake/README.md @@ -0,0 +1,21 @@ +# c-vt-cmake + +Demonstrates consuming libghostty-vt from a CMake project using +`FetchContent`. Creates a terminal, writes VT sequences into it, and +formats the screen contents as plain text. + +## Building + +```shell-session +cd example/c-vt-cmake +cmake -B build +cmake --build build +./build/c_vt_cmake +``` + +To build against a local checkout instead of fetching from GitHub: + +```shell-session +cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../.. +cmake --build build +``` diff --git a/example/c-vt-cmake/src/main.c b/example/c-vt-cmake/src/main.c new file mode 100644 index 000000000..389be5936 --- /dev/null +++ b/example/c-vt-cmake/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"); + + free(buf); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/nix/devShell.nix b/nix/devShell.nix index c78c9081b..df08fc204 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -8,6 +8,7 @@ appstream, flatpak-builder, gdb, + cmake, #, glxinfo # unused ncurses, nodejs, @@ -91,6 +92,7 @@ in packages = [ # For builds + cmake doxygen jq llvmPackages_latest.llvm