build: zig build test runs Xcode tests on macOS

Related to #7879

This commit updates `zig build test` to run Xcode tests, too. These run
in parallel to the Zig tests, so they don't add any time to the test.

The Xcode tests will _not_ run when: (1) the target is not macOS, or (2)
the `-Dtest-filter` option is non-empty. This makes it so that this
change doesn't affect non-macOS and doesn't affect the general dev cycle
because you usually will run `-Dtest-filter` when developing a core
feature.

I didn't add a step to only run Xcode tests because I find that when I'm
working in Xcode I'm probably going to run the tests from there anyways.
The integration with `zig build test` is just a convenience, especially
around CI.

Speaking of CI, this change also makes it so this will run in CI.
This commit is contained in:
Mitchell Hashimoto
2025-07-10 07:10:50 -07:00
parent 9a3a6a8352
commit 9fa26387ef
7 changed files with 121 additions and 57 deletions

View File

@ -8,7 +8,22 @@ comptime {
} }
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
// This defines all the available build options (e.g. `-D`).
const config = try buildpkg.Config.init(b); const config = try buildpkg.Config.init(b);
const test_filter = b.option(
[]const u8,
"test-filter",
"Filter for test. Only applies to Zig tests.",
);
// All our steps which we'll hook up later. The steps are shown
// up here just so that they are more self-documenting.
const run_step = b.step("run", "Run the app");
const test_step = b.step("test", "Run all tests");
const translations_step = b.step(
"update-translations",
"Update translation files",
);
// Ghostty resources like terminfo, shell integration, themes, etc. // Ghostty resources like terminfo, shell integration, themes, etc.
const resources = try buildpkg.GhosttyResources.init(b, &config); const resources = try buildpkg.GhosttyResources.init(b, &config);
@ -131,7 +146,6 @@ pub fn build(b: *std.Build) !void {
b.getInstallPath(.prefix, "share/ghostty"), b.getInstallPath(.prefix, "share/ghostty"),
); );
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step); run_step.dependOn(&run_cmd.step);
break :run; break :run;
} }
@ -157,16 +171,18 @@ pub fn build(b: *std.Build) !void {
}, },
); );
const run_step = b.step("run", "Run the app"); // Run uses the native macOS app
run_step.dependOn(&macos_app_native_only.open.step); run_step.dependOn(&macos_app_native_only.open.step);
// If we have no test filters, install the tests too
if (test_filter == null) {
macos_app_native_only.addTestStepDependencies(test_step);
}
} }
} }
// Tests // Tests
{ {
const test_step = b.step("test", "Run all tests");
const test_filter = b.option([]const u8, "test-filter", "Filter for test");
const test_exe = b.addTest(.{ const test_exe = b.addTest(.{
.name = "ghostty-test", .name = "ghostty-test",
.filters = if (test_filter) |v| &.{v} else &.{}, .filters = if (test_filter) |v| &.{v} else &.{},
@ -180,18 +196,13 @@ pub fn build(b: *std.Build) !void {
}), }),
}); });
{ if (config.emit_test_exe) b.installArtifact(test_exe);
if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe);
_ = try deps.add(test_exe); const test_run = b.addRunArtifact(test_exe);
const test_run = b.addRunArtifact(test_exe); test_step.dependOn(&test_run.step);
test_step.dependOn(&test_run.step);
}
} }
// update-translations does what it sounds like and updates the "pot" // update-translations does what it sounds like and updates the "pot"
// files. These should be committed to the repo. // files. These should be committed to the repo.
{ translations_step.dependOn(i18n.update_step);
const step = b.step("update-translations", "Update translation files");
step.dependOn(i18n.update_step);
}
} }

View File

