diff --git a/build.rs b/build.rs index 4ce698d..3f87cff 100644 --- a/build.rs +++ b/build.rs @@ -1,16 +1,44 @@ -//! Link the final binary with lazy symbol binding. +//! Root build script. //! -//! Rust links executables with `-z now` (eager binding) by default. The GTK -//! apprt compiles in code for newer GTK/libadwaita API (e.g. the GTK 4.16 CSS -//! diagnostics) that is only ever called after a runtime `at_least` check, -//! exactly like the Zig build, which links GTK dynamically with lazy binding. -//! With eager binding the dynamic loader would refuse to start the program on -//! an older library that merely lacks such a symbol; with lazy binding the -//! symbol is only resolved if the guarded code actually runs. +//! * Linux: link the final binary with lazy symbol binding. Rust links +//! executables with `-z now` (eager binding) by default. The GTK apprt +//! compiles in code for newer GTK/libadwaita API (e.g. the GTK 4.16 CSS +//! diagnostics) that is only ever called after a runtime `at_least` check, +//! exactly like the Zig build, which links GTK dynamically with lazy +//! binding. With eager binding the dynamic loader would refuse to start the +//! program on an older library that merely lacks such a symbol; with lazy +//! binding the symbol is only resolved if the guarded code actually runs. +//! * Windows (Zig: `GhosttyExe.zig`): use the GUI subsystem and embed +//! `dist/windows/ghostty.rc` (icon, version resource and the manifest that +//! declares per-monitor DPI awareness and the common-controls v6 activation +//! context). fn main() { println!("cargo:rerun-if-changed=build.rs"); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") { - println!("cargo:rustc-link-arg-bins=-Wl,-z,lazy"); + match std::env::var("CARGO_CFG_TARGET_OS").as_deref() { + Ok("linux") => println!("cargo:rustc-link-arg-bins=-Wl,-z,lazy"), + Ok("windows") => windows(), + _ => {} } } + +fn windows() { + println!("cargo:rerun-if-changed=dist/windows/ghostty.rc"); + println!("cargo:rerun-if-changed=dist/windows/ghostty.ico"); + println!("cargo:rerun-if-changed=dist/windows/ghostty.manifest"); + + // `exe.subsystem = .Windows`. + if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") { + println!("cargo:rustc-link-arg-bins=/SUBSYSTEM:WINDOWS"); + println!("cargo:rustc-link-arg-bins=/ENTRY:mainCRTStartup"); + } else { + println!("cargo:rustc-link-arg-bins=-Wl,--subsystem,windows"); + } + + // `exe.addWin32ResourceFile(.{ .file = "dist/windows/ghostty.rc" })`. + // The resource is only linked into the binary targets, not the + // (nonexistent) library target of this package. + embed_resource::compile_for("dist/windows/ghostty.rc", ["ghostty"], embed_resource::NONE) + .manifest_required() + .expect("failed to embed the Windows resource file"); +} diff --git a/crates/ghostty-build-data/src/lib.rs b/crates/ghostty-build-data/src/lib.rs index e3fa2e6..ee5e2f3 100644 --- a/crates/ghostty-build-data/src/lib.rs +++ b/crates/ghostty-build-data/src/lib.rs @@ -15,6 +15,7 @@ pub mod fish; pub mod mdgen; pub mod sublime; pub mod vim; +pub mod webdata; pub mod zsh; #[allow(clippy::needless_raw_string_hashes)] @@ -43,6 +44,14 @@ pub enum Mode { Man1, /// `ghostty(5)` man page markdown (Zig: `mdgen_ghostty_5`). Man5, + /// Website `config.mdx` (Zig: `webgen_config`). + WebdataConfig, + /// Website `actions.mdx` (keybind actions; Zig: `webgen_actions`). + WebdataActions, + /// Website `commands.mdx` (CLI actions; Zig: `webgen_commands`). + WebdataCommands, + /// The Ghostty version string (embedded into source tarballs as `VERSION`). + Version, } impl Mode { @@ -58,6 +67,10 @@ impl Mode { ("terminfo", Mode::Terminfo), ("mdgen-ghostty-1", Mode::Man1), ("mdgen-ghostty-5", Mode::Man5), + ("webgen-config", Mode::WebdataConfig), + ("webgen-actions", Mode::WebdataActions), + ("webgen-commands", Mode::WebdataCommands), + ("version", Mode::Version), ]; /// Parse `+name` (Zig action syntax) or a bare `name`. @@ -82,6 +95,10 @@ impl Mode { Mode::Terminfo => ghostty_termio::terminfo::ghostty.encode_to_string(), Mode::Man1 => mdgen::ghostty_1(), Mode::Man5 => mdgen::ghostty_5(), + Mode::WebdataConfig => webdata::config_mdx(), + Mode::WebdataActions => webdata::actions_mdx(), + Mode::WebdataCommands => webdata::commands_mdx(), + Mode::Version => ghostty_cli::build_info::get().version_string.clone(), } } } @@ -174,6 +191,27 @@ mod tests { assert!(five.contains("**`new_split`**")); } + #[test] + fn webdata_has_frontmatter_and_sections() { + let config = webdata::config_mdx(); + assert!(config.starts_with( + "---\ntitle: Reference\ndescription: Reference of all Ghostty configuration options.\n" + )); + assert!(config.contains("organize these options.\n\n## `language`\n\n")); + assert!(config.contains("## `font-family`\n")); + assert!(!config.contains("## `_")); + // Callouts and indented code blocks are rewritten for the website. + assert!(config.contains("\n") || config.contains("\n")); + assert!(config.contains("```\n")); + let actions = webdata::actions_mdx(); + assert!(actions.starts_with("---\ntitle: Keybinding Action Reference\n")); + assert!(actions.contains("\n## `ignore`\nIgnore this key combination.\n")); + let commands = webdata::commands_mdx(); + assert!(commands.contains("## version\n")); + assert!(commands.contains("\n```\nghostty --version\n```\n\n")); + assert!(commands.contains("\n```\nghostty +list-fonts\n```\n\n")); + } + /// Every supported locale has a `.po` catalog and vice versa /// (Zig: `GhosttyI18n.zig` iterates `os/i18n_locales.zig`). #[test] diff --git a/crates/ghostty-build-data/src/webdata.rs b/crates/ghostty-build-data/src/webdata.rs new file mode 100644 index 0000000..31f97f7 --- /dev/null +++ b/crates/ghostty-build-data/src/webdata.rs @@ -0,0 +1,210 @@ +//! Website data generators (port of `build/GhosttyWebdata.zig` and +//! `build/webgen/main_{config,actions,commands}.zig`). They produce the +//! `config.mdx`, `actions.mdx` (keybind actions) and `commands.mdx` (CLI +//! actions) files installed to `share/ghostty/webdata/`. + +use ghostty_input::binding::ActionKey; + +use crate::config_fields; + +/// `webgen/main_config.zig`: the configuration reference. +#[must_use] +pub fn config_mdx() -> String { + #[derive(Clone, Copy, PartialEq, Eq)] + enum Block { + Text, + Code, + CalloutNote, + CalloutWarning, + } + + fn end_block(w: &mut String, block: Option) { + match block { + Some(Block::Code) => w.push_str("```\n"), + Some(Block::CalloutNote) => w.push_str("\n"), + Some(Block::CalloutWarning) => w.push_str("\n"), + Some(Block::Text) | None => {} + } + } + + fn starts_with_ignore_case(s: &str, prefix: &str) -> bool { + s.len() >= prefix.len() + && s.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()) + } + + let mut w = String::from( + "---\n\ +title: Reference\n\ +description: Reference of all Ghostty configuration options.\n\ +editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/config/Config.zig\n\ +---\n\ +\n\ +This is a reference of all Ghostty configuration options. These\n\ +options are ordered roughly by how common they are to be used\n\ +and grouped with related options. I recommend utilizing your\n\ +browser's search functionality to find the option you're looking\n\ +for.\n\ +\n\ +In the future, we'll have a more user-friendly way to view and\n\ +organize these options.\n\ +\n", + ); + + let fields = config_fields(); + for (i, field) in fields.iter().enumerate() { + if field.help.is_empty() { + continue; + } + w.push_str("## `"); + w.push_str(field.name); + w.push_str("`\n"); + + // For all subsequent fields with no docs, they are grouped with the + // previous field. (Zig breaks at `_` fields; those are already + // filtered out of `config_fields`, which would otherwise leave a + // gap, so the group may only extend to the next raw field.) + for next in &fields[i + 1..] { + if !next.help.is_empty() { + break; + } + w.push_str("## `"); + w.push_str(next.name); + w.push_str("`\n"); + } + + w.push('\n'); + + let mut block: Option = None; + for s in field.help.split('\n') { + // Empty line resets our block. + if s.is_empty() { + end_block(&mut w, block); + block = None; + w.push('\n'); + continue; + } + + let first = block.is_none(); + if block.is_none() { + if s.starts_with(" ") { + block = Some(Block::Code); + w.push_str("```\n"); + } else if starts_with_ignore_case(s, "note:") { + block = Some(Block::CalloutNote); + w.push_str("\n"); + } else if starts_with_ignore_case(s, "warning:") { + block = Some(Block::CalloutWarning); + w.push_str("\n"); + } else { + block = Some(Block::Text); + } + } + + w.push_str(match block { + Some(Block::CalloutNote) if first => &s["note:".len()..], + Some(Block::CalloutWarning) if first => &s["warning:".len()..], + Some(Block::Code) => s.strip_prefix(" ").unwrap_or(s), + _ => s, + }); + w.push('\n'); + } + end_block(&mut w, block); + w.push('\n'); + } + w +} + +/// `webgen/main_actions.zig` (`input/helpgen_actions.zig`, markdown with +/// docs): the keybind action reference. +#[must_use] +pub fn actions_mdx() -> String { + let mut w = String::from( + "---\n\ +title: Keybinding Action Reference\n\ +description: Reference of all Ghostty keybinding actions.\n\ +editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig\n\ +---\n\ +\n\ +This is a reference of all Ghostty keybinding actions.\n\ +\n", + ); + + // Doc comments are written below all related (consecutive undocumented) + // actions, so they are buffered until the next documented action. + let mut stream = String::new(); + for &key in ActionKey::ALL { + let name = key.name(); + if name.starts_with('_') { + continue; + } + let docs = key.help_string(); + let has_docs = !key.docs().is_empty(); + + if has_docs { + w.push_str(&stream); + w.push('\n'); + stream.clear(); + } + + w.push_str("## `"); + w.push_str(name); + w.push_str("`\n"); + + if has_docs { + let mut iter = docs.split('\n').peekable(); + while let Some(s) = iter.next() { + // If it is the last line and empty, then skip it. + if iter.peek().is_none() && s.is_empty() { + continue; + } + stream.push_str(s); + stream.push('\n'); + } + } + } + if !stream.is_empty() { + w.push_str(&stream); + } + w +} + +/// `webgen/main_commands.zig`: the CLI action reference. +#[must_use] +pub fn commands_mdx() -> String { + let mut w = String::from( + "---\n\ +title: Reference\n\ +description: Reference of all Ghostty action subcommands.\n\ +editOnGithubLink: https://github.com/ghostty-org/ghostty/tree/main/src/cli\n\ +---\n\ +Ghostty includes a number of utility actions that can be accessed as subcommands.\n\ +Actions provide utilities to work with config, list keybinds, list fonts, demo themes,\n\ +and debug.\n", + ); + + for &action in ghostty_cli::Action::ALL { + let name = action.name(); + w.push_str("## "); + w.push_str(name); + w.push('\n'); + + let help = action.help_text(); + if help.is_empty() { + continue; + } + for s in help.split('\n') { + w.push_str(s); + w.push('\n'); + } + w.push_str("\n```\n"); + match action { + ghostty_cli::Action::Help | ghostty_cli::Action::Version => { + w.push_str("ghostty --"); + } + _ => w.push_str("ghostty +"), + } + w.push_str(name); + w.push_str("\n```\n\n"); + } + w +} diff --git a/dist/windows/ghostty.ico b/dist/windows/ghostty.ico new file mode 100644 index 0000000..1c5afc2 Binary files /dev/null and b/dist/windows/ghostty.ico differ diff --git a/dist/windows/ghostty.manifest b/dist/windows/ghostty.manifest new file mode 100644 index 0000000..9a07906 --- /dev/null +++ b/dist/windows/ghostty.manifest @@ -0,0 +1,14 @@ + + + + + true/pm + PerMonitorV2 + + + + + + + + diff --git a/dist/windows/ghostty.rc b/dist/windows/ghostty.rc new file mode 100644 index 0000000..0f3d12f --- /dev/null +++ b/dist/windows/ghostty.rc @@ -0,0 +1,36 @@ +// LANG_NEUTRAL(0), SUBLANG_NEUTRAL(0) +LANGUAGE 0, 0 + +#define RT_MANIFEST 1 +RT_MANIFEST 24 "ghostty.manifest" + +#define ID_ICON_GHOSTTY 1 +ID_ICON_GHOSTTY ICON "ghostty.ico" + +VS_VERSION_INFO VERSIONINFO +//FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT +//PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT +//FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +//FILEFLAGS VER_DBG +//FILEOS VOS_NT +//FILETYPE VFT_APP +//FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + //VALUE "CompanyName", "???" + //VALUE "FileDescription", "???" + //VALUE "FileVersion", VERSION + //VALUE "LegalCopyright", "(C) 2024 ???" + VALUE "OriginalFilename", "ghostty.exe" + VALUE "ProductName", "Ghostty" + //VALUE "ProductVersion", VERSION + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409,1200 + END +END diff --git a/docs/rewrite/notes/final-Z4.md b/docs/rewrite/notes/final-Z4.md new file mode 100644 index 0000000..caf89dd --- /dev/null +++ b/docs/rewrite/notes/final-Z4.md @@ -0,0 +1,27 @@ +# Z4: artifact and distribution gaps + +- Webdata (`GhosttyWebdata.zig`): `ghostty-build-data +webgen-{config,actions,commands}` (`src/webdata.rs`); + `just webdata`, `just install-webdata PREFIX`. Byte-identical to Zig 0.15.2 output of a copy whose webgen mains + gained `stdout.flush()`. Upstream's mains never flush the 2048-byte buffer, so the real Zig files are truncated. + Rust emits the complete files. Also `+version` mode (build-info version string). +- libghostty / vt install: `scripts/install-lib.sh`, `just install-lib PREFIX PROFILE WHAT`; `just install` installs vt + (as Zig's default install does). Layout, headers and `libghostty-vt.pc` match the Zig output (soname `.so.0` set via + patchelf, symlinks `.so`, `.so.0`, `.so.0.1.0`). libghostty adds `ghostty.h` + `module.modulemap`. +- XCFramework: `scripts/xcframework.sh` / `just xcframework` (cargo staticlib per target, `lipo -create`, + `xcodebuild -create-xcframework -headers crates/ghostty-lib/include`, iOS + simulator slices). Verified only with + `DRY_RUN=1`: no macOS tools or SDK here; the darwin cross build was not attempted. +- App id: `just install` defaults to `--profile production` (ReleaseFast); other profiles get `-debug` id/name, like + Zig Debug/ReleaseSafe. Desktop, D-Bus, systemd and metainfo files use it. DESTDIR install checked for production + (`com.mitchellh.ghostty`, build mode .ReleaseFast) and release (`-debug`). +- i18n: `scripts/update-translations.sh` / `just update-translations` (xgettext with the same flags + `--keyword=tr` + over blueprints and apprt-gtk Rust sources, py.pot, merge, msgmerge). msgids identical to upstream's .pot. + Found and fixed: `surface_child_exited.rs` didn't translate "Command succeeded/failed" (now `tr`). Script accepts + an OUT_DIR; po/ was not rewritten in place (location comments would churn). +- Dist: `scripts/dist.sh`, `just dist` / `just distcheck`: `ghostty-.tar.gz` with `VERSION` and the prebuilt + `crates/ghostty-apprt-gtk/dist/ghostty.gresource` (used by apprt-gtk/build.rs outside a git checkout, like Zig's + `ghostty_resources.c/h`). Framedata is already committed. As in Zig, VERSION is for packagers (`GHOSTTY_VERSION_STRING`). + Uses `git ls-files` (includes uncommitted files), not `git archive HEAD`. distcheck passes. +- Windows: `dist/windows/{ghostty.rc,ico,manifest}` copied; root `build.rs` uses `embed-resource` plus the GUI subsystem. + The root binary can't cargo-check for Windows (GTK/wayland deps), so verified with a scratch crate using the same + build.rs for x86_64-pc-windows-gnu: PE subsystem 2, manifest (PerMonitorV2, Common-Controls 6) present. +- Touched outside lane: apprt-gtk `build.rs` (prebuilt gresource) and `surface_child_exited.rs` (tr). diff --git a/justfile b/justfile index b024e4b..e9132a8 100644 --- a/justfile +++ b/justfile @@ -89,22 +89,43 @@ locales OUT="target/locale": # Install bin + share/ghostty (shell-integration, themes, docs) + # share/terminfo (compiled db, ghostty.terminfo, ghostty.termcap) + shell # completions, vim/neovim/bat syntax, man pages, translations, desktop file, -# D-Bus/systemd/metainfo and icons, mirroring Ghostty's GhosttyResources.zig, -# GhosttyDocs.zig and GhosttyI18n.zig. +# D-Bus/systemd/metainfo and icons, plus libghostty-vt (lib, headers, +# libghostty-vt.pc), mirroring Ghostty's GhosttyResources.zig, +# GhosttyDocs.zig, GhosttyI18n.zig and GhosttyLibVt.zig. # -# Usage: `just install [PREFIX] [FLATPAK] [SYSTEM_PACKAGE]`; honours DESTDIR. +# Usage: `just install [PREFIX] [FLATPAK] [SYSTEM_PACKAGE] [PROFILE]`; +# honours DESTDIR. # PREFIX install prefix (default /usr/local) # FLATPAK "true": use the Flatpak D-Bus template, no systemd unit # and no 1024x1024 icon (Zig: -Dflatpak) # SYSTEM_PACKAGE "true": install the systemd user unit to lib/systemd/user # instead of share/systemd/user (Zig: --system / system -# package mode) -install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false": build-release build-data docs locales +# package mode) +# PROFILE cargo profile to build and install (default "production": +# no debug assertions, the Zig ReleaseFast equivalent). +# Like Zig's Debug/ReleaseSafe modes, any other profile +# (dev, release) installs the "-debug" application id +# (com.mitchellh.ghostty-debug, "Ghostty (Debug)"), which is +# the id the binary uses at runtime when built with debug +# assertions. +install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false" PROFILE="production": build-data docs locales #!/usr/bin/env bash set -euo pipefail + cargo build --profile {{ PROFILE }} + just install-lib '{{ PREFIX }}' '{{ PROFILE }}' vt P="${DESTDIR:-}{{ PREFIX }}" BD=target/release/ghostty-build-data - install -Dm755 target/release/ghostty "$P/bin/ghostty" + dir={{ PROFILE }} + if [ "$dir" = dev ]; then dir=debug; fi + # GhosttyResources.zig: Debug/ReleaseSafe get the "-debug" id and name. + if [ "{{ PROFILE }}" = production ]; then + APPID=com.mitchellh.ghostty + NAME=Ghostty + else + APPID=com.mitchellh.ghostty-debug + NAME="Ghostty (Debug)" + fi + install -Dm755 "target/$dir/ghostty" "$P/bin/ghostty" mkdir -p "$P/share/ghostty" "$P/share/terminfo" cp -R resources/shell-integration resources/themes "$P/share/ghostty/" find "$P/share/ghostty/shell-integration" -name '*.md' -delete @@ -150,20 +171,20 @@ install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false": build-releas gen() { # template out mkdir -p "$(dirname "$2")" - sed -e 's|@NAME@|Ghostty|g' -e 's|@APPID@|com.mitchellh.ghostty|g' \ + sed -e "s|@NAME@|$NAME|g" -e "s|@APPID@|$APPID|g" \ -e "s|@GHOSTTY@|{{ PREFIX }}/bin/ghostty|g" "$1" > "$2" } - gen dist/linux/app.desktop.in "$P/share/applications/com.mitchellh.ghostty.desktop" + gen dist/linux/app.desktop.in "$P/share/applications/$APPID.desktop" if [ "{{ FLATPAK }}" = "true" ]; then - gen dist/linux/dbus.service.flatpak.in "$P/share/dbus-1/services/com.mitchellh.ghostty.service" + gen dist/linux/dbus.service.flatpak.in "$P/share/dbus-1/services/$APPID.service" else - gen dist/linux/dbus.service.in "$P/share/dbus-1/services/com.mitchellh.ghostty.service" + gen dist/linux/dbus.service.in "$P/share/dbus-1/services/$APPID.service" # systemd looks in lib/ for system packages and share/ otherwise. sysd=share if [ "{{ SYSTEM_PACKAGE }}" = "true" ]; then sysd=lib; fi - gen dist/linux/systemd.service.in "$P/$sysd/systemd/user/app-com.mitchellh.ghostty.service" + gen dist/linux/systemd.service.in "$P/$sysd/systemd/user/app-$APPID.service" fi - gen dist/linux/com.mitchellh.ghostty.metainfo.xml.in "$P/share/metainfo/com.mitchellh.ghostty.metainfo.xml" + gen dist/linux/com.mitchellh.ghostty.metainfo.xml.in "$P/share/metainfo/$APPID.metainfo.xml" install -Dm644 dist/linux/ghostty_dolphin.desktop "$P/share/kio/servicemenus/com.mitchellh.ghostty.desktop" install -Dm755 dist/linux/ghostty_nautilus.py "$P/share/nautilus-python/extensions/ghostty.py" ic="$P/share/icons/hicolor" @@ -173,8 +194,62 @@ install PREFIX="/usr/local" FLATPAK="false" SYSTEM_PACKAGE="false": build-releas if [ "{{ FLATPAK }}" = "true" ] && [ "${pair%%:*}" = "1024" ]; then continue; fi install -Dm644 "dist/icons/${pair%%:*}.png" "$ic/${pair##*:}/apps/com.mitchellh.ghostty.png" done - echo "installed to $P" + echo "installed to $P (application id $APPID)" # Regenerate resources/terminfo from the terminfo source in ghostty-termio. terminfo: scripts/gen-terminfo.sh + +# Install libghostty and libghostty-vt below PREFIX (honours DESTDIR): +# lib/libghostty.{so,a}, include/ghostty.h, include/module.modulemap +# (GhosttyLib.zig) and lib/libghostty-vt.so{,.0,.0.1.0}, +# include/ghostty/vt*.h, share/pkgconfig/libghostty-vt.pc (GhosttyLibVt.zig). +# WHAT is one of: all, vt, ghostty. +install-lib PREFIX="/usr/local" PROFILE="production" WHAT="all": + scripts/install-lib.sh '{{ PREFIX }}' '{{ PROFILE }}' '{{ WHAT }}' + +# Build libghostty (the embedded C API) as static and shared libraries. +lib PROFILE="production": + cargo build -p ghostty-lib --profile {{ PROFILE }} + +# Build GhosttyKit.xcframework with lipo + xcodebuild (macOS only; Zig: +# GhosttyXCFramework.zig). TARGET is "universal" (macOS arm64+x86_64, iOS, +# iOS simulator) or "native". Set DRY_RUN=1 to only print the commands. +xcframework TARGET="universal" PROFILE="production" OUT="macos/GhosttyKit.xcframework": + scripts/xcframework.sh '{{ TARGET }}' '{{ PROFILE }}' '{{ OUT }}' + +# Generate the website reference data (Zig: GhosttyWebdata.zig): config.mdx, +# actions.mdx (keybind actions) and commands.mdx (CLI actions) in OUT. +webdata OUT="target/webdata": build-data + #!/usr/bin/env bash + set -euo pipefail + mkdir -p "{{ OUT }}" + for m in config actions commands; do + target/release/ghostty-build-data "+webgen-$m" > "{{ OUT }}/$m.mdx" + done + +# Install the website data to PREFIX/share/ghostty/webdata (Zig: +# `-Demit-webdata`); honours DESTDIR. +install-webdata PREFIX="/usr/local": webdata + #!/usr/bin/env bash + set -euo pipefail + for m in config actions commands; do + install -Dm644 "target/webdata/$m.mdx" "${DESTDIR:-}{{ PREFIX }}/share/ghostty/webdata/$m.mdx" + done + +# Regenerate po/com.mitchellh.ghostty.pot (xgettext over the GTK blueprints, +# the GTK apprt Rust sources and the Nautilus extension) and msgmerge every +# po/*.po against it (Zig: `zig build update-translations`). +update-translations: + scripts/update-translations.sh + +# Create the versioned source tarball target/dist/ghostty-.tar.gz with +# the embedded VERSION file and the prebuilt dist resources (Zig: `zig build +# dist`). +dist: + scripts/dist.sh target/dist + +# Extract the dist tarball and check that it builds and tests (Zig: +# `zig build distcheck`). +distcheck: dist + scripts/dist.sh --check diff --git a/scripts/dist.sh b/scripts/dist.sh new file mode 100755 index 0000000..e817698 --- /dev/null +++ b/scripts/dist.sh @@ -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-.tar.gz +# scripts/dist.sh --check [TARBALL] extract it and verify it +# +# The tarball extracts into `ghostty-/` (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" diff --git a/scripts/install-lib.sh b/scripts/install-lib.sh new file mode 100755 index 0000000..22aba13 --- /dev/null +++ b/scripts/install-lib.sh @@ -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" diff --git a/scripts/update-translations.sh b/scripts/update-translations.sh new file mode 100755 index 0000000..ab6d583 --- /dev/null +++ b/scripts/update-translations.sh @@ -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/.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" diff --git a/scripts/xcframework.sh b/scripts/xcframework.sh new file mode 100755 index 0000000..04db917 --- /dev/null +++ b/scripts/xcframework.sh @@ -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"