build.zig Improvements (#4797)

This is a major refactor of `build.zig` aimed at improving
maintainability while also fixing some bugs.

The major idea behind the refactor is to split the `build.zig` file up
into distinct `src/build/*.zig` files. By doing so, we can improve
readability of the primary `build.zig` while also enabling better reuse
of steps. Our `build.zig` is now less than 150 lines of code (of course,
it calls into a lot more lines but they're neatly organized now).

Ignore the commit messages I'm going to squash this whole thing.

Improvements:

  * `build.zig` is less than 150 lines of readable code.
* Help strings and unicode table generators are only run once when
multiple artifacts are built since the results are the same regardless
of target.
* Metal lib is only built once per architecture (rather than once per
artifact)
* Resources (shell integration, terminfo, etc.) and docs are only
built/installed for artifacts that need them
  * Wayland protocols no longer required for source build

Breaking changes:

  * Removed broken wasm build (@gabydd will re-add)
* Removed conformance files, shell scripts are better and we don't run
these anymore
* Removed macOS app bundle creation, we don't use this anymore since we
use Xcode

## Extra: Some History

Our `build.zig` hasn't been significantly refactored since the project
started, when Zig was _version 0.10_. Since then, the build system has
changed significantly. We've only ever duct taped the `build.zig` as we
needed to support new Zig versions, new features, etc. It was a mess.

The major improvement is adapting the entire Ghostty `build.zig` to the
Step and LazyPath changes introduced way back in Zig 0.12. This lets us
better take advantage of parallelism and the dependency graph so that
steps are only executed as they're needed.

As such, you can see in the build.zig that we initialize a lot of
things, but unless a final target (i.e. install, run) references those
steps, _they'll never be executed_. This lets us clean up a lot.
This commit is contained in:
Mitchell Hashimoto
2025-01-07 20:45:14 -08:00
committed by GitHub
30 changed files with 2054 additions and 2159 deletions

1895
build.zig

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,14 @@
.hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62", .hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62",
.lazy = true, .lazy = true,
}, },
.vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
},
.z2d = .{
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
},
.zig_objc = .{ .zig_objc = .{
.url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz", .url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634", .hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
@ -29,6 +37,10 @@
.url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz", .url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
.hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c", .hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
}, },
.zf = .{
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
},
// C libs // C libs
.cimgui = .{ .path = "./pkg/cimgui" }, .cimgui = .{ .path = "./pkg/cimgui" },
@ -50,27 +62,25 @@
.glslang = .{ .path = "./pkg/glslang" }, .glslang = .{ .path = "./pkg/glslang" },
.spirv_cross = .{ .path = "./pkg/spirv-cross" }, .spirv_cross = .{ .path = "./pkg/spirv-cross" },
// Wayland
.wayland = .{
.url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
.hash = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f",
},
.wayland_protocols = .{
.url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
.hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef",
},
.plasma_wayland_protocols = .{
.url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
},
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4762ad5bd6d3906e28babdc2bda8a967d63a63be.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4762ad5bd6d3906e28babdc2bda8a967d63a63be.tar.gz",
.hash = "1220a263b22113273d01bd33e3c06b8119cb2f63b4e5d414a85d88e3aa95bb68a2de", .hash = "1220a263b22113273d01bd33e3c06b8119cb2f63b4e5d414a85d88e3aa95bb68a2de",
}, },
.vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
},
.zf = .{
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
},
.z2d = .{
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
},
.plasma_wayland_protocols = .{
.url = "git+https://invent.kde.org/libraries/plasma-wayland-protocols.git?ref=master#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
},
}, },
} }

View File

@ -1,12 +0,0 @@
//! Reverse Index (RI) - ESC M
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("A\nB\nC", .{});
try stdout.print("\x1BM", .{});
try stdout.print("D\n\n", .{});
// const stdin = std.io.getStdIn().reader();
// _ = try stdin.readByte();
}

View File

@ -1,23 +0,0 @@
//! Reverse Index (RI) - ESC M
//! Case: test that if the cursor is at the top, it scrolls down.
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("A\nB\n\n", .{});
try stdout.print("\x1B[H", .{}); // Top-left
try stdout.print("\x1BM", .{}); // Reverse-Index
try stdout.print("D", .{});
try stdout.print("\x0D", .{}); // CR
try stdout.print("\x0A", .{}); // LF
try stdout.print("\x1B[H", .{}); // Top-left
try stdout.print("\x1BM", .{}); // Reverse-Index
try stdout.print("E", .{});
try stdout.print("\n", .{});
// const stdin = std.io.getStdIn().reader();
// _ = try stdin.readByte();
}

View File

@ -1,99 +0,0 @@
//! Outputs various box glyphs for testing.
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// Box Drawing
{
try stdout.print("\x1b[4mBox Drawing\x1b[0m\n", .{});
var i: usize = 0x2500;
const step: usize = 32;
while (i <= 0x257F) : (i += step) {
var j: usize = 0;
while (j < step) : (j += 1) {
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
}
try stdout.print("\n\n", .{});
}
}
// Block Elements
{
try stdout.print("\x1b[4mBlock Elements\x1b[0m\n", .{});
var i: usize = 0x2580;
const step: usize = 32;
while (i <= 0x259f) : (i += step) {
var j: usize = 0;
while (j < step) : (j += 1) {
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
}
try stdout.print("\n\n", .{});
}
}
// Braille Elements
{
try stdout.print("\x1b[4mBraille\x1b[0m\n", .{});
var i: usize = 0x2800;
const step: usize = 32;
while (i <= 0x28FF) : (i += step) {
var j: usize = 0;
while (j < step) : (j += 1) {
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
}
try stdout.print("\n\n", .{});
}
}
{
try stdout.print("\x1b[4mSextants\x1b[0m\n", .{});
var i: usize = 0x1FB00;
const step: usize = 32;
const end = 0x1FB3B;
while (i <= end) : (i += step) {
var j: usize = 0;
while (j < step) : (j += 1) {
const v = i + j;
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
}
try stdout.print("\n\n", .{});
}
}
{
try stdout.print("\x1b[4mWedge Triangles\x1b[0m\n", .{});
var i: usize = 0x1FB3C;
const step: usize = 32;
const end = 0x1FB6B;
while (i <= end) : (i += step) {
var j: usize = 0;
while (j < step) : (j += 1) {
const v = i + j;
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
}
try stdout.print("\n\n", .{});
}
}
{
try stdout.print("\x1b[4mOther\x1b[0m\n", .{});
var i: usize = 0x1FB70;
const step: usize = 32;
const end = 0x1FB8B;
while (i <= end) : (i += step) {
var j: usize = 0;
while (j < step) : (j += 1) {
const v = i + j;
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
}
try stdout.print("\n\n", .{});
}
}
}

View File

@ -1,15 +0,0 @@
//! Set Top and Bottom Margins (DECSTBM) - ESC [ r
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("A\nB\nC\nD", .{});
try stdout.print("\x1B[1;3r", .{}); // cursor up
try stdout.print("\x1B[1;1H", .{}); // top-left
try stdout.print("\x1B[M", .{}); // delete line
try stdout.print("E\n", .{});
try stdout.print("\x1B[7;1H", .{}); // cursor up
// const stdin = std.io.getStdIn().reader();
// _ = try stdin.readByte();
}

View File

@ -1,14 +0,0 @@
//! Delete Line (DL) - Esc [ M
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("A\nB\nC\nD", .{});
try stdout.print("\x1B[2A", .{}); // cursor up
try stdout.print("\x1B[M", .{});
try stdout.print("E\n", .{});
try stdout.print("\x1B[B", .{});
// const stdin = std.io.getStdIn().reader();
// _ = try stdin.readByte();
}

View File

@ -1,17 +0,0 @@
//! Insert Line (IL) - Esc [ L
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("\x1B[2J", .{}); // clear screen
try stdout.print("\x1B[1;1H", .{}); // set cursor position
try stdout.print("A\nB\nC\nD\nE", .{});
try stdout.print("\x1B[1;2r", .{}); // set scroll region
try stdout.print("\x1B[1;1H", .{}); // set cursor position
try stdout.print("\x1B[1L", .{}); // insert lines
try stdout.print("X", .{});
try stdout.print("\x1B[7;1H", .{}); // set cursor position
// const stdin = std.io.getStdIn().reader();
// _ = try stdin.readByte();
}

View File

@ -1,10 +0,0 @@
//! DECALN - ESC # 8
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("\x1B#8", .{});
// const stdin = std.io.getStdIn().reader();
// _ = try stdin.readByte();
}