@ -303,7 +303,7 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
A54F45F42E1F047A0046BD5C /* GhosttyTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyTests; sourceTree = "<group>"; }; A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -612,7 +612,7 @@
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */, A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */,
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */, 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */,
A54CD6ED299BEB14008C95BB /* Sources */, A54CD6ED299BEB14008C95BB /* Sources */,
A54F45F42E1F047A0046BD5C /* GhosttyTests */, A54F45F42E1F047A0046BD5C /* Tests */,
A5D495A3299BECBA00DD1313 /* Frameworks */, A5D495A3299BECBA00DD1313 /* Frameworks */,
A5A1F8862A489D7400D1E8BC /* Resources */, A5A1F8862A489D7400D1E8BC /* Resources */,
A5B30532299BEAAA0047F10C /* Products */, A5B30532299BEAAA0047F10C /* Products */,
@ -712,7 +712,7 @@
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */, A54F45F82E1F047A0046BD5C /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
A54F45F42E1F047A0046BD5C /* GhosttyTests */, A54F45F42E1F047A0046BD5C /* Tests */,
); );
name = GhosttyTests; name = GhosttyTests;
packageProductDependencies = ( packageProductDependencies = (

View File

@ -15,15 +15,23 @@ pub fn build(b: *std.Build) !void {
}); });
const macos = b.dependency("macos", .{ .target = target, .optimize = optimize }); const macos = b.dependency("macos", .{ .target = target, .optimize = optimize });
const module = b.addModule("harfbuzz", .{ const module = harfbuzz: {
.root_source_file = b.path("main.zig"), const module = b.addModule("harfbuzz", .{
.target = target, .root_source_file = b.path("main.zig"),
.optimize = optimize, .target = target,
.imports = &.{ .optimize = optimize,
.{ .name = "freetype", .module = freetype.module("freetype") }, .imports = &.{
.{ .name = "macos", .module = macos.module("macos") }, .{ .name = "freetype", .module = freetype.module("freetype") },
}, .{ .name = "macos", .module = macos.module("macos") },
}); },
});
const options = b.addOptions();
options.addOption(bool, "coretext", coretext_enabled);
options.addOption(bool, "freetype", freetype_enabled);
module.addOptions("build_options", options);
break :harfbuzz module;
};
// For dynamic linking, we prefer dynamic linking and to search by // For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library // mode first. Mode first will search all paths for a dynamic library

View File

@ -1,7 +1,8 @@
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_options = @import("build_options");
pub const c = @cImport({ pub const c = @cImport({
@cInclude("hb.h"); @cInclude("hb.h");
@cInclude("hb-ft.h"); if (build_options.freetype) @cInclude("hb-ft.h");
if (builtin.os.tag == .macos) @cInclude("hb-coretext.h"); if (build_options.coretext) @cInclude("hb-coretext.h");
}); });

View File

