build: use build options to configure terminal C ABI mode (#8945)

Fixes various issues:

- C ABI detection was faulty, which caused some Zig programs to use the
C ABI mode and some C programs not to. Let's be explicit.

- Unit tests now tests C ABI mode.

- Build binary no longer rebuilds on any terminal change (a regression).

- Zig programs can choose to depend on the C ABI version of the terminal
lib by using the `ghostty-vt-c` module.
This commit is contained in:
Mitchell Hashimoto
2025-09-28 14:22:36 -07:00
committed by GitHub
9 changed files with 72 additions and 24 deletions

View File

@@ -255,6 +255,15 @@ pub fn build(b: *std.Build) !void {
}); });
const mod_vt_test_run = b.addRunArtifact(mod_vt_test); const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
test_lib_vt_step.dependOn(&mod_vt_test_run.step); test_lib_vt_step.dependOn(&mod_vt_test_run.step);
const mod_vt_c_test = b.addTest(.{
.root_module = mod.vt_c,
.target = config.target,
.optimize = config.optimize,
.filters = test_filters,
});
const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test);
test_lib_vt_step.dependOn(&mod_vt_c_test_run.step);
} }
// Tests // Tests

View File

@@ -498,6 +498,7 @@ pub fn terminalOptions(self: *const Config) TerminalBuildOptions {
.artifact = .ghostty, .artifact = .ghostty,
.simd = self.simd, .simd = self.simd,
.oniguruma = true, .oniguruma = true,
.c_abi = false,
.slow_runtime_safety = switch (self.optimize) { .slow_runtime_safety = switch (self.optimize) {
.Debug => true, .Debug => true,
.ReleaseSafe, .ReleaseSafe,

View File

@@ -26,7 +26,7 @@ pub fn initShared(
const target = zig.vt.resolved_target.?; const target = zig.vt.resolved_target.?;
const lib = b.addSharedLibrary(.{ const lib = b.addSharedLibrary(.{
.name = "ghostty-vt", .name = "ghostty-vt",
.root_module = zig.vt, .root_module = zig.vt_c,
}); });
lib.installHeader( lib.installHeader(
b.path("include/ghostty/vt.h"), b.path("include/ghostty/vt.h"),

View File

@@ -5,18 +5,17 @@ const GhosttyZig = @This();
const std = @import("std"); const std = @import("std");
const Config = @import("Config.zig"); const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig"); const SharedDeps = @import("SharedDeps.zig");
const TerminalBuildOptions = @import("../terminal/build_options.zig").Options;
/// The `_c`-suffixed modules are built with the C ABI enabled.
vt: *std.Build.Module, vt: *std.Build.Module,
vt_c: *std.Build.Module,
pub fn init( pub fn init(
b: *std.Build, b: *std.Build,
cfg: *const Config, cfg: *const Config,
deps: *const SharedDeps, deps: *const SharedDeps,
) !GhosttyZig { ) !GhosttyZig {
// General build options
const general_options = b.addOptions();
try cfg.addOptions(general_options);
// Terminal module build options // Terminal module build options
var vt_options = cfg.terminalOptions(); var vt_options = cfg.terminalOptions();
vt_options.artifact = .lib; vt_options.artifact = .lib;
@@ -25,7 +24,41 @@ pub fn init(
// conditionally do this. // conditionally do this.
vt_options.oniguruma = false; vt_options.oniguruma = false;
const vt = b.addModule("ghostty-vt", .{ return .{
.vt = try initVt(
"ghostty-vt",
b,
cfg,
deps,
vt_options,
),
.vt_c = try initVt(
"ghostty-vt-c",
b,
cfg,
deps,
options: {
var dup = vt_options;
dup.c_abi = true;
break :options dup;
},
),
};
}
fn initVt(
name: []const u8,
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
vt_options: TerminalBuildOptions,
) !*std.Build.Module {
// General build options
const general_options = b.addOptions();
try cfg.addOptions(general_options);
const vt = b.addModule(name, .{
.root_source_file = b.path("src/lib_vt.zig"), .root_source_file = b.path("src/lib_vt.zig"),
.target = cfg.target, .target = cfg.target,
.optimize = cfg.optimize, .optimize = cfg.optimize,
@@ -45,5 +78,5 @@ pub fn init(
try SharedDeps.addSimd(b, vt, null); try SharedDeps.addSimd(b, vt, null);
} }
return .{ .vt = vt }; return vt;
} }

View File

@@ -7,6 +7,7 @@
//! by thousands of users for years. However, the API itself (functions, //! by thousands of users for years. However, the API itself (functions,
//! types, etc.) may change without warning. We're working on stabilizing //! types, etc.) may change without warning. We're working on stabilizing
//! this in the future. //! this in the future.
const lib = @This();
// The public API below reproduces a lot of terminal/main.zig but // The public API below reproduces a lot of terminal/main.zig but
// is separate because (1) we need our root file to be in `src/` // is separate because (1) we need our root file to be in `src/`
@@ -68,7 +69,7 @@ pub const Attribute = terminal.Attribute;
comptime { comptime {
// If we're building the C library (vs. the Zig module) then // If we're building the C library (vs. the Zig module) then
// we want to reference the C API so that it gets exported. // we want to reference the C API so that it gets exported.
if (terminal.is_c_lib) { if (@import("root") == lib) {
const c = terminal.c_api; const c = terminal.c_api;
@export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_new, .{ .name = "ghostty_osc_new" });
@export(&c.osc_free, .{ .name = "ghostty_osc_free" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" });
@@ -81,8 +82,8 @@ comptime {
test { test {
_ = terminal; _ = terminal;
// Tests always test the C API and shared C functions
_ = terminal.c_api;
_ = @import("lib/main.zig"); _ = @import("lib/main.zig");
if (comptime terminal.options.c_abi) {
_ = terminal.c_api;
}
} }

View File

@@ -1,8 +1,6 @@
const std = @import("std"); const std = @import("std");
/// True if we're building the C library libghostty-vt. /// Options set by Zig build.zig and exposed via `terminal_options`.
pub const is_c_lib = @import("root") == @import("../lib_vt.zig");
pub const Options = struct { pub const Options = struct {
/// The target artifact to build. This will gate some functionality. /// The target artifact to build. This will gate some functionality.
artifact: Artifact, artifact: Artifact,
@@ -26,6 +24,10 @@ pub const Options = struct {
/// generally be disabled in production builds. /// generally be disabled in production builds.
slow_runtime_safety: bool, slow_runtime_safety: bool,
/// Force C ABI mode on or off. If not set, then it will be set based on
/// Options.
c_abi: bool,
/// Add the required build options for the terminal module. /// Add the required build options for the terminal module.
pub fn add( pub fn add(
self: Options, self: Options,
@@ -34,6 +36,7 @@ pub const Options = struct {
) void { ) void {
const opts = b.addOptions(); const opts = b.addOptions();
opts.addOption(Artifact, "artifact", self.artifact); opts.addOption(Artifact, "artifact", self.artifact);
opts.addOption(bool, "c_abi", self.c_abi);
opts.addOption(bool, "oniguruma", self.oniguruma); opts.addOption(bool, "oniguruma", self.oniguruma);
opts.addOption(bool, "simd", self.simd); opts.addOption(bool, "simd", self.simd);
opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety); opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety);

View File

@@ -73,9 +73,9 @@ test "command type" {
)); ));
defer free(p); defer free(p);
p.next('0'); next(p, '0');
p.next(';'); next(p, ';');
p.next('a'); next(p, 'a');
const cmd = p.end(0); const cmd = end(p, 0);
try testing.expectEqual(.change_window_title, commandType(cmd)); try testing.expectEqual(.change_window_title, commandType(cmd));
} }

View File

@@ -1,5 +1,4 @@
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_options = @import("terminal_options");
const charsets = @import("charsets.zig"); const charsets = @import("charsets.zig");
const sanitize = @import("sanitize.zig"); const sanitize = @import("sanitize.zig");
@@ -21,7 +20,7 @@ pub const page = @import("page.zig");
pub const parse_table = @import("parse_table.zig"); pub const parse_table = @import("parse_table.zig");
pub const search = @import("search.zig"); pub const search = @import("search.zig");
pub const size = @import("size.zig"); pub const size = @import("size.zig");
pub const tmux = if (build_options.tmux_control_mode) @import("tmux.zig") else struct {}; pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {};
pub const x11_color = @import("x11_color.zig"); pub const x11_color = @import("x11_color.zig");
pub const Charset = charsets.Charset; pub const Charset = charsets.Charset;
@@ -62,9 +61,11 @@ pub const Attribute = sgr.Attribute;
pub const isSafePaste = sanitize.isSafePaste; pub const isSafePaste = sanitize.isSafePaste;
pub const Options = @import("build_options.zig").Options;
pub const options = @import("terminal_options");
/// This is set to true when we're building the C library. /// This is set to true when we're building the C library.
pub const is_c_lib = @import("build_options.zig").is_c_lib; pub const c_api = if (options.c_abi) @import("c/main.zig") else void;
pub const c_api = if (is_c_lib) @import("c/main.zig") else void;
test { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());

View File

@@ -7,11 +7,11 @@ const osc = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_options = @import("terminal_options");
const mem = std.mem; const mem = std.mem;
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = mem.Allocator; const Allocator = mem.Allocator;
const LibEnum = @import("../lib/enum.zig").Enum; const LibEnum = @import("../lib/enum.zig").Enum;
const is_c_lib = @import("build_options.zig").is_c_lib;
const RGB = @import("color.zig").RGB; const RGB = @import("color.zig").RGB;
const kitty_color = @import("kitty/color.zig"); const kitty_color = @import("kitty/color.zig");
const osc_color = @import("osc/color.zig"); const osc_color = @import("osc/color.zig");
@@ -175,7 +175,7 @@ pub const Command = union(Key) {
conemu_guimacro: []const u8, conemu_guimacro: []const u8,
pub const Key = LibEnum( pub const Key = LibEnum(
if (is_c_lib) .c else .zig, if (build_options.c_abi) .c else .zig,
// NOTE: Order matters, see LibEnum documentation. // NOTE: Order matters, see LibEnum documentation.
&.{ &.{
"invalid", "invalid",