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 {
// This defines all the available build options (e.g. `-D`).
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.
const resources = try buildpkg.GhosttyResources.init(b, &config);
@ -131,7 +146,6 @@ pub fn build(b: *std.Build) !void {
b.getInstallPath(.prefix, "share/ghostty"),
);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
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);
// If we have no test filters, install the tests too
if (test_filter == null) {
macos_app_native_only.addTestStepDependencies(test_step);
}
}
}
// 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(.{
.name = "ghostty-test",
.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);
_ = try deps.add(test_exe);
const test_run = b.addRunArtifact(test_exe);
test_step.dependOn(&test_run.step);
}
if (config.emit_test_exe) b.installArtifact(test_exe);
_ = try deps.add(test_exe);
const test_run = b.addRunArtifact(test_exe);
test_step.dependOn(&test_run.step);
}
// update-translations does what it sounds like and updates the "pot"
// files. These should be committed to the repo.
{
const step = b.step("update-translations", "Update translation files");
step.dependOn(i18n.update_step);
}
translations_step.dependOn(i18n.update_step);
}

View File

@ -303,7 +303,7 @@
/* End PBXFileReference 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 */
/* Begin PBXFrameworksBuildPhase section */
@ -612,7 +612,7 @@
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */,
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */,
A54CD6ED299BEB14008C95BB /* Sources */,
A54F45F42E1F047A0046BD5C /* GhosttyTests */,
A54F45F42E1F047A0046BD5C /* Tests */,
A5D495A3299BECBA00DD1313 /* Frameworks */,
A5A1F8862A489D7400D1E8BC /* Resources */,
A5B30532299BEAAA0047F10C /* Products */,
@ -712,7 +712,7 @@
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
A54F45F42E1F047A0046BD5C /* GhosttyTests */,
A54F45F42E1F047A0046BD5C /* Tests */,
);
name = GhosttyTests;
packageProductDependencies = (

View File

@ -15,15 +15,23 @@ pub fn build(b: *std.Build) !void {
});
const macos = b.dependency("macos", .{ .target = target, .optimize = optimize });
const module = b.addModule("harfbuzz", .{
.root_source_file = b.path("main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "freetype", .module = freetype.module("freetype") },
.{ .name = "macos", .module = macos.module("macos") },
},
});
const module = harfbuzz: {
const module = b.addModule("harfbuzz", .{
.root_source_file = b.path("main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .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
// mode first. Mode first will search all paths for a dynamic library

View File

@ -1,7 +1,8 @@
const builtin = @import("builtin");
const build_options = @import("build_options");
pub const c = @cImport({
@cInclude("hb.h");
@cInclude("hb-ft.h");
if (builtin.os.tag == .macos) @cInclude("hb-coretext.h");
if (build_options.freetype) @cInclude("hb-ft.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,
open: *std.Build.Step.Run,
copy: *std.Build.Step.Run,
xctest: *std.Build.Step.Run,
pub const Deps = struct {
xcframework: *const XCFramework,
@ -33,6 +34,21 @@ pub fn init(
=> "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});
// Our step to build the Ghostty macOS app.
@ -41,12 +57,13 @@ pub fn init(
// we create a new empty environment.
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 build = RunStep.create(b, "xcodebuild");
build.has_side_effects = true;
build.cwd = b.path("macos");
build.env_map = env_map;
build.addArgs(&.{
const step = RunStep.create(b, "xcodebuild");
step.has_side_effects = true;
step.cwd = b.path("macos");
step.env_map = env_map;
step.addArgs(&.{
"xcodebuild",
"-target",
"Ghostty",
@ -54,36 +71,55 @@ pub fn init(
xc_config,
});
switch (deps.xcframework.target) {
// Universal is our default target, so we don't have to
// add anything.
.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"),
},
}),
}
// If we have a specific architecture, we need to pass it
// to xcodebuild.
if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch });
// We need the xcframework
deps.xcframework.addStepDependencies(&build.step);
deps.xcframework.addStepDependencies(&step.step);
// We also need all these resources because the xcode project
// references them via symlinks.
deps.resources.addStepDependencies(&build.step);
deps.i18n.addStepDependencies(&build.step);
deps.docs.installDummy(&build.step);
deps.resources.addStepDependencies(&step.step);
deps.i18n.addStepDependencies(&step.step);
deps.docs.installDummy(&step.step);
// 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.
@ -143,6 +179,7 @@ pub fn init(
.build = build,
.open = open,
.copy = copy,
.xctest = xctest,
};
}
@ -155,3 +192,10 @@ pub fn installXcframework(self: *const Ghostty) void {
const b = self.build.step.owner;
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", .{
.target = target,
.optimize = optimize,
.@"enable-freetype" = true,
.@"enable-freetype" = self.config.font_backend.hasFreetype(),
.@"enable-coretext" = self.config.font_backend.hasCoretext(),
})) |harfbuzz_dep| {
step.root_module.addImport(