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 = "6837d7027f226355db661e8215a3ad24ffaf4eb5";
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let vendor_include = manifest_dir.join("vendor/include");
let vt_h = vendor_include.join("ghostty/vt.h");
if !vt_h.exists() {
panic!(
"vendored vt.h not found at {}. Re-vendor the ghostty headers \
matching GHOSTTY_COMMIT (see crates/mnml-libghostty-vt-sys/vendor/README.md).",
vt_h.display()
);
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", vt_h.display());
println!(
"cargo:rerun-if-changed={}",
vendor_include.join("ghostty/vt").display()
);
println!("cargo:rerun-if-env-changed=GHOSTTY_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
link_ghostty_vt();
generate_bindings(&vt_h, &vendor_include);
}
fn link_ghostty_vt() {
if env::var_os("GHOSTTY_SOURCE_DIR").is_some() {
#[cfg(feature = "source-build")]
{
source_build();
return;
}
#[cfg(not(feature = "source-build"))]
panic!(
"GHOSTTY_SOURCE_DIR is set but the `source-build` feature is disabled. \
Either drop the env var or enable the feature."
);
}
#[cfg(feature = "pkg-config")]
{
if try_pkg_config() {
return;
}
}
#[cfg(feature = "source-build")]
{
println!(
"cargo:warning=libghostty-vt: pkg-config unavailable, falling back to zig source-build \
(needs zig 0.16.0 + git on PATH)"
);
source_build();
}
#[cfg(not(feature = "source-build"))]
panic!(
"libghostty-vt: no link path succeeded. Enable `source-build` or set PKG_CONFIG_PATH to \
point at a `libghostty-vt.pc` — see workspace `.cargo/config.toml`."
);
}
#[cfg(feature = "pkg-config")]
fn try_pkg_config() -> bool {
let lib = match pkg_config::Config::new()
.statik(true)
.cargo_metadata(false)
.probe("libghostty-vt-static")
.or_else(|_| {
pkg_config::Config::new()
.statik(true)
.cargo_metadata(false)
.probe("libghostty-vt")
}) {
Ok(l) => l,
Err(_) => return false,
};
for path in &lib.link_paths {
println!("cargo:rustc-link-search=native={}", path.display());
}
for file in &lib.link_files {
if let Some(parent) = file.parent() {
println!("cargo:rustc-link-search=native={}", parent.display());
}
}
println!("cargo:rustc-link-lib=static=ghostty-vt");
for l in &lib.libs {
if l != "ghostty-vt" {
println!("cargo:rustc-link-lib={l}");
}
}
emit_platform_link_libs();
true
}
#[cfg(feature = "source-build")]
fn source_build() {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo"));
let target = env::var("TARGET").expect("TARGET set by cargo");
let host = env::var("HOST").expect("HOST set by cargo");
let ghostty_dir = match env::var("GHOSTTY_SOURCE_DIR") {
Ok(dir) => {
require_tool_or_die("zig", ZIG_MISSING_HELP);
let p = PathBuf::from(dir);
assert!(
p.join("build.zig").exists(),
"GHOSTTY_SOURCE_DIR does not contain build.zig: {}",
p.display()
);
p
}
Err(_) => {
require_tool_or_die("zig", ZIG_MISSING_HELP);
require_tool_or_die("git", GIT_MISSING_HELP);
fetch_ghostty(&out_dir)
}
};
let install_prefix = out_dir.join("ghostty-install");
let zig_cache_dir = out_dir.join("zig-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 target != host || target.contains("windows") {
let zig_target = zig_target(&target);
build.arg(format!("-Dtarget={zig_target}"));
}
println!("cargo:warning=zig invocation: {build:?}");
run(build, "zig build libghostty-vt");
let lib_dir = install_prefix.join("lib");
let mut search_dirs = vec![lib_dir.clone()];
if target.contains("windows") {
search_dirs.push(install_prefix.join("bin"));
}
let mut candidates: Vec<PathBuf> = Vec::new();
let mut all_files: Vec<String> = Vec::new();
for dir in &search_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
all_files.push(format!("{}/{}", dir.display(), name));
let is_static = name.ends_with(".a") || name.ends_with(".lib");
let stem = name.rsplit_once('.').map(|(s, _)| s).unwrap_or(name);
let unprefixed = stem.strip_prefix("lib").unwrap_or(stem);
let is_ghostty_vt = unprefixed == "ghostty-vt"
|| unprefixed.starts_with("ghostty-vt-")
|| unprefixed.starts_with("ghostty-vt.");
let is_import_stub = name.contains(".dll.") || name.contains(".dylib");
if is_static && is_ghostty_vt && !is_import_stub {
candidates.push(path);
}
}
}
}
}
if candidates.is_empty() {
panic!(
"no ghostty-vt static library found after zig build. \
search_dirs: {search_dirs:?}. all files seen: {all_files:?}"
);
}
candidates.sort_by_key(|p| {
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
!name.contains("-static")
});
if candidates.len() > 1 {
println!(
"cargo:warning=multiple ghostty-vt static candidates found ({}); \
using the first after -static preference. all matches: {candidates:?}",
candidates.len()
);
}
let a_path = candidates
.into_iter()
.next()
.expect("checked non-empty above");
println!(
"cargo:warning=libghostty-vt static artifact: {}",
a_path.display()
);
for dir in &search_dirs {
println!("cargo:rustc-link-search=native={}", dir.display());
}
let a_filename = a_path
.file_name()
.and_then(|s| s.to_str())
.expect("artifact path has no filename");
let target_wants_lib = target.contains("windows-msvc");
let target_wants_a = !target.contains("windows-msvc");
let file_is_lib = a_filename.ends_with(".lib");
let file_is_a = a_filename.ends_with(".a");
let convention_mismatch = (target_wants_lib && file_is_a) || (target_wants_a && file_is_lib);
if convention_mismatch {
println!("cargo:rustc-link-lib=static:+verbatim={a_filename}");
} else {
let link_name = a_path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.strip_prefix("lib").unwrap_or(s))
.expect("artifact path has no filename stem");
println!("cargo:rustc-link-lib=static={link_name}");
}
emit_platform_link_libs();
}
fn emit_platform_link_libs() {
#[cfg(target_os = "macos")]
{
println!("cargo:rustc-link-lib=framework=CoreFoundation");
println!("cargo:rustc-link-lib=framework=CoreText");
println!("cargo:rustc-link-lib=framework=CoreGraphics");
println!("cargo:rustc-link-lib=framework=CoreServices");
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=IOSurface");
println!("cargo:rustc-link-lib=c++");
}
#[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
{
println!("cargo:rustc-link-lib=stdc++");
println!("cargo:rustc-link-lib=m");
}
}
#[cfg(feature = "source-build")]
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!("mnml-libghostty-vt-sys: cloning 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 commit stamp: {e}"));
src_dir
}
#[cfg(feature = "source-build")]
fn zig_optimize_mode() -> &'static str {
if env::var("DEBUG").as_deref() == Ok("true") {
"Debug"
} else {
match env::var("OPT_LEVEL").as_deref() {
Ok("s") | Ok("z") => "ReleaseSmall",
_ => "ReleaseFast",
}
}
}
#[cfg(feature = "source-build")]
fn zig_target(target: &str) -> String {
let v = 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",
other => panic!("mnml-libghostty-vt-sys: unsupported target for source-build: {other}"),
};
v.to_owned()
}
#[cfg(feature = "source-build")]
fn run(mut command: Command, context: &str) {
let status = command
.status()
.unwrap_or_else(|e| panic!("failed to execute {context}: {e}"));
assert!(status.success(), "{context} failed with status {status}");
}
#[cfg(feature = "source-build")]
fn require_tool_or_die(tool: &str, help: &str) {
let present = Command::new(tool).arg("--version").output().is_ok();
if !present {
println!("cargo:warning=mnml-libghostty-vt-sys: `{tool}` not found on PATH");
panic!("\n\n{help}\n");
}
}
#[cfg(feature = "source-build")]
const ZIG_MISSING_HELP: &str = "\
mnml-libghostty-vt-sys requires the Zig compiler (0.16.0) to build.
Install it:
macOS: brew install zig
Linux: snap install zig --classic --edge
Windows: scoop install zig
Any OS: download from https://ziglang.org/download/ and put it on PATH
Then re-run: cargo install mnml-rs
If you already have a libghostty-vt.a built elsewhere, set PKG_CONFIG_PATH
to point at a directory containing libghostty-vt.pc and this build will
use it instead of source-building. Local ghostty checkout? Set
GHOSTTY_SOURCE_DIR=/path/to/ghostty.
Or install mnml via one of the prebuilt channels which don't need zig:
brew install chris-mclennan/tap/mnml (macOS / Linux)
scoop install mnml (Windows)
https://github.com/chris-mclennan/mnml/releases (all platforms)";
#[cfg(feature = "source-build")]
const GIT_MISSING_HELP: &str = "\
mnml-libghostty-vt-sys requires `git` on PATH to clone ghostty's source
during the build. Install git via your package manager:
macOS: brew install git (or xcode-select --install)
Linux: apt install git (or your distro's equivalent)
Windows: winget install Git.Git (or scoop install git)
Then re-run: cargo install mnml-rs
Already have a ghostty checkout locally? Point at it with
GHOSTTY_SOURCE_DIR=/path/to/ghostty to skip the clone entirely.";
fn generate_bindings(vt_h: &Path, vendor_include: &Path) {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let bindings_out = out_dir.join("bindings.rs");
#[allow(unused_mut)]
let mut builder = bindgen::Builder::default()
.header(vt_h.to_string_lossy())
.clang_arg(format!("-I{}", vendor_include.display()))
.clang_arg("-xc")
.clang_arg("-std=c11")
.allowlist_type("GhosttyVt.*")
.allowlist_type("Ghostty.*")
.allowlist_function("ghostty_.*")
.allowlist_var("GHOSTTY_.*")
.allowlist_var("Ghostty.*")
.allowlist_recursively(true)
.layout_tests(false)
.default_enum_style(bindgen::EnumVariation::NewType {
is_bitfield: false,
is_global: false,
})
.blocklist_type("__.*")
.generate_comments(true)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
#[cfg(target_os = "macos")]
if let Ok(sdk) = std::process::Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
&& sdk.status.success()
{
let sdk_path = String::from_utf8_lossy(&sdk.stdout).trim().to_string();
if !sdk_path.is_empty() {
builder = builder.clang_arg(format!("-isysroot{sdk_path}"));
}
}
let bindings = builder
.generate()
.expect("bindgen failed to generate bindings for ghostty/vt.h");
bindings
.write_to_file(&bindings_out)
.expect("failed to write bindings.rs");
}