feat(dist): website data, library installs, XCFramework, dist tarball

Closes the remaining artifact rows of the final audit
(docs/rewrite/notes/final-Z4.md):
- webgen: config.mdx, actions.mdx, commands.mdx (byte-identical to
  Zig's webgen output once its missing stdout flush is added)
- `just install-lib`: libghostty (+ ghostty.h, modulemap) and
  libghostty-vt with versioned symlinks, headers and libghostty-vt.pc
- scripts/xcframework.sh: per-arch builds, lipo and
  xcodebuild -create-xcframework for GhosttyKit (dry-run verified only)
- `just install` builds --profile production so the installed app id is
  com.mitchellh.ghostty; other profiles install the -debug id/name
- `just update-translations`: xgettext/msgmerge pipeline; extracted
  msgids equal upstream's .pot (found and fixed an untranslated string
  in the child-exited banner)
- `just dist` / `distcheck`: versioned source tarball with VERSION and a
  prebuilt GTK resource bundle
- Windows: icon, version resource and DPI/common-controls manifest
  embedded via embed-resource, GUI subsystem

Test Plan:
- just install-lib /tmp/p release all && pkg-config --cflags \
  --libs /tmp/p/share/pkgconfig/libghostty-vt.pc
- just distcheck
This commit is contained in:
ddidderr committed 2026-10-12 08:14:02 +02:00
1 parent 8af6ab68d3
commit 3f3f6680a5
12 files changed
+795 -23

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Create the versioned source tarball (Zig: `zig build dist`,
# build/GhosttyDist.zig) or check one (`zig build distcheck`).
#
# scripts/dist.sh [OUT_DIR] create OUT_DIR/ghostty-<version>.tar.gz
# scripts/dist.sh --check [TARBALL] extract it and verify it
#
# The tarball extracts into `ghostty-<version>/` (expected by debhelper,
# rpmbuild, ...) and contains, in addition to the checked-in sources:
# * `VERSION`: the embedded Ghostty version (Zig: the `VERSION` file),
# * the prebuilt dist resources that are not committed but built here to cut
# the build-time dependencies of downstream packagers: the compiled GTK
# GResource bundle `crates/ghostty-apprt-gtk/dist/ghostty.gresource`
# (Zig: `src/apprt/gtk/ghostty_resources.{c,h}`; picked up by
# ghostty-apprt-gtk/build.rs outside of a git checkout, replacing
# blueprint-compiler + glib-compile-resources). The boo frame data
# (Zig: `framedata.compressed`) is already committed
# (`crates/ghostty-cli/src/boo_frames.deflate`).
#
# In a git checkout the file set is `git ls-files` (tracked files, plus
# untracked files that are not ignored), otherwise all files except build
# output. Files marked `export-ignore` in .gitattributes are dropped.
set -euo pipefail
cd "$(dirname "$0")/.."
root=$PWD
# Resources that are generated into the tarball (dist path relative to root).
dist_resources=(crates/ghostty-apprt-gtk/dist/ghostty.gresource)
version() {
cargo build -q --release -p ghostty-build-data >&2
"${CARGO_TARGET_DIR:-target}/release/ghostty-build-data" +version
}
if [ "${1:-}" = --check ]; then
version=$(version)
tarball=${2:-target/dist/ghostty-$version.tar.gz}
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
tar -xzf "$tarball" -C "$work"
src="$work/ghostty-$version"
[ -f "$src/VERSION" ] || { echo "missing VERSION" >&2; exit 1; }
[ "$(cat "$src/VERSION")" = "$version" ] || { echo "VERSION mismatch" >&2; exit 1; }
for r in "${dist_resources[@]}"; do
[ -f "$src/$r" ] || { echo "missing dist resource $r" >&2; exit 1; }
done
# Check that the extracted tree builds and passes its tests.
(cd "$src" && cargo test --offline -q -p ghostty-build-data)
echo "distcheck ok: $tarball"
exit 0
fi
out=${1:-target/dist}
version=$(version)
name=ghostty-$version
mkdir -p "$out"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
# Generate the dist resources.
cargo build -q -p ghostty-apprt-gtk
gres=$(find "${CARGO_TARGET_DIR:-target}/debug/build" -path '*ghostty-apprt-gtk-*/out/ghostty.gresource' -printf '%T@ %p\n' |
sort -rn | head -1 | cut -d' ' -f2-)
[ -n "$gres" ] || { echo "ghostty.gresource was not built" >&2; exit 1; }
mkdir -p "$work/stage/crates/ghostty-apprt-gtk/dist"
cp "$gres" "$work/stage/crates/ghostty-apprt-gtk/dist/ghostty.gresource"
printf '%s' "$version" >"$work/stage/VERSION"
# The file list.
if [ -e .git ] && command -v git >/dev/null 2>&1; then
git ls-files -z --cached --others --exclude-standard
else
find . -path ./target -prune -o -path ./.git -prune -o -path ./.zig-cache -prune \
-o -name '*.orig' -prune -o -type f -print0 | sed -z 's|^\./||'
fi >"$work/files0"
# export-ignore (git attribute).
if [ -e .git ] && command -v git >/dev/null 2>&1; then
git check-attr -z export-ignore --stdin -z <"$work/files0" |
tr '\0' '\n' | awk 'NR%3==1{f=$0} NR%3==0 && $0=="set"{print f}' >"$work/ignored" || true
if [ -s "$work/ignored" ]; then
tr '\0' '\n' <"$work/files0" | grep -vxFf "$work/ignored" | tr '\n' '\0' >"$work/files0.new"
mv "$work/files0.new" "$work/files0"
fi
fi
# Drop files that no longer exist (deleted but still tracked).
tr '\0' '\n' <"$work/files0" | while IFS= read -r f; do [ -e "$f" ] && printf '%s\0' "$f"; done >"$work/files"
tar --create --null --files-from "$work/files" \
--transform "s,^,$name/," --sort=name --owner=0 --group=0 --numeric-owner \
-f "$work/src.tar"
(cd "$work/stage" && tar --append -f "$work/src.tar" --transform "s,^,$name/," \
--owner=0 --group=0 --numeric-owner VERSION crates)
gzip -n -c "$work/src.tar" >"$out/$name.tar.gz"
echo "$out/$name.tar.gz"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Install libghostty and/or libghostty-vt below a prefix, mirroring
# build/GhosttyLib.zig (`install`, `installHeader`) and build/GhosttyLibVt.zig.
#
# Usage: scripts/install-lib.sh PREFIX PROFILE [all|vt|ghostty]
# PREFIX install prefix; DESTDIR (env) is prepended for the file copies but
# not for the paths recorded in the pkg-config file.
# PROFILE cargo profile whose artifacts are installed (dev -> target/debug).
# WHAT `vt`: libghostty-vt only (what `zig build` always installs);
# `ghostty`: libghostty only (Zig: `-Dapp-runtime=none`, non-Darwin);
# `all` (default): both.
#
# libghostty-vt (version 0.1.0):
# lib/libghostty-vt.so.0.1.0 (+ libghostty-vt.so.0, libghostty-vt.so symlinks;
# macOS: libghostty-vt.0.1.0.dylib, libghostty-vt.0.dylib, libghostty-vt.dylib)
# include/ghostty/vt.h and include/ghostty/vt/**.h
# share/pkgconfig/libghostty-vt.pc
# libghostty:
# lib/libghostty.so, lib/libghostty.a (macOS: .dylib), include/ghostty.h,
# include/module.modulemap
set -euo pipefail
cd "$(dirname "$0")/.."
prefix=${1:?usage: install-lib.sh PREFIX PROFILE [all|vt|ghostty]}
profile=${2:-production}
what=${3:-all}
dest="${DESTDIR:-}$prefix"
dir=$profile
[ "$profile" = dev ] && dir=debug
tdir=${CARGO_TARGET_DIR:-target}/$dir
cargo_args=(--profile "$profile")
vt_version=0.1.0
vt_major=${vt_version%%.*}
case $(uname -s) in
Darwin) dylib=dylib ;;
*) dylib=so ;;
esac
install_vt() {
cargo build "${cargo_args[@]}" -p ghostty-vt-capi
local src="$tdir/libghostty_vt_capi.$dylib"
install -d "$dest/lib" "$dest/share/pkgconfig"
if [ "$dylib" = so ]; then
local real="libghostty-vt.so.$vt_version"
install -m755 "$src" "$dest/lib/$real"
# The cargo-built soname is the unversioned name; Zig's is `.so.0`.
if command -v patchelf >/dev/null 2>&1; then
patchelf --set-soname "libghostty-vt.so.$vt_major" "$dest/lib/$real"
else
echo "patchelf not found: soname stays libghostty-vt.so" >&2
fi
ln -sf "$real" "$dest/lib/libghostty-vt.so.$vt_major"
ln -sf "libghostty-vt.so.$vt_major" "$dest/lib/libghostty-vt.so"
else
local real="libghostty-vt.$vt_version.dylib"
install -m755 "$src" "$dest/lib/$real"
if command -v install_name_tool >/dev/null 2>&1; then
install_name_tool -id "@rpath/libghostty-vt.$vt_major.dylib" "$dest/lib/$real"
fi
ln -sf "$real" "$dest/lib/libghostty-vt.$vt_major.dylib"
ln -sf "libghostty-vt.$vt_major.dylib" "$dest/lib/libghostty-vt.dylib"
fi
# Headers: include/ghostty/**.h (lib.installHeadersDirectory, .h only).
local inc=crates/ghostty-vt-capi/include/ghostty
while IFS= read -r h; do
install -Dm644 "$inc/$h" "$dest/include/ghostty/$h"
done < <(cd "$inc" && find . -type f -name '*.h' | sed 's|^\./||' | LC_ALL=C sort)
# pkg-config file (no trailing newline, like the Zig `b.fmt` literal).
printf 'prefix=%s\nincludedir=${prefix}/include\nlibdir=${prefix}/lib\n\nName: libghostty-vt\nURL: https://github.com/ghostty-org/ghostty\nDescription: Ghostty VT library\nVersion: %s\nCflags: -I${includedir}\nLibs: -L${libdir} -lghostty-vt' \
"$prefix" "$vt_version" >"$dest/share/pkgconfig/libghostty-vt.pc"
chmod 644 "$dest/share/pkgconfig/libghostty-vt.pc"
}
install_ghostty() {
cargo build "${cargo_args[@]}" -p ghostty-lib
install -d "$dest/lib" "$dest/include"
install -m755 "$tdir/libghostty.$dylib" "$dest/lib/libghostty.$dylib"
install -m644 "$tdir/libghostty.a" "$dest/lib/libghostty.a"
install -m644 crates/ghostty-lib/include/ghostty.h "$dest/include/ghostty.h"
install -m644 crates/ghostty-lib/include/module.modulemap "$dest/include/module.modulemap"
}
case $what in
all) install_vt; install_ghostty ;;
vt) install_vt ;;
ghostty) install_ghostty ;;
*) echo "unknown component: $what" >&2; exit 2 ;;
esac
echo "installed libraries to $dest"
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Update the translation catalogs in po/ (Zig: `zig build update-translations`,
# build/GhosttyI18n.zig `createUpdateStep`).
#
# 1. xgettext over the GTK blueprint files and the Rust sources of the GTK
# apprt (Zig: src/apprt/gtk/**/*.zig) -> gtk.pot. The `tr(...)` function
# of `ghostty_os::i18n` is the Rust counterpart of Zig's `_(...)`.
# 2. xgettext over the Nautilus extension -> py.pot.
# 3. xgettext merge (py.pot first, because of xgettext's charset behaviour
# when merging) -> po/com.mitchellh.ghostty.pot.
# 4. msgmerge every po/<locale>.po against the new template.
#
# Usage: scripts/update-translations.sh [OUT_DIR]
# OUT_DIR where to write the .pot/.po files (default: po/, i.e. in place).
# The existing catalogs are always read from po/.
set -euo pipefail
cd "$(dirname "$0")/.."
domain=com.mitchellh.ghostty
out=${1:-po}
mkdir -p "$out"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
gtk=crates/ghostty-apprt-gtk
# Blueprint files in the order of the GResource list (build.rs BLUEPRINTS,
# Zig: gresource.blueprints). The arguments to xgettext must be relative
# paths in the repo root so the location comments don't depend on where the
# checkout lives.
mapfile -t blueprints < <(
sed -n 's/^[[:space:]]*(\([0-9]*\), \([0-9]*\), "\([^"]*\)"),.*/\1 \2 \3/p' "$gtk/build.rs" |
while read -r major minor name; do
echo "$gtk/resources/ui/$major.$minor/$name.blp"
done
)
# Rust sources of the GTK apprt, sorted for a deterministic order.
mapfile -t sources < <(cd . && find "$gtk/src" -type f -name '*.rs' | LC_ALL=C sort)
xgettext \
--language=C \
--from-code=UTF-8 \
--keyword=tr \
--keyword=_ \
--keyword=C_:1c,2 \
-o "$tmp/gtk.pot" \
"${blueprints[@]}" "${sources[@]}"
xgettext \
--language=Python \
--from-code=UTF-8 \
-o "$tmp/py.pot" \
dist/linux/ghostty_nautilus.py
xgettext \
--add-comments=Translators \
--package-name="$domain" \
--msgid-bugs-address=m@mitchellh.com \
'--copyright-holder="Mitchell Hashimoto, Ghostty contributors"' \
-o - \
"$tmp/py.pot" "$tmp/gtk.pot" >"$tmp/$domain.pot"
for po in po/*.po; do
lang=$(basename "$po" .po)
msgmerge --quiet --no-fuzzy-matching "$po" "$tmp/$domain.pot" >"$tmp/$lang.po"
done
# Only write once everything succeeded (Zig: UpdateSourceFiles).
cp "$tmp/$domain.pot" "$out/$domain.pot"
for po in po/*.po; do
lang=$(basename "$po" .po)
cp "$tmp/$lang.po" "$out/$lang.po"
done
echo "updated $out/$domain.pot and $(ls po/*.po | wc -l) catalogs"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Build GhosttyKit.xcframework (macOS only; mirrors build/GhosttyLib.zig
# `initMacOSUniversal`/`initStatic`, build/GhosttyXCFramework.zig,
# build/LipoStep.zig and build/XCFrameworkStep.zig).
#
# Usage: scripts/xcframework.sh [universal|native] [PROFILE] [OUT]
# universal (default) a universal (arm64 + x86_64, via lipo) macOS static
# library, an iOS device library and an iOS simulator library.
# native only the static library for the host macOS architecture.
# PROFILE cargo profile (default production)
# OUT output path (default macos/GhosttyKit.xcframework, like Zig)
#
# Each slice is `libghostty.a` (the Rust staticlib already bundles every
# dependency, so there is no libtool step) plus the `include/` directory with
# ghostty.h and module.modulemap. Static libraries have no dSYM.
#
# DRY_RUN=1 prints the commands instead of running them (usable on any host).
set -euo pipefail
cd "$(dirname "$0")/.."
target=${1:-universal}
profile=${2:-production}
out=${3:-macos/GhosttyKit.xcframework}
dir=$profile
[ "$profile" = dev ] && dir=debug
tdir=${CARGO_TARGET_DIR:-target}
# Config.zig osVersionMin: macOS 13, iOS 17.
export MACOSX_DEPLOYMENT_TARGET=13.0
export IPHONEOS_DEPLOYMENT_TARGET=17.0
run() {
if [ "${DRY_RUN:-}" = 1 ]; then
printf '+'
printf ' %q' "$@"
printf '\n'
else
"$@"
fi
}
# Build only the static library for a Rust target triple (the crate also
# declares a cdylib, which needs no linking for the xcframework).
build() {
run cargo rustc --profile "$profile" -p ghostty-lib --lib --crate-type staticlib --target "$1"
}
lib() { echo "$tdir/$1/$dir/libghostty.a"; }
host_arch=$(uname -m)
case $host_arch in
arm64 | aarch64) native=aarch64-apple-darwin ;;
*) native=x86_64-apple-darwin ;;
esac
args=()
case $target in
universal)
build aarch64-apple-darwin
build x86_64-apple-darwin
build aarch64-apple-ios
build aarch64-apple-ios-sim
universal_dir="$tdir/universal-apple-darwin/$dir"
run mkdir -p "$universal_dir"
run lipo -create -output "$universal_dir/libghostty.a" \
"$(lib aarch64-apple-darwin)" "$(lib x86_64-apple-darwin)"
args+=(-library "$universal_dir/libghostty.a" -headers crates/ghostty-lib/include)
args+=(-library "$(lib aarch64-apple-ios)" -headers crates/ghostty-lib/include)
args+=(-library "$(lib aarch64-apple-ios-sim)" -headers crates/ghostty-lib/include)
;;
native)
build "$native"
args+=(-library "$(lib "$native")" -headers crates/ghostty-lib/include)
;;
*)
echo "usage: $0 [universal|native] [PROFILE] [OUT]" >&2
exit 2
;;
esac
# The output path is static, so delete the old framework first.
run rm -rf "$out"
run xcodebuild -create-xcframework "${args[@]}" -output "$out"