View File

@ -53,7 +53,6 @@
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
lib.fileset.unions [ lib.fileset.unions [
../dist/linux ../dist/linux
../conformance
../images ../images
../include ../include
../pkg ../pkg

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for # This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details. # more details.
"sha256-eUY6MS3//r6pA/w9b+E4+YqmqUbzpUfL3afJJlnMhLY=" "sha256-PnfSy793kcVt85q47kWR0xkivXoMOZAAmuUyKO9vqAI="

503
src/build/Config.zig Normal file
View File

@ -0,0 +1,503 @@
/// Build configuration. This is the configuration that is populated
/// during `zig build` to control the rest of the build process.
const Config = @This();
const std = @import("std");
const builtin = @import("builtin");
const apprt = @import("../apprt.zig");
const font = @import("../font/main.zig");
const renderer = @import("../renderer.zig");
const Command = @import("../Command.zig");
const WasmTarget = @import("../os/wasm/target.zig").Target;
const gtk = @import("gtk.zig");
const GitVersion = @import("GitVersion.zig");
/// The version of the next release.
///
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 2 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,
target: std.Build.ResolvedTarget,
wasm_target: WasmTarget,
/// Comptime interfaces
app_runtime: apprt.Runtime = .none,
renderer: renderer.Impl = .opengl,
font_backend: font.Backend = .freetype,
/// Feature flags
adwaita: bool = false,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
wasm_shared: bool = true,
/// Ghostty exe properties
exe_entrypoint: ExeEntrypoint = .ghostty,
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
/// Binary properties
pie: bool = false,
strip: bool = false,
patch_rpath: ?[]const u8 = null,
/// Artifacts
flatpak: bool = false,
emit_test_exe: bool = false,
emit_bench: bool = false,
emit_helpgen: bool = false,
emit_docs: bool = false,
emit_webdata: bool = false,
emit_xcframework: bool = false,
/// Environmental properties
env: std.process.EnvMap,
pub fn init(b: *std.Build) !Config {
// Setup our standard Zig target and optimize options, i.e.
// `-Doptimize` and `-Dtarget`.
const optimize = b.standardOptimizeOption(.{});
const target = target: {
var result = b.standardTargetOptions(.{});
// If we're building for macOS and we're on macOS, we need to
// use a generic target to workaround compilation issues.
if (result.result.os.tag == .macos and builtin.target.isDarwin()) {
result = genericMacOSTarget(b, null);
}
// If we have no minimum OS version, we set the default based on
// our tag. Not all tags have a minimum so this may be null.
if (result.query.os_version_min == null) {
result.query.os_version_min = osVersionMin(result.result.os.tag);
}
break :target result;
};
// This is set to true when we're building a system package. For now
// this is trivially detected using the "system_package_mode" bool
// but we may want to make this more sophisticated in the future.
const system_package: bool = b.graph.system_package_mode;
// This specifies our target wasm runtime. For now only one semi-usable
// one exists so this is hardcoded.
const wasm_target: WasmTarget = .browser;
// Determine whether GTK supports X11 and Wayland. This is always safe
// to run even on non-Linux platforms because any failures result in
// defaults.
const gtk_targets = gtk.targets(b);
// We use env vars throughout the build so we grab them immediately here.
var env = try std.process.getEnvMap(b.allocator);
errdefer env.deinit();
var config: Config = .{
.optimize = optimize,
.target = target,
.wasm_target = wasm_target,
.env = env,
};
//---------------------------------------------------------------
// Comptime Interfaces
config.font_backend = b.option(
font.Backend,
"font-backend",
"The font backend to use for discovery and rasterization.",
) orelse font.Backend.default(target.result, wasm_target);
config.app_runtime = b.option(
apprt.Runtime,
"app-runtime",
"The app runtime to use. Not all values supported on all platforms.",
) orelse apprt.Runtime.default(target.result);
config.renderer = b.option(
renderer.Impl,
"renderer",
"The app runtime to use. Not all values supported on all platforms.",
) orelse renderer.Impl.default(target.result, wasm_target);
//---------------------------------------------------------------
// Feature Flags
config.adwaita = b.option(
bool,
"gtk-adwaita",
"Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true;
config.flatpak = b.option(
bool,
"flatpak",
"Build for Flatpak (integrates with Flatpak APIs). Only has an effect targeting Linux.",
) orelse false;
config.sentry = b.option(
bool,
"sentry",
"Build with Sentry crash reporting. Default for macOS is true, false for any other system.",
) orelse sentry: {
switch (target.result.os.tag) {
.macos, .ios => break :sentry true,
// Note its false for linux because the crash reports on Linux
// don't have much useful information.
else => break :sentry false,
}
};
config.wayland = b.option(
bool,
"gtk-wayland",
"Enables linking against Wayland libraries when using the GTK rendering backend.",
) orelse gtk_targets.wayland;
config.x11 = b.option(
bool,
"gtk-x11",
"Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse gtk_targets.x11;
//---------------------------------------------------------------
// Ghostty Exe Properties
const version_string = b.option(
[]const u8,
"version-string",
"A specific version string to use for the build. " ++
"If not specified, git will be used. This must be a semantic version.",
);
config.version = if (version_string) |v|
// If an explicit version is given, we always use it.
try std.SemanticVersion.parse(v)
else version: {
// If no explicit version is given, we try to detect it from git.
const vsn = GitVersion.detect(b) catch |err| switch (err) {
// If Git isn't available we just make an unknown dev version.
error.GitNotFound,
error.GitNotRepository,
=> break :version .{
.major = app_version.major,
.minor = app_version.minor,
.patch = app_version.patch,
.pre = "dev",
.build = "0000000",
},
else => return err,
};
if (vsn.tag) |tag| {
// Tip releases behave just like any other pre-release so we skip.
if (!std.mem.eql(u8, tag, "tip")) {
const expected = b.fmt("v{d}.{d}.{d}", .{
app_version.major,
app_version.minor,
app_version.patch,
});
if (!std.mem.eql(u8, tag, expected)) {
@panic("tagged releases must be in vX.Y.Z format matching build.zig");
}
break :version .{
.major = app_version.major,
.minor = app_version.minor,
.patch = app_version.patch,
};
}
}
break :version .{
.major = app_version.major,
.minor = app_version.minor,
.patch = app_version.patch,
.pre = vsn.branch,
.build = vsn.short_hash,
};
};
//---------------------------------------------------------------
// Binary Properties
// On NixOS, the built binary from `zig build` needs to patch the rpath
// into the built binary for it to be portable across the NixOS system
// it was built for. We default this to true if we can detect we're in
// a Nix shell and have LD_LIBRARY_PATH set.
config.patch_rpath = b.option(
[]const u8,
"patch-rpath",
"Inject the LD_LIBRARY_PATH as the rpath in the built binary. " ++
"This defaults to LD_LIBRARY_PATH if we're in a Nix shell environment on NixOS.",
) orelse patch_rpath: {
// We only do the patching if we're targeting our own CPU and its Linux.
if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_rpath null;
// If we're in a nix shell we default to doing this.
// Note: we purposely never deinit envmap because we leak the strings
if (env.get("IN_NIX_SHELL") == null) break :patch_rpath null;
break :patch_rpath env.get("LD_LIBRARY_PATH");
};
config.pie = b.option(
bool,
"pie",
"Build a Position Independent Executable. Default true for system packages.",
) orelse system_package;
config.strip = b.option(
bool,
"strip",
"Strip the final executable. Default true for fast and small releases",
) orelse switch (optimize) {
.Debug => false,
.ReleaseSafe => false,
.ReleaseFast, .ReleaseSmall => true,
};
//---------------------------------------------------------------
// Artifacts to Emit
config.emit_test_exe = b.option(
bool,
"emit-test-exe",
"Build and install test executables with 'build'",
) orelse false;
config.emit_bench = b.option(
bool,
"emit-bench",
"Build and install the benchmark executables.",
) orelse false;
config.emit_helpgen = b.option(
bool,
"emit-helpgen",
"Build and install the helpgen executable.",
) orelse false;
config.emit_docs = b.option(
bool,
"emit-docs",
"Build and install auto-generated documentation (requires pandoc)",
) orelse emit_docs: {
// If we are emitting any other artifacts then we default to false.
if (config.emit_bench or
config.emit_test_exe or
config.emit_helpgen) break :emit_docs false;
// We always emit docs in system package mode.
if (system_package) break :emit_docs true;
// We only default to true if we can find pandoc.
const path = Command.expandPath(b.allocator, "pandoc") catch
break :emit_docs false;
defer if (path) |p| b.allocator.free(p);
break :emit_docs path != null;
};
config.emit_webdata = b.option(
bool,
"emit-webdata",
"Build the website data for the website.",
) orelse false;
config.emit_xcframework = b.option(
bool,
"emit-xcframework",
"Build and install the xcframework for the macOS library.",
) orelse builtin.target.isDarwin() and
target.result.os.tag == .macos and
config.app_runtime == .none and
(!config.emit_bench and
!config.emit_test_exe and
!config.emit_helpgen);
//---------------------------------------------------------------
// System Packages
// These are all our dependencies that can be used with system
// packages if they exist. We set them up here so that we can set
// their defaults early. The first call configures the integration and
// subsequent calls just return the configured value. This lets them
// show up properly in `--help`.
{
// These dependencies we want to default false if we're on macOS.
// On macOS we don't want to use system libraries because we
// generally want a fat binary. This can be overridden with the
// `-fsys` flag.
for (&[_][]const u8{
"freetype",
"harfbuzz",
"fontconfig",
"libpng",
"zlib",
"oniguruma",
}) |dep| {
_ = b.systemIntegrationOption(
dep,
.{
// If we're not on darwin we want to use whatever the
// default is via the system package mode
.default = if (target.result.isDarwin()) false else null,
},
);
}
// These default to false because they're rarely available as
// system packages so we usually want to statically link them.
for (&[_][]const u8{
"glslang",
"spirv-cross",
"simdutf",
}) |dep| {
_ = b.systemIntegrationOption(dep, .{ .default = false });
}
}
return config;
}
/// Configure the build options with our values.
pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't
// support all types.
step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend);
step.addOption(renderer.Impl, "renderer", self.renderer);
step.addOption(ExeEntrypoint, "exe_entrypoint", self.exe_entrypoint);
step.addOption(WasmTarget, "wasm_target", self.wasm_target);
step.addOption(bool, "wasm_shared", self.wasm_shared);
// Our version. We also add the string version so we don't need
// to do any allocations at runtime. This has to be long enough to
// accommodate realistic large branch names for dev versions.
var buf: [1024]u8 = undefined;
step.addOption(std.SemanticVersion, "app_version", self.version);
step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ(
&buf,
"{}",
.{self.version},
));
step.addOption(
ReleaseChannel,
"release_channel",
channel: {
const pre = self.version.pre orelse break :channel .stable;
if (pre.len == 0) break :channel .stable;
break :channel .tip;
},
);
}
/// Rehydrate our Config from the comptime options. Note that not all
/// options are available at comptime, so look closely at this implementation
/// to see what is and isn't available.
pub fn fromOptions() Config {
const options = @import("build_options");
return .{
// Unused at runtime.
.optimize = undefined,
.target = undefined,
.env = undefined,
.version = options.app_version,
.flatpak = options.flatpak,
.adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?,
.exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?,
.wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?,
.wasm_shared = options.wasm_shared,
};
}
/// Returns the minimum OS version for the given OS tag. This shouldn't
/// be used generally, it should only be used for Darwin-based OS currently.
pub fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion {
return switch (tag) {
// We support back to the earliest officially supported version
// of macOS by Apple. EOL versions are not supported.
.macos => .{ .semver = .{
.major = 13,
.minor = 0,
.patch = 0,
} },
// iOS 17 picked arbitrarily
.ios => .{ .semver = .{
.major = 17,
.minor = 0,
.patch = 0,
} },
// This should never happen currently. If we add a new target then
// we should add a new case here.
else => null,
};
}
// Returns a ResolvedTarget for a mac with a `target.result.cpu.model.name` of `generic`.
// `b.standardTargetOptions()` returns a more specific cpu like `apple_a15`.
//
// This is used to workaround compilation issues on macOS.
// (see for example https://github.com/mitchellh/ghostty/issues/1640).
pub fn genericMacOSTarget(
b: *std.Build,
arch: ?std.Target.Cpu.Arch,
) std.Build.ResolvedTarget {
return b.resolveTargetQuery(.{
.cpu_arch = arch orelse builtin.target.cpu.arch,
.os_tag = .macos,
.os_version_min = osVersionMin(.macos),
});
}
/// The possible entrypoints for the exe artifact. This has no effect on
/// other artifact types (i.e. lib, wasm_module).
///
/// The whole existence of this enum is to workaround the fact that Zig
/// doesn't allow the main function to be in a file in a subdirctory
/// from the "root" of the module, and I don't want to pollute our root
/// directory with a bunch of individual zig files for each entrypoint.
///
/// Therefore, main.zig uses this to switch between the different entrypoints.
pub const ExeEntrypoint = enum {
ghostty,
helpgen,
mdgen_ghostty_1,
mdgen_ghostty_5,
webgen_config,
webgen_actions,
bench_parser,
bench_stream,
bench_codepoint_width,
bench_grapheme_break,
bench_page_init,
};
/// The release channel for the build.
pub const ReleaseChannel = enum {
/// Unstable builds on every commit.
tip,
/// Stable tagged releases.
stable,
};

View File

@ -0,0 +1,69 @@
//! GhosttyBench generates all the Ghostty benchmark helper binaries.
const GhosttyBench = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
steps: []*std.Build.Step.Compile,
pub fn init(
b: *std.Build,
deps: *const SharedDeps,
) !GhosttyBench {
var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);
errdefer steps.deinit();
// Open the directory ./src/bench
const c_dir_path = b.pathFromRoot("src/bench");
var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true });
defer c_dir.close();
// Go through and add each as a step
var c_dir_it = c_dir.iterate();
while (try c_dir_it.next()) |entry| {
// Get the index of the last '.' so we can strip the extension.
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
if (index == 0) continue;
// If it doesn't end in 'zig' then ignore
if (!std.mem.eql(u8, entry.name[index + 1 ..], "zig")) continue;
// Name of the conformance app and full path to the entrypoint.
const name = entry.name[0..index];
// Executable builder.
const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name});
const c_exe = b.addExecutable(.{
.name = bin_name,
.root_source_file = b.path("src/main.zig"),
.target = deps.config.target,
// We always want our benchmarks to be in release mode.
.optimize = .ReleaseFast,
});
c_exe.linkLibC();
// Update our entrypoint
var enum_name: [64]u8 = undefined;
@memcpy(enum_name[0..name.len], name);
std.mem.replaceScalar(u8, enum_name[0..name.len], '-', '_');
var buf: [64]u8 = undefined;
const new_deps = try deps.changeEntrypoint(b, std.meta.stringToEnum(
Config.ExeEntrypoint,
try std.fmt.bufPrint(&buf, "bench_{s}", .{enum_name[0..name.len]}),
).?);
_ = try new_deps.add(c_exe);
try steps.append(c_exe);
}
return .{ .steps = steps.items };
}
pub fn install(self: *const GhosttyBench) void {
const b = self.steps[0].step.owner;
for (self.steps) |step| b.installArtifact(step);
}

