fix: preserve non-UTF-8 bytes in OS, config and CLI layers

Zig treats paths, arguments and environment values as byte slices; the
port converted them lossily in many places. Now (final-Z1.md):
- argv, env (incl. present non-UTF-8 values), HOME, PATH lookup,
  passwd, resources dir, TMPDIR, cgroup, config path canonicalization,
  +validate-config, +list-themes and Zig-string escapes (\xFF) keep raw
  bytes. String-based config values carry bytes via ghostty_os::osbytes,
  a bijective escape (noncharacter U+FDD0 + byte) that leaves all valid
  UTF-8 - including Nerd Font private-use icons - unchanged.
- os/file rlimit fallbacks exist on all targets.
- i18n locale canonicalization is an exact port of libintl's tables
  (e.g. pa-Guru -> pa_IN).
- New ghostty-build-info crate is the single source of version, channel,
  commit and build mode for all consumers.

Test Plan:
- cargo test -p ghostty-os -p ghostty-config -p ghostty-cli
  (incl. randomized osbytes round-trip property tests)
This commit is contained in:
ddidderr committed 2026-10-12 08:14:02 +02:00
1 parent 3f3f6680a5
commit 8519f1ba1b
32 files changed
+1464 -333

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
[package]
name = "ghostty-build-info"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Single source of truth for the derived build version, channel, commit and mode (Zig: build_config.zig)"
[lints]
workspace = true
+144
View File
@@ -0,0 +1,144 @@
//! Derives the build version metadata for [`ghostty_build_info`](crate):
//!
//! * the app version, release channel and commit (Zig: `build/Config.zig`
//! version detection + `build/GitVersion.zig`)
//! * the build mode (Zig: `builtin.mode`)
//!
//! The version can be overridden with `GHOSTTY_VERSION_STRING` (Zig:
//! `-Dversion-string`), which must be a semantic version.
use std::{
path::{Path, PathBuf},
process::Command,
};
fn run(cmd: &str, args: &[&str], dir: &Path) -> Option<String> {
let out = Command::new(cmd)
.args(args)
.current_dir(dir)
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim_end().to_string())
}
/// The `app-version` from `[workspace.metadata.ghostty]` of the workspace
/// manifest (two levels above this crate).
fn app_version(manifest_dir: &Path) -> (u64, u64, u64) {
let root = manifest_dir.join("../../Cargo.toml");
println!("cargo:rerun-if-changed={}", root.display());
let text = std::fs::read_to_string(root).unwrap_or_default();
let v = text
.lines()
.find_map(|l| l.trim().strip_prefix("app-version"))
.and_then(|l| l.split('"').nth(1))
.unwrap_or("1.3.1");
let mut it = v.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
(
it.next().unwrap_or(1),
it.next().unwrap_or(3),
it.next().unwrap_or(1),
)
}
/// Returns `(version_string, commit)` mirroring `Config.zig`.
fn detect_version(manifest_dir: &Path) -> (String, Option<String>) {
println!("cargo:rerun-if-env-changed=GHOSTTY_VERSION_STRING");
if let Ok(v) = std::env::var("GHOSTTY_VERSION_STRING") {
let commit = v.split_once('+').map(|(_, b)| b.to_string());
return (v, commit);
}
let (maj, min, pat) = app_version(manifest_dir);
let base = format!("{maj}.{min}.{pat}");
// Git unavailable or not a repository: unknown dev version.
let Some(branch) = run("git", &["rev-parse", "--abbrev-ref", "HEAD"], manifest_dir) else {
return (format!("{base}-dev+0000000"), Some("0000000".into()));
};
if let Some(git_dir) = run("git", &["rev-parse", "--absolute-git-dir"], manifest_dir) {
let git_dir = PathBuf::from(git_dir);
println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display());
println!("cargo:rerun-if-changed={}", git_dir.join("refs").display());
println!(
"cargo:rerun-if-changed={}",
git_dir.join("packed-refs").display()
);
}
let hash = run(
"git",
&[
"-c",
"log.showSignature=false",
"log",
"--pretty=format:%h",
"-n",
"1",
],
manifest_dir,
)
.unwrap_or_default();
let tag = run(
"git",
&["describe", "--exact-match", "--tags"],
manifest_dir,
)
.filter(|t| !t.is_empty());
if let Some(tag) = tag {
// Tip releases behave just like any other pre-release so we skip.
if tag != "tip" {
assert!(
tag == format!("v{base}"),
"tagged releases must be in vX.Y.Z format matching the workspace app-version"
);
return (base, None);
}
}
// Slashes in branch names are replaced like in `GitVersion.zig`.
let branch = branch.replace('/', "-");
(format!("{base}-{branch}+{hash}"), Some(hash))
}
/// The Zig optimize-mode name for the Cargo profile being built.
fn build_mode() -> &'static str {
// The profile name is the directory below `target/` that holds OUT_DIR.
let out = std::env::var("OUT_DIR").unwrap_or_default();
let profile = Path::new(&out)
.ancestors()
.skip_while(|p| p.file_name().is_none_or(|n| n != "build"))
.nth(1)
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let debug_assertions = std::env::var_os("CARGO_CFG_DEBUG_ASSERTIONS").is_some();
let opt = std::env::var("OPT_LEVEL").unwrap_or_default();
if profile == "production" || !debug_assertions {
".ReleaseFast"
} else if opt == "0" {
".Debug"
} else {
".ReleaseSafe"
}
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let (version_string, commit) = detect_version(&manifest_dir);
// Release channel: stable unless the version has a pre-release part.
let channel = if version_string.contains('-') {
"tip"
} else {
"stable"
};
println!("cargo:rustc-env=GHOSTTY_VERSION_STRING={version_string}");
println!(
"cargo:rustc-env=GHOSTTY_COMMIT={}",
commit.unwrap_or_default()
);
println!("cargo:rustc-env=GHOSTTY_RELEASE_CHANNEL={channel}");
println!("cargo:rustc-env=GHOSTTY_BUILD_MODE={}", build_mode());
}
+106
View File
@@ -0,0 +1,106 @@
//! Single source of truth for the build version metadata (Zig:
//! `build_config.zig` `version`, `version_string`, `release_channel`, plus
//! `builtin.mode`).
//!
//! Everything is derived at compile time by this crate's `build.rs` (git
//! describe / workspace `app-version`, overridable with the
//! `GHOSTTY_VERSION_STRING` environment variable). Consumers (XTVERSION,
//! `TERM_PROGRAM_VERSION`, the About dialog, crash metadata, `+version`) must
//! use these constants instead of hardcoding a version.
/// `build_config.version_string`, e.g. `1.3.1` for tagged releases or
/// `1.3.1-main+abc1234` for development builds.
pub const VERSION_STRING: &str = env!("GHOSTTY_VERSION_STRING");
/// The git commit hash (`build_config.version.build`), if the version has
/// one (tagged releases do not).
pub const COMMIT: Option<&str> = {
let c = env!("GHOSTTY_COMMIT");
if c.is_empty() { None } else { Some(c) }
};
/// `build_config.release_channel` tag name: `stable` or `tip`.
pub const RELEASE_CHANNEL: &str = env!("GHOSTTY_RELEASE_CHANNEL");
/// `{}` of `builtin.mode` (`.Debug`, `.ReleaseSafe`, `.ReleaseFast`).
pub const BUILD_MODE: &str = env!("GHOSTTY_BUILD_MODE");
/// A semantic version (Zig: `std.SemanticVersion`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticVersion<'a> {
pub major: u64,
pub minor: u64,
pub patch: u64,
/// Pre-release part (after `-`), e.g. `main`.
pub pre: Option<&'a str>,
/// Build metadata (after `+`), e.g. the commit hash.
pub build: Option<&'a str>,
}
/// Parse `X.Y.Z[-pre][+build]`.
pub fn parse(v: &str) -> Option<SemanticVersion<'_>> {
let (rest, build) = match v.split_once('+') {
Some((a, b)) => (a, Some(b)),
None => (v, None),
};
let (core, pre) = match rest.split_once('-') {
Some((a, b)) => (a, Some(b)),
None => (rest, None),
};
let mut it = core.split('.');
let major = it.next()?.parse().ok()?;
let minor = it.next()?.parse().ok()?;
let patch = it.next()?.parse().ok()?;
if it.next().is_some() {
return None;
}
Some(SemanticVersion {
major,
minor,
patch,
pre,
build,
})
}
/// `build_config.version` parsed from [`VERSION_STRING`].
pub fn version() -> Option<SemanticVersion<'static>> {
parse(VERSION_STRING)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn version_string_is_semver() {
let v = version().expect("derived version must be semantic");
assert!(v.major >= 1);
assert_eq!(v.build, COMMIT.filter(|_| v.build.is_some()));
}
#[test]
fn channel_matches_prerelease() {
let v = version().unwrap();
assert_eq!(RELEASE_CHANNEL == "tip", v.pre.is_some());
}
#[test]
fn parse_cases() {
assert_eq!(
parse("1.3.1"),
Some(SemanticVersion {
major: 1,
minor: 3,
patch: 1,
pre: None,
build: None
})
);
let v = parse("1.4.0-main+abc1234").unwrap();
assert_eq!((v.pre, v.build), (Some("main"), Some("abc1234")));
assert_eq!(parse("1.3"), None);
assert_eq!(parse("1.3.1.2"), None);
}
}