use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
const GHOSTTY_REPO: &str = "https://github.com/ghostty-org/ghostty.git";
const GHOSTTY_COMMIT: &str = "fdbf9ff3a31d7531b691cb49c98fc465a1a503a0";
#[derive(Clone, Copy)]
enum LinkMode {
Dynamic,
Static,
}
impl LinkMode {
fn current() -> Self {
if cfg!(feature = "link-dynamic") {
Self::Dynamic
} else {
Self::Static
}
}
fn artifact_kind(self) -> &'static str {
match self {
Self::Dynamic => "shared library",
Self::Static => "static library",
}
}
fn matches_library(self, target: &str, file_name: &str) -> bool {
match self {
Self::Dynamic => {
if target.contains("darwin") {
file_name.starts_with("libghostty-vt") && file_name.ends_with(".dylib")
} else if target.contains("windows") {
file_name == "ghostty-vt.lib"
|| file_name == "ghostty-vt.dll"
|| file_name == "libghostty-vt.dll.lib"
|| file_name == "libghostty-vt.dll.a"
} else {
file_name == "libghostty-vt.so" || file_name.starts_with("libghostty-vt.so.")
}
}
Self::Static => {
if target.contains("windows") {
file_name == "ghostty-vt-static.lib"
} else {
file_name == "libghostty-vt.a"
}
}
}
}
#[cfg(feature = "pkg-config")]
fn pkg_config_name(self) -> &'static str {
match self {
Self::Dynamic => "libghostty-vt",
Self::Static => "libghostty-vt-static",
}
}
}
fn main() {
if env::var("DOCS_RS").is_ok() {
return;
}
let link_mode = LinkMode::current();
println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_SYS_OPTIMIZE");
println!("cargo:rerun-if-env-changed=GHOSTTY_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=GHOSTTY_ZIG_SYSTEM_DIR");
println!("cargo:rerun-if-env-changed=TARGET");
println!("cargo:rerun-if-env-changed=HOST");
println!("cargo:rerun-if-env-changed=DEBUG");
println!("cargo:rerun-if-env-changed=OPT_LEVEL");
println!("cargo:rerun-if-changed=crates/libghostty-vt-sys/build.rs");
if env::var_os("GHOSTTY_SOURCE_DIR").is_some() {
build_vendored(link_mode);
return;
}
#[cfg(feature = "pkg-config")]
if try_pkg_config(link_mode) {
return;
}
build_vendored(link_mode);
}
fn build_vendored(link_mode: LinkMode) {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must be set"));
let target = env::var("TARGET").expect("TARGET must be set");
let host = env::var("HOST").expect("HOST must be set");
let ghostty_dir = match env::var("GHOSTTY_SOURCE_DIR") {
Ok(dir) => {
let p = PathBuf::from(dir);
assert!(
p.join("build.zig").exists(),
"GHOSTTY_SOURCE_DIR does not contain build.zig: {}",
p.display()
);
p
}
Err(_) => fetch_ghostty(&out_dir),
};
let install_prefix = out_dir.join("ghostty-install");
let zig_cache_dir = out_dir.join("zig-cache");
let zig_global_cache_dir = out_dir.join("zig-global-cache");
let optimize = zig_optimize_mode();
let mut build = Command::new("zig");
build
.arg("build")
.arg("-Demit-lib-vt")
.arg(format!("-Doptimize={optimize}"))
.arg("-Demit-xcframework=false")
.arg("-Dapp-runtime=none")
.arg("--prefix")
.arg(&install_prefix)
.arg("--cache-dir")
.arg(&zig_cache_dir)
.current_dir(&ghostty_dir);
if let Ok(dir) = env::var("GHOSTTY_ZIG_SYSTEM_DIR") {
assert!(
!dir.is_empty(),
"GHOSTTY_ZIG_SYSTEM_DIR must not be empty when set"
);
let zig_system_dir = PathBuf::from(dir);
assert!(
zig_system_dir.exists(),
"GHOSTTY_ZIG_SYSTEM_DIR does not exist: {}",
zig_system_dir.display()
);
build
.arg("--system")
.arg(&zig_system_dir)
.arg("--global-cache-dir")
.arg(&zig_global_cache_dir);
}
if target != host {
let zig_target = zig_target(&target);
build.arg(format!("-Dtarget={zig_target}"));
}
run(build, "zig build");
let lib_dir = install_prefix.join("lib");
let include_dir = install_prefix.join("include");
let search_dirs = library_search_dirs(&target, &install_prefix);
warn_unused_xcframework(&lib_dir);
let has_requested_library = search_dirs.iter().any(|dir| {
std::fs::read_dir(dir)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", dir.display()))
.any(|entry| {
let entry = entry.unwrap_or_else(|error| {
panic!("failed to read entry from {}: {error}", dir.display())
});
let file_name = entry.file_name();
let Some(file_name) = file_name.to_str() else {
return false;
};
link_mode.matches_library(&target, file_name)
})
});
assert!(
has_requested_library,
"expected libghostty-vt {} in one of {:?}",
link_mode.artifact_kind(),
search_dirs
);
assert!(
include_dir.join("ghostty").join("vt.h").exists(),
"expected header at {}",
include_dir.join("ghostty").join("vt.h").display()
);
for dir in &search_dirs {
println!("cargo:rustc-link-search=native={}", dir.display());
}
match link_mode {
LinkMode::Dynamic => println!("cargo:rustc-link-lib=dylib=ghostty-vt"),
LinkMode::Static => println!("cargo:rustc-link-lib=static=ghostty-vt"),
}
emit_include_metadata(&[include_dir]);
}
fn warn_unused_xcframework(lib_dir: &Path) {
let xcframework = lib_dir.join("ghostty-vt.xcframework");
if xcframework.exists() {
println!(
"cargo:warning=unused libghostty-vt XCFramework emitted at {}; Cargo links the dylib or archive directly",
xcframework.display()
);
}
}
#[cfg(feature = "pkg-config")]
fn try_pkg_config(link_mode: LinkMode) -> bool {
let mut config = pkg_config::Config::new();
let lib = match link_mode {
LinkMode::Dynamic => config.probe(link_mode.pkg_config_name()),
LinkMode::Static => config
.statik(true)
.cargo_metadata(false)
.probe(link_mode.pkg_config_name()),
};
let lib = match lib {
Ok(lib) => lib,
Err(_) => return false,
};
if let LinkMode::Static = link_mode {
emit_static_pkg_config_metadata(&lib);
}
emit_include_metadata(&lib.include_paths);
true
}
#[cfg(feature = "pkg-config")]
fn emit_static_pkg_config_metadata(lib: &pkg_config::Library) {
for path in &lib.link_paths {
println!("cargo:rustc-link-search=native={}", path.display());
}
for path in &lib.link_files {
if let Some(parent) = path.parent() {
println!("cargo:rustc-link-search=native={}", parent.display());
}
}
for path in &lib.framework_paths {
println!("cargo:rustc-link-search=framework={}", path.display());
}
for framework in &lib.frameworks {
println!("cargo:rustc-link-lib=framework={framework}");
}
println!("cargo:rustc-link-lib=static=ghostty-vt");
for library in &lib.libs {
if library != "ghostty-vt" {
println!("cargo:rustc-link-lib={library}");
}
}
for args in &lib.ld_args {
if !args.is_empty() {
println!("cargo:rustc-link-arg=-Wl,{}", args.join(","));
}
}
}
fn emit_include_metadata(include_paths: &[PathBuf]) {
if include_paths.is_empty() {
return;
}
let joined = env::join_paths(include_paths)
.unwrap_or_else(|error| panic!("failed to join include paths for cargo metadata: {error}"));
println!("cargo:include={}", joined.to_string_lossy());
}
fn zig_optimize_mode() -> &'static str {
if let Ok(override_mode) = env::var("LIBGHOSTTY_VT_SYS_OPTIMIZE") {
return match override_mode.as_str() {
"Debug" => "Debug",
"ReleaseSafe" => "ReleaseSafe",
"ReleaseFast" => "ReleaseFast",
"ReleaseSmall" => "ReleaseSmall",
other => panic!(
"LIBGHOSTTY_VT_SYS_OPTIMIZE must be one of Debug, ReleaseSafe, ReleaseFast, ReleaseSmall (got '{other}')"
),
};
}
if env::var("DEBUG").as_deref() == Ok("true") {
return "Debug";
}
match env::var("OPT_LEVEL").as_deref() {
Ok("s") | Ok("z") => "ReleaseSmall",
_ => "ReleaseFast",
}
}
fn fetch_ghostty(out_dir: &Path) -> PathBuf {
let src_dir = out_dir.join("ghostty-src");
let stamp = src_dir.join(".ghostty-commit");
if stamp.exists()
&& let Ok(existing) = std::fs::read_to_string(&stamp)
&& existing.trim() == GHOSTTY_COMMIT
{
return src_dir;
}
if src_dir.exists() {
std::fs::remove_dir_all(&src_dir)
.unwrap_or_else(|e| panic!("failed to remove {}: {e}", src_dir.display()));
}
eprintln!("Fetching ghostty {GHOSTTY_COMMIT} ...");
let mut clone = Command::new("git");
clone
.arg("clone")
.arg("--filter=blob:none")
.arg("--no-checkout")
.arg(GHOSTTY_REPO)
.arg(&src_dir);
run(clone, "git clone ghostty");
let mut checkout = Command::new("git");
checkout
.arg("checkout")
.arg(GHOSTTY_COMMIT)
.current_dir(&src_dir);
run(checkout, "git checkout ghostty commit");
std::fs::write(&stamp, GHOSTTY_COMMIT).unwrap_or_else(|e| panic!("failed to write stamp: {e}"));
src_dir
}
fn run(mut command: Command, context: &str) {
let status = command
.status()
.unwrap_or_else(|error| panic!("failed to execute {context}: {error}"));
assert!(status.success(), "{context} failed with status {status}");
}
fn library_search_dirs(target: &str, install_prefix: &Path) -> Vec<PathBuf> {
let mut dirs = vec![install_prefix.join("lib")];
if target.contains("windows") {
dirs.push(install_prefix.join("bin"));
}
dirs
}
fn zig_target(target: &str) -> String {
let value = match target {
"x86_64-unknown-linux-gnu" => "x86_64-linux-gnu",
"x86_64-unknown-linux-musl" => "x86_64-linux-musl",
"aarch64-unknown-linux-gnu" => "aarch64-linux-gnu",
"aarch64-unknown-linux-musl" => "aarch64-linux-musl",
"aarch64-apple-darwin" => "aarch64-macos-none",
"x86_64-apple-darwin" => "x86_64-macos-none",
"x86_64-pc-windows-gnu" => "x86_64-windows-gnu",
"aarch64-pc-windows-gnullvm" => "aarch64-windows-gnu",
"x86_64-pc-windows-msvc" => "x86_64-windows-msvc",
"aarch64-pc-windows-msvc" => "aarch64-windows-msvc",
"aarch64-linux-android" => "aarch64-linux-android",
"x86_64-linux-android" => "x86_64-linux-android",
other => panic!("unsupported Rust target for vendored build: {other}"),
};
value.to_owned()
}