aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/build/Config.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/build/Config.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/build/Config.zig')
-rw-r--r--vendor/libghostty-vt/src/build/Config.zig251
1 files changed, 204 insertions, 47 deletions
diff --git a/vendor/libghostty-vt/src/build/Config.zig b/vendor/libghostty-vt/src/build/Config.zig
index 0a994731..71fa4d28 100644
--- a/vendor/libghostty-vt/src/build/Config.zig
+++ b/vendor/libghostty-vt/src/build/Config.zig
@@ -43,7 +43,7 @@ lib_version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
/// Binary properties
pie: bool = false,
strip: bool = false,
-patch_rpath: ?[]const u8 = null,
+patchelf: ?PatchElf = null,
/// Artifacts
flatpak: bool = false,
@@ -62,17 +62,30 @@ emit_xcframework: bool = false,
emit_webdata: bool = false,
emit_unicode_table_gen: bool = false,
+/// Feature gates for libghostty-vt artifacts (-Dvt-features). The full
+/// Ghostty application ignores this and always enables everything.
+vt_features: TerminalBuildOptions.Features = .{},
+
/// True when Ghostty is being built as a dependency of another project
/// rather than as the root project.
is_dep: bool = false,
/// Environmental properties
-env: std.process.EnvMap,
+env: *const std.process.Environ.Map,
pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Config {
// Setup our standard Zig target and optimize options, i.e.
// `-Doptimize` and `-Dtarget`.
const optimize = b.standardOptimizeOption(.{});
+
+ // Default dependency builds to libghostty-vt-only mode. Consumers can
+ // still explicitly disable this to request the full Ghostty build.
+ const is_dep = b.dep_prefix.len > 0;
+ const emit_lib_vt = b.option(
+ bool,
+ "emit-lib-vt",
+ "Set defaults for a libghostty-vt-only build (disables xcframework, macOS app, and docs).",
+ ) orelse is_dep;
const target = target: {
var result = b.standardTargetOptions(.{});
@@ -97,19 +110,48 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
result = b.resolveTargetQuery(query);
}
+ // On wasm, default to enabling the simd128 feature. Every
+ // browser engine has supported it since 2023 or earlier
+ // (Chrome 91, Firefox 89, Safari 16.4) and it is a large
+ // performance win for the terminal hot paths (50%+ on
+ // print-heavy VT streams). Only apply when no explicit CPU
+ // was requested; targets for exotic non-browser runtimes
+ // can opt out with `-Dcpu=generic`.
+ if (result.result.cpu.arch.isWasm() and
+ result.query.cpu_model == .determined_by_arch_os and
+ result.query.cpu_features_add.isEmpty() and
+ result.query.cpu_features_sub.isEmpty())
+ {
+ var query = result.query;
+ query.cpu_features_add.addFeature(
+ @intFromEnum(std.Target.wasm.Feature.simd128),
+ );
+ result = b.resolveTargetQuery(query);
+ }
+
+ // The full Ghostty build no longer supports iOS; Fail early
+ // with a clear message rather than partway through the build.
+ if (result.result.os.tag == .ios and !emit_lib_vt) {
+ std.log.err(
+ "iOS is not a supported target for the full Ghostty build; " ++
+ "only libghostty-vt supports iOS (-Demit-lib-vt)",
+ .{},
+ );
+ return error.UnsupportedTarget;
+ }
+
// 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);
+ result.query.os_version_min = if (emit_lib_vt)
+ osVersionMinLibVt(result.result.os.tag)
+ else
+ osVersionMin(result.result.os.tag);
}
break :target result;
};
- // Detect if Ghostty is a dependency of another project.
- // dep_prefix is non-empty when this build is running as a dependency.
- const is_dep = b.dep_prefix.len > 0;
-
// 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.
@@ -124,10 +166,10 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
// 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();
+ // Grab the environment from build state
+ const env = &b.graph.environ_map;
+ // We use env vars throughout the build so we grab them immediately here.
var config: Config = .{
.optimize = optimize,
.target = target,
@@ -198,9 +240,10 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
"simd",
"Build with SIMD-accelerated code paths. Results in significant performance improvements.",
) orelse simd: {
- // We can't build our SIMD dependencies for Wasm. Note that we may
- // still use SIMD features in the Wasm-builds.
- if (target.result.cpu.arch.isWasm()) break :simd false;
+ // We can't build our SIMD dependencies for Wasm or freestanding
+ // targets. Note that we may still use SIMD features in the Wasm-builds.
+ if (target.result.cpu.arch.isWasm() or
+ target.result.os.tag == .freestanding) break :simd false;
break :simd true;
};
@@ -312,24 +355,55 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
//---------------------------------------------------------------
// 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(
+ // On NixOS, the built binary from `zig build` needs to patch the interp
+ // and 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.
+ //
+ // Note that this option is only available on Linux and if building
+ // natively.
+ //
+ // NOTE: Zig does now have an option to pass in the dynamic linker
+ // (-Ddynamic-linker), but it does seem to have some issues in 0.16.0 that
+ // may be fixed in 0.17.0. We may want to revisit this afterwards; although
+ // I'm not too sure if that helps to clean up rpath, this may just be the
+ // better option. See https://codeberg.org/ziglang/zig/issues/31760.
+ if (b.option(
+ []const u8,
+ "patch-interp",
+ "Inject the supplied path as the dynamic linker in the built binary. " ++
+ "Under Nix, this defaults to the dynamic linker found in PATH.",
+ )) |interp| {
+ PatchElf.setInterp(&config, interp);
+ } else patch_interp: {
+ if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_interp;
+ if (env.get("IN_NIX_SHELL") == null) break :patch_interp;
+
+ if (b.findProgram(&.{"ld.so"}, &.{})) |ld_so| {
+ PatchElf.setInterp(
+ &config,
+ std.Io.Dir.realPathFileAbsoluteAlloc(b.graph.io, ld_so, b.allocator) catch break :patch_interp,
+ );
+ } else |_| {}
+ }
+
+ if (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");
- };
+ "Inject the supplied colon-delimited search path as the rpath in the built binary. " ++
+ "This defaults to LD_LIBRARY_PATH if we're in a Nix shell environment.",
+ )) |rpath| {
+ PatchElf.setRpath(&config, rpath);
+ } else patch_rpath: {
+ if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_rpath;
+ if (env.get("IN_NIX_SHELL") == null) break :patch_rpath;
+
+ if (env.get("LD_LIBRARY_PATH")) |ld_library_path| {
+ if (ld_library_path.len > 0) {
+ PatchElf.setRpath(&config, ld_library_path);
+ }
+ }
+ }
config.pie = b.option(
bool,
@@ -350,11 +424,31 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
//---------------------------------------------------------------
// Artifacts to Emit
- config.emit_lib_vt = b.option(
- bool,
- "emit-lib-vt",
- "Set defaults for a libghostty-vt-only build (disables xcframework, macOS app, and docs).",
- ) orelse false;
+ config.emit_lib_vt = emit_lib_vt;
+
+ config.vt_features = features: {
+ const list = b.option(
+ []const u8,
+ "vt-features",
+ "Comma-separated libghostty-vt feature modifications applied " ++
+ "to the default all-enabled set, -Dcpu style: `+feature` " ++
+ "or `feature` enables, `-feature` disables, and `all` " ++
+ "means every feature (e.g. `-all,+render-state` for a " ++
+ "render-only build). Only applies to lib artifacts.",
+ ) orelse break :features .{};
+ break :features TerminalBuildOptions.Features.parse(list) catch {
+ var valid: std.ArrayList(u8) = .empty;
+ inline for (@typeInfo(TerminalBuildOptions.Features).@"struct".fields) |field| {
+ if (valid.items.len > 0) try valid.appendSlice(b.allocator, ", ");
+ try valid.appendSlice(b.allocator, field.name);
+ }
+ std.log.err(
+ "-Dvt-features={s} contains an unknown feature. Valid features: all, {s}",
+ .{ list, valid.items },
+ );
+ return error.UnknownVtFeature;
+ };
+ };
config.emit_exe = b.option(
bool,
@@ -401,7 +495,7 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
if (system_package) break :emit_docs true;
// We only default to true if we can find pandoc.
- const path = expandPath(b.allocator, "pandoc") catch
+ const path = expandPath(b.graph.io, b.allocator, &b.graph.environ_map, "pandoc") catch
break :emit_docs false;
defer if (path) |p| b.allocator.free(p);
break :emit_docs path != null;
@@ -450,7 +544,7 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
if (config.emit_lib_vt) {
// In lib-vt mode default to whether xcodebuild is available,
// since xcodebuild is required to produce the XCFramework.
- const path = expandPath(b.allocator, "xcodebuild") catch
+ const path = expandPath(b.graph.io, b.allocator, &b.graph.environ_map, "xcodebuild") catch
break :emit_xcfw false;
defer if (path) |p| b.allocator.free(p);
break :emit_xcfw path != null;
@@ -522,6 +616,45 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
return config;
}
+const PatchElf = struct {
+ interp: ?[]const u8 = null,
+ rpath: ?[]const u8 = null,
+
+ fn setInterp(config: *Config, interp: []const u8) void {
+ if (config.patchelf) |*patchelf| {
+ patchelf.interp = interp;
+ return;
+ }
+
+ config.patchelf = .{ .interp = interp };
+ }
+
+ fn setRpath(config: *Config, rpath: []const u8) void {
+ if (config.patchelf) |*patchelf| {
+ patchelf.rpath = rpath;
+ return;
+ }
+
+ config.patchelf = .{ .rpath = rpath };
+ }
+};
+
+/// Add a patchelf step for the supplied `artifact`, depending on the supplied
+/// `step`, if `patchelf` is present in the config.
+pub fn addPatchElf(self: *const Config, artifact: *std.Build.Step.Compile, step: *std.Build.Step) void {
+ const b = artifact.step.owner;
+ std.debug.assert(b == step.owner);
+
+ if (self.patchelf) |patchelf| {
+ const run = std.Build.Step.Run.create(b, "patchelf");
+ run.addArgs(&.{"patchelf"});
+ if (patchelf.interp) |interp| run.addArgs(&.{ "--set-interpreter", interp });
+ if (patchelf.rpath) |rpath| run.addArgs(&.{ "--set-rpath", rpath });
+ run.addArtifactArg(artifact);
+ step.dependOn(&run.step);
+ }
+}
+
/// 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
@@ -570,17 +703,27 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
/// Returns the build options for the terminal module. This assumes a
/// Ghostty executable being built. Callers should modify this as needed.
-pub fn terminalOptions(self: *const Config, artifact: TerminalBuildOptions.Artifact) TerminalBuildOptions {
+pub fn terminalOptions(
+ self: *const Config,
+ artifact: TerminalBuildOptions.Artifact,
+ optimize: std.builtin.OptimizeMode,
+) TerminalBuildOptions {
return .{
.artifact = artifact,
.simd = self.simd,
.oniguruma = true,
.c_abi = false,
+ // The application requires every feature; only lib artifacts
+ // may trim them.
+ .features = switch (artifact) {
+ .ghostty => .{},
+ .lib => self.vt_features,
+ },
.version = switch (artifact) {
.ghostty => self.version,
.lib => self.lib_version,
},
- .slow_runtime_safety = switch (self.optimize) {
+ .slow_runtime_safety = switch (optimize) {
.Debug => true,
.ReleaseSafe,
.ReleaseSmall,
@@ -591,7 +734,7 @@ pub fn terminalOptions(self: *const Config, artifact: TerminalBuildOptions.Artif
}
/// Returns a baseline CPU target retaining all the other CPU configs.
-pub fn baselineTarget(self: *const Config) std.Build.ResolvedTarget {
+pub fn baselineTarget(self: *const Config, io: std.Io) std.Build.ResolvedTarget {
// Set our cpu model as baseline. There may need to be other modifications
// we need to make such as resetting CPU features but for now this works.
var q = self.target.query;
@@ -601,7 +744,7 @@ pub fn baselineTarget(self: *const Config) std.Build.ResolvedTarget {
// handle the native case.
return .{
.query = q,
- .result = std.zig.system.resolveTargetQuery(q) catch
+ .result = std.zig.system.resolveTargetQuery(io, q) catch
@panic("unable to resolve baseline query"),
};
}
@@ -630,6 +773,17 @@ pub fn fromOptions() Config {
};
}
+/// Whether release artifacts should omit frame pointers.
+///
+/// Stripped release builds omit them in general, but we always keep frame
+/// pointers on Apple platforms: the Apple arm64 ABI expects x29 to
+/// be a valid frame pointer and Apple's profiling and crash reporting
+/// tools rely on it for backtraces.
+pub fn omitFramePointer(self: *const Config) bool {
+ if (self.target.result.os.tag.isDarwin()) return false;
+ return self.strip;
+}
+
/// 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 {
@@ -642,19 +796,22 @@ pub fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion {
.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 the minimum OS version for lib-vt build.
+///
+/// This should only be used for Darwin targets.
+pub fn osVersionMinLibVt(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion {
+ // lib-vt is the only thing we still build for iOS, so its deployment
+ // target lives here rather than in osVersionMin.
+ if (tag == .ios) return .{ .semver = .{ .major = 13, .minor = 0, .patch = 0 } };
+ return osVersionMin(tag);
+}
+
// 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`.
//