build: refactoring our use of translate-c

This commit refactors our use of translate-c, in preparation for larger
removal of cImport and better co-ordination between building of C
dependencies and translation of headers.

The major update is the creation of an internal helper package that
wraps our use of the external translate-c library. This allows us to not
only have better shorthand and a data-driven, declarative approach to C
translation (versus the otherwise more imperative approach), it also
funnels the external dependency into a single package instead of
spreading it out among what will be an increasingly larger amount of
places as dependencies in "pkg/" get updated.

It also includes some refactors, namely to the harfbuzz package, which
has had its individual settings refactored into helpers to allow for the
settings to be better shared between translation and the build of the
c-based static library.

Wuffs has also had a bit of a refactor too so that we don't generate a
file with all of the macro defines in it - we just send these in as "-D"
flags now.
This commit is contained in:
Chris Marchesi
2026-09-08 18:55:46 -07:00
parent 82232ecde5
commit c0c5473daf
13 changed files with 586 additions and 504 deletions

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const buildpkg = @import("src/build/main.zig");
const translate_c = @import("translate_c");
/// App version from build.zig.zon.
const app_zon_version = @import("build.zig.zon").version;
@@ -437,21 +438,12 @@ fn addGhosttyH(
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) void {
const translate_c = b.lazyImport(@This(), "translate_c") orelse return;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse return;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add(
"hb_c.h",
\\#include <ghostty.h>
,
),
translate_c.addImportToModule(b, "ghostty.h", module, .{
.source = .{ .includes = .{ .files = &.{
.{ .path = "ghostty.h" },
} } },
.target = target,
.optimize = optimize,
.link_libc = true,
});
translated.addSystemIncludePath(b.path("include"));
module.addImport("ghostty.h", translated.mod);
.system_include_paths = &.{b.path("include")},
}) catch unreachable;
}

View File