92
src/build/GhosttyDocs.zig Normal file
View File

@ -0,0 +1,92 @@
//! GhosttyDocs generates all the on-disk documentation that Ghostty is
//! installed with (man pages, html, markdown, etc.)
const GhosttyDocs = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
steps: []*std.Build.Step,
pub fn init(
b: *std.Build,
deps: *const SharedDeps,
) !GhosttyDocs {
var steps = std.ArrayList(*std.Build.Step).init(b.allocator);
errdefer steps.deinit();
const manpages = [_]struct {
name: []const u8,
section: []const u8,
}{
.{ .name = "ghostty", .section = "1" },
.{ .name = "ghostty", .section = "5" },
};
inline for (manpages) |manpage| {
const generate_markdown = b.addExecutable(.{
.name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section,
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
deps.help_strings.addImport(generate_markdown);
const gen_config = config: {
var copy = deps.config.*;
copy.exe_entrypoint = @field(
Config.ExeEntrypoint,
"mdgen_" ++ manpage.name ++ "_" ++ manpage.section,
);
break :config copy;
};
const generate_markdown_options = b.addOptions();
try gen_config.addOptions(generate_markdown_options);
generate_markdown.root_module.addOptions("build_options", generate_markdown_options);
const generate_markdown_step = b.addRunArtifact(generate_markdown);
const markdown_output = generate_markdown_step.captureStdOut();
try steps.append(&b.addInstallFile(
markdown_output,
"share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".md",
).step);
const generate_html = b.addSystemCommand(&.{"pandoc"});
generate_html.addArgs(&.{
"--standalone",
"--from",
"markdown",
"--to",
"html",
});
generate_html.addFileArg(markdown_output);
try steps.append(&b.addInstallFile(
generate_html.captureStdOut(),
"share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html",
).step);
const generate_manpage = b.addSystemCommand(&.{"pandoc"});
generate_manpage.addArgs(&.{
"--standalone",
"--from",
"markdown",
"--to",
"man",
});
generate_manpage.addFileArg(markdown_output);
try steps.append(&b.addInstallFile(
generate_manpage.captureStdOut(),
"share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section,
).step);
}
return .{ .steps = steps.items };
}
pub fn install(self: *const GhosttyDocs) void {
const b = self.steps[0].owner;
for (self.steps) |step| b.getInstallStep().dependOn(step);
}

120
src/build/GhosttyExe.zig Normal file
View File

@ -0,0 +1,120 @@
const Ghostty = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
/// The primary Ghostty executable.
exe: *std.Build.Step.Compile,
/// The install step for the executable.
install_step: *std.Build.Step.InstallArtifact,
pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty {
const exe: *std.Build.Step.Compile = b.addExecutable(.{
.name = "ghostty",
.root_source_file = b.path("src/main.zig"),
.target = cfg.target,
.optimize = cfg.optimize,
.strip = cfg.strip,
});
const install_step = b.addInstallArtifact(exe, .{});
// Set PIE if requested
if (cfg.pie) exe.pie = true;
// Add the shared dependencies
_ = try deps.add(exe);
// Check for possible issues
try checkNixShell(exe, cfg);
// Patch our rpath if that option is specified.
if (cfg.patch_rpath) |rpath| {
if (rpath.len > 0) {
const run = std.Build.Step.Run.create(b, "patchelf rpath");
run.addArgs(&.{ "patchelf", "--set-rpath", rpath });
run.addArtifactArg(exe);
install_step.step.dependOn(&run.step);
}
}
// OS-specific
switch (cfg.target.result.os.tag) {
.windows => {
exe.subsystem = .Windows;
exe.addWin32ResourceFile(.{
.file = b.path("dist/windows/ghostty.rc"),
});
},
else => {},
}
return .{
.exe = exe,
.install_step = install_step,
};
}
/// Add the ghostty exe to the install target.
pub fn install(self: *const Ghostty) void {
const b = self.install_step.step.owner;
b.getInstallStep().dependOn(&self.install_step.step);
}
/// If we're in NixOS but not in the shell environment then we issue
/// a warning because the rpath may not be setup properly. This doesn't modify
/// our build in any way but addresses a common build-from-source issue
/// for a subset of users.
fn checkNixShell(exe: *std.Build.Step.Compile, cfg: *const Config) !void {
// Non-Linux doesn't have rpath issues.
if (cfg.target.result.os.tag != .linux) return;
// When cross-compiling, we don't need to worry about matching our
// Nix shell rpath since the resulting binary will be run on a
// separate system.
if (!cfg.target.query.isNativeCpu()) return;
if (!cfg.target.query.isNativeOs()) return;
// Verify we're in NixOS
std.fs.accessAbsolute("/etc/NIXOS", .{}) catch return;
// If we're in a nix shell, not a problem
if (cfg.env.get("IN_NIX_SHELL") != null) return;
try exe.step.addError(
"\x1b[" ++ color_map.get("yellow").? ++
"\x1b[" ++ color_map.get("d").? ++
\\Detected building on and for NixOS outside of the Nix shell environment.
\\
\\The resulting ghostty binary will likely fail on launch because it is
\\unable to dynamically load the windowing libs (X11, Wayland, etc.).
\\We highly recommend running only within the Nix build environment
\\and the resulting binary will be portable across your system.
\\
\\To run in the Nix build environment, use the following command.
\\Append any additional options like (`-Doptimize` flags). The resulting
\\binary will be in zig-out as usual.
\\
\\ nix develop -c zig build
\\
++
"\x1b[0m",
.{},
);
}
/// ANSI escape codes for colored log output
const color_map = std.StaticStringMap([]const u8).initComptime(.{
&.{ "black", "30m" },
&.{ "blue", "34m" },
&.{ "b", "1m" },
&.{ "d", "2m" },
&.{ "cyan", "36m" },
&.{ "green", "32m" },
&.{ "magenta", "35m" },
&.{ "red", "31m" },
&.{ "white", "37m" },
&.{ "yellow", "33m" },
});

110
src/build/GhosttyLib.zig Normal file
View File

@ -0,0 +1,110 @@
const GhosttyLib = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
const LibtoolStep = @import("LibtoolStep.zig");
const LipoStep = @import("LipoStep.zig");
/// The step that generates the file.
step: *std.Build.Step,
/// The final static library file
output: std.Build.LazyPath,
pub fn initStatic(
b: *std.Build,
deps: *const SharedDeps,
) !GhosttyLib {
const lib = b.addStaticLibrary(.{
.name = "ghostty",
.root_source_file = b.path("src/main_c.zig"),
.target = deps.config.target,
.optimize = deps.config.optimize,
});
lib.bundle_compiler_rt = true;
lib.linkLibC();
// Add our dependencies. Get the list of all static deps so we can
// build a combined archive if necessary.
var lib_list = try deps.add(lib);
try lib_list.append(lib.getEmittedBin());
if (!deps.config.target.result.isDarwin()) return .{
.step = &lib.step,
.output = lib.getEmittedBin(),
};
// Create a static lib that contains all our dependencies.
const libtool = LibtoolStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty-fat.a",
.sources = lib_list.items,
});
libtool.step.dependOn(&lib.step);
return .{
.step = libtool.step,
.output = libtool.output,
};
}
pub fn initShared(
b: *std.Build,
deps: *const SharedDeps,
) !GhosttyLib {
const lib = b.addSharedLibrary(.{
.name = "ghostty",
.root_source_file = b.path("src/main_c.zig"),
.target = deps.config.target,
.optimize = deps.config.optimize,
.strip = deps.config.strip,
});
_ = try deps.add(lib);
return .{
.step = &lib.step,
.output = lib.getEmittedBin(),
};
}
pub fn initMacOSUniversal(
b: *std.Build,
original_deps: *const SharedDeps,
) !GhosttyLib {
const aarch64 = try initStatic(b, &try original_deps.retarget(
b,
Config.genericMacOSTarget(b, .aarch64),
));
const x86_64 = try initStatic(b, &try original_deps.retarget(
b,
Config.genericMacOSTarget(b, .x86_64),
));
const universal = LipoStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty.a",
.input_a = aarch64.output,
.input_b = x86_64.output,
});
return .{
.step = universal.step,
.output = universal.output,
};
}
pub fn install(self: *const GhosttyLib, name: []const u8) void {
const b = self.step.owner;
const lib_install = b.addInstallLibFile(self.output, name);
b.getInstallStep().dependOn(&lib_install.step);
}
pub fn installHeader(self: *const GhosttyLib) void {
const b = self.step.owner;
const header_install = b.addInstallHeaderFile(
b.path("include/ghostty.h"),
"ghostty.h",
);
b.getInstallStep().dependOn(&header_install.step);
}

