Packages

Terminal emulator library for the BEAM — libghostty-vt NIFs with OTP integration.

Retired package: Release invalid - Hex package omitted Ghostty.TTY backend modules; use 0.4.7.

Current section

Files

Jump to
ghostty lib mix tasks ghostty.setup.ex
Raw

lib/mix/tasks/ghostty.setup.ex

defmodule Mix.Tasks.Ghostty.Setup do
@shortdoc "Build libghostty-vt and install into priv/"
@moduledoc """
Clones the Ghostty source, builds libghostty-vt, and installs the
shared library and headers into `priv/`.
This only needs to run once. After that, `GHOSTTY_BUILD=1 mix compile`
will find the library in `priv/` and build the NIF from source.
Set `GHOSTTY_SOURCE_DIR` to skip cloning and use a local checkout.
## Examples
mix ghostty.setup
GHOSTTY_SOURCE_DIR=~/code/ghostty mix ghostty.setup
"""
use Mix.Task
@ghostty_repo "https://github.com/ghostty-org/ghostty.git"
@ghostty_ref "baad0aa6669dc576872831752be0f30debecbfd1"
@impl true
def run(_args) do
source_dir = source_dir()
ensure_source(source_dir)
build(source_dir)
install(source_dir)
install_to_build()
Mix.shell().info("Done. Run: GHOSTTY_BUILD=1 mix compile")
end
defp source_dir do
System.get_env("GHOSTTY_SOURCE_DIR") ||
Path.join(System.tmp_dir!(), "ghostty-src")
end
defp ensure_source(dir) do
if System.get_env("GHOSTTY_SOURCE_DIR") do
unless File.dir?(dir) do
Mix.raise("GHOSTTY_SOURCE_DIR=#{dir} does not exist")
end
Mix.shell().info("Using local Ghostty source: #{dir}")
else
if File.dir?(Path.join(dir, ".git")) do
Mix.shell().info("Ghostty source already cloned at #{dir}")
else
Mix.shell().info("Cloning ghostty (#{short_ref()})...")
cmd!("git", ["clone", "--depth", "1", @ghostty_repo, dir])
cmd!("git", ["-C", dir, "fetch", "--depth", "1", "origin", @ghostty_ref])
cmd!("git", ["-C", dir, "checkout", @ghostty_ref])
end
end
end
defp build(source_dir) do
zig_out = Path.join(source_dir, "zig-out")
if File.dir?(Path.join(zig_out, "lib")) do
Mix.shell().info("libghostty-vt already built (delete #{zig_out} to rebuild)")
else
Mix.shell().info("Building libghostty-vt...")
cmd!("zig", ["build", "-Demit-lib-vt", "-Doptimize=ReleaseFast"], cd: source_dir)
end
end
defp install(source_dir) do
zig_out = Path.join(source_dir, "zig-out")
priv_dir = Path.join(project_root(), "priv")
lib_dir = Path.join(priv_dir, "lib")
include_dir = Path.join(priv_dir, "include")
File.rm_rf!(lib_dir)
File.rm_rf!(include_dir)
File.mkdir_p!(lib_dir)
File.mkdir_p!(include_dir)
copy_libs(Path.join(zig_out, "lib"), lib_dir)
copy_include_tree(Path.join([zig_out, "include", "ghostty"]), Path.join(include_dir, "ghostty"))
fix_dylib_install_name(lib_dir)
end
defp install_to_build do
src_priv = Path.join(project_root(), "priv")
for env <- ["dev", "test"] do
dest_priv = Path.join([project_root(), "_build", env, "lib", "ghostty", "priv"])
dest_lib = Path.join(dest_priv, "lib")
dest_include = Path.join(dest_priv, "include")
File.rm_rf!(dest_priv)
File.mkdir_p!(dest_lib)
File.mkdir_p!(dest_include)
copy_libs(Path.join(src_priv, "lib"), dest_lib)
copy_include_tree(Path.join(src_priv, "include"), dest_include)
fix_dylib_install_name(dest_lib)
end
end
defp copy_libs(src_dir, dest_dir) do
src_dir
|> File.ls!()
|> Enum.filter(&String.contains?(&1, "ghostty-vt"))
|> Enum.each(fn file ->
src = Path.join(src_dir, file)
dest = Path.join(dest_dir, file)
File.cp!(src, dest)
Mix.shell().info(" #{dest}")
end)
end
defp copy_include_tree(src, dest) do
unless File.dir?(src) do
Mix.raise("missing include directory: #{src}")
end
File.rm_rf!(dest)
File.mkdir_p!(Path.dirname(dest))
File.cp_r!(src, dest)
Mix.shell().info(" #{dest}/")
end
defp fix_dylib_install_name(lib_dir) do
case :os.type() do
{:unix, :darwin} ->
dylib = Path.join(lib_dir, "libghostty-vt.dylib")
absolute_id = Path.expand(dylib)
for file <- File.ls!(lib_dir), String.ends_with?(file, ".dylib") do
path = Path.join(lib_dir, file)
System.cmd("install_name_tool", ["-id", absolute_id, path], stderr_to_stdout: true)
end
_ ->
:ok
end
end
defp cmd!(cmd, args, opts \\ []) do
{output, status} = System.cmd(cmd, args, [{:stderr_to_stdout, true} | opts])
if status != 0 do
Mix.raise("#{cmd} #{Enum.join(args, " ")} failed:\n#{output}")
end
end
defp project_root do
Mix.Project.project_file()
|> Path.dirname()
end
defp short_ref, do: String.slice(@ghostty_ref, 0, 7)
end