feat-deviant(os): embed a configurable resource directory
Relocating a Cargo-installed binary loses executable-relative resource lookup even when themes and shell integration exist under /usr/share. Allow packagers to set GHOSTTY_BUILD_RESOURCES_DIR when compiling. Absolute paths are used directly; relative paths resolve against the running executable's directory for relocatable installations. Explicit paths do not require terminfo sentinels. A nonempty runtime override wins in every profile when the build option is configured. Unset or empty build options preserve existing discovery, including debug precedence. Cargo watches the build option so changing or unsetting it recompiles lookup. Resource files still need to be installed separately. Document this requested upstream deviation and the build option in the README, architecture overview, and deviation register. Test Plan: - Eight resource lookup unit tests pass. - Nightly formatting and ghostty-os Clippy with -D warnings pass. - Production build with /usr/share/ghostty succeeds; a copied executable lists all 463 themes with no runtime override. - Compiled probes verify absolute, relative, unset, and empty options, Cargo rebuilds, relocation, runtime overrides, and unrelated cwd. Deviation-Register: docs/rewrite/DEVIANT_FIXES.md
This commit is contained in:
1 parent
4cfeee71cc
commit
85489a164e
5 files changed
+142
-15
No files matched your search
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<ResourcesDir> {
|
||||
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<ResourcesDir> {
|
||||
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<OsString>) -> 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<OsString>,
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 `<workspace>/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 `<workspace>/resources` directory is used when the binary lives inside this workspace | `crates/ghostty-os/src/resourcesdir.rs` (`dev_resources_dir`) | 967e3a1 |
|
||||
|
||||
## Maintenance
|
||||
|
||||
|
||||
Reference in new issue
Block a user