View File

@ -0,0 +1,257 @@
const GhosttyResources = @This();
const std = @import("std");
const buildpkg = @import("main.zig");
const Config = @import("Config.zig");
const config_vim = @import("../config/vim.zig");
const config_sublime_syntax = @import("../config/sublime_syntax.zig");
const terminfo = @import("../terminfo/main.zig");
const RunStep = std.Build.Step.Run;
steps: []*std.Build.Step,
pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
var steps = std.ArrayList(*std.Build.Step).init(b.allocator);
errdefer steps.deinit();
// Terminfo
terminfo: {
// Encode our terminfo
var str = std.ArrayList(u8).init(b.allocator);
defer str.deinit();
try terminfo.ghostty.encode(str.writer());
// Write it
var wf = b.addWriteFiles();
const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items);
const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo");
try steps.append(&src_install.step);
// Windows doesn't have the binaries below.
if (cfg.target.result.os.tag == .windows) break :terminfo;
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
// that is used for other commands.
{
const run_step = RunStep.create(b, "infotocap");
run_step.addArg("infotocap");
run_step.addFileArg(src_source);
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap");
try steps.append(&cap_install.step);
}
// Compile the terminfo source into a terminfo database
{
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
const path = run_step.addOutputFileArg("terminfo");
run_step.addFileArg(src_source);
_ = run_step.captureStdErr(); // so we don't see stderr
// Depend on the terminfo source install step so that Zig build
// creates the "share" directory for us.
run_step.step.dependOn(&src_install.step);
{
// Use cp -R instead of Step.InstallDir because we need to preserve
// symlinks in the terminfo database. Zig's InstallDir step doesn't
// handle symlinks correctly yet.
const copy_step = RunStep.create(b, "copy terminfo db");
copy_step.addArgs(&.{ "cp", "-R" });
copy_step.addFileArg(path);
copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
try steps.append(&copy_step.step);
}
}
}
// Shell-integration
{
const install_step = b.addInstallDirectory(.{
.source_dir = b.path("src/shell-integration"),
.install_dir = .{ .custom = "share" },
.install_subdir = b.pathJoin(&.{ "ghostty", "shell-integration" }),
.exclude_extensions = &.{".md"},
});
try steps.append(&install_step.step);
}
// Themes
{
const upstream = b.dependency("iterm2_themes", .{});
const install_step = b.addInstallDirectory(.{
.source_dir = upstream.path("ghostty"),
.install_dir = .{ .custom = "share" },
.install_subdir = b.pathJoin(&.{ "ghostty", "themes" }),
.exclude_extensions = &.{".md"},
});
try steps.append(&install_step.step);
}
// Fish shell completions
{
const wf = b.addWriteFiles();
_ = wf.add("ghostty.fish", buildpkg.fish_completions);
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/fish/vendor_completions.d",
});
try steps.append(&install_step.step);
}
// zsh shell completions
{
const wf = b.addWriteFiles();
_ = wf.add("_ghostty", buildpkg.zsh_completions);
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/zsh/site-functions",
});
try steps.append(&install_step.step);
}
// bash shell completions
{
const wf = b.addWriteFiles();
_ = wf.add("ghostty.bash", buildpkg.bash_completions);
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/bash-completion/completions",
});
try steps.append(&install_step.step);
}
// Vim plugin
{
const wf = b.addWriteFiles();
_ = wf.add("syntax/ghostty.vim", config_vim.syntax);
_ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect);
_ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin);
_ = wf.add("compiler/ghostty.vim", config_vim.compiler);
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/vim/vimfiles",
});
try steps.append(&install_step.step);
}
// Neovim plugin
// This is just a copy-paste of the Vim plugin, but using a Neovim subdir.
// By default, Neovim doesn't look inside share/vim/vimfiles. Some distros
// configure it to do that however. Fedora, does not as a counterexample.
{
const wf = b.addWriteFiles();
_ = wf.add("syntax/ghostty.vim", config_vim.syntax);
_ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect);
_ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin);
_ = wf.add("compiler/ghostty.vim", config_vim.compiler);
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/nvim/site",
});
try steps.append(&install_step.step);
}
// Sublime syntax highlighting for bat cli tool
// NOTE: The current implementation requires symlinking the generated
// 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes'
// directory. The syntax then needs to be mapped to the correct language in
// the config file within the '~.config/bat' directory
// (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config").
{
const wf = b.addWriteFiles();
_ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax);
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/bat/syntaxes",
});
try steps.append(&install_step.step);
}
// App (Linux)
if (cfg.target.result.os.tag == .linux) {
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
// Desktop file so that we have an icon and other metadata
try steps.append(&b.addInstallFile(
b.path("dist/linux/app.desktop"),
"share/applications/com.mitchellh.ghostty.desktop",
).step);
// Right click menu action for Plasma desktop
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_dolphin.desktop"),
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16.png"),
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32.png"),
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128.png"),
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256.png"),
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_512.png"),
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
).step);
// Flatpaks only support icons up to 512x512.
if (!cfg.flatpak) {
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_1024.png"),
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
).step);
}
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16@2x.png"),
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32@2x.png"),
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128@2x.png"),
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256@2x.png"),
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
).step);
}
return .{ .steps = steps.items };
}
pub fn install(self: *const GhosttyResources) void {
const b = self.steps[0].owner;
for (self.steps) |step| b.getInstallStep().dependOn(step);
}

