Zillowe FoundationZillowe Documentation

Packaging Ghostty

A real-world guide to packaging Ghostty with Zoi, including native dependencies, Zig builds, and split packages.

This guide walks through packaging Ghostty as a Zoi package. Ghostty is a good real-world example because it is not just a single downloaded binary: it has native Linux desktop dependencies, Zig build tooling, generated resources, shell integration files, terminfo files, manuals, desktop files, icons, shared libraries, and optional Nautilus integration.

The example adopts the rigorous authoritative standards required for the Zoidberg main tier:

  • fetch the Ghostty release tarball from GitHub
  • mandatory integrity verification with sha512
  • install native build and runtime dependencies with native:
  • run Ghostty's Zig cache bootstrap script
  • build with zig build
  • split the package into functional sub-packages using the standard :lib, :dev, and :docs convention.

Linux-focused guide

This is a Linux source-build guide. Ghostty packaging differs across platforms, and the exact native dependency names can vary by distribution. Treat this as a strong starting point for a maintained registry package, not as a universal package definition for every operating system.

Package Shape

Ghostty maps naturally to a split package with 13 sub-packages, offering granular control over what gets installed:

Sub-packageOriginPurpose
ghostty:mainPKGBUILD + RPMPrimary Binary: The terminal binary, desktop files, and icons.
ghostty:libRPM specRuntime: Shared library (libghostty-vt.so)
ghostty:devRPM specDevelopment: Headers and .pc file
ghostty:terminfobothTerminfo entries, useful on hosts accessed over SSH
ghostty:bash-completionRPM specBash completions + bash shell integration
ghostty:fish-completionRPM specFish completions + fish shell integration
ghostty:zsh-completionRPM specZsh completions + zsh shell integration
ghostty:nushell-completionRPM specNushell completions
ghostty:nautilusbothOptional GNOME Files integration
ghostty:neovimRPM specNeovim syntax highlighting and compiler
ghostty:vimRPM specVim syntax highlighting and compiler
ghostty:docsRPM specDocumentation: HTML documentation
ghostty:langRPM specTranslation files (if present)

Installing ghostty installs the main terminal plus shared library and terminfo by default, while leaving optional components like shell completions, editor integrations, and Nautilus integration for explicit selection.

Full Source

The complete package definition is available at:

examples/ghostty/ghostty.pkg.lua

It covers all 13 sub-packages listed above and the full build lifecycle. The remainder of this guide explains the key design decisions.

Key Design Decisions

Build Once, Split Later

The prepare() function downloads, extracts, and builds Ghostty in a single pass into a DESTDIR install tree. The package(args) function then copies different parts of that tree depending on which sub-package is being assembled:

function prepare()
  -- Mandatory integrity check
  local file = UTILS.DOWNLOAD(url, archive, "sha512-...")
  UTILS.EXTRACT(file, "src")
  cmd("cd src/" .. src_dir
    .. " && ZIG_GLOBAL_CACHE_DIR=" .. zig_global_cache_dir
    .. " ./nix/build-support/fetch-zig-cache.sh")
  cmd("cd src/" .. src_dir
    .. " && DESTDIR=" .. BUILD_DIR .. "/install zig build"
    .. " --summary all --prefix /usr"
    .. " --system " .. zig_global_cache_dir .. "/p"
    .. " -Doptimize=ReleaseFast -Dgtk-x11=true -Dcpu=baseline"
    .. " -Dpie=true -Demit-docs"
    .. " -Dversion-string=" .. PKG.version .. "-zoi"
    .. " --build-id=sha1")
end

Per-Sub-Package Dependencies

The dependencies() block defines shared runtime deps (GTK4, Wayland, etc.) and sub-package-specific deps. For example, shell completion sub-packages require the corresponding shell, and Nautilus requires GNOME-specific Python bindings:

dependencies({
  build = {
    types = {
      source = {
        required = {
          "native:zig", "native:blueprint-compiler",
          "native:pandoc-cli", "native:pkg-config"
        }
      }
    }
  },
  runtime = {
    required = {
      "native:bzip2", "native:fontconfig", "native:freetype2",
      "native:glib2", "native:gtk4", "native:gtk4-layer-shell",
      "native:harfbuzz", "native:libadwaita", "native:libpng",
      "native:libx11", "native:oniguruma", "native:pixman",
      "native:wayland", "native:zlib"
    },
    sub_packages = {
      ["bash-completion"] = { required = { "native:bash-completion" } },
      ["fish-completion"] = { required = { "native:fish" } },
      ["zsh-completion"]  = { required = { "native:zsh" } },
      ["nushell-completion"] = { required = { "native:nushell" } },
      nautilus = {
        required = {
          "native:nautilus-python",
          "native:python3-gobject"
        }
      },
      neovim = { required = { "native:neovim" } },
      vim    = { required = { "native:vim" } },
      dev    = { required = { "native:zig" } },
    }
  }
})

Library and Development Split

Following the Zoidberg policy, the shared library and development artefacts are separate sub-packages. The :lib sub-package contains only the versioned .so.* runtime files, while :dev ships headers, the unversioned .so symlink, and the pkg-config file:

elseif sub == "lib" then
  cmd("rm -rf lib-runtime && mkdir -p lib-runtime")
  cmd("cp -a " .. root .. "/lib/. lib-runtime/ 2>/dev/null; true")
  cmd("rm -rf lib-runtime/pkgconfig 2>/dev/null; true")
  zcp("lib-runtime", "${pkgstore}/lib")
elseif sub == "dev" then
  if UTILS.FS.exists(root .. "/include") then
    zcp(root .. "/include", "${pkgstore}/include")
  end
  if UTILS.FS.exists(root .. "/lib/pkgconfig") then
    zcp(root .. "/lib/pkgconfig", "${pkgstore}/lib/pkgconfig")
  end
  if UTILS.FS.exists(root .. "/lib/libghostty-vt.so") then
    cmd("mkdir -p dev-lib")
    cmd("cp -a " .. root .. "/lib/libghostty-vt.so dev-lib/")
    zcp("dev-lib", "${pkgstore}/lib")
  end
end

Platform-Specific Optimization

The file includes a sys_libs variable for distributions that package Zig dependencies as system libraries (like openSUSE). Uncomment and set the flag when building on such distributions:

-- On openSUSE, set to:
-- local sys_libs = "-fsys=freetype -fsys=harfbuzz -fsys=fontconfig -fsys=libpng
--   -fsys=zlib -fsys=oniguruma -fsys=gtk4-layer-shell"
local sys_libs = ""

Build and Validate

Validate the package:

zoi package doctor ./ghostty.pkg.lua --platform linux-amd64

Run package tests:

zoi package test ./ghostty.pkg.lua --platform linux-amd64

Build distributable archives:

zoi package build ./ghostty.pkg.lua --platform linux-amd64 --test

Install only the main terminal:

zoi package install ./ghostty-1.3.1-linux-amd64.zpa --sub main

Install the default sub-packages from the package definition:

zoi install ./ghostty.pkg.lua

Maintainer Notes

When turning this into a registry package:

  • MUST confirm the SHA512 hash in prepare() matches the current release
  • MUST use valid SPDX license expressions
  • confirm native package names for each supported distro
  • adjust copied resource paths based on the current Ghostty install tree
  • consider a separate pre-compiled package once trusted release archives are available
  • keep Nautilus, shell completions, and editor integrations optional because they pull in extra dependencies

For package fields and lifecycle details, see Creating Packages.


A software organization

2026 © All Rights Reserved.

  • All the content is available under CC BY-SA 4.0, expect where otherwise stated.

Last updated on