@ -12,6 +12,7 @@ const XCFramework = @import("GhosttyXCFramework.zig");
build: *std.Build.Step.Run, build: *std.Build.Step.Run,
open: *std.Build.Step.Run, open: *std.Build.Step.Run,
copy: *std.Build.Step.Run, copy: *std.Build.Step.Run,
xctest: *std.Build.Step.Run,
pub const Deps = struct { pub const Deps = struct {
xcframework: *const XCFramework, xcframework: *const XCFramework,
@ -33,6 +34,21 @@ pub fn init(
=> "Release", => "Release",
}; };
const xc_arch: ?[]const u8 = switch (deps.xcframework.target) {
// Universal is our default target, so we don't have to
// add anything.
.universal => null,
// Native we need to override the architecture in the Xcode
// project with the -arch flag.
.native => switch (builtin.cpu.arch) {
.aarch64 => "arm64",
.x86_64 => "x86_64",
else => @panic("unsupported macOS arch"),
},
};
const env = try std.process.getEnvMap(b.allocator);
const app_path = b.fmt("macos/build/{s}/Ghostty.app", .{xc_config}); const app_path = b.fmt("macos/build/{s}/Ghostty.app", .{xc_config});
// Our step to build the Ghostty macOS app. // Our step to build the Ghostty macOS app.
@ -41,12 +57,13 @@ pub fn init(
// we create a new empty environment. // we create a new empty environment.
const env_map = try b.allocator.create(std.process.EnvMap); const env_map = try b.allocator.create(std.process.EnvMap);
env_map.* = .init(b.allocator); env_map.* = .init(b.allocator);
if (env.get("PATH")) |v| try env_map.put("PATH", v);
const build = RunStep.create(b, "xcodebuild"); const step = RunStep.create(b, "xcodebuild");
build.has_side_effects = true; step.has_side_effects = true;
build.cwd = b.path("macos"); step.cwd = b.path("macos");
build.env_map = env_map; step.env_map = env_map;
build.addArgs(&.{ step.addArgs(&.{
"xcodebuild", "xcodebuild",
"-target", "-target",
"Ghostty", "Ghostty",
@ -54,36 +71,55 @@ pub fn init(
xc_config, xc_config,
}); });
switch (deps.xcframework.target) { // If we have a specific architecture, we need to pass it
// Universal is our default target, so we don't have to // to xcodebuild.
// add anything. if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch });
.universal => {},
// Native we need to override the architecture in the Xcode
// project with the -arch flag.
.native => build.addArgs(&.{
"-arch",
switch (builtin.cpu.arch) {
.aarch64 => "arm64",
.x86_64 => "x86_64",
else => @panic("unsupported macOS arch"),
},
}),
}
// We need the xcframework // We need the xcframework
deps.xcframework.addStepDependencies(&build.step); deps.xcframework.addStepDependencies(&step.step);
// We also need all these resources because the xcode project // We also need all these resources because the xcode project
// references them via symlinks. // references them via symlinks.
deps.resources.addStepDependencies(&build.step); deps.resources.addStepDependencies(&step.step);
deps.i18n.addStepDependencies(&build.step); deps.i18n.addStepDependencies(&step.step);
deps.docs.installDummy(&build.step); deps.docs.installDummy(&step.step);
// Expect success // Expect success
build.expectExitCode(0); step.expectExitCode(0);
break :build build; break :build step;
};
const xctest = xctest: {
const env_map = try b.allocator.create(std.process.EnvMap);
env_map.* = .init(b.allocator);
if (env.get("PATH")) |v| try env_map.put("PATH", v);
const step = RunStep.create(b, "xcodebuild test");
step.has_side_effects = true;
step.cwd = b.path("macos");
step.env_map = env_map;
step.addArgs(&.{
"xcodebuild",
"test",
"-scheme",
"Ghostty",
});
if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch });
// We need the xcframework
deps.xcframework.addStepDependencies(&step.step);
// We also need all these resources because the xcode project
// references them via symlinks.
deps.resources.addStepDependencies(&step.step);
deps.i18n.addStepDependencies(&step.step);
deps.docs.installDummy(&step.step);
// Expect success
step.expectExitCode(0);
break :xctest step;
}; };
// Our step to open the resulting Ghostty app. // Our step to open the resulting Ghostty app.
@ -143,6 +179,7 @@ pub fn init(
.build = build, .build = build,
.open = open, .open = open,
.copy = copy, .copy = copy,
.xctest = xctest,
}; };
} }
@ -155,3 +192,10 @@ pub fn installXcframework(self: *const Ghostty) void {
const b = self.build.step.owner; const b = self.build.step.owner;
b.getInstallStep().dependOn(&self.build.step); b.getInstallStep().dependOn(&self.build.step);
} }
pub fn addTestStepDependencies(
self: *const Ghostty,
other_step: *std.Build.Step,
) void {
other_step.dependOn(&self.xctest.step);
}

View File

@ -139,7 +139,7 @@ pub fn add(
if (b.lazyDependency("harfbuzz", .{ if (b.lazyDependency("harfbuzz", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.@"enable-freetype" = true, .@"enable-freetype" = self.config.font_backend.hasFreetype(),
.@"enable-coretext" = self.config.font_backend.hasCoretext(), .@"enable-coretext" = self.config.font_backend.hasCoretext(),
})) |harfbuzz_dep| { })) |harfbuzz_dep| {
step.root_module.addImport( step.root_module.addImport(