View File

@ -0,0 +1,82 @@
//! GhosttyWebdata generates all the Ghostty website data that is
//! merged with the website for things like config references.
const GhosttyWebdata = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
steps: []*std.Build.Step,
pub fn init(
b: *std.Build,
deps: *const SharedDeps,
) !GhosttyWebdata {
var steps = std.ArrayList(*std.Build.Step).init(b.allocator);
errdefer steps.deinit();
{
const webgen_config = b.addExecutable(.{
.name = "webgen_config",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
deps.help_strings.addImport(webgen_config);
{
const buildconfig = config: {
var copy = deps.config.*;
copy.exe_entrypoint = .webgen_config;
break :config copy;
};
const options = b.addOptions();
try buildconfig.addOptions(options);
webgen_config.root_module.addOptions("build_options", options);
}
const webgen_config_step = b.addRunArtifact(webgen_config);
const webgen_config_out = webgen_config_step.captureStdOut();
try steps.append(&b.addInstallFile(
webgen_config_out,
"share/ghostty/webdata/config.mdx",
).step);
}
{
const webgen_actions = b.addExecutable(.{
.name = "webgen_actions",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
deps.help_strings.addImport(webgen_actions);
{
const buildconfig = config: {
var copy = deps.config.*;
copy.exe_entrypoint = .webgen_actions;
break :config copy;
};
const options = b.addOptions();
try buildconfig.addOptions(options);
webgen_actions.root_module.addOptions("build_options", options);
}
const webgen_actions_step = b.addRunArtifact(webgen_actions);
const webgen_actions_out = webgen_actions_step.captureStdOut();
try steps.append(&b.addInstallFile(
webgen_actions_out,
"share/ghostty/webdata/actions.mdx",
).step);
}
return .{ .steps = steps.items };
}
pub fn install(self: *const GhosttyWebdata) void {
const b = self.steps[0].owner;
for (self.steps) |step| b.getInstallStep().dependOn(step);
}

View File

@ -0,0 +1,68 @@
const GhosttyXCFramework = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
const GhosttyLib = @import("GhosttyLib.zig");
const XCFrameworkStep = @import("XCFrameworkStep.zig");
xcframework: *XCFrameworkStep,
macos: GhosttyLib,
pub fn init(b: *std.Build, deps: *const SharedDeps) !GhosttyXCFramework {
// Create our universal macOS static library.
const macos = try GhosttyLib.initMacOSUniversal(b, deps);
// iOS
const ios = try GhosttyLib.initStatic(b, &try deps.retarget(
b,
b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .ios,
.os_version_min = Config.osVersionMin(.ios),
.abi = null,
}),
));
// iOS Simulator
const ios_sim = try GhosttyLib.initStatic(b, &try deps.retarget(
b,
b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .ios,
.os_version_min = Config.osVersionMin(.ios),
.abi = .simulator,
}),
));
// The xcframework wraps our ghostty library so that we can link
// it to the final app built with Swift.
const xcframework = XCFrameworkStep.create(b, .{
.name = "GhosttyKit",
.out_path = "macos/GhosttyKit.xcframework",
.libraries = &.{
.{
.library = macos.output,
.headers = b.path("include"),
},
.{
.library = ios.output,
.headers = b.path("include"),
},
.{
.library = ios_sim.output,
.headers = b.path("include"),
},
},
});
return .{
.xcframework = xcframework,
.macos = macos,
};
}
pub fn install(self: *const GhosttyXCFramework) void {
const b = self.xcframework.step.owner;
b.getInstallStep().dependOn(self.xcframework.step);
}

46
src/build/HelpStrings.zig Normal file
View File

@ -0,0 +1,46 @@
const HelpStrings = @This();
const std = @import("std");
const Config = @import("Config.zig");
/// The "helpgen" exe.
exe: *std.Build.Step.Compile,
/// The output path for the help strings.
output: std.Build.LazyPath,
pub fn init(b: *std.Build, cfg: *const Config) !HelpStrings {
const exe = b.addExecutable(.{
.name = "helpgen",
.root_source_file = b.path("src/helpgen.zig"),
.target = b.host,
});
const help_config = config: {
var copy = cfg.*;
copy.exe_entrypoint = .helpgen;
break :config copy;
};
const options = b.addOptions();
try help_config.addOptions(options);
exe.root_module.addOptions("build_options", options);
const help_run = b.addRunArtifact(exe);
return .{
.exe = exe,
.output = help_run.captureStdOut(),
};
}
/// Add the "help_strings" import.
pub fn addImport(self: *const HelpStrings, step: *std.Build.Step.Compile) void {
self.output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("help_strings", .{
.root_source_file = self.output,
});
}
/// Install the help exe
pub fn install(self: *const HelpStrings) void {
self.exe.step.owner.installArtifact(self.exe);
}

