const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); const buildpkg = @import("src/build/main.zig"); /// App version from build.zig.zon. const app_zon_version = @import("build.zig.zon").version; /// Libghostty version. We use a separate version from the app. const lib_version = "0.1.0"; /// Minimum required zig version. const minimum_zig_version = @import("build.zig.zon").minimum_zig_version; comptime { buildpkg.requireZig(minimum_zig_version); } pub fn build(b: *std.Build) !void { // This defines all the available build options (e.g. `-D`). If you // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. // If we have a VERSION file (present in source tarballs) then we // use that as the version source of truth. Otherwise we fall back // to what is in the build.zig.zon. const file_version: ?[]const u8 = if (b.build_root.handle.readFileAlloc( b.allocator, "VERSION", 128, )) |content| std.mem.trim( u8, content, &std.ascii.whitespace, ) else |_| null; const config = try buildpkg.Config.init( b, file_version orelse app_zon_version, ); const test_filters = b.option( [][]const u8, "test-filter", "Filter for test. Only applies to Zig tests.", ) orelse &[0][]const u8{}; // Ghostty dependencies used by many artifacts. const deps = try buildpkg.SharedDeps.init(b, &config); // The modules exported for Zig consumers of libghostty. If you're // writing a Zig program that uses libghostty, read this file. const mod = try buildpkg.GhosttyZig.init( b, &config, &deps, ); // 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 run_valgrind_step = b.step( "run-valgrind", "Run the app under valgrind", ); const test_step = b.step("test", "Run tests"); const test_lib_vt_step = b.step( "test-lib-vt", "Run libghostty-vt tests", ); const test_valgrind_step = b.step( "test-valgrind", "Run tests under valgrind", ); 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, &deps); const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null; // Ghostty executable, the actual runnable Ghostty program. const exe = try buildpkg.GhosttyExe.init(b, &config, &deps); // Ghostty docs const docs = try buildpkg.GhosttyDocs.init(b, &deps); if (config.emit_docs) { docs.install(); } else if (config.target.result.os.tag.isDarwin()) { // If we aren't emitting docs we need to emit a placeholder so // our macOS xcodeproject builds since it expects the `share/man` // directory to exist to copy into the app bundle. docs.installDummy(b.getInstallStep()); } // Ghostty webdata const webdata = try buildpkg.GhosttyWebdata.init(b, &deps); if (config.emit_webdata) webdata.install(); // Ghostty bench tools const bench = try buildpkg.GhosttyBench.init(b, &deps); if (config.emit_bench) bench.install(); // Ghostty dist tarball const dist = try buildpkg.GhosttyDist.init(b, &config); { const step = b.step("dist", "Build the dist tarball"); step.dependOn(dist.install_step); const check_step = b.step("distcheck", "Install and validate the dist tarball"); check_step.dependOn(dist.check_step); check_step.dependOn(dist.install_step); } // libghostty-vt const libghostty_vt_shared = shared: { if (config.target.result.cpu.arch.isWasm()) { break :shared try buildpkg.GhosttyLibVt.initWasm( b, &mod, ); } break :shared try buildpkg.GhosttyLibVt.initShared( b, &mod, ); }; libghostty_vt_shared.install(b.getInstallStep()); // libghostty-vt static lib const libghostty_vt_static = try buildpkg.GhosttyLibVt.initStatic( b, &mod, ); if (config.is_dep) { // If we're a dependency, we need to install everything as-is // so that dep.artifact("ghostty-vt-static") works. libghostty_vt_static.install(b.getInstallStep()); } else { // If we're not a dependency, we rename the static lib to // be idiomatic. On Windows, we use a distinct name to avoid // colliding with the DLL import library (ghostty-vt.lib). const static_lib_name = if (config.target.result.os.tag == .windows) "ghostty-vt-static.lib" else "libghostty-vt.a"; b.getInstallStep().dependOn(&b.addInstallLibFile( libghostty_vt_static.output, static_lib_name, ).step); } // Helpgen if (config.emit_helpgen) deps.help_strings.install(); // Runtime "none" is libghostty, anything else is an executable. if (config.app_runtime != .none) { if (config.emit_exe) { exe.install(); resources.install(); if (i18n) |v| v.install(); } } else if (!config.emit_lib_vt) { // The macOS Ghostty Library // // This is NOT libghostty (even though its named that for historical // reasons). It is just the glue between Ghostty GUI on macOS and // the full Ghostty GUI core. const lib_shared = try buildpkg.GhosttyLib.initShared(b, &deps); const lib_static = try buildpkg.GhosttyLib.initStatic(b, &deps); // We shouldn't have this guard but we don't currently // build on macOS this way ironically so we need to fix that. if (!config.target.result.os.tag.isDarwin()) { lib_shared.installHeader(); // Only need one header if (config.target.result.os.tag == .windows) { lib_shared.install("ghostty.dll"); lib_static.install("ghostty-static.lib"); } else { lib_shared.install("libghostty.so"); lib_static.install("libghostty.a"); } } } // macOS only artifacts. These will error if they're initialized for // other targets. if (config.target.result.os.tag.isDarwin() and (config.emit_xcframework or config.emit_macos_app)) { // Ghostty xcframework const xcframework = try buildpkg.GhosttyXCFramework.init( b, &deps, config.xcframework_target, ); if (config.emit_xcframework) { xcframework.install(); // The xcframework build always installs resources because our // macOS xcode project contains references to them. resources.install(); if (i18n) |v| v.install(); } // Ghostty macOS app const macos_app = try buildpkg.GhosttyXcodebuild.init( b, &config, .{ .xcframework = &xcframework, .docs = &docs, .i18n = if (i18n) |v| &v else null, .resources = &resources, }, ); if (config.emit_macos_app) { macos_app.install(); } } // Run step run: { if (config.app_runtime != .none) { const run_cmd = b.addRunArtifact(exe.exe); if (b.args) |args| run_cmd.addArgs(args); // Set the proper resources dir so things like shell integration // work correctly. If we're running `zig build run` in Ghostty, // this also ensures it overwrites the release one with our debug // build. run_cmd.setEnvironmentVariable( "GHOSTTY_RESOURCES_DIR", b.getInstallPath(.prefix, "share/ghostty"), ); run_step.dependOn(&run_cmd.step); break :run; } assert(config.app_runtime == .none); // On macOS we can run the macOS app. For "run" we always force // a native-only build so that we can run as quickly as possible. if (config.target.result.os.tag.isDarwin() and (config.emit_xcframework or config.emit_macos_app)) { const xcframework_native = try buildpkg.GhosttyXCFramework.init( b, &deps, .native, ); const macos_app_native_only = try buildpkg.GhosttyXcodebuild.init( b, &config, .{ .xcframework = &xcframework_native, .docs = &docs, .i18n = if (i18n) |v| &v else null, .resources = &resources, }, ); // 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_filters.len == 0) { macos_app_native_only.addTestStepDependencies(test_step); } } } // Valgrind if (config.app_runtime != .none) { // We need to rebuild Ghostty with a baseline CPU target. const valgrind_exe = exe: { var valgrind_config = config; valgrind_config.target = valgrind_config.baselineTarget(); break :exe try buildpkg.GhosttyExe.init( b, &valgrind_config, &deps, ); }; const run_cmd = b.addSystemCommand(&.{ "valgrind", "--leak-check=full", "--num-callers=50", b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}), "--gen-suppressions=all", }); run_cmd.addArtifactArg(valgrind_exe.exe); if (b.args) |args| run_cmd.addArgs(args); run_valgrind_step.dependOn(&run_cmd.step); } // Zig module tests { const mod_vt_test = b.addTest(.{ .root_module = mod.vt, .filters = test_filters, }); const mod_vt_test_run = b.addRunArtifact(mod_vt_test); test_lib_vt_step.dependOn(&mod_vt_test_run.step); const mod_vt_c_test = b.addTest(.{ .root_module = mod.vt_c, .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 { // Full unit tests const test_exe = b.addTest(.{ .name = "ghostty-test", .filters = test_filters, .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = config.baselineTarget(), .optimize = .Debug, .strip = false, .omit_frame_pointer = false, .unwind_tables = .sync, }), // Crash on x86_64 without this .use_llvm = true, }); if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe); // Verify our internal libghostty header. const ghostty_h = b.addTranslateC(.{ .root_source_file = b.path("include/ghostty.h"), .target = config.baselineTarget(), .optimize = .Debug, }); test_exe.root_module.addImport("ghostty.h", ghostty_h.createModule()); // Normal test running const test_run = b.addRunArtifact(test_exe); test_step.dependOn(&test_run.step); // Normal tests always test our libghostty modules //test_step.dependOn(test_lib_vt_step); // Valgrind test running const valgrind_run = b.addSystemCommand(&.{ "valgrind", "--leak-check=full", "--num-callers=50", b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}), "--gen-suppressions=all", }); valgrind_run.addArtifactArg(test_exe); test_valgrind_step.dependOn(&valgrind_run.step); } // update-translations does what it sounds like and updates the "pot" // files. These should be committed to the repo. if (i18n) |v| { translations_step.dependOn(v.update_step); } else { try translations_step.addError("cannot update translations when i18n is disabled", .{}); } }