diff --git a/README.md b/README.md index 94733aa..a85f2c8 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,32 @@ just install /usr/local # binary, share/ghostty, terminfo, desktop file just install /usr false true # system-package layout (lib/systemd/user) ``` +### Build-time resource directory + +Set `GHOSTTY_BUILD_RESOURCES_DIR` while building or installing to embed an +explicit resource directory: + +```sh +GHOSTTY_BUILD_RESOURCES_DIR=/usr/share/ghostty cargo build --profile production +GHOSTTY_BUILD_RESOURCES_DIR=/usr/share/ghostty cargo install --path . --profile production +GHOSTTY_BUILD_RESOURCES_DIR=../share/ghostty cargo build --profile production +``` + +Absolute paths are used directly. Relative paths resolve from the running +executable's directory, not the current working directory; `../share/ghostty` +allows moving an installed `bin/` and `share/` tree together. The directory +contains `themes/` and `shell-integration/`; these files still need to be installed +separately (`cargo install` only installs the executable). + +A nonempty runtime `GHOSTTY_RESOURCES_DIR` overrides the embedded path in every +profile. The embedded path takes precedence over automatic resource discovery +and does not require a terminfo sentinel. An unset or empty build option keeps +the existing discovery rules, including the debug profile's preference for +installed resources over the runtime override. Changing or unsetting the build +option causes Cargo to rebuild resource lookup. Paths are literal: `~` and shell +variables are not expanded by Ghostty. Flatpak's sandbox resource layout remains +managed by its runtime integration. + ## Testing ```sh diff --git a/crates/ghostty-os/build.rs b/crates/ghostty-os/build.rs index a572eab..f95dcf2 100644 --- a/crates/ghostty-os/build.rs +++ b/crates/ghostty-os/build.rs @@ -5,6 +5,8 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-env-changed=PROFILE"); + // Recompile resource lookup when the packager changes the embedded path. + println!("cargo:rerun-if-env-changed=GHOSTTY_BUILD_RESOURCES_DIR"); println!("cargo:rustc-check-cfg=cfg(ghostty_debug_build)"); if std::env::var("PROFILE").is_ok_and(|p| p == "debug") { println!("cargo:rustc-cfg=ghostty_debug_build"); diff --git a/crates/ghostty-os/src/resourcesdir.rs b/crates/ghostty-os/src/resourcesdir.rs index 0f84c1c..51124e1 100644 --- a/crates/ghostty-os/src/resourcesdir.rs +++ b/crates/ghostty-os/src/resourcesdir.rs @@ -73,6 +73,11 @@ const DEV_SENTINELS: [&str; 1] = ["terminfo/x/xterm-ghostty"]; /// Zig's `builtin.mode == .Debug` is mapped to the Cargo `dev` profile /// (`cfg(ghostty_debug_build)`, set by `build.rs`), not to `debug_assertions`: /// the repository's `release` profile enables assertions like ReleaseSafe. +/// +/// `GHOSTTY_BUILD_RESOURCES_DIR` optionally embeds an absolute path or a path +/// relative to the executable directory. When configured, a nonempty runtime +/// `GHOSTTY_RESOURCES_DIR` overrides it in every profile; automatic discovery +/// applies only when the build option is unset or empty. pub fn resources_dir() -> io::Result { let exe = std::env::current_exe().ok(); let env = std::env::var_os("GHOSTTY_RESOURCES_DIR"); @@ -80,23 +85,50 @@ pub fn resources_dir() -> io::Result { cfg!(ghostty_debug_build), exe.as_deref(), env, + option_env!("GHOSTTY_BUILD_RESOURCES_DIR").map(Path::new), )) } -fn resources_dir_impl(debug: bool, exe: Option<&Path>, env: Option) -> ResourcesDir { +/// An explicit build path takes precedence over automatic discovery. Relative +/// build paths are anchored to the executable, never the working directory. +fn resources_dir_impl( + debug: bool, + exe: Option<&Path>, + env: Option, + build_dir: Option<&Path>, +) -> ResourcesDir { + let build_dir = build_dir.filter(|dir| !dir.as_os_str().is_empty()); // Use the GHOSTTY_RESOURCES_DIR environment variable in release builds. // // In debug builds we try using terminfo detection first instead, since // if debug Ghostty is launched by an older version of Ghostty, it // would inherit the old, stale resources of older Ghostty instead of the // freshly built ones under zig-out/share/ghostty. - if !debug && let Some(dir) = env.as_ref().filter(|d| !d.is_empty()) { + // Configured builds allow the runtime override in every profile. Without a + // build path, retain upstream's debug preference for automatic discovery. + if (!debug || build_dir.is_some()) + && let Some(dir) = env.as_ref().filter(|d| !d.is_empty()) + { return ResourcesDir { app_path: Some(EncodedOs::from_os(dir)), host_path: None, }; } + if let Some(dir) = build_dir { + let path = if dir.is_absolute() { + Some(dir.to_path_buf()) + } else { + exe.and_then(Path::parent).map(|base| base.join(dir)) + }; + if let Some(path) = path { + return ResourcesDir { + app_path: Some(enc(&path)), + host_path: None, + }; + } + } + // Get the path to our running binary let Some(mut exe) = exe else { return ResourcesDir::default(); @@ -215,7 +247,7 @@ mod tests { fn detects_share_dir_from_exe() { let td = make_tree(); let exe = td.path().join("bin/ghostty"); - let r = resources_dir_impl(true, Some(&exe), None); + let r = resources_dir_impl(true, Some(&exe), None, None); let expected = format!("{}/share/ghostty", td.path().display()); assert_eq!(r.app().map(EncodedOs::as_encoded), Some(expected.as_str())); } @@ -226,27 +258,27 @@ mod tests { let exe = td.path().join("bin/ghostty"); let env = Some(OsString::from("/env/res")); assert_eq!( - resources_dir_impl(false, Some(&exe), env.clone()) + resources_dir_impl(false, Some(&exe), env.clone(), None) .app() .map(EncodedOs::as_encoded), Some("/env/res") ); assert_ne!( - resources_dir_impl(true, Some(&exe), env.clone()) + resources_dir_impl(true, Some(&exe), env.clone(), None) .app() .map(EncodedOs::as_encoded), Some("/env/res") ); // Debug fallback when detection fails. assert_eq!( - resources_dir_impl(true, Some(Path::new("/nonexistent/bin/x")), env) + resources_dir_impl(true, Some(Path::new("/nonexistent/bin/x")), env, None) .app() .map(EncodedOs::as_encoded), Some("/env/res") ); // Empty env is ignored. assert_eq!( - resources_dir_impl(false, None, Some(OsString::new())) + resources_dir_impl(false, None, Some(OsString::new()), None) .app() .map(EncodedOs::as_encoded), None @@ -256,25 +288,83 @@ mod tests { #[test] fn dev_fallback_finds_workspace_resources() { let exe = workspace_root().join("target/debug/ghostty"); - let r = resources_dir_impl(true, Some(&exe), None); + let r = resources_dir_impl(true, Some(&exe), None, None); assert!(r.app().is_some_and(|d| d.ends_with_plain("/resources"))); // Outside the workspace there is no fallback. - let r = resources_dir_impl(true, Some(Path::new("/nonexistent/bin/x")), None); + let r = resources_dir_impl(true, Some(Path::new("/nonexistent/bin/x")), None, None); assert_eq!(r.app().map(EncodedOs::as_encoded), None); // Release builds run from the workspace use it too... let exe = workspace_root().join("target/production/ghostty"); - let r = resources_dir_impl(false, Some(&exe), None); + let r = resources_dir_impl(false, Some(&exe), None, None); assert!(r.app().is_some_and(|d| d.ends_with_plain("/resources"))); // ...but GHOSTTY_RESOURCES_DIR still takes precedence there. - let r = resources_dir_impl(false, Some(&exe), Some("/env/res".into())); + let r = resources_dir_impl(false, Some(&exe), Some("/env/res".into()), None); assert_eq!(r.app_os().unwrap(), Path::new("/env/res")); } + #[test] + fn configured_absolute_dir_overrides_discovery_without_requiring_exe() { + let td = make_tree(); + let exe = td.path().join("bin/ghostty"); + let configured = Path::new("/configured/resources"); + for debug in [false, true] { + for exe in [Some(exe.as_path()), None] { + let r = resources_dir_impl(debug, exe, None, Some(configured)); + assert_eq!(r.app_os().as_deref(), Some(configured)); + assert_eq!(r.host_os(), r.app_os()); + } + } + } + + #[test] + fn configured_relative_dir_is_relative_to_executable() { + let td = make_tree(); + for name in ["bin/ghostty", "relocated/bin/ghostty"] { + let exe = td.path().join(name); + let expected = exe.parent().unwrap().join("../share/ghostty"); + for debug in [false, true] { + let r = resources_dir_impl( + debug, + Some(&exe), + None, + Some(Path::new("../share/ghostty")), + ); + assert_eq!(r.app_os(), Some(expected.clone())); + } + } + // Without an executable path a relative directory cannot be resolved. + assert_eq!( + resources_dir_impl(false, None, None, Some(Path::new("relative"))), + ResourcesDir::default() + ); + } + + #[test] + fn runtime_env_overrides_configured_dir_in_every_profile() { + let td = make_tree(); + let exe = td.path().join("bin/ghostty"); + let configured = Path::new("/configured/resources"); + for debug in [false, true] { + let r = resources_dir_impl( + debug, + Some(&exe), + Some("/runtime/resources".into()), + Some(configured), + ); + assert_eq!(r.app_os().unwrap(), Path::new("/runtime/resources")); + let r = resources_dir_impl(debug, Some(&exe), Some(OsString::new()), Some(configured)); + assert_eq!(r.app_os().as_deref(), Some(configured)); + // An empty build option preserves automatic discovery. + let r = resources_dir_impl(debug, Some(&exe), None, Some(Path::new(""))); + assert_eq!(r.app_os().unwrap(), td.path().join("share/ghostty")); + } + } + #[test] fn non_utf8_env_dir_is_honored_and_exe_dir_detected() { use std::os::unix::ffi::{OsStrExt, OsStringExt}; let env = OsString::from_vec(b"/env/\xffres".to_vec()); - let r = resources_dir_impl(false, None, Some(env.clone())); + let r = resources_dir_impl(false, None, Some(env.clone()), None); assert_eq!(r.app_os().unwrap().as_os_str(), env.as_os_str()); // Executable lives under a directory with a non-UTF-8 name. @@ -283,7 +373,7 @@ mod tests { std::fs::create_dir_all(root.join("share/terminfo/x")).unwrap(); std::fs::write(root.join("share/terminfo/x/xterm-ghostty"), b"").unwrap(); std::fs::create_dir_all(root.join("bin")).unwrap(); - let r = resources_dir_impl(true, Some(&root.join("bin/ghostty")), None); + let r = resources_dir_impl(true, Some(&root.join("bin/ghostty")), None, None); assert_eq!(r.app_os().unwrap(), root.join("share/ghostty")); } } diff --git a/docs/rewrite/ARCHITECTURE.md b/docs/rewrite/ARCHITECTURE.md index 5862419..d0b0147 100644 --- a/docs/rewrite/ARCHITECTURE.md +++ b/docs/rewrite/ARCHITECTURE.md @@ -70,6 +70,14 @@ ghostty-bench (benchmarks), ghostty-build-data (completions, man pages, syntax). Non-code: `resources/` (shell-integration, themes, terminfo), `dist/` (desktop integration), `po/` (translations), crate-local `res/`/`shaders/`/`resources/`. +Resource lookup: `ghostty-os::resourcesdir` accepts the optional build-time +`GHOSTTY_BUILD_RESOURCES_DIR` path (watched by `ghostty-os/build.rs`). Runtime +`GHOSTTY_RESOURCES_DIR` overrides a configured build path; otherwise that path +precedes automatic discovery. Relative build paths resolve from the executable's +directory, allowing relocatable installations. Without a build path, upstream's +environment/terminfo lookup and the checkout resource fallback apply. See +`README.md` (Build-time resource directory) and `DEVIANT_FIXES.md`. + Build inputs: `ghostty-build-info/build.rs` derives version metadata from the workspace manifest and Git HEAD/refs, watching `packed-refs` only when it exists (packing loose refs also changes the watched `refs` directory). The GTK build diff --git a/docs/rewrite/DEVIANT_FIXES.md b/docs/rewrite/DEVIANT_FIXES.md index 515b591..e5f57f5 100644 --- a/docs/rewrite/DEVIANT_FIXES.md +++ b/docs/rewrite/DEVIANT_FIXES.md @@ -83,7 +83,7 @@ the port matches Ghostty. - Windows: no file locking in the ssh-cache `DiskCache` (Zig does not lock on Windows either). - Config theme-name basename and numeric parsing follow Zig 0.15.2, not 0.16 (`fix(config)` commits 3068e3a, a57ff41). -## Other deliberate deviations (platform/Rust-imposed or out of scope) +## Other deliberate deviations (platform/Rust-imposed or explicitly requested) Only entries with an observable behavioural difference are listed. @@ -105,7 +105,8 @@ Only entries with an observable behavioural difference are listed. | OSC 52 base64 acceptance | Surface.zig:2149,2163 rules | Was different in both directions (Codex core audit); fixed to match Zig | `notes/review-core-codex.md` | 2f58f02 | | Benchmark / synthetic tooling extensions | `ghostty-bench` has no `--time` option and only the simplified terminal handler; `ghostty-synthetic` has the actions `ascii`, `osc`, `utf8` | `ghostty-bench --time` prints the measured step time to stderr; `ghostty-bench --handler=full` runs the full `stream_readonly` handler instead of the print-only one; `ghostty-synthetic +sgr` generates SGR-heavy output. Dev/benchmark tooling only; otherwise upstream behaviour is unchanged | `crates/ghostty-bench/src/main.rs`, `src/benches.rs`, `src/bin/ghostty-synthetic.rs`; `notes/review-rv16-codex.md` | 0c4f0ee | | Inspector font rasterizer (Windows, Apple, wasm) | Dear ImGui is built with `IMGUI_ENABLE_FREETYPE` (`pkg/dcimgui`), so the 32 px inspector font is rasterized by FreeType | Linux and other Unix targets: FreeType (`dear-imgui-rs` feature `freetype`, system FreeType through pkg-config), 32 px. Windows, Apple and wasm32 keep Dear ImGui's stb_truetype loader at 32 px, because `dear-imgui-sys` can only locate FreeType through pkg-config/vcpkg and those targets cannot be built or run here. Glyph anti-aliasing differs slightly there. | `crates/ghostty-inspector/Cargo.toml`, `src/inspector.rs` (`embedded_font_source`) | Z20 (uncommitted) | -| Development resources fallback | `resourcesDir` returns nothing when no terminfo bundle is found next to the binary | In the Cargo `dev` profile, if neither installed resources nor `GHOSTTY_RESOURCES_DIR` apply, the in-repo `/resources` directory is used when the binary lives inside this workspace | `crates/ghostty-os/src/resourcesdir.rs` (`dev_resources_dir`) | 967e3a1 | +| Build-time resource directory | No build-time resource path; relocated binaries need executable-relative terminfo detection or the runtime environment override | Optional `GHOSTTY_BUILD_RESOURCES_DIR` embeds an absolute path or a path relative to the executable directory. A nonempty runtime override wins in every profile when configured, then the build path; unset/empty retains existing discovery. No terminfo sentinel is required for an explicit path. User-requested packaging extension. | `crates/ghostty-os/src/resourcesdir.rs`, `crates/ghostty-os/build.rs`; tests `configured_absolute_dir_overrides_discovery_without_requiring_exe`, `configured_relative_dir_is_relative_to_executable`, `runtime_env_overrides_configured_dir_in_every_profile` | This commit (feat-deviant(os): embed a configurable resource directory) | +| Development resources fallback | `resourcesDir` returns nothing when no terminfo bundle is found next to the binary | In any Cargo profile, if neither installed resources nor `GHOSTTY_RESOURCES_DIR` apply, the in-repo `/resources` directory is used when the binary lives inside this workspace | `crates/ghostty-os/src/resourcesdir.rs` (`dev_resources_dir`) | 967e3a1 | ## Maintenance