@@ -5,15 +5,9 @@
.fingerprint = 0x64407a2a0b4147e5,
.minimum_zig_version = "0.16.0",
.dependencies = .{
// External translate-c
// NOTE: We might not need to keep this around forever, but we need it
// for at least the 0.16.0 release cycle since we need fixes in
// Aro/translate-c itself.
.translate_c = .{
.lazy = true,
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
},
// Our higher-level C translation helper (this also helps ensure we
// only need to maintain one external dependency point)
.translate_c = .{ .path = "./pkg/translate-c" },
// Zig libs
.libxev = .{

View File

@@ -1,4 +1,5 @@
const std = @import("std");
const translate_c = @import("translate_c");
const version = @import("build.zig.zon").version;
@@ -19,36 +20,32 @@ pub fn build(b: *std.Build) !void {
});
translate: {
const translate_c = b.lazyImport(@This(), "translate_c") orelse break :translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :translate;
const Translator = translate_c.Translator;
const link_system_libs: []const []const u8, const include_paths =
if (b.systemIntegrationOption("gtk4-layer-shell", .{}))
.{ &.{ "gtk4", "gtk4-layer-shell-0" }, &.{} }
else
.{
&.{"gtk4"},
paths: {
// local deps (non-system layer-shell/wayland)
const deps = try LocalDeps.get(b) orelse break :translate;
break :paths &.{
deps.upstream.path("include"),
deps.upstream.path("src"),
deps.client_header_directory,
};
},
};
const link_system_libs_full: [2]Translator.LinkSystemLib = .{
.{ .name = "gtk4", .options = dynamic_link_opts },
.{ .name = "gtk4-layer-shell-0", .options = dynamic_link_opts },
};
const headers = Translator.init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("c.h",
\\#include <gtk4-layer-shell.h>
),
try translate_c.addImportToModule(b, "c", module, .{
.source = .{ .includes = .{
.files = &.{.{ .path = "gtk4-layer-shell.h" }},
} },
.target = target,
.optimize = optimize,
.link_system_libs = if (b.systemIntegrationOption("gtk4-layer-shell", .{}))
&link_system_libs_full
else
link_system_libs_full[0..1],
.link_system_libs = link_system_libs,
.include_paths = include_paths,
});
if (!b.systemIntegrationOption("gtk4-layer-shell", .{})) {
// local deps (non-system layer-shell/wayland)
const deps = try LocalDeps.get(b) orelse break :translate;
headers.addIncludePath(deps.upstream.path("include"));
headers.addIncludePath(deps.upstream.path("src"));
headers.addIncludePath(deps.client_header_directory);
}
module.addImport("c", headers.mod);
}
if (!b.systemIntegrationOption("gtk4-layer-shell", .{})) {

View File

@@ -4,6 +4,8 @@
.fingerprint = 0x4b96f9483c6feeb1,
.paths = .{""},
.dependencies = .{
.translate_c = .{ .path = "../translate-c" },
.gtk4_layer_shell = .{
.url = "https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz",
.hash = "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr",
@@ -14,10 +16,5 @@
.hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S",
.lazy = true,
},
.translate_c = .{
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
.lazy = true,
},
},
}

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const apple_sdk = @import("apple_sdk");
const translate_c = @import("translate_c");
const root_build_container = @This();
@@ -18,12 +19,35 @@ pub fn build(b: *std.Build) !void {
.search_strategy = .mode_first,
};
const freetype = b.dependency("freetype", .{
const freetype_dep = b.dependency("freetype", .{
.target = target,
.optimize = optimize,
.@"enable-libpng" = true,
});
const harfbuzz_c_builder: HarfBuzzC = .{
.builder = b,
.options = .{
.target = target,
.optimize = optimize,
.harfbuzz = if (b.systemIntegrationOption("harfbuzz", .{}))
.{ .dynamic = dynamic_link_opts }
else
.static,
.coretext = coretext_enabled,
.freetype = if (freetype_enabled)
.{
.dependency = freetype_dep,
.link_mode = if (b.systemIntegrationOption("freetype", .{}))
.{ .dynamic = dynamic_link_opts }
else
.static,
}
else
null,
},
};
const module = harfbuzz: {
const module = b.addModule("harfbuzz", .{
.root_source_file = b.path("main.zig"),
@@ -31,7 +55,7 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
.imports = if (target.result.os.tag.isDarwin())
&.{
.{ .name = "freetype", .module = freetype.module("freetype") },
.{ .name = "freetype", .module = freetype_dep.module("freetype") },
.{
.name = "macos",
.module = b.dependency("macos", .{ .target = target, .optimize = optimize })
@@ -40,22 +64,11 @@ pub fn build(b: *std.Build) !void {
}
else
&.{
.{ .name = "freetype", .module = freetype.module("freetype") },
.{ .name = "freetype", .module = freetype_dep.module("freetype") },
},
});
try HarfBuzzC.addImportToModule(b, module, .{
.target = target,
.optimize = optimize,
.harfbuzz = if (b.systemIntegrationOption("harfbuzz", .{})) .{ .dynamic = dynamic_link_opts } else .static,
.coretext = coretext_enabled,
.freetype = if (freetype_enabled) ft: {
break :ft if (b.systemIntegrationOption("freetype", .{}))
.{ .dynamic = dynamic_link_opts }
else
.static;
} else null,
});
try harfbuzz_c_builder.addImportToModule(module);
const options = b.addOptions();
options.addOption(bool, "coretext", coretext_enabled);
@@ -74,120 +87,11 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&tests_run.step);
if (!b.systemIntegrationOption("harfbuzz", .{})) {
const lib = try buildLib(b, .{
.target = target,
.optimize = optimize,
.coretext_enabled = coretext_enabled,
.freetype_enabled = freetype_enabled,
.dynamic_link_opts = dynamic_link_opts,
});
const lib = try harfbuzz_c_builder.buildLib();
test_exe.root_module.linkLibrary(lib);
}
}
fn buildLib(b: *std.Build, options: anytype) !*std.Build.Step.Compile {
const target = options.target;
const optimize = options.optimize;
const coretext_enabled = options.coretext_enabled;
const freetype_enabled = options.freetype_enabled;
const freetype = b.dependency("freetype", .{
.target = target,
.optimize = optimize,
.@"enable-libpng" = true,
});
const lib = b.addLibrary(.{
.name = "harfbuzz",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
// On MSVC, we must not use linkLibCpp because Zig unconditionally
// passes -nostdinc++ and then adds its bundled libc++/libc++abi
// include paths, which conflict with MSVC's own C++ runtime
// headers. The MSVC SDK include directories (added via linkLibC)
// contain both C and C++ headers, so linkLibCpp is not needed.
.link_libcpp = target.result.abi != .msvc,
}),
.linkage = .static,
});
if (target.result.os.tag.isDarwin()) {
try apple_sdk.addPaths(b, lib);
}
const dynamic_link_opts = options.dynamic_link_opts;
var flags: std.ArrayList([]const u8) = .empty;
defer flags.deinit(b.allocator);
try flags.appendSlice(b.allocator, &.{
"-DHAVE_STDBOOL_H",
});
// Disable ubsan for MSVC: Zig's ubsan runtime cannot be bundled
// on Windows (LNK4229), leaving __ubsan_handle_* unresolved when
// the static archive is consumed by an external linker.
if (target.result.abi == .msvc) {
try flags.appendSlice(b.allocator, &.{
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
});
}
if (target.result.os.tag != .windows) {
try flags.appendSlice(b.allocator, &.{
"-DHAVE_UNISTD_H",
"-DHAVE_SYS_MMAN_H",
"-DHAVE_PTHREAD=1",
});
}
// Freetype
_ = b.systemIntegrationOption("freetype", .{}); // So it shows up in help
if (freetype_enabled) {
try flags.appendSlice(b.allocator, &.{
"-DHAVE_FREETYPE=1",
// Let's just assume a new freetype
"-DHAVE_FT_GET_VAR_BLEND_COORDINATES=1",
"-DHAVE_FT_SET_VAR_BLEND_COORDINATES=1",
"-DHAVE_FT_DONE_MM_VAR=1",
"-DHAVE_FT_GET_TRANSFORM=1",
});
if (b.systemIntegrationOption("freetype", .{})) {
lib.root_module.linkSystemLibrary("freetype2", dynamic_link_opts);
} else {
lib.root_module.linkLibrary(freetype.artifact("freetype"));
}
}
if (coretext_enabled) {
try flags.appendSlice(b.allocator, &.{"-DHAVE_CORETEXT=1"});
lib.root_module.linkFramework("CoreText", .{});
}
if (b.lazyDependency("harfbuzz", .{})) |upstream| {
lib.root_module.addIncludePath(upstream.path("src"));
lib.root_module.addCSourceFile(.{
.file = upstream.path("src/harfbuzz.cc"),
.flags = flags.items,
});
lib.installHeadersDirectory(
upstream.path("src"),
"",
.{ .include_extensions = &.{".h"} },
);
}
b.installArtifact(lib);
return lib;
}
const HarfBuzzC = struct {
const AddImportToModuleOptions = struct {
const LinkMode = union(enum) {
@@ -199,161 +103,193 @@ const HarfBuzzC = struct {
optimize: std.builtin.OptimizeMode,
harfbuzz: LinkMode,
coretext: bool,
freetype: ?LinkMode,
freetype: ?struct {
dependency: *std.Build.Dependency,
link_mode: LinkMode,
},
};
fn fmtInclude(w: *std.Io.Writer, name: []const u8, mode: AddImportToModuleOptions.LinkMode) !void {
if (mode == .dynamic) {
try w.print("#include <{s}>\n", .{name});
} else {
try w.print("#include \"{s}\"\n", .{name});
}
builder: *std.Build,
options: AddImportToModuleOptions,
fn appendInclude(
list: *std.ArrayList(translate_c.Options.IncludeFile),
name: []const u8,
mode: AddImportToModuleOptions.LinkMode,
) void {
list.appendAssumeCapacity(.{
.path = name,
.type = switch (mode) {
.static => .user,
.dynamic => .system,
},
});
}
fn addImportToModule(
b: *std.Build,
module: *std.Build.Module,
options: AddImportToModuleOptions,
) !void {
// TODO: There's a decent amount of duplication here right now.
// Basically we want to mirror what we're passing in buildLib to make
// sure that the translation is generated as correct as possible.
// Eventually, we want to try and unravel this as much as we can, to
// the point that ultimately all C flags and even link options are
// self-contained in the translation artifact.
//
// This is a bit tricky right now as there are situations where we
// provide a static library built straight off of the C file, hence the
// duplication.
//
// NOTE: This function de-allocates nothing as b.allocator is an arena
// (unfortunately not documented, but a cursory search in various
// communities or the issue trackers should turn up confirmation).
fn includeFiles(self: *const HarfBuzzC) ![]translate_c.Options.IncludeFile {
var len: usize = 1;
if (self.options.coretext) len += 1;
if (self.options.freetype != null) len += 1;
var includes_builder: std.ArrayList(translate_c.Options.IncludeFile) =
try .initCapacity(self.builder.allocator, len);
const translate_c = b.lazyImport(root_build_container, "translate_c") orelse return;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse return;
appendInclude(&includes_builder, "hb.h", self.options.harfbuzz);
if (self.options.coretext) appendInclude(&includes_builder, "hb-coretext.h", self.options.harfbuzz);
if (self.options.freetype != null) appendInclude(&includes_builder, "hb-ft.h", self.options.harfbuzz);
const c_source = c_source: {
var source_builder: std.Io.Writer.Allocating = .init(b.allocator);
try fmtInclude(&source_builder.writer, "hb.h", options.harfbuzz);
return includes_builder.items;
}
if (options.coretext) {
try fmtInclude(&source_builder.writer, "hb-coretext.h", options.harfbuzz);
}
fn systemLibs(self: *const HarfBuzzC) ![][]const u8 {
var len: usize = 1;
if (self.options.harfbuzz == .dynamic) len += 1;
if (self.options.freetype != null and self.options.freetype.?.link_mode == .dynamic) len += 1;
var libs_builder: std.ArrayList([]const u8) = try .initCapacity(self.builder.allocator, len);
if (options.freetype != null) {
try fmtInclude(&source_builder.writer, "hb-ft.h", options.harfbuzz);
}
if (self.options.harfbuzz == .dynamic)
libs_builder.appendAssumeCapacity("harfbuzz");
if (self.options.freetype != null and self.options.freetype.?.link_mode == .dynamic)
libs_builder.appendAssumeCapacity("freetype2");
break :c_source source_builder.written();
};
return libs_builder.items;
}
// Assemble system libs
const system_libs = libs: {
var libs_builder: std.ArrayList(translate_c.Translator.LinkSystemLib) = .empty;
if (options.harfbuzz == .dynamic)
try libs_builder.append(b.allocator, .{ .name = "harfbuzz", .options = options.harfbuzz.dynamic });
if (options.freetype) |ft| {
if (ft == .dynamic)
try libs_builder.append(b.allocator, .{ .name = "freetype2", .options = ft.dynamic });
}
fn includePaths(self: *const HarfBuzzC) ![]std.Build.LazyPath {
const hb_upstream: ?*std.Build.Dependency = if (self.options.harfbuzz == .static)
self.builder.lazyDependency("harfbuzz", .{})
else
null;
break :libs libs_builder.items;
};
const ft_upstream: ?*std.Build.Dependency =
if (self.options.freetype) |ft| ft: {
if (ft.link_mode == .static) break :ft ft.dependency.builder.lazyDependency("freetype", .{});
break :ft null;
} else null;
// Assemble flags
const flags = flags: {
var flag_builder: std.ArrayList([]const u8) = .empty;
try flag_builder.appendSlice(b.allocator, &.{
"-DHAVE_STDBOOL_H",
});
// Disable ubsan for MSVC: Zig's ubsan runtime cannot be bundled
// on Windows (LNK4229), leaving __ubsan_handle_* unresolved when
// the static archive is consumed by an external linker.
if (options.target.result.abi == .msvc) {
try flag_builder.appendSlice(b.allocator, &.{
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
});
}
if (options.target.result.os.tag != .windows) {
try flag_builder.appendSlice(b.allocator, &.{
"-DHAVE_UNISTD_H",
"-DHAVE_SYS_MMAN_H",
"-DHAVE_PTHREAD=1",
});
}
var len: usize = 0;
if (hb_upstream != null) len += 1;
if (ft_upstream != null) len += 1;
var paths_builder: std.ArrayList(std.Build.LazyPath) = try .initCapacity(self.builder.allocator, len);
// Freetype flags/non-system include paths
if (options.freetype != null) {
try flag_builder.appendSlice(b.allocator, &.{
"-DHAVE_FREETYPE=1",
if (hb_upstream) |upstream| paths_builder.appendAssumeCapacity(upstream.path("src"));
if (ft_upstream) |upstream| paths_builder.appendAssumeCapacity(upstream.path("include"));
// Let's just assume a new freetype
"-DHAVE_FT_GET_VAR_BLEND_COORDINATES=1",
"-DHAVE_FT_SET_VAR_BLEND_COORDINATES=1",
"-DHAVE_FT_DONE_MM_VAR=1",
"-DHAVE_FT_GET_TRANSFORM=1",
});
}
return paths_builder.items;
}
// Coretext
if (options.coretext) {
try flag_builder.appendSlice(b.allocator, &.{"-DHAVE_CORETEXT=1"});
try flag_builder.appendSlice(b.allocator, &.{"-fblocks"});
}
fn frameworks(self: *const HarfBuzzC) []const []const u8 {
return if (self.options.coretext) &.{"CoreText"} else &.{};
}
break :flags flag_builder.items;
};
const hb_c: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add(
"hb_c.h",
c_source,
),
.target = options.target,
.optimize = options.optimize,
.link_libc = true,
.link_system_libs = system_libs,
.libc_file = if (options.target.result.os.tag.isDarwin()) libc_file: {
switch (try @import("apple_sdk").pathsForTarget(b, options.target.result)) {
inline else => |paths| break :libc_file paths.libc,
}
} else null,
.extra_args = flags,
fn flags(self: *const HarfBuzzC) ![][]const u8 {
var flag_builder: std.ArrayList([]const u8) = .empty;
try flag_builder.appendSlice(self.builder.allocator, &.{
"-DHAVE_STDBOOL_H",
});
if (options.harfbuzz == .static) {
if (b.lazyDependency("harfbuzz", .{})) |upstream| {
hb_c.addIncludePath(upstream.path("src"));
}
// Disable ubsan for MSVC: Zig's ubsan runtime cannot be bundled
// on Windows (LNK4229), leaving __ubsan_handle_* unresolved when
// the static archive is consumed by an external linker.
if (self.options.target.result.abi == .msvc) {
try flag_builder.appendSlice(self.builder.allocator, &.{
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
});
}
if (self.options.target.result.os.tag != .windows) {
try flag_builder.appendSlice(self.builder.allocator, &.{
"-DHAVE_UNISTD_H",
"-DHAVE_SYS_MMAN_H",
"-DHAVE_PTHREAD=1",
});
}
// Freetype non-system include paths
if (options.freetype) |freetype_enabled| {
if (freetype_enabled == .static) {
const ft_dep = b.dependency("freetype", .{
.target = options.target,
.optimize = options.optimize,
.@"enable-libpng" = true,
});
// Freetype flags/non-system include paths
if (self.options.freetype != null) {
try flag_builder.appendSlice(self.builder.allocator, &.{
"-DHAVE_FREETYPE=1",
if (ft_dep.builder.lazyDependency(
"freetype",
.{},
)) |freetype_lazy_dep| {
hb_c.addIncludePath(freetype_lazy_dep.path("include"));
}
}
// Let's just assume a new freetype
"-DHAVE_FT_GET_VAR_BLEND_COORDINATES=1",
"-DHAVE_FT_SET_VAR_BLEND_COORDINATES=1",
"-DHAVE_FT_DONE_MM_VAR=1",
"-DHAVE_FT_GET_TRANSFORM=1",
});
}
// Coretext
if (options.coretext) {
// NOTE: We should not necessarily need to add this directly to C
// translation, so we just add it to the module.
hb_c.mod.linkFramework("CoreText", .{});
if (self.options.coretext) {
try flag_builder.appendSlice(self.builder.allocator, &.{"-DHAVE_CORETEXT=1"});
}
module.addImport("hb_c", hb_c.mod);
return flag_builder.items;
}
fn addImportToModule(
self: *const HarfBuzzC,
module: *std.Build.Module,
) !void {
try translate_c.addImportToModule(self.builder, "hb_c", module, .{
.source = .{ .includes = .{ .files = try self.includeFiles() } },
.target = self.options.target,
.optimize = self.options.optimize,
.link_system_libs = try self.systemLibs(),
.include_paths = try self.includePaths(),
.link_frameworks = self.frameworks(),
.extra_args = try self.flags(),
});
}
fn buildLib(self: *const HarfBuzzC) !*std.Build.Step.Compile {
const target = self.options.target;
const optimize = self.options.optimize;
const lib = self.builder.addLibrary(.{
.name = "harfbuzz",
.root_module = self.builder.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
// On MSVC, we must not use linkLibCpp because Zig unconditionally
// passes -nostdinc++ and then adds its bundled libc++/libc++abi
// include paths, which conflict with MSVC's own C++ runtime
// headers. The MSVC SDK include directories (added via linkLibC)
// contain both C and C++ headers, so linkLibCpp is not needed.
.link_libcpp = target.result.abi != .msvc,
}),
.linkage = .static,
});
// Freetype
if (self.options.freetype) |ft| {
switch (ft.link_mode) {
.dynamic => |opts| lib.root_module.linkSystemLibrary("freetype2", opts),
.static => {
lib.root_module.linkLibrary(ft.dependency.artifact("freetype"));
},
}
}
// CoreText stuff
for (self.frameworks()) |framework| lib.root_module.linkFramework(framework, .{});
if (target.result.os.tag.isDarwin()) {
try apple_sdk.addPaths(self.builder, lib);
}
if (self.builder.lazyDependency("harfbuzz", .{})) |upstream| {
lib.root_module.addIncludePath(upstream.path("src"));
lib.root_module.addCSourceFile(.{
.file = upstream.path("src/harfbuzz.cc"),
.flags = try self.flags(),
});
lib.installHeadersDirectory(
upstream.path("src"),
"",
.{ .include_extensions = &.{".h"} },
);
}
self.builder.installArtifact(lib);
return lib;
}
};

View File

@@ -4,12 +4,6 @@
.fingerprint = 0xbd60917cd18865d8,
.paths = .{""},
.dependencies = .{
.translate_c = .{
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
.lazy = true,
},
// harfbuzz/harfbuzz
.harfbuzz = .{
.url = "https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz",
@@ -20,5 +14,6 @@
.freetype = .{ .path = "../freetype" },
.macos = .{ .path = "../macos" },
.apple_sdk = .{ .path = "../apple-sdk" },
.translate_c = .{ .path = "../translate-c" },
},
}

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const apple_sdk = @import("apple_sdk");
const translate_c = @import("translate_c");
const Framework = struct {
const Tag = enum { all, macos };
@@ -26,34 +27,28 @@ const extra_headers = [_][]const u8{
"os/signpost.h",
};
const framework_header_fmt = "#include <{s}/{s}>\n";
const extra_header_fmt = "#include <{s}>\n";
fn cSourceLen(tag: Framework.Tag) usize {
fn includeFiles(b: *std.Build, tag: Framework.Tag) ![]translate_c.Options.IncludeFile {
var len: usize = 0;
for (frameworks) |framework| {
if (tag != .macos and framework.tag == .macos) continue;
for (framework.headers) |h| len += std.fmt.count(framework_header_fmt, .{ framework.name, h });
len += framework.headers.len;
}
for (extra_headers) |h| len += std.fmt.count(extra_header_fmt, .{h});
return len;
}
len += extra_headers.len;
var includes_builder: std.ArrayList(translate_c.Options.IncludeFile) =
try .initCapacity(b.allocator, len);
fn genCSource(comptime tag: Framework.Tag) [cSourceLen(tag):0]u8 {
const len = cSourceLen(tag);
var buf: [len:0]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
for (frameworks) |framework| {
if (tag != .macos and framework.tag == .macos) continue;
for (framework.headers) |h| writer.print(framework_header_fmt, .{ framework.name, h }) catch unreachable;
for (framework.headers) |h| {
const path = try std.fmt.allocPrint(b.allocator, "{s}/{s}", .{ framework.name, h });
includes_builder.appendAssumeCapacity(.{ .path = path });
}
}
for (extra_headers) |h| writer.print(extra_header_fmt, .{h}) catch unreachable;
buf[len] = 0;
return buf;
}
const c_source_macos = genCSource(.macos);
const c_source_other = genCSource(.all);
for (extra_headers) |h| includes_builder.appendAssumeCapacity(.{ .path = h });
return includes_builder.items;
}
fn linkFrameworks(tag: Framework.Tag, module: *std.Build.Module) !void {
for (frameworks) |framework| {
@@ -72,28 +67,14 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
translate: {
const translate_c = b.lazyImport(@This(), "translate_c") orelse break :translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :translate;
const macos_c: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add(
"macos_c.h",
if (target.result.os.tag == .macos) &c_source_macos else &c_source_other,
),
.target = target,
.optimize = optimize,
.libc_file = if (target.result.os.tag.isDarwin()) libc_file: {
switch (try apple_sdk.pathsForTarget(b, target.result)) {
inline else => |paths| break :libc_file paths.libc,
}
} else null,
});
// Blocks need to be enabled to use MacOS headers
macos_c.run.addArg("-fblocks");
module.addImport("macos_c", macos_c.mod);
}
try translate_c.addImportToModule(b, "macos_c", module, .{
.source = .{ .includes = .{ .files = try includeFiles(
b,
if (target.result.os.tag == .macos) .macos else .all,
) } },
.target = target,
.optimize = optimize,
});
const lib = b.addLibrary(.{
.name = "macos",

View File

@@ -5,10 +5,6 @@
.paths = .{""},
.dependencies = .{
.apple_sdk = .{ .path = "../apple-sdk" },
.translate_c = .{
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
.lazy = true,
},
.translate_c = .{ .path = "../translate-c" },
},
}

218
pkg/translate-c/build.zig Normal file
View File

@@ -0,0 +1,218 @@
//! This is a wrapper package for our use of translate-c. It provides helpers
//! for short-hand addition of the translation of C files and headers, along
//! with lower-level control of the process a la the standard external
//! translate-c package.
const std = @import("std");
const apple_sdk = @import("apple_sdk");
pub const Translator = @import("translate_c").Translator;
/// Options for translation.
pub const Options = struct {
/// Describes the specification for a single include file.
pub const IncludeFile = struct {
/// Describes the type of an include file.
pub const Type = enum {
/// A system include, included as `<file.h>`.
system,
/// A user-defined include, included as `"file.h"`.
user,
};
/// The path to the include. Should be either a base path or a relative
/// path, depending on what is expected via translation based on the
/// library directory structure.
path: []const u8,
/// The type of include file.
type: Type = .system,
};
/// The subject of the translation.
source: union(enum) {
/// The subject is an on-disk path and will be passed through directly
/// for translation.
file: std.Build.LazyPath,
/// The subject is a collection of include files. These files will be
/// included (in order) as system includes (e.g., `#include <foo.h>`).
includes: struct {
/// The name of the generated source file in cache. If not
/// specified, will be inferred from the operation, usually the
/// name of the import (e.g., `c.h` if the import name was "c").
generated_name: ?[]const u8 = null,
/// The files to include.
files: []const IncludeFile,
},
},
/// The target to perform translation as.
target: std.Build.ResolvedTarget,
/// The optimization mode to perform translation as.
optimize: std.builtin.OptimizeMode,
/// Whether or not to link in libc. Generally you want this.
link_libc: bool = true,
/// The system libraries to link against. These will likely line up to
/// whatever you are translating.
///
/// These system libraries are always linked against preferred-dynamic with
/// a fallback to static.
link_system_libs: []const []const u8 = &.{},
/// Any additional include paths. These will be added using `-I` to the
/// translation process, and made available to the translated code, in the
/// order they are specified.
include_paths: []const std.Build.LazyPath = &.{},
/// Any additional system include paths. These will be added using
/// `-isystem` to the translation process, and made available to the
/// translated code, in the order they are specified.
system_include_paths: []const std.Build.LazyPath = &.{},
/// If supplied, these frameworks will be linked to the underlying
/// generated Zig module via `linkFramework` in the order they are
/// received. It does not affect translation.
///
/// You likely don't need this if you are not building for an Apple
/// platform.
link_frameworks: []const []const u8 = &.{},
/// Supply an external libc file. The expected format here is exactly what
/// you would get if you ran `zig libc` and can be used if the toolchain on
/// a particular target has a hard time auto-detecting these paths.
libc_file: union(enum) {
/// Auto-detect if we are targeting Darwin in the target options, and
/// if we are, generate a libc file to use here automatically. This
/// ensures that translation can correctly locate a MacOS SDK versus
/// the Zig-supplied generic Darwin headers.
detect_darwin,
/// Supply a direct file for use here.
direct: ?std.Build.LazyPath,
} = .detect_darwin,
/// Extra arguments passed to Aro. Use this if you need to pass along extra
/// compiler flags to the translation process to make sure the headers are
/// pre-processed correctly before translation.
extra_args: []const []const u8 = &.{},
/// The name of this dependency in the caller's build.zig.zon file. If you
/// name the dependency anything else other than `translate_c`, change this
/// to match.
dependency_name: []const u8 = "translate_c",
};
/// Creates a translation step and adds the result as import referred to by
/// `name` to the module defined by `module`, making all translated objects
/// available to the module behind the import name.
pub fn addImportToModule(
b: *std.Build,
name: []const u8,
module: *std.Build.Module,
options: Options,
) !void {
var init_opts = options;
if (init_opts.source == .includes and init_opts.source.includes.generated_name == null) {
init_opts.source.includes.generated_name = try std.fmt.allocPrint(
b.allocator,
"{s}.h",
.{name},
);
}
const translated = try init(b, init_opts);
module.addImport(name, translated.mod);
}
/// Mainly serves as a pass-through for the independent translate-c
/// `Translator.init`, but also adds additional paths before returning.
///
/// Unless you need the actual translation object for more complex build
/// chains, it's recommended to use the higher-level methods such as
/// `addImportToModule`.
pub fn init(b: *std.Build, options: Options) !Translator {
const translated = try initTranslator(b, options);
for (options.include_paths) |path| translated.addIncludePath(path);
for (options.system_include_paths) |path| translated.addSystemIncludePath(path);
for (options.link_frameworks) |framework| translated.mod.linkFramework(framework, .{});
return translated;
}
/// Mainly serves as a pass-through for the independent translate-c
/// `Translator.init`. Unless you need the actual translation object for more
/// complex build chains, it's recommended to use the higher-level methods such
/// as `addImportToModule`.
pub fn initTranslator(b: *std.Build, options: Options) !Translator {
const this_dep = b.dependency(options.dependency_name, .{});
const translate_c_dep = this_dep.builder.dependency("translate_c", .{});
return .init(translate_c_dep, .{
.c_source_file = switch (options.source) {
.file => |f| f,
.includes => |includes| b.addWriteFiles().add(
includes.generated_name orelse "c.h",
try buildSource(b, includes.files),
),
},
.target = options.target,
.optimize = options.optimize,
.link_libc = options.link_libc,
.link_system_libs = try marshalSystemLibs(b, options.link_system_libs),
.libc_file = switch (options.libc_file) {
.detect_darwin => if (options.target.result.os.tag.isDarwin()) libc_file: {
switch (try apple_sdk.pathsForTarget(this_dep.builder, options.target.result)) {
inline else => |paths| break :libc_file paths.libc,
}
} else null,
.direct => |libc_file| libc_file,
},
.extra_args = options.extra_args,
});
}
/// Marshals linked system libraries into the `Translator.LinkSystemLib`
/// format, which includes the link options for each library.
///
/// All system libraries linked this way are linked dynamic-preferred with a
/// fallback to static.
///
/// Note that this uses the builder arena and as such does not need to be freed.
fn marshalSystemLibs(b: *std.Build, libs: []const []const u8) ![]Translator.LinkSystemLib {
var result: std.ArrayList(Translator.LinkSystemLib) = .empty;
try result.ensureTotalCapacityPrecise(b.allocator, libs.len);
for (libs) |name| {
result.appendAssumeCapacity(.{
.name = name,
.options = .{
.preferred_link_mode = .dynamic,
.search_strategy = .mode_first,
},
});
}
return result.items;
}
/// Builds the source for a set of `IncludeFile`s.
///
/// Note that this uses the builder arena and as such does not need to be freed.
fn buildSource(b: *std.Build, files: []const Options.IncludeFile) ![]const u8 {
var source_builder: std.Io.Writer.Allocating = .init(b.allocator);
for (files) |file| try fmtInclude(&source_builder.writer, file);
return source_builder.written();
}
fn fmtInclude(w: *std.Io.Writer, file: Options.IncludeFile) !void {
if (file.type == .system) {
try w.print("#include <{s}>\n", .{file.path});
} else {
try w.print("#include \"{s}\"\n", .{file.path});
}
}
pub fn build(b: *std.Build) void {
_ = b;
}

View File

@@ -0,0 +1,19 @@
.{
.name = .translate_c,
.version = "0.0.1",
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
},
.fingerprint = 0x1c84f6452d0e8347,
.dependencies = .{
.apple_sdk = .{
.path = "../apple-sdk",
},
.translate_c = .{
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
},
},
}

View File

@@ -1,4 +1,5 @@
const std = @import("std");
const translate_c = @import("translate_c");
// All the C macros defined so that the header matches the build.
const defines = [_][]const u8{
@@ -14,25 +15,6 @@ const defines = [_][]const u8{
"WUFFS_CONFIG__MODULE__ZLIB",
};
// Generated C code, includes the macros above. Designed to mimic old c.zig.
// TODO: is this still needed, or are the -D flags enough?
const wuffs_c_source = wuffs_c_source: {
const include: []const u8 = "#include <wuffs-v0.4.c>";
const len = len: {
var len: usize = 0;
for (defines) |d| len += std.fmt.count("#define {s}\n", .{d});
len += std.fmt.count("{s}\n", .{include});
break :len len;
};
var buf: [len:0]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
for (defines) |d| writer.print("#define {s}\n", .{d}) catch unreachable;
writer.print("{s}\n", .{include}) catch unreachable;
buf[len] = 0;
break :wuffs_c_source buf;
};
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
@@ -52,46 +34,66 @@ pub fn build(b: *std.Build) !void {
const windows = target.result.os.tag == .windows;
translate: {
const translate_c = b.lazyImport(@This(), "translate_c") orelse break :translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :translate;
const wuffs_c: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("wuffs_c.h", &wuffs_c_source),
const wuffs_dep = b.lazyDependency("wuffs", .{}) orelse break :translate;
const include_paths: []const std.Build.LazyPath = switch (windows) {
true => &.{wuffs_dep.path("release/c")},
// Wuffs only needs stdlib.h and string.h from libc, and only for
// a handful of declarations. We provide minimal versions of these
// headers so that wuffs can be translated and compiled without
// libc, notably for freestanding targets (wasm32) but this also
// avoids requiring an Apple SDK for translate-c on macOS.
false => &.{ b.path("include"), wuffs_dep.path("release/c") },
};
// Split up macro flags so that we can add them to translation
const macro_flags = macro_flags: {
var flag_builder: std.ArrayList([]const u8) = try .initCapacity(b.allocator, defines.len);
inline for (defines) |key| {
flag_builder.appendAssumeCapacity("-D" ++ key);
}
break :macro_flags flag_builder.items;
};
// Larger flag set for C file build within module
const c_flags = c_flags: {
var len: usize = macro_flags.len + 1;
if (windows) len += 2;
var flag_builder: std.ArrayList([]const u8) = try .initCapacity(b.allocator, len);
flag_builder.appendAssumeCapacity("-DWUFFS_IMPLEMENTATION");
// Disable ubsan on Windows to avoid undefined __ubsan_handle_*
// references: Zig's ubsan runtime can't be bundled on Windows
// (its /exclude-symbols directives break the MSVC linker), so
// these handlers would go unresolved. This affects both the
// MSVC and GNU ABIs.
if (windows) {
flag_builder.appendAssumeCapacity("-fno-sanitize=undefined");
flag_builder.appendAssumeCapacity("-fno-sanitize-trap=undefined");
}
for (macro_flags) |f| flag_builder.appendAssumeCapacity(f);
break :c_flags flag_builder.items;
};
const wuffs_c = try translate_c.init(b, .{
.source = .{ .includes = .{
.generated_name = "wuffs_c.h",
.files = &.{.{ .path = "wuffs-v0.4.c" }},
} },
.target = target,
.optimize = optimize,
.include_paths = include_paths,
.link_libc = windows,
.extra_args = macro_flags,
});
// Wuffs only needs stdlib.h and string.h from libc, and only for
// a handful of declarations. We provide minimal versions of these
// headers so that wuffs can be translated and compiled without
// libc, notably for freestanding targets (wasm32) but this also
// avoids requiring an Apple SDK for translate-c on macOS.
if (!windows) wuffs_c.addIncludePath(b.path("include"));
var flags: std.ArrayList([]const u8) = .empty;
defer flags.deinit(b.allocator);
try flags.append(b.allocator, "-DWUFFS_IMPLEMENTATION");
// Disable ubsan on Windows to avoid undefined __ubsan_handle_*
// references: Zig's ubsan runtime can't be bundled on Windows
// (its /exclude-symbols directives break the MSVC linker), so
// these handlers would go unresolved. This affects both the
// MSVC and GNU ABIs.
if (windows) {
try flags.append(b.allocator, "-fno-sanitize=undefined");
try flags.append(b.allocator, "-fno-sanitize-trap=undefined");
}
inline for (defines) |key| {
try flags.append(b.allocator, "-D" ++ key);
}
if (b.lazyDependency("wuffs", .{})) |wuffs_dep| {
wuffs_c.addIncludePath(wuffs_dep.path("release/c"));
wuffs_c.mod.addCSourceFile(.{
.file = wuffs_dep.path("release/c/wuffs-v0.4.c"),
.flags = flags.items,
});
}
wuffs_c.mod.addCSourceFile(.{
.file = wuffs_dep.path("release/c/wuffs-v0.4.c"),
.flags = c_flags,
});
module.addImport("wuffs_c", wuffs_c.mod);
}

View File

@@ -3,11 +3,7 @@
.version = "0.0.0",
.fingerprint = 0x67c0c059de921c4f,
.dependencies = .{
.translate_c = .{
.lazy = true,
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
},
.translate_c = .{ .path = "../translate-c" },
// google/wuffs
.wuffs = .{

View File

@@ -10,6 +10,7 @@ const UnicodeTables = @import("UnicodeTables.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig");
const DistResource = @import("GhosttyDist.zig").Resource;
const gtk_helpers = @import("gtk.zig");
const translate_c = @import("translate_c");
config: *const Config,
@@ -666,25 +667,15 @@ pub fn add(
step.root_module.linkSystemLibrary("gtk4", dynamic_link_opts);
// We need to translate gio headers too
gio_translate: {
// translate-c stuff
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse
break :gio_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse
break :gio_translate;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("gio_c.h",
\\#include <gio/gio.h>
\\#include <gio/gunixfdlist.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "gio-2.0", .options = dynamic_link_opts },
},
});
step.root_module.addImport("gio_c", translated.mod);
}
try translate_c.addImportToModule(b, "gio_c", step.root_module, .{
.source = .{ .includes = .{ .files = &.{
.{ .path = "gio/gio.h" },
.{ .path = "gio/gunixfdlist.h" },
} } },
.target = target,
.optimize = optimize,
.link_system_libs = &.{"gio-2.0"},
});
}
switch (self.config.app_runtime) {
@@ -729,41 +720,22 @@ fn addGtkNg(
step.root_module.addImport(name, gobject.module(module));
}
}
gtk_adw_translate: {
// translate-c stuff
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse break :gtk_adw_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :gtk_adw_translate;
const Translator = translate_c.Translator;
{
// GTK headers
const translated: Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("gtk_c.h",
\\#include <gtk/gtk.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "gtk4", .options = dynamic_link_opts },
},
});
step.root_module.addImport("gtk_c", translated.mod);
}
{
// Adwaita headers
const translated: Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("adw_c.h",
\\#include <adwaita.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "libadwaita-1", .options = dynamic_link_opts },
},
});
step.root_module.addImport("adw_c", translated.mod);
}
}
// GTK C translation
try translate_c.addImportToModule(b, "gtk_c", step.root_module, .{
.source = .{ .includes = .{ .files = &.{.{ .path = "gtk/gtk.h" }} } },
.target = target,
.optimize = optimize,
.link_system_libs = &.{"gtk4"},
});
// Adwaita C translation
try translate_c.addImportToModule(b, "adw_c", step.root_module, .{
.source = .{ .includes = .{ .files = &.{.{ .path = "adwaita.h" }} } },
.target = target,
.optimize = optimize,
.link_system_libs = &.{"libadwaita-1"},
});
if (self.config.x11) {
step.root_module.linkSystemLibrary("X11", dynamic_link_opts);
@@ -869,24 +841,19 @@ fn addGtkNg(
step.root_module.linkSystemLibrary("wayland-client", dynamic_link_opts);
}
ghostty_resources_translate: {
{
// Get our gresource c/h files and add them to our build.
const dist = gtkNgDistResources(b);
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse
break :ghostty_resources_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse
break :ghostty_resources_translate;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("c.h",
\\#include <ghostty_resources.h>
),
const translated = try translate_c.init(b, .{
.source = .{ .includes = .{
.generated_name = "ghostty_gtk_resources_c.h",
.files = &.{.{ .path = "ghostty_resources.h" }},
} },
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "glib-2.0", .options = dynamic_link_opts },
},
.link_system_libs = &.{"glib-2.0"},
.include_paths = &.{dist.resources_h.path(b).dirname()},
});
translated.addIncludePath(dist.resources_h.path(b).dirname());
translated.mod.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
step.root_module.addImport("ghostty_gtk_resources", translated.mod);
}
@@ -1037,22 +1004,14 @@ pub fn gtkNgDistResources(
.link_libc = true,
}),
});
adw_translate: {
// Adwaita headers
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse break :adw_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :adw_translate;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("adw_c.h",
\\#include <adwaita.h>
),
.target = b.graph.host,
.optimize = .Debug,
.link_system_libs = &.{
.{ .name = "libadwaita-1", .options = dynamic_link_opts },
},
});
blueprint_exe.root_module.addImport("adw_c", translated.mod);
}
// Adwaita headers
translate_c.addImportToModule(b, "adw_c", blueprint_exe.root_module, .{
.source = .{ .includes = .{ .files = &.{.{ .path = "adwaita.h" }} } },
.target = b.graph.host,
.optimize = .Debug,
.link_system_libs = &.{"libadwaita-1"},
}) catch unreachable;
for (gresource.blueprints) |bp| {
const blueprint_run = b.addRunArtifact(blueprint_exe);