View File

@ -21,13 +21,13 @@ pub const Options = struct {
step: *Step, step: *Step,
output: LazyPath, output: LazyPath,
pub fn create(b: *std.Build, opts: Options) *MetallibStep { pub fn create(b: *std.Build, opts: Options) ?*MetallibStep {
const self = b.allocator.create(MetallibStep) catch @panic("OOM"); const self = b.allocator.create(MetallibStep) catch @panic("OOM");
const sdk = switch (opts.target.result.os.tag) { const sdk = switch (opts.target.result.os.tag) {
.macos => "macosx", .macos => "macosx",
.ios => "iphoneos", .ios => "iphoneos",
else => @panic("unsupported metallib OS"), else => return null,
}; };
const min_version = if (opts.target.query.os_version_min) |v| const min_version = if (opts.target.query.os_version_min) |v|

510
src/build/SharedDeps.zig Normal file
View File

@ -0,0 +1,510 @@
const SharedDeps = @This();
const std = @import("std");
const Scanner = @import("zig_wayland").Scanner;
const Config = @import("Config.zig");
const HelpStrings = @import("HelpStrings.zig");
const MetallibStep = @import("MetallibStep.zig");
const UnicodeTables = @import("UnicodeTables.zig");
config: *const Config,
options: *std.Build.Step.Options,
help_strings: HelpStrings,
metallib: ?*MetallibStep,
unicode_tables: UnicodeTables,
/// Used to keep track of a list of file sources.
pub const LazyPathList = std.ArrayList(std.Build.LazyPath);
pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps {
var result: SharedDeps = .{
.config = cfg,
.help_strings = try HelpStrings.init(b, cfg),
.unicode_tables = try UnicodeTables.init(b),
// Setup by retarget
.options = undefined,
.metallib = undefined,
};
try result.initTarget(b, cfg.target);
return result;
}
/// Retarget our dependencies for another build target. Modifies in-place.
pub fn retarget(
self: *const SharedDeps,
b: *std.Build,
target: std.Build.ResolvedTarget,
) !SharedDeps {
var result = self.*;
try result.initTarget(b, target);
return result;
}
/// Change the exe entrypoint.
pub fn changeEntrypoint(
self: *const SharedDeps,
b: *std.Build,
entrypoint: Config.ExeEntrypoint,
) !SharedDeps {
// Change our config
const config = try b.allocator.create(Config);
config.* = self.config.*;
config.exe_entrypoint = entrypoint;
var result = self.*;
result.config = config;
return result;
}
fn initTarget(
self: *SharedDeps,
b: *std.Build,
target: std.Build.ResolvedTarget,
) !void {
// Update our metallib
self.metallib = MetallibStep.create(b, .{
.name = "Ghostty",
.target = target,
.sources = &.{b.path("src/renderer/shaders/cell.metal")},
});
// Change our config
const config = try b.allocator.create(Config);
config.* = self.config.*;
config.target = target;
self.config = config;
// Setup our shared build options
self.options = b.addOptions();
try self.config.addOptions(self.options);
}
pub fn add(
self: *const SharedDeps,
step: *std.Build.Step.Compile,
) !LazyPathList {
const b = step.step.owner;
// We could use our config.target/optimize fields here but its more
// correct to always match our step.
const target = step.root_module.resolved_target.?;
const optimize = step.root_module.optimize.?;
// We maintain a list of our static libraries and return it so that
// we can build a single fat static library for the final app.
var static_libs = LazyPathList.init(b.allocator);
errdefer static_libs.deinit();
// Every exe gets build options populated
step.root_module.addOptions("build_options", self.options);
// Freetype
_ = b.systemIntegrationOption("freetype", .{}); // Shows it in help
if (self.config.font_backend.hasFreetype()) {
const freetype_dep = b.dependency("freetype", .{
.target = target,
.optimize = optimize,
.@"enable-libpng" = true,
});
step.root_module.addImport("freetype", freetype_dep.module("freetype"));
if (b.systemIntegrationOption("freetype", .{})) {
step.linkSystemLibrary2("bzip2", dynamic_link_opts);
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
} else {
step.linkLibrary(freetype_dep.artifact("freetype"));
try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin());
}
}
// Harfbuzz
_ = b.systemIntegrationOption("harfbuzz", .{}); // Shows it in help
if (self.config.font_backend.hasHarfbuzz()) {
const harfbuzz_dep = b.dependency("harfbuzz", .{
.target = target,
.optimize = optimize,
.@"enable-freetype" = true,
.@"enable-coretext" = self.config.font_backend.hasCoretext(),
});
step.root_module.addImport(
"harfbuzz",
harfbuzz_dep.module("harfbuzz"),
);
if (b.systemIntegrationOption("harfbuzz", .{})) {
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
} else {
step.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin());
}
}
// Fontconfig
_ = b.systemIntegrationOption("fontconfig", .{}); // Shows it in help
if (self.config.font_backend.hasFontconfig()) {
const fontconfig_dep = b.dependency("fontconfig", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport(
"fontconfig",
fontconfig_dep.module("fontconfig"),
);
if (b.systemIntegrationOption("fontconfig", .{})) {
step.linkSystemLibrary2("fontconfig", dynamic_link_opts);
} else {
step.linkLibrary(fontconfig_dep.artifact("fontconfig"));
try static_libs.append(fontconfig_dep.artifact("fontconfig").getEmittedBin());
}
}
// Libpng - Ghostty doesn't actually use this directly, its only used
// through dependencies, so we only need to add it to our static
// libs list if we're not using system integration. The dependencies
// will handle linking it.
if (!b.systemIntegrationOption("libpng", .{})) {
const libpng_dep = b.dependency("libpng", .{
.target = target,
.optimize = optimize,
});
step.linkLibrary(libpng_dep.artifact("png"));
try static_libs.append(libpng_dep.artifact("png").getEmittedBin());
}
// Zlib - same as libpng, only used through dependencies.
if (!b.systemIntegrationOption("zlib", .{})) {
const zlib_dep = b.dependency("zlib", .{
.target = target,
.optimize = optimize,
});
step.linkLibrary(zlib_dep.artifact("z"));
try static_libs.append(zlib_dep.artifact("z").getEmittedBin());
}
// Oniguruma
const oniguruma_dep = b.dependency("oniguruma", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma"));
if (b.systemIntegrationOption("oniguruma", .{})) {
step.linkSystemLibrary2("oniguruma", dynamic_link_opts);
} else {
step.linkLibrary(oniguruma_dep.artifact("oniguruma"));
try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin());
}
// Glslang
const glslang_dep = b.dependency("glslang", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport("glslang", glslang_dep.module("glslang"));
if (b.systemIntegrationOption("glslang", .{})) {
step.linkSystemLibrary2("glslang", dynamic_link_opts);
step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts);
} else {
step.linkLibrary(glslang_dep.artifact("glslang"));
try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin());
}
// Spirv-cross
const spirv_cross_dep = b.dependency("spirv_cross", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport("spirv_cross", spirv_cross_dep.module("spirv_cross"));
if (b.systemIntegrationOption("spirv-cross", .{})) {
step.linkSystemLibrary2("spirv-cross", dynamic_link_opts);
} else {
step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin());
}
// Simdutf
if (b.systemIntegrationOption("simdutf", .{})) {
step.linkSystemLibrary2("simdutf", dynamic_link_opts);
} else {
const simdutf_dep = b.dependency("simdutf", .{
.target = target,
.optimize = optimize,
});
step.linkLibrary(simdutf_dep.artifact("simdutf"));
try static_libs.append(simdutf_dep.artifact("simdutf").getEmittedBin());
}
// Sentry
if (self.config.sentry) {
const sentry_dep = b.dependency("sentry", .{
.target = target,
.optimize = optimize,
.backend = .breakpad,
});
step.root_module.addImport("sentry", sentry_dep.module("sentry"));
// Sentry
step.linkLibrary(sentry_dep.artifact("sentry"));
try static_libs.append(sentry_dep.artifact("sentry").getEmittedBin());
// We also need to include breakpad in the static libs.
const breakpad_dep = sentry_dep.builder.dependency("breakpad", .{
.target = target,
.optimize = optimize,
});
try static_libs.append(breakpad_dep.artifact("breakpad").getEmittedBin());
}
// Wasm we do manually since it is such a different build.
if (step.rootModuleTarget().cpu.arch == .wasm32) {
const js_dep = b.dependency("zig_js", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport("zig-js", js_dep.module("zig-js"));
return static_libs;
}
// On Linux, we need to add a couple common library paths that aren't
// on the standard search list. i.e. GTK is often in /usr/lib/x86_64-linux-gnu
// on x86_64.
if (step.rootModuleTarget().os.tag == .linux) {
const triple = try step.rootModuleTarget().linuxTriple(b.allocator);
step.addLibraryPath(.{ .cwd_relative = b.fmt("/usr/lib/{s}", .{triple}) });
}
// C files
step.linkLibC();
step.addIncludePath(b.path("src/stb"));
step.addCSourceFiles(.{ .files = &.{"src/stb/stb.c"} });
if (step.rootModuleTarget().os.tag == .linux) {
step.addIncludePath(b.path("src/apprt/gtk"));
}
// C++ files
step.linkLibCpp();
step.addIncludePath(b.path("src"));
{
// From hwy/detect_targets.h
const HWY_AVX3_SPR: c_int = 1 << 4;
const HWY_AVX3_ZEN4: c_int = 1 << 6;
const HWY_AVX3_DL: c_int = 1 << 7;
const HWY_AVX3: c_int = 1 << 8;
// Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414
// To workaround this we just disable AVX512 support completely.
// The performance difference between AVX2 and AVX512 is not
// significant for our use case and AVX512 is very rare on consumer
// hardware anyways.
const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
step.addCSourceFiles(.{
.files = &.{
"src/simd/base64.cpp",
"src/simd/codepoint_width.cpp",
"src/simd/index_of.cpp",
"src/simd/vt.cpp",
},
.flags = if (step.rootModuleTarget().cpu.arch == .x86_64) &.{
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
} else &.{},
});
}
// We always require the system SDK so that our system headers are available.
// This makes things like `os/log.h` available for cross-compiling.
if (step.rootModuleTarget().isDarwin()) {
try @import("apple_sdk").addPaths(b, &step.root_module);
const metallib = self.metallib.?;
metallib.output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("ghostty_metallib", .{
.root_source_file = metallib.output,
});
}
// Other dependencies, mostly pure Zig
step.root_module.addImport("opengl", b.dependency(
"opengl",
.{},
).module("opengl"));
step.root_module.addImport("vaxis", b.dependency("vaxis", .{
.target = target,
.optimize = optimize,
}).module("vaxis"));
step.root_module.addImport("wuffs", b.dependency("wuffs", .{
.target = target,
.optimize = optimize,
}).module("wuffs"));
step.root_module.addImport("xev", b.dependency("libxev", .{
.target = target,
.optimize = optimize,
}).module("xev"));
step.root_module.addImport("z2d", b.addModule("z2d", .{
.root_source_file = b.dependency("z2d", .{}).path("src/z2d.zig"),
.target = target,
.optimize = optimize,
}));
step.root_module.addImport("ziglyph", b.dependency("ziglyph", .{
.target = target,
.optimize = optimize,
}).module("ziglyph"));
step.root_module.addImport("zf", b.dependency("zf", .{
.target = target,
.optimize = optimize,
.with_tui = false,
}).module("zf"));
// Mac Stuff
if (step.rootModuleTarget().isDarwin()) {
const objc_dep = b.dependency("zig_objc", .{
.target = target,
.optimize = optimize,
});
const macos_dep = b.dependency("macos", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport("objc", objc_dep.module("objc"));
step.root_module.addImport("macos", macos_dep.module("macos"));
step.linkLibrary(macos_dep.artifact("macos"));
try static_libs.append(macos_dep.artifact("macos").getEmittedBin());
if (self.config.renderer == .opengl) {
step.linkFramework("OpenGL");
}
}
// cimgui
const cimgui_dep = b.dependency("cimgui", .{
.target = target,
.optimize = optimize,
});
step.root_module.addImport("cimgui", cimgui_dep.module("cimgui"));
step.linkLibrary(cimgui_dep.artifact("cimgui"));
try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin());
// Highway
const highway_dep = b.dependency("highway", .{
.target = target,
.optimize = optimize,
});
step.linkLibrary(highway_dep.artifact("highway"));
try static_libs.append(highway_dep.artifact("highway").getEmittedBin());
// utfcpp - This is used as a dependency on our hand-written C++ code
const utfcpp_dep = b.dependency("utfcpp", .{
.target = target,
.optimize = optimize,
});
step.linkLibrary(utfcpp_dep.artifact("utfcpp"));
try static_libs.append(utfcpp_dep.artifact("utfcpp").getEmittedBin());
// If we're building an exe then we have additional dependencies.
if (step.kind != .lib) {
// We always statically compile glad
step.addIncludePath(b.path("vendor/glad/include/"));
step.addCSourceFile(.{
.file = b.path("vendor/glad/src/gl.c"),
.flags = &.{},
});
// When we're targeting flatpak we ALWAYS link GTK so we
// get access to glib for dbus.
if (self.config.flatpak) step.linkSystemLibrary2("gtk4", dynamic_link_opts);
switch (self.config.app_runtime) {
.none => {},
.glfw => glfw: {
const mach_glfw_dep = b.lazyDependency("mach_glfw", .{
.target = target,
.optimize = optimize,
}) orelse break :glfw;
step.root_module.addImport("glfw", mach_glfw_dep.module("mach-glfw"));
},
.gtk => {
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (self.config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
if (self.config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
if (self.config.wayland) {
const scanner = Scanner.create(b, .{
// We shouldn't be using getPath but we need to for now
// https://codeberg.org/ifreund/zig-wayland/issues/66
.wayland_xml_path = b.dependency("wayland", .{})
.path("protocol/wayland.xml")
.getPath(b),
.wayland_protocols_path = b.dependency("wayland_protocols", .{})
.path("")
.getPath(b),
});
const wayland = b.createModule(.{ .root_source_file = scanner.result });
const plasma_wayland_protocols = b.dependency("plasma_wayland_protocols", .{
.target = target,
.optimize = optimize,
});
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
scanner.generate("wl_compositor", 1);
scanner.generate("org_kde_kwin_blur_manager", 1);
step.root_module.addImport("wayland", wayland);
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
}
{
const gresource = @import("../apprt/gtk/gresource.zig");
const wf = b.addWriteFiles();
const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml);
const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-source",
"--target",
});
const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c");
generate_resources_c.addFileArg(gresource_xml);
generate_resources_c.extra_file_dependencies = &gresource.dependencies;
step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} });
const generate_resources_h = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-header",
"--target",
});
const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h");
generate_resources_h.addFileArg(gresource_xml);
generate_resources_h.extra_file_dependencies = &gresource.dependencies;
step.addIncludePath(ghostty_resources_h.dirname());
}
},
}
}
self.help_strings.addImport(step);
self.unicode_tables.addImport(step);
return static_libs;
}
// For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library
// before falling back to static.
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
.preferred_link_mode = .dynamic,
.search_strategy = .mode_first,
};

