diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/build/Config.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 251 |
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`. // |