View File

@ -0,0 +1,43 @@
const UnicodeTables = @This();
const std = @import("std");
const Config = @import("Config.zig");
/// The exe.
exe: *std.Build.Step.Compile,
/// The output path for the unicode tables
output: std.Build.LazyPath,
pub fn init(b: *std.Build) !UnicodeTables {
const exe = b.addExecutable(.{
.name = "unigen",
.root_source_file = b.path("src/unicode/props.zig"),
.target = b.host,
});
exe.linkLibC();
const ziglyph_dep = b.dependency("ziglyph", .{
.target = b.host,
});
exe.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph"));
const run = b.addRunArtifact(exe);
return .{
.exe = exe,
.output = run.captureStdOut(),
};
}
/// Add the "unicode_tables" import.
pub fn addImport(self: *const UnicodeTables, step: *std.Build.Step.Compile) void {
self.output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("unicode_tables", .{
.root_source_file = self.output,
});
}
/// Install the exe
pub fn install(self: *const UnicodeTables, b: *std.Build) void {
b.installArtifact(self.exe);
}

View File

@ -14,7 +14,7 @@ const Action = @import("../cli/action.zig").Action;
/// it's part of an on going completion like --<key>=. Working around this requires looking /// it's part of an on going completion like --<key>=. Working around this requires looking
/// backward in the command line args to pretend the = is an empty string /// backward in the command line args to pretend the = is an empty string
/// see: https://www.gnu.org/software/gnuastro/manual/html_node/Bash-TAB-completion-tutorial.html /// see: https://www.gnu.org/software/gnuastro/manual/html_node/Bash-TAB-completion-tutorial.html
pub const bash_completions = comptimeGenerateBashCompletions(); pub const completions = comptimeGenerateBashCompletions();
fn comptimeGenerateBashCompletions() []const u8 { fn comptimeGenerateBashCompletions() []const u8 {
comptime { comptime {

View File

@ -5,7 +5,7 @@ const Action = @import("../cli/action.zig").Action;
/// A fish completions configuration that contains all the available commands /// A fish completions configuration that contains all the available commands
/// and options. /// and options.
pub const fish_completions = comptimeGenerateFishCompletions(); pub const completions = comptimeGenerateFishCompletions();
fn comptimeGenerateFishCompletions() []const u8 { fn comptimeGenerateFishCompletions() []const u8 {
comptime { comptime {

24
src/build/gtk.zig Normal file
View File

@ -0,0 +1,24 @@
const std = @import("std");
pub const Targets = packed struct {
x11: bool = false,
wayland: bool = false,
};
/// Returns the targets that GTK4 was compiled with.
pub fn targets(b: *std.Build) Targets {
// Run pkg-config. We allow it to fail so that zig build --help
// works without all dependencies. The build will fail later when
// GTK isn't found anyways.
var code: u8 = undefined;
const output = b.runAllowFail(
&.{ "pkg-config", "--variable=targets", "gtk4" },
&code,
.Ignore,
) catch return .{};
return .{
.x11 = std.mem.indexOf(u8, output, "x11") != null,
.wayland = std.mem.indexOf(u8, output, "wayland") != null,
};
}

30
src/build/main.zig Normal file
View File

@ -0,0 +1,30 @@
//! Build logic for Ghostty. A single "build.zig" file became far too complex
//! and spaghetti, so this package extracts the build logic into smaller,
//! more manageable pieces.
pub const gtk = @import("gtk.zig");
pub const Config = @import("Config.zig");
pub const GitVersion = @import("GitVersion.zig");
// Artifacts
pub const GhosttyBench = @import("GhosttyBench.zig");
pub const GhosttyDocs = @import("GhosttyDocs.zig");
pub const GhosttyExe = @import("GhosttyExe.zig");
pub const GhosttyLib = @import("GhosttyLib.zig");
pub const GhosttyResources = @import("GhosttyResources.zig");
pub const GhosttyXCFramework = @import("GhosttyXCFramework.zig");
pub const GhosttyWebdata = @import("GhosttyWebdata.zig");
pub const HelpStrings = @import("HelpStrings.zig");
pub const SharedDeps = @import("SharedDeps.zig");
pub const UnicodeTables = @import("UnicodeTables.zig");
// Steps
pub const LibtoolStep = @import("LibtoolStep.zig");
pub const LipoStep = @import("LipoStep.zig");
pub const MetallibStep = @import("MetallibStep.zig");
pub const XCFrameworkStep = @import("XCFrameworkStep.zig");
// Shell completions
pub const fish_completions = @import("fish_completions.zig").completions;
pub const zsh_completions = @import("zsh_completions.zig").completions;
pub const bash_completions = @import("bash_completions.zig").completions;

View File

@ -5,7 +5,7 @@ const Action = @import("../cli/action.zig").Action;
/// A zsh completions configuration that contains all the available commands /// A zsh completions configuration that contains all the available commands
/// and options. /// and options.
pub const zsh_completions = comptimeGenerateZshCompletions(); pub const completions = comptimeGenerateZshCompletions();
const equals_required = "=-:::"; const equals_required = "=-:::";

View File

@ -10,88 +10,9 @@ const apprt = @import("apprt.zig");
const font = @import("font/main.zig"); const font = @import("font/main.zig");
const rendererpkg = @import("renderer.zig"); const rendererpkg = @import("renderer.zig");
const WasmTarget = @import("os/wasm/target.zig").Target; const WasmTarget = @import("os/wasm/target.zig").Target;
const BuildConfig = @import("build/Config.zig");
/// The build configurations options. This may not be all available options pub const ReleaseChannel = BuildConfig.ReleaseChannel;
/// to `zig build` but it contains all the options that the Ghostty source
/// needs to know about at comptime.
///
/// We put this all in a single struct so that we can check compatibility
/// between options, make it easy to copy and mutate options for different
/// build types, etc.
pub const BuildConfig = struct {
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
flatpak: bool = false,
adwaita: bool = false,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
app_runtime: apprt.Runtime = .none,
renderer: rendererpkg.Impl = .opengl,
font_backend: font.Backend = .freetype,
/// The entrypoint for exe targets.
exe_entrypoint: ExeEntrypoint = .ghostty,
/// The target runtime for the wasm build and whether to use wasm shared
/// memory or not. These are both legacy wasm-specific options that we
/// will probably have to revisit when we get back to work on wasm.
wasm_target: WasmTarget = .browser,
wasm_shared: bool = true,
/// Configure the build options with our values.
pub fn addOptions(self: BuildConfig, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't
// support all types.
step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend);
step.addOption(rendererpkg.Impl, "renderer", self.renderer);
step.addOption(ExeEntrypoint, "exe_entrypoint", self.exe_entrypoint);
step.addOption(WasmTarget, "wasm_target", self.wasm_target);
step.addOption(bool, "wasm_shared", self.wasm_shared);
// Our version. We also add the string version so we don't need
// to do any allocations at runtime. This has to be long enough to
// accommodate realistic large branch names for dev versions.
var buf: [1024]u8 = undefined;
step.addOption(std.SemanticVersion, "app_version", self.version);
step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ(
&buf,
"{}",
.{self.version},
));
step.addOption(
ReleaseChannel,
"release_channel",
channel: {
const pre = self.version.pre orelse break :channel .stable;
if (pre.len == 0) break :channel .stable;
break :channel .tip;
},
);
}
/// Rehydrate our BuildConfig from the comptime options. Note that not all
/// options are available at comptime, so look closely at this implementation
/// to see what is and isn't available.
pub fn fromOptions() BuildConfig {
return .{
.version = options.app_version,
.flatpak = options.flatpak,
.adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?,
.exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?,
.wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?,
.wasm_shared = options.wasm_shared,
};
}
};
/// The semantic version of this build. /// The semantic version of this build.
pub const version = options.app_version; pub const version = options.app_version;
@ -114,7 +35,7 @@ pub const artifact = Artifact.detect();
/// Our build configuration. We re-export a lot of these back at the /// Our build configuration. We re-export a lot of these back at the
/// top-level so its a bit cleaner to use throughout the code. See the doc /// top-level so its a bit cleaner to use throughout the code. See the doc
/// comments in BuildConfig for details on each. /// comments in BuildConfig for details on each.
pub const config = BuildConfig.fromOptions(); const config = BuildConfig.fromOptions();
pub const exe_entrypoint = config.exe_entrypoint; pub const exe_entrypoint = config.exe_entrypoint;
pub const flatpak = options.flatpak; pub const flatpak = options.flatpak;
pub const app_runtime: apprt.Runtime = config.app_runtime; pub const app_runtime: apprt.Runtime = config.app_runtime;
@ -175,35 +96,3 @@ pub const Artifact = enum {
}; };
} }
}; };
/// The possible entrypoints for the exe artifact. This has no effect on
/// other artifact types (i.e. lib, wasm_module).
///
/// The whole existence of this enum is to workaround the fact that Zig
/// doesn't allow the main function to be in a file in a subdirctory
/// from the "root" of the module, and I don't want to pollute our root
/// directory with a bunch of individual zig files for each entrypoint.
///
/// Therefore, main.zig uses this to switch between the different entrypoints.
pub const ExeEntrypoint = enum {
ghostty,
helpgen,
mdgen_ghostty_1,
mdgen_ghostty_5,
webgen_config,
webgen_actions,
bench_parser,
bench_stream,
bench_codepoint_width,
bench_grapheme_break,
bench_page_init,
};
/// The release channel for the build.
pub const ReleaseChannel = enum {
/// Unstable builds on every commit.
tip,
/// Stable tagged releases.
stable,
};