diff --git a/build.zig b/build.zig index 7f0bf84c5..1364745ce 100644 --- a/build.zig +++ b/build.zig @@ -1,30 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const fs = std.fs; -const CompileStep = std.Build.Step.Compile; -const RunStep = std.Build.Step.Run; -const ResolvedTarget = std.Build.ResolvedTarget; - -const apprt = @import("src/apprt.zig"); -const font = @import("src/font/main.zig"); -const renderer = @import("src/renderer.zig"); -const terminfo = @import("src/terminfo/main.zig"); -const config_vim = @import("src/config/vim.zig"); -const config_sublime_syntax = @import("src/config/sublime_syntax.zig"); -const fish_completions = @import("src/build/fish_completions.zig"); -const zsh_completions = @import("src/build/zsh_completions.zig"); -const bash_completions = @import("src/build/bash_completions.zig"); -const build_config = @import("src/build_config.zig"); -const BuildConfig = build_config.BuildConfig; -const WasmTarget = @import("src/os/wasm/target.zig").Target; -const LibtoolStep = @import("src/build/LibtoolStep.zig"); -const LipoStep = @import("src/build/LipoStep.zig"); -const MetallibStep = @import("src/build/MetallibStep.zig"); -const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig"); -const Version = @import("src/build/Version.zig"); -const Command = @import("src/Command.zig"); - -const Scanner = @import("zig_wayland").Scanner; +const buildpkg = @import("src/build/main.zig"); comptime { // This is the required Zig version for building this project. We allow @@ -44,898 +20,80 @@ comptime { } } -/// The version of the next release. -const app_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 2 }; - pub fn build(b: *std.Build) !void { - const optimize = b.standardOptimizeOption(.{}); - const target = target: { - var result = b.standardTargetOptions(.{}); + const config = try buildpkg.Config.init(b); - // If we have no minimum OS version, we set the default based on - // our tag. Not all tags have a minimum so this may be null. - if (result.query.os_version_min == null) { - result.query.os_version_min = osVersionMin(result.result.os.tag); - } + // Ghostty resources like terminfo, shell integration, themes, etc. + const resources = try buildpkg.GhosttyResources.init(b, &config); - break :target result; - }; + // Ghostty dependencies used by many artifacts. + const deps = try buildpkg.SharedDeps.init(b, &config); + const exe = try buildpkg.GhosttyExe.init(b, &config, &deps); + if (config.emit_helpgen) deps.help_strings.install(); - // This is set to true when we're building a system package. For now - // this is trivially detected using the "system_package_mode" bool - // but we may want to make this more sophisticated in the future. - const system_package: bool = b.graph.system_package_mode; - - const wasm_target: WasmTarget = .browser; - - // We use env vars throughout the build so we grab them immediately here. - var env = try std.process.getEnvMap(b.allocator); - defer env.deinit(); - - // Our build configuration. This is all on a struct so that we can easily - // modify it for specific build types (for example, wasm we strictly - // control our backends). - var config: BuildConfig = .{}; - - config.flatpak = b.option( - bool, - "flatpak", - "Build for Flatpak (integrates with Flatpak APIs). Only has an effect targeting Linux.", - ) orelse false; - - config.font_backend = b.option( - font.Backend, - "font-backend", - "The font backend to use for discovery and rasterization.", - ) orelse font.Backend.default(target.result, wasm_target); - - config.app_runtime = b.option( - apprt.Runtime, - "app-runtime", - "The app runtime to use. Not all values supported on all platforms.", - ) orelse apprt.Runtime.default(target.result); - - config.renderer = b.option( - renderer.Impl, - "renderer", - "The app runtime to use. Not all values supported on all platforms.", - ) orelse renderer.Impl.default(target.result, wasm_target); - - config.adwaita = b.option( - bool, - "gtk-adwaita", - "Enables the use of Adwaita when using the GTK rendering backend.", - ) orelse true; - - var x11 = false; - var wayland = false; - - if (target.result.os.tag == .linux) pkgconfig: { - var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator); - - pkgconfig.stdout_behavior = .Pipe; - pkgconfig.stderr_behavior = .Pipe; - - pkgconfig.spawn() catch |err| { - std.log.warn("failed to spawn pkg-config - disabling X11 and Wayland integrations: {}", .{err}); - break :pkgconfig; - }; - - const output_max_size = 50 * 1024; - - var stdout = std.ArrayList(u8).init(b.allocator); - var stderr = std.ArrayList(u8).init(b.allocator); - defer { - stdout.deinit(); - stderr.deinit(); - } - - try pkgconfig.collectOutput(&stdout, &stderr, output_max_size); - - const term = try pkgconfig.wait(); - - if (stderr.items.len > 0) { - std.log.warn("pkg-config had errors:\n{s}", .{stderr.items}); - } - - switch (term) { - .Exited => |code| { - if (code == 0) { - if (std.mem.indexOf(u8, stdout.items, "x11")) |_| x11 = true; - if (std.mem.indexOf(u8, stdout.items, "wayland")) |_| wayland = true; - } else { - std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); - } - }, - inline else => |code| { - std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); - return error.Unexpected; - }, - } + // Ghostty docs + if (config.emit_docs) { + const docs = try buildpkg.GhosttyDocs.init(b, &deps); + docs.install(); } - config.x11 = b.option( - bool, - "gtk-x11", - "Enables linking against X11 libraries when using the GTK rendering backend.", - ) orelse x11; + // Ghostty webdata + if (config.emit_webdata) { + const webdata = try buildpkg.GhosttyWebdata.init(b, &deps); + webdata.install(); + } - config.wayland = b.option( - bool, - "gtk-wayland", - "Enables linking against Wayland libraries when using the GTK rendering backend.", - ) orelse wayland; + // Ghostty bench tools + if (config.emit_bench) { + const bench = try buildpkg.GhosttyBench.init(b, &deps); + bench.install(); + } - config.sentry = b.option( - bool, - "sentry", - "Build with Sentry crash reporting. Default for macOS is true, false for any other system.", - ) orelse sentry: { - switch (target.result.os.tag) { - .macos, .ios => break :sentry true, + // If we're not building libghostty, then install the exe and resources. + if (config.app_runtime != .none) { + exe.install(); + resources.install(); + } - // Note its false for linux because the crash reports on Linux - // don't have much useful information. - else => break :sentry false, - } - }; + // Libghostty + // + // Note: libghostty is not stable for general purpose use. It is used + // heavily by Ghostty on macOS but it isn't built to be reusable yet. + // As such, these build steps are lacking. For example, the Darwin + // build only produces an xcframework. + if (config.app_runtime == .none) { + if (config.target.result.isDarwin()) darwin: { + if (!config.emit_xcframework) break :darwin; - const pie = b.option( - bool, - "pie", - "Build a Position Independent Executable. Default true for system packages.", - ) orelse system_package; + // Build the xcframework + const xcframework = try buildpkg.GhosttyXCFramework.init(b, &deps); + xcframework.install(); - const strip = b.option( - bool, - "strip", - "Strip the final executable. Default true for fast and small releases", - ) orelse switch (optimize) { - .Debug => false, - .ReleaseSafe => false, - .ReleaseFast, .ReleaseSmall => true, - }; + // The xcframework build always installs resources because our + // macOS xcode project contains references to them. + resources.install(); - const conformance = b.option( - []const u8, - "conformance", - "Name of the conformance app to run with 'run' option.", - ); - - const emit_test_exe = b.option( - bool, - "emit-test-exe", - "Build and install test executables with 'build'", - ) orelse false; - - const emit_bench = b.option( - bool, - "emit-bench", - "Build and install the benchmark executables.", - ) orelse false; - - const emit_helpgen = b.option( - bool, - "emit-helpgen", - "Build and install the helpgen executable.", - ) orelse false; - - const emit_docs = b.option( - bool, - "emit-docs", - "Build and install auto-generated documentation (requires pandoc)", - ) orelse emit_docs: { - // If we are emitting any other artifacts then we default to false. - if (emit_bench or emit_test_exe or emit_helpgen) break :emit_docs false; - - // We always emit docs in system package mode. - if (system_package) break :emit_docs true; - - // We only default to true if we can find pandoc. - const path = Command.expandPath(b.allocator, "pandoc") catch - break :emit_docs false; - defer if (path) |p| b.allocator.free(p); - break :emit_docs path != null; - }; - - const emit_webdata = b.option( - bool, - "emit-webdata", - "Build the website data for the website.", - ) orelse false; - - const emit_xcframework = b.option( - bool, - "emit-xcframework", - "Build and install the xcframework for the macOS library.", - ) orelse builtin.target.isDarwin() and - target.result.os.tag == .macos and - config.app_runtime == .none and - (!emit_bench and !emit_test_exe and !emit_helpgen); - - // On NixOS, the built binary from `zig build` needs to patch the rpath - // into the built binary for it to be portable across the NixOS system - // it was built for. We default this to true if we can detect we're in - // a Nix shell and have LD_LIBRARY_PATH set. - const patch_rpath: ?[]const u8 = b.option( - []const u8, - "patch-rpath", - "Inject the LD_LIBRARY_PATH as the rpath in the built binary. " ++ - "This defaults to LD_LIBRARY_PATH if we're in a Nix shell environment on NixOS.", - ) orelse patch_rpath: { - // We only do the patching if we're targeting our own CPU and its Linux. - if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_rpath null; - - // If we're in a nix shell we default to doing this. - // Note: we purposely never deinit envmap because we leak the strings - if (env.get("IN_NIX_SHELL") == null) break :patch_rpath null; - break :patch_rpath env.get("LD_LIBRARY_PATH"); - }; - - const version_string = b.option( - []const u8, - "version-string", - "A specific version string to use for the build. " ++ - "If not specified, git will be used. This must be a semantic version.", - ); - - config.version = if (version_string) |v| - try std.SemanticVersion.parse(v) - else version: { - const vsn = Version.detect(b) catch |err| switch (err) { - // If Git isn't available we just make an unknown dev version. - error.GitNotFound, - error.GitNotRepository, - => break :version .{ - .major = app_version.major, - .minor = app_version.minor, - .patch = app_version.patch, - .pre = "dev", - .build = "0000000", - }, - - else => return err, - }; - if (vsn.tag) |tag| { - // Tip releases behave just like any other pre-release so we skip. - if (!std.mem.eql(u8, tag, "tip")) { - const expected = b.fmt("v{d}.{d}.{d}", .{ - app_version.major, - app_version.minor, - app_version.patch, - }); - - if (!std.mem.eql(u8, tag, expected)) { - @panic("tagged releases must be in vX.Y.Z format matching build.zig"); - } - - break :version .{ - .major = app_version.major, - .minor = app_version.minor, - .patch = app_version.patch, - }; + // If we aren't emitting docs we need to emit a placeholder so + // our macOS xcodeproject builds. + if (!config.emit_docs) { + var wf = b.addWriteFiles(); + const path = "share/man/.placeholder"; + const placeholder = wf.add(path, "emit-docs not true so no man pages"); + b.getInstallStep().dependOn(&b.addInstallFile(placeholder, path).step); } + } else { + const libghostty_shared = try buildpkg.GhosttyLib.initShared(b, &deps); + const libghostty_static = try buildpkg.GhosttyLib.initStatic(b, &deps); + libghostty_shared.installHeader(); // Only need one header + libghostty_shared.install("libghostty.so"); + libghostty_static.install("libghostty.a"); } + } - break :version .{ - .major = app_version.major, - .minor = app_version.minor, - .patch = app_version.patch, - .pre = vsn.branch, - .build = vsn.short_hash, - }; - }; - - // These are all our dependencies that can be used with system - // packages if they exist. We set them up here so that we can set - // their defaults early. The first call configures the integration and - // subsequent calls just return the configured value. + // Run runs the Ghostty exe { - // These dependencies we want to default false if we're on macOS. - // On macOS we don't want to use system libraries because we - // generally want a fat binary. This can be overridden with the - // `-fsys` flag. - for (&[_][]const u8{ - "freetype", - "harfbuzz", - "fontconfig", - "libpng", - "zlib", - "oniguruma", - }) |dep| { - _ = b.systemIntegrationOption( - dep, - .{ - // If we're not on darwin we want to use whatever the - // default is via the system package mode - .default = if (target.result.isDarwin()) false else null, - }, - ); - } - - // These default to false because they're rarely available as - // system packages so we usually want to statically link them. - for (&[_][]const u8{ - "glslang", - "spirv-cross", - "simdutf", - }) |dep| { - _ = b.systemIntegrationOption(dep, .{ .default = false }); - } - } - - // We can use wasmtime to test wasm - b.enable_wasmtime = true; - - // Help exe. This must be run before any dependent executables because - // otherwise the build will be cached without emit. That's clunky but meh. - if (emit_helpgen) try addHelp(b, null, config); - - // Add our benchmarks - try benchSteps(b, target, config, emit_bench); - - // We only build an exe if we have a runtime set. - const exe_: ?*std.Build.Step.Compile = if (config.app_runtime != .none) b.addExecutable(.{ - .name = "ghostty", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - .strip = strip, - }) else null; - - // Exe - if (exe_) |exe| { - // Set PIE if requested - if (pie) exe.pie = true; - - // Add the shared dependencies - _ = try addDeps(b, exe, config); - - // If we're in NixOS but not in the shell environment then we issue - // a warning because the rpath may not be setup properly. - const is_nixos = is_nixos: { - if (target.result.os.tag != .linux) break :is_nixos false; - if (!target.query.isNativeCpu()) break :is_nixos false; - if (!target.query.isNativeOs()) break :is_nixos false; - break :is_nixos if (std.fs.accessAbsolute("/etc/NIXOS", .{})) true else |_| false; - }; - if (is_nixos and env.get("IN_NIX_SHELL") == null) { - try exe.step.addError( - "\x1b[" ++ color_map.get("yellow").? ++ - "\x1b[" ++ color_map.get("d").? ++ - \\Detected building on and for NixOS outside of the Nix shell environment. - \\ - \\The resulting ghostty binary will likely fail on launch because it is - \\unable to dynamically load the windowing libs (X11, Wayland, etc.). - \\We highly recommend running only within the Nix build environment - \\and the resulting binary will be portable across your system. - \\ - \\To run in the Nix build environment, use the following command. - \\Append any additional options like (`-Doptimize` flags). The resulting - \\binary will be in zig-out as usual. - \\ - \\ nix develop -c zig build - \\ - ++ - "\x1b[0m", - .{}, - ); - } - - if (target.result.os.tag == .windows) { - exe.subsystem = .Windows; - exe.addWin32ResourceFile(.{ - .file = b.path("dist/windows/ghostty.rc"), - }); - } - - // If we're installing, we get the install step so we can add - // additional dependencies to it. - const install_step = if (config.app_runtime != .none) step: { - const step = b.addInstallArtifact(exe, .{}); - b.getInstallStep().dependOn(&step.step); - break :step step; - } else null; - - // Patch our rpath if that option is specified. - if (patch_rpath) |rpath| { - if (rpath.len > 0) { - const run = RunStep.create(b, "patchelf rpath"); - run.addArgs(&.{ "patchelf", "--set-rpath", rpath }); - run.addArtifactArg(exe); - - if (install_step) |step| { - step.step.dependOn(&run.step); - } - } - } - - // App (Mac) - if (target.result.os.tag == .macos) { - const bin_install = b.addInstallFile( - exe.getEmittedBin(), - "Ghostty.app/Contents/MacOS/ghostty", - ); - b.getInstallStep().dependOn(&bin_install.step); - b.installFile("dist/macos/Info.plist", "Ghostty.app/Contents/Info.plist"); - b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns"); - } - } - - // Shell-integration - { - const install = b.addInstallDirectory(.{ - .source_dir = b.path("src/shell-integration"), - .install_dir = .{ .custom = "share" }, - .install_subdir = b.pathJoin(&.{ "ghostty", "shell-integration" }), - .exclude_extensions = &.{".md"}, - }); - b.getInstallStep().dependOn(&install.step); - - if (target.result.os.tag == .macos and exe_ != null) { - const mac_install = b.addInstallDirectory(options: { - var copy = install.options; - copy.install_dir = .{ - .custom = "Ghostty.app/Contents/Resources", - }; - break :options copy; - }); - b.getInstallStep().dependOn(&mac_install.step); - } - } - - // Themes - { - const upstream = b.dependency("iterm2_themes", .{}); - const install = b.addInstallDirectory(.{ - .source_dir = upstream.path("ghostty"), - .install_dir = .{ .custom = "share" }, - .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), - .exclude_extensions = &.{".md"}, - }); - b.getInstallStep().dependOn(&install.step); - - if (target.result.os.tag == .macos and exe_ != null) { - const mac_install = b.addInstallDirectory(options: { - var copy = install.options; - copy.install_dir = .{ - .custom = "Ghostty.app/Contents/Resources", - }; - break :options copy; - }); - b.getInstallStep().dependOn(&mac_install.step); - } - } - - // Terminfo - { - // Encode our terminfo - var str = std.ArrayList(u8).init(b.allocator); - defer str.deinit(); - try terminfo.ghostty.encode(str.writer()); - - // Write it - var wf = b.addWriteFiles(); - const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items); - const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo"); - b.getInstallStep().dependOn(&src_install.step); - if (target.result.os.tag == .macos and exe_ != null) { - const mac_src_install = b.addInstallFile( - src_source, - "Ghostty.app/Contents/Resources/terminfo/ghostty.terminfo", - ); - b.getInstallStep().dependOn(&mac_src_install.step); - } - - // Convert to termcap source format if thats helpful to people and - // install it. The resulting value here is the termcap source in case - // that is used for other commands. - if (target.result.os.tag != .windows) { - const run_step = RunStep.create(b, "infotocap"); - run_step.addArg("infotocap"); - run_step.addFileArg(src_source); - const out_source = run_step.captureStdOut(); - _ = run_step.captureStdErr(); // so we don't see stderr - - const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap"); - b.getInstallStep().dependOn(&cap_install.step); - - if (target.result.os.tag == .macos and exe_ != null) { - const mac_cap_install = b.addInstallFile( - out_source, - "Ghostty.app/Contents/Resources/terminfo/ghostty.termcap", - ); - b.getInstallStep().dependOn(&mac_cap_install.step); - } - } - - // Compile the terminfo source into a terminfo database - if (target.result.os.tag != .windows) { - const run_step = RunStep.create(b, "tic"); - run_step.addArgs(&.{ "tic", "-x", "-o" }); - const path = run_step.addOutputFileArg("terminfo"); - run_step.addFileArg(src_source); - _ = run_step.captureStdErr(); // so we don't see stderr - - // Depend on the terminfo source install step so that Zig build - // creates the "share" directory for us. - run_step.step.dependOn(&src_install.step); - - { - // Use cp -R instead of Step.InstallDir because we need to preserve - // symlinks in the terminfo database. Zig's InstallDir step doesn't - // handle symlinks correctly yet. - const copy_step = RunStep.create(b, "copy terminfo db"); - copy_step.addArgs(&.{ "cp", "-R" }); - copy_step.addFileArg(path); - copy_step.addArg(b.fmt("{s}/share", .{b.install_path})); - b.getInstallStep().dependOn(©_step.step); - } - - if (target.result.os.tag == .macos and exe_ != null) { - // Use cp -R instead of Step.InstallDir because we need to preserve - // symlinks in the terminfo database. Zig's InstallDir step doesn't - // handle symlinks correctly yet. - const copy_step = RunStep.create(b, "copy terminfo db"); - copy_step.addArgs(&.{ "cp", "-R" }); - copy_step.addFileArg(path); - copy_step.addArg( - b.fmt("{s}/Ghostty.app/Contents/Resources", .{b.install_path}), - ); - b.getInstallStep().dependOn(©_step.step); - } - } - } - - // Fish shell completions - { - const wf = b.addWriteFiles(); - _ = wf.add("ghostty.fish", fish_completions.fish_completions); - - b.installDirectory(.{ - .source_dir = wf.getDirectory(), - .install_dir = .prefix, - .install_subdir = "share/fish/vendor_completions.d", - }); - } - - // zsh shell completions - { - const wf = b.addWriteFiles(); - _ = wf.add("_ghostty", zsh_completions.zsh_completions); - - b.installDirectory(.{ - .source_dir = wf.getDirectory(), - .install_dir = .prefix, - .install_subdir = "share/zsh/site-functions", - }); - } - - // bash shell completions - { - const wf = b.addWriteFiles(); - _ = wf.add("ghostty.bash", bash_completions.bash_completions); - - b.installDirectory(.{ - .source_dir = wf.getDirectory(), - .install_dir = .prefix, - .install_subdir = "share/bash-completion/completions", - }); - } - - // Vim plugin - { - const wf = b.addWriteFiles(); - _ = wf.add("syntax/ghostty.vim", config_vim.syntax); - _ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect); - _ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin); - _ = wf.add("compiler/ghostty.vim", config_vim.compiler); - b.installDirectory(.{ - .source_dir = wf.getDirectory(), - .install_dir = .prefix, - .install_subdir = "share/vim/vimfiles", - }); - } - - // Neovim plugin - // This is just a copy-paste of the Vim plugin, but using a Neovim subdir. - // By default, Neovim doesn't look inside share/vim/vimfiles. Some distros - // configure it to do that however. Fedora, does not as a counterexample. - { - const wf = b.addWriteFiles(); - _ = wf.add("syntax/ghostty.vim", config_vim.syntax); - _ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect); - _ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin); - _ = wf.add("compiler/ghostty.vim", config_vim.compiler); - b.installDirectory(.{ - .source_dir = wf.getDirectory(), - .install_dir = .prefix, - .install_subdir = "share/nvim/site", - }); - } - - // Sublime syntax highlighting for bat cli tool - // NOTE: The current implementation requires symlinking the generated - // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' - // directory. The syntax then needs to be mapped to the correct language in - // the config file within the '~.config/bat' directory - // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). - { - const wf = b.addWriteFiles(); - _ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax); - b.installDirectory(.{ - .source_dir = wf.getDirectory(), - .install_dir = .prefix, - .install_subdir = "share/bat/syntaxes", - }); - } - - // Documentation - if (emit_docs) { - try buildDocumentation(b, config); - } else { - // We need to create the zig-out/share/man directory so that - // macOS builds continue to work even if emit-docs doesn't - // work. - var wf = b.addWriteFiles(); - const path = "share/man/.placeholder"; - const placeholder = wf.add(path, "emit-docs not true so no man pages"); - b.getInstallStep().dependOn(&b.addInstallFile(placeholder, path).step); - } - - // Web data - if (emit_webdata) { - try buildWebData(b, config); - } - - // App (Linux) - if (target.result.os.tag == .linux and config.app_runtime != .none) { - // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html - - // Desktop file so that we have an icon and other metadata - b.installFile("dist/linux/app.desktop", "share/applications/com.mitchellh.ghostty.desktop"); - - // Right click menu action for Plasma desktop - b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/com.mitchellh.ghostty.desktop"); - - // Various icons that our application can use, including the icon - // that will be used for the desktop. - b.installFile("images/icons/icon_16.png", "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_32.png", "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_128.png", "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_256.png", "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_512.png", "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png"); - - // Flatpaks only support icons up to 512x512. - if (!config.flatpak) { - b.installFile("images/icons/icon_1024.png", "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png"); - } - - b.installFile("images/icons/icon_16@2x.png", "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_32@2x.png", "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_128@2x.png", "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png"); - b.installFile("images/icons/icon_256@2x.png", "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png"); - } - - // libghostty (non-Darwin) - if (!builtin.target.isDarwin() and config.app_runtime == .none) { - // Shared - { - const lib = b.addSharedLibrary(.{ - .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .optimize = optimize, - .target = target, - .strip = strip, - }); - _ = try addDeps(b, lib, config); - - const lib_install = b.addInstallLibFile( - lib.getEmittedBin(), - "libghostty.so", - ); - b.getInstallStep().dependOn(&lib_install.step); - } - - // Static - { - const lib = b.addStaticLibrary(.{ - .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .optimize = optimize, - .target = target, - .strip = strip, - }); - _ = try addDeps(b, lib, config); - - const lib_install = b.addInstallLibFile( - lib.getEmittedBin(), - "libghostty.a", - ); - b.getInstallStep().dependOn(&lib_install.step); - } - - // Copy our ghostty.h to include. - const header_install = b.addInstallHeaderFile( - b.path("include/ghostty.h"), - "ghostty.h", - ); - b.getInstallStep().dependOn(&header_install.step); - } - - // On Mac we can build the embedding library. This only handles the macOS lib. - if (emit_xcframework) { - // Create the universal macOS lib. - const macos_lib_step, const macos_lib_path = try createMacOSLib( - b, - optimize, - config, - ); - - // Add our library to zig-out - const lib_install = b.addInstallLibFile( - macos_lib_path, - "libghostty-macos.a", - ); - b.getInstallStep().dependOn(&lib_install.step); - - // Create the universal iOS lib. - const ios_lib_step, const ios_lib_path = try createIOSLib( - b, - null, - optimize, - config, - ); - - // Add our library to zig-out - const ios_lib_install = b.addInstallLibFile( - ios_lib_path, - "libghostty-ios.a", - ); - b.getInstallStep().dependOn(&ios_lib_install.step); - - // Create the iOS simulator lib. - const ios_sim_lib_step, const ios_sim_lib_path = try createIOSLib( - b, - .simulator, - optimize, - config, - ); - - // Add our library to zig-out - const ios_sim_lib_install = b.addInstallLibFile( - ios_sim_lib_path, - "libghostty-ios-simulator.a", - ); - b.getInstallStep().dependOn(&ios_sim_lib_install.step); - - // Copy our ghostty.h to include. The header file is shared by - // all embedded targets. - const header_install = b.addInstallHeaderFile( - b.path("include/ghostty.h"), - "ghostty.h", - ); - b.getInstallStep().dependOn(&header_install.step); - - // The xcframework wraps our ghostty library so that we can link - // it to the final app built with Swift. - const xcframework = XCFrameworkStep.create(b, .{ - .name = "GhosttyKit", - .out_path = "macos/GhosttyKit.xcframework", - .libraries = &.{ - .{ - .library = macos_lib_path, - .headers = b.path("include"), - }, - .{ - .library = ios_lib_path, - .headers = b.path("include"), - }, - .{ - .library = ios_sim_lib_path, - .headers = b.path("include"), - }, - }, - }); - xcframework.step.dependOn(ios_lib_step); - xcframework.step.dependOn(ios_sim_lib_step); - xcframework.step.dependOn(macos_lib_step); - xcframework.step.dependOn(&header_install.step); - b.default_step.dependOn(xcframework.step); - } - - // wasm - { - // Build our Wasm target. - const wasm_crosstarget: std.Target.Query = .{ - .cpu_arch = .wasm32, - .os_tag = .freestanding, - .cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp }, - .cpu_features_add = std.Target.wasm.featureSet(&.{ - // We use this to explicitly request shared memory. - .atomics, - - // Not explicitly used but compiler could use them if they want. - .bulk_memory, - .reference_types, - .sign_ext, - }), - }; - - // Whether we're using wasm shared memory. Some behaviors change. - // For now we require this but I wanted to make the code handle both - // up front. - const wasm_shared: bool = true; - - // Modify our build configuration for wasm builds. - const wasm_config: BuildConfig = config: { - var copy = config; - - // Backends that are fixed for wasm - copy.font_backend = .web_canvas; - - // Wasm-specific options - copy.wasm_shared = wasm_shared; - copy.wasm_target = wasm_target; - - break :config copy; - }; - - const wasm = b.addSharedLibrary(.{ - .name = "ghostty-wasm", - .root_source_file = b.path("src/main_wasm.zig"), - .target = b.resolveTargetQuery(wasm_crosstarget), - .optimize = optimize, - }); - - // So that we can use web workers with our wasm binary - wasm.import_memory = true; - wasm.initial_memory = 65536 * 25; - wasm.max_memory = 65536 * 65536; // Maximum number of pages in wasm32 - wasm.shared_memory = wasm_shared; - - // Stack protector adds extern requirements that we don't satisfy. - wasm.root_module.stack_protector = false; - - // Wasm-specific deps - _ = try addDeps(b, wasm, wasm_config); - - // Install - const wasm_install = b.addInstallArtifact(wasm, .{}); - wasm_install.dest_dir = .{ .prefix = {} }; - - const step = b.step("wasm", "Build the wasm library"); - step.dependOn(&wasm_install.step); - - // We support tests via wasmtime. wasmtime uses WASI so this - // isn't an exact match to our freestanding target above but - // it lets us test some basic functionality. - const test_step = b.step("test-wasm", "Run all tests for wasm"); - const main_test = b.addTest(.{ - .name = "wasm-test", - .root_source_file = b.path("src/main_wasm.zig"), - .target = b.resolveTargetQuery(wasm_crosstarget), - }); - - _ = try addDeps(b, main_test, wasm_config); - test_step.dependOn(&main_test.step); - } - - // Run - run: { - // Build our run step, which runs the main app by default, but will - // run a conformance app if `-Dconformance` is set. - const run_exe = if (conformance) |name| blk: { - var conformance_exes = try conformanceSteps(b, target, optimize); - defer conformance_exes.deinit(); - break :blk conformance_exes.get(name) orelse return error.InvalidConformance; - } else exe_ orelse break :run; - - const run_cmd = b.addRunArtifact(run_exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } - + const run_cmd = b.addRunArtifact(exe.exe); + if (b.args) |args| run_cmd.addArgs(args); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); } @@ -945,943 +103,18 @@ pub fn build(b: *std.Build) !void { const test_step = b.step("test", "Run all tests"); const test_filter = b.option([]const u8, "test-filter", "Filter for test"); - // Force all Mac builds to use a `generic` CPU. This avoids - // potential issues with `highway` compile errors due to missing - // `arm_neon` features (see for example https://github.com/mitchellh/ghostty/issues/1640). - const test_target = if (target.result.os.tag == .macos and builtin.target.isDarwin()) - genericMacOSTarget(b, null) - else - target; - - const main_test = b.addTest(.{ + const test_exe = b.addTest(.{ .name = "ghostty-test", .root_source_file = b.path("src/main.zig"), - .target = test_target, + .target = config.target, .filter = test_filter, }); { - if (emit_test_exe) b.installArtifact(main_test); - _ = try addDeps(b, main_test, config); - const test_run = b.addRunArtifact(main_test); + 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); } } } - -/// Returns the minimum OS version for the given OS tag. This shouldn't -/// be used generally, it should only be used for Darwin-based OS currently. -fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion { - return switch (tag) { - // We support back to the earliest officially supported version - // of macOS by Apple. EOL versions are not supported. - .macos => .{ .semver = .{ - .major = 13, - .minor = 0, - .patch = 0, - } }, - - // iOS 17 picked arbitrarily - .ios => .{ .semver = .{ - .major = 17, - .minor = 0, - .patch = 0, - } }, - - // This should never happen currently. If we add a new target then - // we should add a new case here. - else => null, - }; -} - -// Returns a ResolvedTarget for a mac with a `target.result.cpu.model.name` of `generic`. -// `b.standardTargetOptions()` returns a more specific cpu like `apple_a15`. -fn genericMacOSTarget(b: *std.Build, arch: ?std.Target.Cpu.Arch) ResolvedTarget { - return b.resolveTargetQuery(.{ - .cpu_arch = arch orelse builtin.target.cpu.arch, - .os_tag = .macos, - .os_version_min = osVersionMin(.macos), - }); -} - -/// Creates a universal macOS libghostty library and returns the path -/// to the final library. -/// -/// The library is always a fat static library currently because this is -/// expected to be used directly with Xcode and Swift. In the future, we -/// probably want to change this because it makes it harder to use the -/// library in other contexts. -fn createMacOSLib( - b: *std.Build, - optimize: std.builtin.OptimizeMode, - config: BuildConfig, -) !struct { *std.Build.Step, std.Build.LazyPath } { - const static_lib_aarch64 = lib: { - const lib = b.addStaticLibrary(.{ - .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .target = genericMacOSTarget(b, .aarch64), - .optimize = optimize, - }); - lib.bundle_compiler_rt = true; - lib.linkLibC(); - - // Create a single static lib with all our dependencies merged - var lib_list = try addDeps(b, lib, config); - try lib_list.append(lib.getEmittedBin()); - const libtool = LibtoolStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty-aarch64-fat.a", - .sources = lib_list.items, - }); - libtool.step.dependOn(&lib.step); - b.default_step.dependOn(libtool.step); - - break :lib libtool; - }; - - const static_lib_x86_64 = lib: { - const lib = b.addStaticLibrary(.{ - .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .target = genericMacOSTarget(b, .x86_64), - .optimize = optimize, - }); - lib.bundle_compiler_rt = true; - lib.linkLibC(); - - // Create a single static lib with all our dependencies merged - var lib_list = try addDeps(b, lib, config); - try lib_list.append(lib.getEmittedBin()); - const libtool = LibtoolStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty-x86_64-fat.a", - .sources = lib_list.items, - }); - libtool.step.dependOn(&lib.step); - b.default_step.dependOn(libtool.step); - - break :lib libtool; - }; - - const static_lib_universal = LipoStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty.a", - .input_a = static_lib_aarch64.output, - .input_b = static_lib_x86_64.output, - }); - static_lib_universal.step.dependOn(static_lib_aarch64.step); - static_lib_universal.step.dependOn(static_lib_x86_64.step); - - return .{ - static_lib_universal.step, - static_lib_universal.output, - }; -} - -/// Create an Apple iOS/iPadOS build. -fn createIOSLib( - b: *std.Build, - abi: ?std.Target.Abi, - optimize: std.builtin.OptimizeMode, - config: BuildConfig, -) !struct { *std.Build.Step, std.Build.LazyPath } { - const lib = b.addStaticLibrary(.{ - .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .optimize = optimize, - .target = b.resolveTargetQuery(.{ - .cpu_arch = .aarch64, - .os_tag = .ios, - .os_version_min = osVersionMin(.ios), - .abi = abi, - }), - }); - lib.bundle_compiler_rt = true; - lib.linkLibC(); - - // Create a single static lib with all our dependencies merged - var lib_list = try addDeps(b, lib, config); - try lib_list.append(lib.getEmittedBin()); - const libtool = LibtoolStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty-ios-fat.a", - .sources = lib_list.items, - }); - libtool.step.dependOn(&lib.step); - - return .{ - libtool.step, - libtool.output, - }; -} - -/// Used to keep track of a list of file sources. -const LazyPathList = std.ArrayList(std.Build.LazyPath); - -/// Adds and links all of the primary dependencies for the exe. -fn addDeps( - b: *std.Build, - step: *std.Build.Step.Compile, - config: BuildConfig, -) !LazyPathList { - // All object targets get access to a standard build_options module - const exe_options = b.addOptions(); - try config.addOptions(exe_options); - step.root_module.addOptions("build_options", exe_options); - - // We maintain a list of our static libraries and return it so that - // we can build a single fat static library for the final app. - var static_libs = LazyPathList.init(b.allocator); - errdefer static_libs.deinit(); - - const target = step.root_module.resolved_target.?; - const optimize = step.root_module.optimize.?; - - // For dynamic linking, we prefer dynamic linking and to search by - // mode first. Mode first will search all paths for a dynamic library - // before falling back to static. - const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ - .preferred_link_mode = .dynamic, - .search_strategy = .mode_first, - }; - - // Freetype - _ = b.systemIntegrationOption("freetype", .{}); // Shows it in help - if (config.font_backend.hasFreetype()) { - const freetype_dep = b.dependency("freetype", .{ - .target = target, - .optimize = optimize, - .@"enable-libpng" = true, - }); - step.root_module.addImport("freetype", freetype_dep.module("freetype")); - - if (b.systemIntegrationOption("freetype", .{})) { - step.linkSystemLibrary2("bzip2", dynamic_link_opts); - step.linkSystemLibrary2("freetype2", dynamic_link_opts); - } else { - step.linkLibrary(freetype_dep.artifact("freetype")); - try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin()); - } - } - - // Harfbuzz - _ = b.systemIntegrationOption("harfbuzz", .{}); // Shows it in help - if (config.font_backend.hasHarfbuzz()) { - const harfbuzz_dep = b.dependency("harfbuzz", .{ - .target = target, - .optimize = optimize, - .@"enable-freetype" = true, - .@"enable-coretext" = config.font_backend.hasCoretext(), - }); - - step.root_module.addImport( - "harfbuzz", - harfbuzz_dep.module("harfbuzz"), - ); - if (b.systemIntegrationOption("harfbuzz", .{})) { - step.linkSystemLibrary2("harfbuzz", dynamic_link_opts); - } else { - step.linkLibrary(harfbuzz_dep.artifact("harfbuzz")); - try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin()); - } - } - - // Fontconfig - _ = b.systemIntegrationOption("fontconfig", .{}); // Shows it in help - if (config.font_backend.hasFontconfig()) { - const fontconfig_dep = b.dependency("fontconfig", .{ - .target = target, - .optimize = optimize, - }); - step.root_module.addImport( - "fontconfig", - fontconfig_dep.module("fontconfig"), - ); - - if (b.systemIntegrationOption("fontconfig", .{})) { - step.linkSystemLibrary2("fontconfig", dynamic_link_opts); - } else { - step.linkLibrary(fontconfig_dep.artifact("fontconfig")); - try static_libs.append(fontconfig_dep.artifact("fontconfig").getEmittedBin()); - } - } - - // Libpng - Ghostty doesn't actually use this directly, its only used - // through dependencies, so we only need to add it to our static - // libs list if we're not using system integration. The dependencies - // will handle linking it. - if (!b.systemIntegrationOption("libpng", .{})) { - const libpng_dep = b.dependency("libpng", .{ - .target = target, - .optimize = optimize, - }); - step.linkLibrary(libpng_dep.artifact("png")); - try static_libs.append(libpng_dep.artifact("png").getEmittedBin()); - } - - // Zlib - same as libpng, only used through dependencies. - if (!b.systemIntegrationOption("zlib", .{})) { - const zlib_dep = b.dependency("zlib", .{ - .target = target, - .optimize = optimize, - }); - step.linkLibrary(zlib_dep.artifact("z")); - try static_libs.append(zlib_dep.artifact("z").getEmittedBin()); - } - - // Oniguruma - const oniguruma_dep = b.dependency("oniguruma", .{ - .target = target, - .optimize = optimize, - }); - step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma")); - if (b.systemIntegrationOption("oniguruma", .{})) { - step.linkSystemLibrary2("oniguruma", dynamic_link_opts); - } else { - step.linkLibrary(oniguruma_dep.artifact("oniguruma")); - try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin()); - } - - // Glslang - const glslang_dep = b.dependency("glslang", .{ - .target = target, - .optimize = optimize, - }); - step.root_module.addImport("glslang", glslang_dep.module("glslang")); - if (b.systemIntegrationOption("glslang", .{})) { - step.linkSystemLibrary2("glslang", dynamic_link_opts); - step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts); - } else { - step.linkLibrary(glslang_dep.artifact("glslang")); - try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); - } - - // Spirv-cross - const spirv_cross_dep = b.dependency("spirv_cross", .{ - .target = target, - .optimize = optimize, - }); - step.root_module.addImport("spirv_cross", spirv_cross_dep.module("spirv_cross")); - if (b.systemIntegrationOption("spirv-cross", .{})) { - step.linkSystemLibrary2("spirv-cross", dynamic_link_opts); - } else { - step.linkLibrary(spirv_cross_dep.artifact("spirv_cross")); - try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin()); - } - - // Simdutf - if (b.systemIntegrationOption("simdutf", .{})) { - step.linkSystemLibrary2("simdutf", dynamic_link_opts); - } else { - const simdutf_dep = b.dependency("simdutf", .{ - .target = target, - .optimize = optimize, - }); - step.linkLibrary(simdutf_dep.artifact("simdutf")); - try static_libs.append(simdutf_dep.artifact("simdutf").getEmittedBin()); - } - - // Sentry - if (config.sentry) { - const sentry_dep = b.dependency("sentry", .{ - .target = target, - .optimize = optimize, - .backend = .breakpad, - }); - - step.root_module.addImport("sentry", sentry_dep.module("sentry")); - - // Sentry - step.linkLibrary(sentry_dep.artifact("sentry")); - try static_libs.append(sentry_dep.artifact("sentry").getEmittedBin()); - - // We also need to include breakpad in the static libs. - const breakpad_dep = sentry_dep.builder.dependency("breakpad", .{ - .target = target, - .optimize = optimize, - }); - try static_libs.append(breakpad_dep.artifact("breakpad").getEmittedBin()); - } - - // Wasm we do manually since it is such a different build. - if (step.rootModuleTarget().cpu.arch == .wasm32) { - const js_dep = b.dependency("zig_js", .{ - .target = target, - .optimize = optimize, - }); - step.root_module.addImport("zig-js", js_dep.module("zig-js")); - - return static_libs; - } - - // On Linux, we need to add a couple common library paths that aren't - // on the standard search list. i.e. GTK is often in /usr/lib/x86_64-linux-gnu - // on x86_64. - if (step.rootModuleTarget().os.tag == .linux) { - const triple = try step.rootModuleTarget().linuxTriple(b.allocator); - step.addLibraryPath(.{ .cwd_relative = b.fmt("/usr/lib/{s}", .{triple}) }); - } - - // C files - step.linkLibC(); - step.addIncludePath(b.path("src/stb")); - step.addCSourceFiles(.{ .files = &.{"src/stb/stb.c"} }); - if (step.rootModuleTarget().os.tag == .linux) { - step.addIncludePath(b.path("src/apprt/gtk")); - } - - // C++ files - step.linkLibCpp(); - step.addIncludePath(b.path("src")); - { - // From hwy/detect_targets.h - const HWY_AVX3_SPR: c_int = 1 << 4; - const HWY_AVX3_ZEN4: c_int = 1 << 6; - const HWY_AVX3_DL: c_int = 1 << 7; - const HWY_AVX3: c_int = 1 << 8; - - // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 - // To workaround this we just disable AVX512 support completely. - // The performance difference between AVX2 and AVX512 is not - // significant for our use case and AVX512 is very rare on consumer - // hardware anyways. - const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3; - - step.addCSourceFiles(.{ - .files = &.{ - "src/simd/base64.cpp", - "src/simd/codepoint_width.cpp", - "src/simd/index_of.cpp", - "src/simd/vt.cpp", - }, - .flags = if (step.rootModuleTarget().cpu.arch == .x86_64) &.{ - b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}), - } else &.{}, - }); - } - - // We always require the system SDK so that our system headers are available. - // This makes things like `os/log.h` available for cross-compiling. - if (step.rootModuleTarget().isDarwin()) { - try @import("apple_sdk").addPaths(b, &step.root_module); - try addMetallib(b, step); - } - - // Other dependencies, mostly pure Zig - step.root_module.addImport("opengl", b.dependency( - "opengl", - .{}, - ).module("opengl")); - step.root_module.addImport("vaxis", b.dependency("vaxis", .{ - .target = target, - .optimize = optimize, - }).module("vaxis")); - step.root_module.addImport("wuffs", b.dependency("wuffs", .{ - .target = target, - .optimize = optimize, - }).module("wuffs")); - step.root_module.addImport("xev", b.dependency("libxev", .{ - .target = target, - .optimize = optimize, - }).module("xev")); - step.root_module.addImport("z2d", b.addModule("z2d", .{ - .root_source_file = b.dependency("z2d", .{}).path("src/z2d.zig"), - .target = target, - .optimize = optimize, - })); - step.root_module.addImport("ziglyph", b.dependency("ziglyph", .{ - .target = target, - .optimize = optimize, - }).module("ziglyph")); - step.root_module.addImport("zf", b.dependency("zf", .{ - .target = target, - .optimize = optimize, - .with_tui = false, - }).module("zf")); - - // Mac Stuff - if (step.rootModuleTarget().isDarwin()) { - const objc_dep = b.dependency("zig_objc", .{ - .target = target, - .optimize = optimize, - }); - const macos_dep = b.dependency("macos", .{ - .target = target, - .optimize = optimize, - }); - - step.root_module.addImport("objc", objc_dep.module("objc")); - step.root_module.addImport("macos", macos_dep.module("macos")); - step.linkLibrary(macos_dep.artifact("macos")); - try static_libs.append(macos_dep.artifact("macos").getEmittedBin()); - - if (config.renderer == .opengl) { - step.linkFramework("OpenGL"); - } - } - - // cimgui - const cimgui_dep = b.dependency("cimgui", .{ - .target = target, - .optimize = optimize, - }); - step.root_module.addImport("cimgui", cimgui_dep.module("cimgui")); - step.linkLibrary(cimgui_dep.artifact("cimgui")); - try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin()); - - // Highway - const highway_dep = b.dependency("highway", .{ - .target = target, - .optimize = optimize, - }); - step.linkLibrary(highway_dep.artifact("highway")); - try static_libs.append(highway_dep.artifact("highway").getEmittedBin()); - - // utfcpp - This is used as a dependency on our hand-written C++ code - const utfcpp_dep = b.dependency("utfcpp", .{ - .target = target, - .optimize = optimize, - }); - step.linkLibrary(utfcpp_dep.artifact("utfcpp")); - try static_libs.append(utfcpp_dep.artifact("utfcpp").getEmittedBin()); - - // If we're building an exe then we have additional dependencies. - if (step.kind != .lib) { - // We always statically compile glad - step.addIncludePath(b.path("vendor/glad/include/")); - step.addCSourceFile(.{ - .file = b.path("vendor/glad/src/gl.c"), - .flags = &.{}, - }); - - // When we're targeting flatpak we ALWAYS link GTK so we - // get access to glib for dbus. - if (config.flatpak) step.linkSystemLibrary2("gtk4", dynamic_link_opts); - - switch (config.app_runtime) { - .none => {}, - - .glfw => glfw: { - const mach_glfw_dep = b.lazyDependency("mach_glfw", .{ - .target = target, - .optimize = optimize, - }) orelse break :glfw; - step.root_module.addImport("glfw", mach_glfw_dep.module("mach-glfw")); - }, - - .gtk => { - step.linkSystemLibrary2("gtk4", dynamic_link_opts); - if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); - if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts); - - if (config.wayland) { - const scanner = Scanner.create(b, .{}); - - const wayland = b.createModule(.{ .root_source_file = scanner.result }); - - const plasma_wayland_protocols = b.dependency("plasma_wayland_protocols", .{ - .target = target, - .optimize = optimize, - }); - scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml")); - - scanner.generate("wl_compositor", 1); - scanner.generate("org_kde_kwin_blur_manager", 1); - - step.root_module.addImport("wayland", wayland); - step.linkSystemLibrary2("wayland-client", dynamic_link_opts); - } - - { - const gresource = @import("src/apprt/gtk/gresource.zig"); - - const wf = b.addWriteFiles(); - const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml); - - const generate_resources_c = b.addSystemCommand(&.{ - "glib-compile-resources", - "--c-name", - "ghostty", - "--generate-source", - "--target", - }); - const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c"); - generate_resources_c.addFileArg(gresource_xml); - generate_resources_c.extra_file_dependencies = &gresource.dependencies; - step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} }); - - const generate_resources_h = b.addSystemCommand(&.{ - "glib-compile-resources", - "--c-name", - "ghostty", - "--generate-header", - "--target", - }); - const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h"); - generate_resources_h.addFileArg(gresource_xml); - generate_resources_h.extra_file_dependencies = &gresource.dependencies; - step.addIncludePath(ghostty_resources_h.dirname()); - } - }, - } - } - - try addHelp(b, step, config); - try addUnicodeTables(b, step); - - return static_libs; -} - -/// Generate Metal shader library -fn addMetallib( - b: *std.Build, - step: *std.Build.Step.Compile, -) !void { - const metal_step = MetallibStep.create(b, .{ - .name = "Ghostty", - .target = step.root_module.resolved_target.?, - .sources = &.{b.path("src/renderer/shaders/cell.metal")}, - }); - - metal_step.output.addStepDependencies(&step.step); - step.root_module.addAnonymousImport("ghostty_metallib", .{ - .root_source_file = metal_step.output, - }); -} - -/// Generate help files -fn addHelp( - b: *std.Build, - step_: ?*std.Build.Step.Compile, - config: BuildConfig, -) !void { - // Our static state between runs. We memoize our help strings - // so that we only execute the help generation once. - const HelpState = struct { - var generated: ?std.Build.LazyPath = null; - }; - - const help_output = HelpState.generated orelse strings: { - const help_exe = b.addExecutable(.{ - .name = "helpgen", - .root_source_file = b.path("src/helpgen.zig"), - .target = b.host, - }); - if (step_ == null) b.installArtifact(help_exe); - - const help_config = config: { - var copy = config; - copy.exe_entrypoint = .helpgen; - break :config copy; - }; - const options = b.addOptions(); - try help_config.addOptions(options); - help_exe.root_module.addOptions("build_options", options); - - const help_run = b.addRunArtifact(help_exe); - HelpState.generated = help_run.captureStdOut(); - break :strings HelpState.generated.?; - }; - - if (step_) |step| { - help_output.addStepDependencies(&step.step); - step.root_module.addAnonymousImport("help_strings", .{ - .root_source_file = help_output, - }); - } -} - -/// Generate unicode fast lookup tables -fn addUnicodeTables( - b: *std.Build, - step_: ?*std.Build.Step.Compile, -) !void { - // Our static state between runs. We memoize our output to gen once - const State = struct { - var generated: ?std.Build.LazyPath = null; - }; - - const output = State.generated orelse strings: { - const exe = b.addExecutable(.{ - .name = "unigen", - .root_source_file = b.path("src/unicode/props.zig"), - .target = b.host, - }); - exe.linkLibC(); - if (step_ == null) b.installArtifact(exe); - - const ziglyph_dep = b.dependency("ziglyph", .{ - .target = b.host, - }); - exe.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph")); - - const help_run = b.addRunArtifact(exe); - State.generated = help_run.captureStdOut(); - break :strings State.generated.?; - }; - - if (step_) |step| { - output.addStepDependencies(&step.step); - step.root_module.addAnonymousImport("unicode_tables", .{ - .root_source_file = output, - }); - } -} - -/// Generate documentation (manpages, etc.) from help strings -fn buildDocumentation( - b: *std.Build, - config: BuildConfig, -) !void { - const manpages = [_]struct { - name: []const u8, - section: []const u8, - }{ - .{ .name = "ghostty", .section = "1" }, - .{ .name = "ghostty", .section = "5" }, - }; - - inline for (manpages) |manpage| { - const generate_markdown = b.addExecutable(.{ - .name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, - .root_source_file = b.path("src/main.zig"), - .target = b.host, - }); - try addHelp(b, generate_markdown, config); - - const gen_config = config: { - var copy = config; - copy.exe_entrypoint = @field( - build_config.ExeEntrypoint, - "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, - ); - break :config copy; - }; - - const generate_markdown_options = b.addOptions(); - try gen_config.addOptions(generate_markdown_options); - generate_markdown.root_module.addOptions("build_options", generate_markdown_options); - - const generate_markdown_step = b.addRunArtifact(generate_markdown); - const markdown_output = generate_markdown_step.captureStdOut(); - - b.getInstallStep().dependOn(&b.addInstallFile( - markdown_output, - "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".md", - ).step); - - const generate_html = b.addSystemCommand(&.{"pandoc"}); - generate_html.addArgs(&.{ - "--standalone", - "--from", - "markdown", - "--to", - "html", - }); - generate_html.addFileArg(markdown_output); - - b.getInstallStep().dependOn(&b.addInstallFile( - generate_html.captureStdOut(), - "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html", - ).step); - - const generate_manpage = b.addSystemCommand(&.{"pandoc"}); - generate_manpage.addArgs(&.{ - "--standalone", - "--from", - "markdown", - "--to", - "man", - }); - generate_manpage.addFileArg(markdown_output); - - b.getInstallStep().dependOn(&b.addInstallFile( - generate_manpage.captureStdOut(), - "share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section, - ).step); - } -} - -/// Generate the website reference data that we merge into the -/// official Ghostty website. This isn't meant to be part of any -/// actual build. -fn buildWebData( - b: *std.Build, - config: BuildConfig, -) !void { - { - const webgen_config = b.addExecutable(.{ - .name = "webgen_config", - .root_source_file = b.path("src/main.zig"), - .target = b.host, - }); - try addHelp(b, webgen_config, config); - - { - const buildconfig = config: { - var copy = config; - copy.exe_entrypoint = .webgen_config; - break :config copy; - }; - - const options = b.addOptions(); - try buildconfig.addOptions(options); - webgen_config.root_module.addOptions("build_options", options); - } - - const webgen_config_step = b.addRunArtifact(webgen_config); - const webgen_config_out = webgen_config_step.captureStdOut(); - - b.getInstallStep().dependOn(&b.addInstallFile( - webgen_config_out, - "share/ghostty/webdata/config.mdx", - ).step); - } - - { - const webgen_actions = b.addExecutable(.{ - .name = "webgen_actions", - .root_source_file = b.path("src/main.zig"), - .target = b.host, - }); - try addHelp(b, webgen_actions, config); - - { - const buildconfig = config: { - var copy = config; - copy.exe_entrypoint = .webgen_actions; - break :config copy; - }; - - const options = b.addOptions(); - try buildconfig.addOptions(options); - webgen_actions.root_module.addOptions("build_options", options); - } - - const webgen_actions_step = b.addRunArtifact(webgen_actions); - const webgen_actions_out = webgen_actions_step.captureStdOut(); - - b.getInstallStep().dependOn(&b.addInstallFile( - webgen_actions_out, - "share/ghostty/webdata/actions.mdx", - ).step); - } -} - -fn benchSteps( - b: *std.Build, - target: std.Build.ResolvedTarget, - config: BuildConfig, - install: bool, -) !void { - // Open the directory ./src/bench - const c_dir_path = (comptime root()) ++ "/src/bench"; - var c_dir = try fs.cwd().openDir(c_dir_path, .{ .iterate = true }); - defer c_dir.close(); - - // Go through and add each as a step - var c_dir_it = c_dir.iterate(); - while (try c_dir_it.next()) |entry| { - // Get the index of the last '.' so we can strip the extension. - const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue; - if (index == 0) continue; - - // If it doesn't end in 'zig' then ignore - if (!std.mem.eql(u8, entry.name[index + 1 ..], "zig")) continue; - - // Name of the conformance app and full path to the entrypoint. - const name = entry.name[0..index]; - - // Executable builder. - const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name}); - const c_exe = b.addExecutable(.{ - .name = bin_name, - .root_source_file = b.path("src/main.zig"), - .target = target, - - // We always want our benchmarks to be in release mode. - .optimize = .ReleaseFast, - }); - c_exe.linkLibC(); - if (install) b.installArtifact(c_exe); - _ = try addDeps(b, c_exe, config: { - var copy = config; - var enum_name: [64]u8 = undefined; - @memcpy(enum_name[0..name.len], name); - std.mem.replaceScalar(u8, enum_name[0..name.len], '-', '_'); - - var buf: [64]u8 = undefined; - copy.exe_entrypoint = std.meta.stringToEnum( - build_config.ExeEntrypoint, - try std.fmt.bufPrint(&buf, "bench_{s}", .{enum_name[0..name.len]}), - ).?; - - break :config copy; - }); - } -} - -fn conformanceSteps( - b: *std.Build, - target: std.Build.ResolvedTarget, - optimize: std.builtin.Mode, -) !std.StringHashMap(*CompileStep) { - var map = std.StringHashMap(*CompileStep).init(b.allocator); - - // Open the directory ./conformance - const c_dir_path = (comptime root()) ++ "/conformance"; - var c_dir = try fs.cwd().openDir(c_dir_path, .{ .iterate = true }); - defer c_dir.close(); - - // Go through and add each as a step - var c_dir_it = c_dir.iterate(); - while (try c_dir_it.next()) |entry| { - // Get the index of the last '.' so we can strip the extension. - const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue; - if (index == 0) continue; - - // Name of the conformance app and full path to the entrypoint. - const name = try b.allocator.dupe(u8, entry.name[0..index]); - const path = try fs.path.join(b.allocator, &[_][]const u8{ - c_dir_path, - entry.name, - }); - - // Executable builder. - const c_exe = b.addExecutable(.{ - .name = name, - .root_source_file = b.path(path), - .target = target, - .optimize = optimize, - }); - - const install = b.addInstallArtifact(c_exe, .{}); - install.dest_sub_path = "conformance"; - b.getInstallStep().dependOn(&install.step); - - // Store the mapping - try map.put(name, c_exe); - } - - return map; -} - -/// Path to the directory with the build.zig. -fn root() []const u8 { - return std.fs.path.dirname(@src().file) orelse unreachable; -} - -/// ANSI escape codes for colored log output -const color_map = std.StaticStringMap([]const u8).initComptime(.{ - &.{ "black", "30m" }, - &.{ "blue", "34m" }, - &.{ "b", "1m" }, - &.{ "d", "2m" }, - &.{ "cyan", "36m" }, - &.{ "green", "32m" }, - &.{ "magenta", "35m" }, - &.{ "red", "31m" }, - &.{ "white", "37m" }, - &.{ "yellow", "33m" }, -}); diff --git a/build.zig.zon b/build.zig.zon index 33be26193..518022486 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,6 +13,14 @@ .hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62", .lazy = true, }, + .vaxis = .{ + .url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b", + .hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f", + }, + .z2d = .{ + .url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a", + .hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a", + }, .zig_objc = .{ .url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz", .hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634", @@ -29,6 +37,10 @@ .url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz", .hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c", }, + .zf = .{ + .url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd", + .hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8", + }, // C libs .cimgui = .{ .path = "./pkg/cimgui" }, @@ -50,27 +62,25 @@ .glslang = .{ .path = "./pkg/glslang" }, .spirv_cross = .{ .path = "./pkg/spirv-cross" }, + // Wayland + .wayland = .{ + .url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", + .hash = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f", + }, + .wayland_protocols = .{ + .url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", + .hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef", + }, + .plasma_wayland_protocols = .{ + .url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86", + .hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566", + }, + // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4762ad5bd6d3906e28babdc2bda8a967d63a63be.tar.gz", .hash = "1220a263b22113273d01bd33e3c06b8119cb2f63b4e5d414a85d88e3aa95bb68a2de", }, - .vaxis = .{ - .url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b", - .hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f", - }, - .zf = .{ - .url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd", - .hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8", - }, - .z2d = .{ - .url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a", - .hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a", - }, - .plasma_wayland_protocols = .{ - .url = "git+https://invent.kde.org/libraries/plasma-wayland-protocols.git?ref=master#db525e8f9da548cffa2ac77618dd0fbe7f511b86", - .hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566", - }, }, } diff --git a/conformance/ansi_ri.zig b/conformance/ansi_ri.zig deleted file mode 100644 index 991804295..000000000 --- a/conformance/ansi_ri.zig +++ /dev/null @@ -1,12 +0,0 @@ -//! Reverse Index (RI) - ESC M -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - try stdout.print("A\nB\nC", .{}); - try stdout.print("\x1BM", .{}); - try stdout.print("D\n\n", .{}); - - // const stdin = std.io.getStdIn().reader(); - // _ = try stdin.readByte(); -} diff --git a/conformance/ansi_ri_top.zig b/conformance/ansi_ri_top.zig deleted file mode 100644 index b2258f069..000000000 --- a/conformance/ansi_ri_top.zig +++ /dev/null @@ -1,23 +0,0 @@ -//! Reverse Index (RI) - ESC M -//! Case: test that if the cursor is at the top, it scrolls down. -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - try stdout.print("A\nB\n\n", .{}); - - try stdout.print("\x1B[H", .{}); // Top-left - try stdout.print("\x1BM", .{}); // Reverse-Index - try stdout.print("D", .{}); - - try stdout.print("\x0D", .{}); // CR - try stdout.print("\x0A", .{}); // LF - try stdout.print("\x1B[H", .{}); // Top-left - try stdout.print("\x1BM", .{}); // Reverse-Index - try stdout.print("E", .{}); - - try stdout.print("\n", .{}); - - // const stdin = std.io.getStdIn().reader(); - // _ = try stdin.readByte(); -} diff --git a/conformance/blocks.zig b/conformance/blocks.zig deleted file mode 100644 index a977ca4ea..000000000 --- a/conformance/blocks.zig +++ /dev/null @@ -1,99 +0,0 @@ -//! Outputs various box glyphs for testing. -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - - // Box Drawing - { - try stdout.print("\x1b[4mBox Drawing\x1b[0m\n", .{}); - var i: usize = 0x2500; - const step: usize = 32; - while (i <= 0x257F) : (i += step) { - var j: usize = 0; - while (j < step) : (j += 1) { - try stdout.print("{u} ", .{@as(u21, @intCast(i + j))}); - } - - try stdout.print("\n\n", .{}); - } - } - - // Block Elements - { - try stdout.print("\x1b[4mBlock Elements\x1b[0m\n", .{}); - var i: usize = 0x2580; - const step: usize = 32; - while (i <= 0x259f) : (i += step) { - var j: usize = 0; - while (j < step) : (j += 1) { - try stdout.print("{u} ", .{@as(u21, @intCast(i + j))}); - } - - try stdout.print("\n\n", .{}); - } - } - - // Braille Elements - { - try stdout.print("\x1b[4mBraille\x1b[0m\n", .{}); - var i: usize = 0x2800; - const step: usize = 32; - while (i <= 0x28FF) : (i += step) { - var j: usize = 0; - while (j < step) : (j += 1) { - try stdout.print("{u} ", .{@as(u21, @intCast(i + j))}); - } - - try stdout.print("\n\n", .{}); - } - } - - { - try stdout.print("\x1b[4mSextants\x1b[0m\n", .{}); - var i: usize = 0x1FB00; - const step: usize = 32; - const end = 0x1FB3B; - while (i <= end) : (i += step) { - var j: usize = 0; - while (j < step) : (j += 1) { - const v = i + j; - if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))}); - } - - try stdout.print("\n\n", .{}); - } - } - - { - try stdout.print("\x1b[4mWedge Triangles\x1b[0m\n", .{}); - var i: usize = 0x1FB3C; - const step: usize = 32; - const end = 0x1FB6B; - while (i <= end) : (i += step) { - var j: usize = 0; - while (j < step) : (j += 1) { - const v = i + j; - if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))}); - } - - try stdout.print("\n\n", .{}); - } - } - - { - try stdout.print("\x1b[4mOther\x1b[0m\n", .{}); - var i: usize = 0x1FB70; - const step: usize = 32; - const end = 0x1FB8B; - while (i <= end) : (i += step) { - var j: usize = 0; - while (j < step) : (j += 1) { - const v = i + j; - if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))}); - } - - try stdout.print("\n\n", .{}); - } - } -} diff --git a/conformance/csi_decstbm.zig b/conformance/csi_decstbm.zig deleted file mode 100644 index f8b652427..000000000 --- a/conformance/csi_decstbm.zig +++ /dev/null @@ -1,15 +0,0 @@ -//! Set Top and Bottom Margins (DECSTBM) - ESC [ r -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - try stdout.print("A\nB\nC\nD", .{}); - try stdout.print("\x1B[1;3r", .{}); // cursor up - try stdout.print("\x1B[1;1H", .{}); // top-left - try stdout.print("\x1B[M", .{}); // delete line - try stdout.print("E\n", .{}); - try stdout.print("\x1B[7;1H", .{}); // cursor up - - // const stdin = std.io.getStdIn().reader(); - // _ = try stdin.readByte(); -} diff --git a/conformance/csi_dl.zig b/conformance/csi_dl.zig deleted file mode 100644 index 175637d9b..000000000 --- a/conformance/csi_dl.zig +++ /dev/null @@ -1,14 +0,0 @@ -//! Delete Line (DL) - Esc [ M -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - try stdout.print("A\nB\nC\nD", .{}); - try stdout.print("\x1B[2A", .{}); // cursor up - try stdout.print("\x1B[M", .{}); - try stdout.print("E\n", .{}); - try stdout.print("\x1B[B", .{}); - - // const stdin = std.io.getStdIn().reader(); - // _ = try stdin.readByte(); -} diff --git a/conformance/csi_il.zig b/conformance/csi_il.zig deleted file mode 100644 index 52d2c392f..000000000 --- a/conformance/csi_il.zig +++ /dev/null @@ -1,17 +0,0 @@ -//! Insert Line (IL) - Esc [ L -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - try stdout.print("\x1B[2J", .{}); // clear screen - try stdout.print("\x1B[1;1H", .{}); // set cursor position - try stdout.print("A\nB\nC\nD\nE", .{}); - try stdout.print("\x1B[1;2r", .{}); // set scroll region - try stdout.print("\x1B[1;1H", .{}); // set cursor position - try stdout.print("\x1B[1L", .{}); // insert lines - try stdout.print("X", .{}); - try stdout.print("\x1B[7;1H", .{}); // set cursor position - - // const stdin = std.io.getStdIn().reader(); - // _ = try stdin.readByte(); -} diff --git a/conformance/esc_decaln.zig b/conformance/esc_decaln.zig deleted file mode 100644 index aeb1887a4..000000000 --- a/conformance/esc_decaln.zig +++ /dev/null @@ -1,10 +0,0 @@ -//! DECALN - ESC # 8 -const std = @import("std"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - try stdout.print("\x1B#8", .{}); - - // const stdin = std.io.getStdIn().reader(); - // _ = try stdin.readByte(); -} diff --git a/nix/package.nix b/nix/package.nix index 166a3c4fb..2f7825a56 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -53,7 +53,6 @@ fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( lib.fileset.unions [ ../dist/linux - ../conformance ../images ../include ../pkg diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index f2592adf4..3806c64c9 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-eUY6MS3//r6pA/w9b+E4+YqmqUbzpUfL3afJJlnMhLY=" +"sha256-PnfSy793kcVt85q47kWR0xkivXoMOZAAmuUyKO9vqAI=" diff --git a/src/build/Config.zig b/src/build/Config.zig new file mode 100644 index 000000000..71dffce4a --- /dev/null +++ b/src/build/Config.zig @@ -0,0 +1,503 @@ +/// Build configuration. This is the configuration that is populated +/// during `zig build` to control the rest of the build process. +const Config = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); + +const apprt = @import("../apprt.zig"); +const font = @import("../font/main.zig"); +const renderer = @import("../renderer.zig"); +const Command = @import("../Command.zig"); +const WasmTarget = @import("../os/wasm/target.zig").Target; + +const gtk = @import("gtk.zig"); +const GitVersion = @import("GitVersion.zig"); + +/// The version of the next release. +/// +/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. +/// Until then this MUST match build.zig.zon and should always be the +/// _next_ version to release. +const app_version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 2 }; + +/// Standard build configuration options. +optimize: std.builtin.OptimizeMode, +target: std.Build.ResolvedTarget, +wasm_target: WasmTarget, + +/// Comptime interfaces +app_runtime: apprt.Runtime = .none, +renderer: renderer.Impl = .opengl, +font_backend: font.Backend = .freetype, + +/// Feature flags +adwaita: bool = false, +x11: bool = false, +wayland: bool = false, +sentry: bool = true, +wasm_shared: bool = true, + +/// Ghostty exe properties +exe_entrypoint: ExeEntrypoint = .ghostty, +version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, + +/// Binary properties +pie: bool = false, +strip: bool = false, +patch_rpath: ?[]const u8 = null, + +/// Artifacts +flatpak: bool = false, +emit_test_exe: bool = false, +emit_bench: bool = false, +emit_helpgen: bool = false, +emit_docs: bool = false, +emit_webdata: bool = false, +emit_xcframework: bool = false, + +/// Environmental properties +env: std.process.EnvMap, + +pub fn init(b: *std.Build) !Config { + // Setup our standard Zig target and optimize options, i.e. + // `-Doptimize` and `-Dtarget`. + const optimize = b.standardOptimizeOption(.{}); + const target = target: { + var result = b.standardTargetOptions(.{}); + + // If we're building for macOS and we're on macOS, we need to + // use a generic target to workaround compilation issues. + if (result.result.os.tag == .macos and builtin.target.isDarwin()) { + result = genericMacOSTarget(b, null); + } + + // If we have no minimum OS version, we set the default based on + // our tag. Not all tags have a minimum so this may be null. + if (result.query.os_version_min == null) { + result.query.os_version_min = osVersionMin(result.result.os.tag); + } + + break :target result; + }; + + // This is set to true when we're building a system package. For now + // this is trivially detected using the "system_package_mode" bool + // but we may want to make this more sophisticated in the future. + const system_package: bool = b.graph.system_package_mode; + + // This specifies our target wasm runtime. For now only one semi-usable + // one exists so this is hardcoded. + const wasm_target: WasmTarget = .browser; + + // Determine whether GTK supports X11 and Wayland. This is always safe + // to run even on non-Linux platforms because any failures result in + // defaults. + const gtk_targets = gtk.targets(b); + + // We use env vars throughout the build so we grab them immediately here. + var env = try std.process.getEnvMap(b.allocator); + errdefer env.deinit(); + + var config: Config = .{ + .optimize = optimize, + .target = target, + .wasm_target = wasm_target, + .env = env, + }; + + //--------------------------------------------------------------- + // Comptime Interfaces + + config.font_backend = b.option( + font.Backend, + "font-backend", + "The font backend to use for discovery and rasterization.", + ) orelse font.Backend.default(target.result, wasm_target); + + config.app_runtime = b.option( + apprt.Runtime, + "app-runtime", + "The app runtime to use. Not all values supported on all platforms.", + ) orelse apprt.Runtime.default(target.result); + + config.renderer = b.option( + renderer.Impl, + "renderer", + "The app runtime to use. Not all values supported on all platforms.", + ) orelse renderer.Impl.default(target.result, wasm_target); + + //--------------------------------------------------------------- + // Feature Flags + + config.adwaita = b.option( + bool, + "gtk-adwaita", + "Enables the use of Adwaita when using the GTK rendering backend.", + ) orelse true; + + config.flatpak = b.option( + bool, + "flatpak", + "Build for Flatpak (integrates with Flatpak APIs). Only has an effect targeting Linux.", + ) orelse false; + + config.sentry = b.option( + bool, + "sentry", + "Build with Sentry crash reporting. Default for macOS is true, false for any other system.", + ) orelse sentry: { + switch (target.result.os.tag) { + .macos, .ios => break :sentry true, + + // Note its false for linux because the crash reports on Linux + // don't have much useful information. + else => break :sentry false, + } + }; + + config.wayland = b.option( + bool, + "gtk-wayland", + "Enables linking against Wayland libraries when using the GTK rendering backend.", + ) orelse gtk_targets.wayland; + + config.x11 = b.option( + bool, + "gtk-x11", + "Enables linking against X11 libraries when using the GTK rendering backend.", + ) orelse gtk_targets.x11; + + //--------------------------------------------------------------- + // Ghostty Exe Properties + + const version_string = b.option( + []const u8, + "version-string", + "A specific version string to use for the build. " ++ + "If not specified, git will be used. This must be a semantic version.", + ); + + config.version = if (version_string) |v| + // If an explicit version is given, we always use it. + try std.SemanticVersion.parse(v) + else version: { + // If no explicit version is given, we try to detect it from git. + const vsn = GitVersion.detect(b) catch |err| switch (err) { + // If Git isn't available we just make an unknown dev version. + error.GitNotFound, + error.GitNotRepository, + => break :version .{ + .major = app_version.major, + .minor = app_version.minor, + .patch = app_version.patch, + .pre = "dev", + .build = "0000000", + }, + + else => return err, + }; + if (vsn.tag) |tag| { + // Tip releases behave just like any other pre-release so we skip. + if (!std.mem.eql(u8, tag, "tip")) { + const expected = b.fmt("v{d}.{d}.{d}", .{ + app_version.major, + app_version.minor, + app_version.patch, + }); + + if (!std.mem.eql(u8, tag, expected)) { + @panic("tagged releases must be in vX.Y.Z format matching build.zig"); + } + + break :version .{ + .major = app_version.major, + .minor = app_version.minor, + .patch = app_version.patch, + }; + } + } + + break :version .{ + .major = app_version.major, + .minor = app_version.minor, + .patch = app_version.patch, + .pre = vsn.branch, + .build = vsn.short_hash, + }; + }; + + //--------------------------------------------------------------- + // Binary Properties + + // On NixOS, the built binary from `zig build` needs to patch the rpath + // into the built binary for it to be portable across the NixOS system + // it was built for. We default this to true if we can detect we're in + // a Nix shell and have LD_LIBRARY_PATH set. + config.patch_rpath = b.option( + []const u8, + "patch-rpath", + "Inject the LD_LIBRARY_PATH as the rpath in the built binary. " ++ + "This defaults to LD_LIBRARY_PATH if we're in a Nix shell environment on NixOS.", + ) orelse patch_rpath: { + // We only do the patching if we're targeting our own CPU and its Linux. + if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_rpath null; + + // If we're in a nix shell we default to doing this. + // Note: we purposely never deinit envmap because we leak the strings + if (env.get("IN_NIX_SHELL") == null) break :patch_rpath null; + break :patch_rpath env.get("LD_LIBRARY_PATH"); + }; + + config.pie = b.option( + bool, + "pie", + "Build a Position Independent Executable. Default true for system packages.", + ) orelse system_package; + + config.strip = b.option( + bool, + "strip", + "Strip the final executable. Default true for fast and small releases", + ) orelse switch (optimize) { + .Debug => false, + .ReleaseSafe => false, + .ReleaseFast, .ReleaseSmall => true, + }; + + //--------------------------------------------------------------- + // Artifacts to Emit + + config.emit_test_exe = b.option( + bool, + "emit-test-exe", + "Build and install test executables with 'build'", + ) orelse false; + + config.emit_bench = b.option( + bool, + "emit-bench", + "Build and install the benchmark executables.", + ) orelse false; + + config.emit_helpgen = b.option( + bool, + "emit-helpgen", + "Build and install the helpgen executable.", + ) orelse false; + + config.emit_docs = b.option( + bool, + "emit-docs", + "Build and install auto-generated documentation (requires pandoc)", + ) orelse emit_docs: { + // If we are emitting any other artifacts then we default to false. + if (config.emit_bench or + config.emit_test_exe or + config.emit_helpgen) break :emit_docs false; + + // We always emit docs in system package mode. + if (system_package) break :emit_docs true; + + // We only default to true if we can find pandoc. + const path = Command.expandPath(b.allocator, "pandoc") catch + break :emit_docs false; + defer if (path) |p| b.allocator.free(p); + break :emit_docs path != null; + }; + + config.emit_webdata = b.option( + bool, + "emit-webdata", + "Build the website data for the website.", + ) orelse false; + + config.emit_xcframework = b.option( + bool, + "emit-xcframework", + "Build and install the xcframework for the macOS library.", + ) orelse builtin.target.isDarwin() and + target.result.os.tag == .macos and + config.app_runtime == .none and + (!config.emit_bench and + !config.emit_test_exe and + !config.emit_helpgen); + + //--------------------------------------------------------------- + // System Packages + + // These are all our dependencies that can be used with system + // packages if they exist. We set them up here so that we can set + // their defaults early. The first call configures the integration and + // subsequent calls just return the configured value. This lets them + // show up properly in `--help`. + + { + // These dependencies we want to default false if we're on macOS. + // On macOS we don't want to use system libraries because we + // generally want a fat binary. This can be overridden with the + // `-fsys` flag. + for (&[_][]const u8{ + "freetype", + "harfbuzz", + "fontconfig", + "libpng", + "zlib", + "oniguruma", + }) |dep| { + _ = b.systemIntegrationOption( + dep, + .{ + // If we're not on darwin we want to use whatever the + // default is via the system package mode + .default = if (target.result.isDarwin()) false else null, + }, + ); + } + + // These default to false because they're rarely available as + // system packages so we usually want to statically link them. + for (&[_][]const u8{ + "glslang", + "spirv-cross", + "simdutf", + }) |dep| { + _ = b.systemIntegrationOption(dep, .{ .default = false }); + } + } + + return config; +} + +/// Configure the build options with our values. +pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void { + // We need to break these down individual because addOption doesn't + // support all types. + step.addOption(bool, "flatpak", self.flatpak); + step.addOption(bool, "adwaita", self.adwaita); + step.addOption(bool, "x11", self.x11); + step.addOption(bool, "wayland", self.wayland); + step.addOption(bool, "sentry", self.sentry); + step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); + step.addOption(font.Backend, "font_backend", self.font_backend); + step.addOption(renderer.Impl, "renderer", self.renderer); + step.addOption(ExeEntrypoint, "exe_entrypoint", self.exe_entrypoint); + step.addOption(WasmTarget, "wasm_target", self.wasm_target); + step.addOption(bool, "wasm_shared", self.wasm_shared); + + // Our version. We also add the string version so we don't need + // to do any allocations at runtime. This has to be long enough to + // accommodate realistic large branch names for dev versions. + var buf: [1024]u8 = undefined; + step.addOption(std.SemanticVersion, "app_version", self.version); + step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ( + &buf, + "{}", + .{self.version}, + )); + step.addOption( + ReleaseChannel, + "release_channel", + channel: { + const pre = self.version.pre orelse break :channel .stable; + if (pre.len == 0) break :channel .stable; + break :channel .tip; + }, + ); +} + +/// Rehydrate our Config from the comptime options. Note that not all +/// options are available at comptime, so look closely at this implementation +/// to see what is and isn't available. +pub fn fromOptions() Config { + const options = @import("build_options"); + return .{ + // Unused at runtime. + .optimize = undefined, + .target = undefined, + .env = undefined, + + .version = options.app_version, + .flatpak = options.flatpak, + .adwaita = options.adwaita, + .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, + .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, + .renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?, + .exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?, + .wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?, + .wasm_shared = options.wasm_shared, + }; +} + +/// Returns the minimum OS version for the given OS tag. This shouldn't +/// be used generally, it should only be used for Darwin-based OS currently. +pub fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion { + return switch (tag) { + // We support back to the earliest officially supported version + // of macOS by Apple. EOL versions are not supported. + .macos => .{ .semver = .{ + .major = 13, + .minor = 0, + .patch = 0, + } }, + + // iOS 17 picked arbitrarily + .ios => .{ .semver = .{ + .major = 17, + .minor = 0, + .patch = 0, + } }, + + // This should never happen currently. If we add a new target then + // we should add a new case here. + else => null, + }; +} + +// Returns a ResolvedTarget for a mac with a `target.result.cpu.model.name` of `generic`. +// `b.standardTargetOptions()` returns a more specific cpu like `apple_a15`. +// +// This is used to workaround compilation issues on macOS. +// (see for example https://github.com/mitchellh/ghostty/issues/1640). +pub fn genericMacOSTarget( + b: *std.Build, + arch: ?std.Target.Cpu.Arch, +) std.Build.ResolvedTarget { + return b.resolveTargetQuery(.{ + .cpu_arch = arch orelse builtin.target.cpu.arch, + .os_tag = .macos, + .os_version_min = osVersionMin(.macos), + }); +} + +/// The possible entrypoints for the exe artifact. This has no effect on +/// other artifact types (i.e. lib, wasm_module). +/// +/// The whole existence of this enum is to workaround the fact that Zig +/// doesn't allow the main function to be in a file in a subdirctory +/// from the "root" of the module, and I don't want to pollute our root +/// directory with a bunch of individual zig files for each entrypoint. +/// +/// Therefore, main.zig uses this to switch between the different entrypoints. +pub const ExeEntrypoint = enum { + ghostty, + helpgen, + mdgen_ghostty_1, + mdgen_ghostty_5, + webgen_config, + webgen_actions, + bench_parser, + bench_stream, + bench_codepoint_width, + bench_grapheme_break, + bench_page_init, +}; + +/// The release channel for the build. +pub const ReleaseChannel = enum { + /// Unstable builds on every commit. + tip, + + /// Stable tagged releases. + stable, +}; diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig new file mode 100644 index 000000000..27f40abff --- /dev/null +++ b/src/build/GhosttyBench.zig @@ -0,0 +1,69 @@ +//! GhosttyBench generates all the Ghostty benchmark helper binaries. +const GhosttyBench = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); +const SharedDeps = @import("SharedDeps.zig"); + +steps: []*std.Build.Step.Compile, + +pub fn init( + b: *std.Build, + deps: *const SharedDeps, +) !GhosttyBench { + var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); + errdefer steps.deinit(); + + // Open the directory ./src/bench + const c_dir_path = b.pathFromRoot("src/bench"); + var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true }); + defer c_dir.close(); + + // Go through and add each as a step + var c_dir_it = c_dir.iterate(); + while (try c_dir_it.next()) |entry| { + // Get the index of the last '.' so we can strip the extension. + const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue; + if (index == 0) continue; + + // If it doesn't end in 'zig' then ignore + if (!std.mem.eql(u8, entry.name[index + 1 ..], "zig")) continue; + + // Name of the conformance app and full path to the entrypoint. + const name = entry.name[0..index]; + + // Executable builder. + const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name}); + const c_exe = b.addExecutable(.{ + .name = bin_name, + .root_source_file = b.path("src/main.zig"), + .target = deps.config.target, + + // We always want our benchmarks to be in release mode. + .optimize = .ReleaseFast, + }); + c_exe.linkLibC(); + + // Update our entrypoint + var enum_name: [64]u8 = undefined; + @memcpy(enum_name[0..name.len], name); + std.mem.replaceScalar(u8, enum_name[0..name.len], '-', '_'); + + var buf: [64]u8 = undefined; + const new_deps = try deps.changeEntrypoint(b, std.meta.stringToEnum( + Config.ExeEntrypoint, + try std.fmt.bufPrint(&buf, "bench_{s}", .{enum_name[0..name.len]}), + ).?); + + _ = try new_deps.add(c_exe); + + try steps.append(c_exe); + } + + return .{ .steps = steps.items }; +} + +pub fn install(self: *const GhosttyBench) void { + const b = self.steps[0].step.owner; + for (self.steps) |step| b.installArtifact(step); +} diff --git a/src/build/GhosttyDocs.zig b/src/build/GhosttyDocs.zig new file mode 100644 index 000000000..28cbea2c9 --- /dev/null +++ b/src/build/GhosttyDocs.zig @@ -0,0 +1,92 @@ +//! GhosttyDocs generates all the on-disk documentation that Ghostty is +//! installed with (man pages, html, markdown, etc.) +const GhosttyDocs = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); +const SharedDeps = @import("SharedDeps.zig"); + +steps: []*std.Build.Step, + +pub fn init( + b: *std.Build, + deps: *const SharedDeps, +) !GhosttyDocs { + var steps = std.ArrayList(*std.Build.Step).init(b.allocator); + errdefer steps.deinit(); + + const manpages = [_]struct { + name: []const u8, + section: []const u8, + }{ + .{ .name = "ghostty", .section = "1" }, + .{ .name = "ghostty", .section = "5" }, + }; + + inline for (manpages) |manpage| { + const generate_markdown = b.addExecutable(.{ + .name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, + .root_source_file = b.path("src/main.zig"), + .target = b.host, + }); + deps.help_strings.addImport(generate_markdown); + + const gen_config = config: { + var copy = deps.config.*; + copy.exe_entrypoint = @field( + Config.ExeEntrypoint, + "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, + ); + break :config copy; + }; + + const generate_markdown_options = b.addOptions(); + try gen_config.addOptions(generate_markdown_options); + generate_markdown.root_module.addOptions("build_options", generate_markdown_options); + + const generate_markdown_step = b.addRunArtifact(generate_markdown); + const markdown_output = generate_markdown_step.captureStdOut(); + + try steps.append(&b.addInstallFile( + markdown_output, + "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".md", + ).step); + + const generate_html = b.addSystemCommand(&.{"pandoc"}); + generate_html.addArgs(&.{ + "--standalone", + "--from", + "markdown", + "--to", + "html", + }); + generate_html.addFileArg(markdown_output); + + try steps.append(&b.addInstallFile( + generate_html.captureStdOut(), + "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html", + ).step); + + const generate_manpage = b.addSystemCommand(&.{"pandoc"}); + generate_manpage.addArgs(&.{ + "--standalone", + "--from", + "markdown", + "--to", + "man", + }); + generate_manpage.addFileArg(markdown_output); + + try steps.append(&b.addInstallFile( + generate_manpage.captureStdOut(), + "share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section, + ).step); + } + + return .{ .steps = steps.items }; +} + +pub fn install(self: *const GhosttyDocs) void { + const b = self.steps[0].owner; + for (self.steps) |step| b.getInstallStep().dependOn(step); +} diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig new file mode 100644 index 000000000..ef5303baa --- /dev/null +++ b/src/build/GhosttyExe.zig @@ -0,0 +1,120 @@ +const Ghostty = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); +const SharedDeps = @import("SharedDeps.zig"); + +/// The primary Ghostty executable. +exe: *std.Build.Step.Compile, + +/// The install step for the executable. +install_step: *std.Build.Step.InstallArtifact, + +pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty { + const exe: *std.Build.Step.Compile = b.addExecutable(.{ + .name = "ghostty", + .root_source_file = b.path("src/main.zig"), + .target = cfg.target, + .optimize = cfg.optimize, + .strip = cfg.strip, + }); + const install_step = b.addInstallArtifact(exe, .{}); + + // Set PIE if requested + if (cfg.pie) exe.pie = true; + + // Add the shared dependencies + _ = try deps.add(exe); + + // Check for possible issues + try checkNixShell(exe, cfg); + + // Patch our rpath if that option is specified. + if (cfg.patch_rpath) |rpath| { + if (rpath.len > 0) { + const run = std.Build.Step.Run.create(b, "patchelf rpath"); + run.addArgs(&.{ "patchelf", "--set-rpath", rpath }); + run.addArtifactArg(exe); + install_step.step.dependOn(&run.step); + } + } + + // OS-specific + switch (cfg.target.result.os.tag) { + .windows => { + exe.subsystem = .Windows; + exe.addWin32ResourceFile(.{ + .file = b.path("dist/windows/ghostty.rc"), + }); + }, + + else => {}, + } + + return .{ + .exe = exe, + .install_step = install_step, + }; +} + +/// Add the ghostty exe to the install target. +pub fn install(self: *const Ghostty) void { + const b = self.install_step.step.owner; + b.getInstallStep().dependOn(&self.install_step.step); +} + +/// If we're in NixOS but not in the shell environment then we issue +/// a warning because the rpath may not be setup properly. This doesn't modify +/// our build in any way but addresses a common build-from-source issue +/// for a subset of users. +fn checkNixShell(exe: *std.Build.Step.Compile, cfg: *const Config) !void { + // Non-Linux doesn't have rpath issues. + if (cfg.target.result.os.tag != .linux) return; + + // When cross-compiling, we don't need to worry about matching our + // Nix shell rpath since the resulting binary will be run on a + // separate system. + if (!cfg.target.query.isNativeCpu()) return; + if (!cfg.target.query.isNativeOs()) return; + + // Verify we're in NixOS + std.fs.accessAbsolute("/etc/NIXOS", .{}) catch return; + + // If we're in a nix shell, not a problem + if (cfg.env.get("IN_NIX_SHELL") != null) return; + + try exe.step.addError( + "\x1b[" ++ color_map.get("yellow").? ++ + "\x1b[" ++ color_map.get("d").? ++ + \\Detected building on and for NixOS outside of the Nix shell environment. + \\ + \\The resulting ghostty binary will likely fail on launch because it is + \\unable to dynamically load the windowing libs (X11, Wayland, etc.). + \\We highly recommend running only within the Nix build environment + \\and the resulting binary will be portable across your system. + \\ + \\To run in the Nix build environment, use the following command. + \\Append any additional options like (`-Doptimize` flags). The resulting + \\binary will be in zig-out as usual. + \\ + \\ nix develop -c zig build + \\ + ++ + "\x1b[0m", + .{}, + ); +} + +/// ANSI escape codes for colored log output +const color_map = std.StaticStringMap([]const u8).initComptime(.{ + &.{ "black", "30m" }, + &.{ "blue", "34m" }, + &.{ "b", "1m" }, + &.{ "d", "2m" }, + &.{ "cyan", "36m" }, + &.{ "green", "32m" }, + &.{ "magenta", "35m" }, + &.{ "red", "31m" }, + &.{ "white", "37m" }, + &.{ "yellow", "33m" }, +}); diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig new file mode 100644 index 000000000..53aee0e24 --- /dev/null +++ b/src/build/GhosttyLib.zig @@ -0,0 +1,110 @@ +const GhosttyLib = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); +const SharedDeps = @import("SharedDeps.zig"); +const LibtoolStep = @import("LibtoolStep.zig"); +const LipoStep = @import("LipoStep.zig"); + +/// The step that generates the file. +step: *std.Build.Step, + +/// The final static library file +output: std.Build.LazyPath, + +pub fn initStatic( + b: *std.Build, + deps: *const SharedDeps, +) !GhosttyLib { + const lib = b.addStaticLibrary(.{ + .name = "ghostty", + .root_source_file = b.path("src/main_c.zig"), + .target = deps.config.target, + .optimize = deps.config.optimize, + }); + lib.bundle_compiler_rt = true; + lib.linkLibC(); + + // Add our dependencies. Get the list of all static deps so we can + // build a combined archive if necessary. + var lib_list = try deps.add(lib); + try lib_list.append(lib.getEmittedBin()); + + if (!deps.config.target.result.isDarwin()) return .{ + .step = &lib.step, + .output = lib.getEmittedBin(), + }; + + // Create a static lib that contains all our dependencies. + const libtool = LibtoolStep.create(b, .{ + .name = "ghostty", + .out_name = "libghostty-fat.a", + .sources = lib_list.items, + }); + libtool.step.dependOn(&lib.step); + + return .{ + .step = libtool.step, + .output = libtool.output, + }; +} + +pub fn initShared( + b: *std.Build, + deps: *const SharedDeps, +) !GhosttyLib { + const lib = b.addSharedLibrary(.{ + .name = "ghostty", + .root_source_file = b.path("src/main_c.zig"), + .target = deps.config.target, + .optimize = deps.config.optimize, + .strip = deps.config.strip, + }); + _ = try deps.add(lib); + + return .{ + .step = &lib.step, + .output = lib.getEmittedBin(), + }; +} + +pub fn initMacOSUniversal( + b: *std.Build, + original_deps: *const SharedDeps, +) !GhosttyLib { + const aarch64 = try initStatic(b, &try original_deps.retarget( + b, + Config.genericMacOSTarget(b, .aarch64), + )); + const x86_64 = try initStatic(b, &try original_deps.retarget( + b, + Config.genericMacOSTarget(b, .x86_64), + )); + + const universal = LipoStep.create(b, .{ + .name = "ghostty", + .out_name = "libghostty.a", + .input_a = aarch64.output, + .input_b = x86_64.output, + }); + + return .{ + .step = universal.step, + .output = universal.output, + }; +} + +pub fn install(self: *const GhosttyLib, name: []const u8) void { + const b = self.step.owner; + const lib_install = b.addInstallLibFile(self.output, name); + b.getInstallStep().dependOn(&lib_install.step); +} + +pub fn installHeader(self: *const GhosttyLib) void { + const b = self.step.owner; + const header_install = b.addInstallHeaderFile( + b.path("include/ghostty.h"), + "ghostty.h", + ); + b.getInstallStep().dependOn(&header_install.step); +} diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig new file mode 100644 index 000000000..9c5f7f809 --- /dev/null +++ b/src/build/GhosttyResources.zig @@ -0,0 +1,257 @@ +const GhosttyResources = @This(); + +const std = @import("std"); +const buildpkg = @import("main.zig"); +const Config = @import("Config.zig"); +const config_vim = @import("../config/vim.zig"); +const config_sublime_syntax = @import("../config/sublime_syntax.zig"); +const terminfo = @import("../terminfo/main.zig"); +const RunStep = std.Build.Step.Run; + +steps: []*std.Build.Step, + +pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { + var steps = std.ArrayList(*std.Build.Step).init(b.allocator); + errdefer steps.deinit(); + + // Terminfo + terminfo: { + // Encode our terminfo + var str = std.ArrayList(u8).init(b.allocator); + defer str.deinit(); + try terminfo.ghostty.encode(str.writer()); + + // Write it + var wf = b.addWriteFiles(); + const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items); + const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo"); + try steps.append(&src_install.step); + + // Windows doesn't have the binaries below. + if (cfg.target.result.os.tag == .windows) break :terminfo; + + // Convert to termcap source format if thats helpful to people and + // install it. The resulting value here is the termcap source in case + // that is used for other commands. + { + const run_step = RunStep.create(b, "infotocap"); + run_step.addArg("infotocap"); + run_step.addFileArg(src_source); + const out_source = run_step.captureStdOut(); + _ = run_step.captureStdErr(); // so we don't see stderr + + const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap"); + try steps.append(&cap_install.step); + } + + // Compile the terminfo source into a terminfo database + { + const run_step = RunStep.create(b, "tic"); + run_step.addArgs(&.{ "tic", "-x", "-o" }); + const path = run_step.addOutputFileArg("terminfo"); + run_step.addFileArg(src_source); + _ = run_step.captureStdErr(); // so we don't see stderr + + // Depend on the terminfo source install step so that Zig build + // creates the "share" directory for us. + run_step.step.dependOn(&src_install.step); + + { + // Use cp -R instead of Step.InstallDir because we need to preserve + // symlinks in the terminfo database. Zig's InstallDir step doesn't + // handle symlinks correctly yet. + const copy_step = RunStep.create(b, "copy terminfo db"); + copy_step.addArgs(&.{ "cp", "-R" }); + copy_step.addFileArg(path); + copy_step.addArg(b.fmt("{s}/share", .{b.install_path})); + try steps.append(©_step.step); + } + } + } + + // Shell-integration + { + const install_step = b.addInstallDirectory(.{ + .source_dir = b.path("src/shell-integration"), + .install_dir = .{ .custom = "share" }, + .install_subdir = b.pathJoin(&.{ "ghostty", "shell-integration" }), + .exclude_extensions = &.{".md"}, + }); + try steps.append(&install_step.step); + } + + // Themes + { + const upstream = b.dependency("iterm2_themes", .{}); + const install_step = b.addInstallDirectory(.{ + .source_dir = upstream.path("ghostty"), + .install_dir = .{ .custom = "share" }, + .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), + .exclude_extensions = &.{".md"}, + }); + try steps.append(&install_step.step); + } + + // Fish shell completions + { + const wf = b.addWriteFiles(); + _ = wf.add("ghostty.fish", buildpkg.fish_completions); + + const install_step = b.addInstallDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/fish/vendor_completions.d", + }); + try steps.append(&install_step.step); + } + + // zsh shell completions + { + const wf = b.addWriteFiles(); + _ = wf.add("_ghostty", buildpkg.zsh_completions); + + const install_step = b.addInstallDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/zsh/site-functions", + }); + try steps.append(&install_step.step); + } + + // bash shell completions + { + const wf = b.addWriteFiles(); + _ = wf.add("ghostty.bash", buildpkg.bash_completions); + + const install_step = b.addInstallDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/bash-completion/completions", + }); + try steps.append(&install_step.step); + } + + // Vim plugin + { + const wf = b.addWriteFiles(); + _ = wf.add("syntax/ghostty.vim", config_vim.syntax); + _ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect); + _ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin); + _ = wf.add("compiler/ghostty.vim", config_vim.compiler); + + const install_step = b.addInstallDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/vim/vimfiles", + }); + try steps.append(&install_step.step); + } + + // Neovim plugin + // This is just a copy-paste of the Vim plugin, but using a Neovim subdir. + // By default, Neovim doesn't look inside share/vim/vimfiles. Some distros + // configure it to do that however. Fedora, does not as a counterexample. + { + const wf = b.addWriteFiles(); + _ = wf.add("syntax/ghostty.vim", config_vim.syntax); + _ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect); + _ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin); + _ = wf.add("compiler/ghostty.vim", config_vim.compiler); + + const install_step = b.addInstallDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/nvim/site", + }); + try steps.append(&install_step.step); + } + + // Sublime syntax highlighting for bat cli tool + // NOTE: The current implementation requires symlinking the generated + // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' + // directory. The syntax then needs to be mapped to the correct language in + // the config file within the '~.config/bat' directory + // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). + { + const wf = b.addWriteFiles(); + _ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax); + + const install_step = b.addInstallDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/bat/syntaxes", + }); + try steps.append(&install_step.step); + } + + // App (Linux) + if (cfg.target.result.os.tag == .linux) { + // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html + + // Desktop file so that we have an icon and other metadata + try steps.append(&b.addInstallFile( + b.path("dist/linux/app.desktop"), + "share/applications/com.mitchellh.ghostty.desktop", + ).step); + + // Right click menu action for Plasma desktop + try steps.append(&b.addInstallFile( + b.path("dist/linux/ghostty_dolphin.desktop"), + "share/kio/servicemenus/com.mitchellh.ghostty.desktop", + ).step); + + // Various icons that our application can use, including the icon + // that will be used for the desktop. + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_16.png"), + "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_32.png"), + "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_128.png"), + "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_256.png"), + "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_512.png"), + "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png", + ).step); + // Flatpaks only support icons up to 512x512. + if (!cfg.flatpak) { + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_1024.png"), + "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png", + ).step); + } + + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_16@2x.png"), + "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_32@2x.png"), + "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_128@2x.png"), + "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_256@2x.png"), + "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png", + ).step); + } + + return .{ .steps = steps.items }; +} + +pub fn install(self: *const GhosttyResources) void { + const b = self.steps[0].owner; + for (self.steps) |step| b.getInstallStep().dependOn(step); +} diff --git a/src/build/GhosttyWebdata.zig b/src/build/GhosttyWebdata.zig new file mode 100644 index 000000000..6e0acaf17 --- /dev/null +++ b/src/build/GhosttyWebdata.zig @@ -0,0 +1,82 @@ +//! GhosttyWebdata generates all the Ghostty website data that is +//! merged with the website for things like config references. +const GhosttyWebdata = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); +const SharedDeps = @import("SharedDeps.zig"); + +steps: []*std.Build.Step, + +pub fn init( + b: *std.Build, + deps: *const SharedDeps, +) !GhosttyWebdata { + var steps = std.ArrayList(*std.Build.Step).init(b.allocator); + errdefer steps.deinit(); + + { + const webgen_config = b.addExecutable(.{ + .name = "webgen_config", + .root_source_file = b.path("src/main.zig"), + .target = b.host, + }); + deps.help_strings.addImport(webgen_config); + + { + const buildconfig = config: { + var copy = deps.config.*; + copy.exe_entrypoint = .webgen_config; + break :config copy; + }; + + const options = b.addOptions(); + try buildconfig.addOptions(options); + webgen_config.root_module.addOptions("build_options", options); + } + + const webgen_config_step = b.addRunArtifact(webgen_config); + const webgen_config_out = webgen_config_step.captureStdOut(); + + try steps.append(&b.addInstallFile( + webgen_config_out, + "share/ghostty/webdata/config.mdx", + ).step); + } + + { + const webgen_actions = b.addExecutable(.{ + .name = "webgen_actions", + .root_source_file = b.path("src/main.zig"), + .target = b.host, + }); + deps.help_strings.addImport(webgen_actions); + + { + const buildconfig = config: { + var copy = deps.config.*; + copy.exe_entrypoint = .webgen_actions; + break :config copy; + }; + + const options = b.addOptions(); + try buildconfig.addOptions(options); + webgen_actions.root_module.addOptions("build_options", options); + } + + const webgen_actions_step = b.addRunArtifact(webgen_actions); + const webgen_actions_out = webgen_actions_step.captureStdOut(); + + try steps.append(&b.addInstallFile( + webgen_actions_out, + "share/ghostty/webdata/actions.mdx", + ).step); + } + + return .{ .steps = steps.items }; +} + +pub fn install(self: *const GhosttyWebdata) void { + const b = self.steps[0].owner; + for (self.steps) |step| b.getInstallStep().dependOn(step); +} diff --git a/src/build/GhosttyXCFramework.zig b/src/build/GhosttyXCFramework.zig new file mode 100644 index 000000000..38bc2c43f --- /dev/null +++ b/src/build/GhosttyXCFramework.zig @@ -0,0 +1,68 @@ +const GhosttyXCFramework = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); +const SharedDeps = @import("SharedDeps.zig"); +const GhosttyLib = @import("GhosttyLib.zig"); +const XCFrameworkStep = @import("XCFrameworkStep.zig"); + +xcframework: *XCFrameworkStep, +macos: GhosttyLib, + +pub fn init(b: *std.Build, deps: *const SharedDeps) !GhosttyXCFramework { + // Create our universal macOS static library. + const macos = try GhosttyLib.initMacOSUniversal(b, deps); + + // iOS + const ios = try GhosttyLib.initStatic(b, &try deps.retarget( + b, + b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = .ios, + .os_version_min = Config.osVersionMin(.ios), + .abi = null, + }), + )); + + // iOS Simulator + const ios_sim = try GhosttyLib.initStatic(b, &try deps.retarget( + b, + b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = .ios, + .os_version_min = Config.osVersionMin(.ios), + .abi = .simulator, + }), + )); + + // The xcframework wraps our ghostty library so that we can link + // it to the final app built with Swift. + const xcframework = XCFrameworkStep.create(b, .{ + .name = "GhosttyKit", + .out_path = "macos/GhosttyKit.xcframework", + .libraries = &.{ + .{ + .library = macos.output, + .headers = b.path("include"), + }, + .{ + .library = ios.output, + .headers = b.path("include"), + }, + .{ + .library = ios_sim.output, + .headers = b.path("include"), + }, + }, + }); + + return .{ + .xcframework = xcframework, + .macos = macos, + }; +} + +pub fn install(self: *const GhosttyXCFramework) void { + const b = self.xcframework.step.owner; + b.getInstallStep().dependOn(self.xcframework.step); +} diff --git a/src/build/Version.zig b/src/build/GitVersion.zig similarity index 100% rename from src/build/Version.zig rename to src/build/GitVersion.zig diff --git a/src/build/HelpStrings.zig b/src/build/HelpStrings.zig new file mode 100644 index 000000000..0244670cc --- /dev/null +++ b/src/build/HelpStrings.zig @@ -0,0 +1,46 @@ +const HelpStrings = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); + +/// The "helpgen" exe. +exe: *std.Build.Step.Compile, + +/// The output path for the help strings. +output: std.Build.LazyPath, + +pub fn init(b: *std.Build, cfg: *const Config) !HelpStrings { + const exe = b.addExecutable(.{ + .name = "helpgen", + .root_source_file = b.path("src/helpgen.zig"), + .target = b.host, + }); + + const help_config = config: { + var copy = cfg.*; + copy.exe_entrypoint = .helpgen; + break :config copy; + }; + const options = b.addOptions(); + try help_config.addOptions(options); + exe.root_module.addOptions("build_options", options); + + const help_run = b.addRunArtifact(exe); + return .{ + .exe = exe, + .output = help_run.captureStdOut(), + }; +} + +/// Add the "help_strings" import. +pub fn addImport(self: *const HelpStrings, step: *std.Build.Step.Compile) void { + self.output.addStepDependencies(&step.step); + step.root_module.addAnonymousImport("help_strings", .{ + .root_source_file = self.output, + }); +} + +/// Install the help exe +pub fn install(self: *const HelpStrings) void { + self.exe.step.owner.installArtifact(self.exe); +} diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index 587d276c1..12adf3edb 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -21,13 +21,13 @@ pub const Options = struct { step: *Step, output: LazyPath, -pub fn create(b: *std.Build, opts: Options) *MetallibStep { +pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { const self = b.allocator.create(MetallibStep) catch @panic("OOM"); const sdk = switch (opts.target.result.os.tag) { .macos => "macosx", .ios => "iphoneos", - else => @panic("unsupported metallib OS"), + else => return null, }; const min_version = if (opts.target.query.os_version_min) |v| diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig new file mode 100644 index 000000000..077da96a6 --- /dev/null +++ b/src/build/SharedDeps.zig @@ -0,0 +1,510 @@ +const SharedDeps = @This(); + +const std = @import("std"); +const Scanner = @import("zig_wayland").Scanner; +const Config = @import("Config.zig"); +const HelpStrings = @import("HelpStrings.zig"); +const MetallibStep = @import("MetallibStep.zig"); +const UnicodeTables = @import("UnicodeTables.zig"); + +config: *const Config, + +options: *std.Build.Step.Options, +help_strings: HelpStrings, +metallib: ?*MetallibStep, +unicode_tables: UnicodeTables, + +/// Used to keep track of a list of file sources. +pub const LazyPathList = std.ArrayList(std.Build.LazyPath); + +pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { + var result: SharedDeps = .{ + .config = cfg, + .help_strings = try HelpStrings.init(b, cfg), + .unicode_tables = try UnicodeTables.init(b), + + // Setup by retarget + .options = undefined, + .metallib = undefined, + }; + try result.initTarget(b, cfg.target); + return result; +} + +/// Retarget our dependencies for another build target. Modifies in-place. +pub fn retarget( + self: *const SharedDeps, + b: *std.Build, + target: std.Build.ResolvedTarget, +) !SharedDeps { + var result = self.*; + try result.initTarget(b, target); + return result; +} + +/// Change the exe entrypoint. +pub fn changeEntrypoint( + self: *const SharedDeps, + b: *std.Build, + entrypoint: Config.ExeEntrypoint, +) !SharedDeps { + // Change our config + const config = try b.allocator.create(Config); + config.* = self.config.*; + config.exe_entrypoint = entrypoint; + + var result = self.*; + result.config = config; + return result; +} + +fn initTarget( + self: *SharedDeps, + b: *std.Build, + target: std.Build.ResolvedTarget, +) !void { + // Update our metallib + self.metallib = MetallibStep.create(b, .{ + .name = "Ghostty", + .target = target, + .sources = &.{b.path("src/renderer/shaders/cell.metal")}, + }); + + // Change our config + const config = try b.allocator.create(Config); + config.* = self.config.*; + config.target = target; + self.config = config; + + // Setup our shared build options + self.options = b.addOptions(); + try self.config.addOptions(self.options); +} + +pub fn add( + self: *const SharedDeps, + step: *std.Build.Step.Compile, +) !LazyPathList { + const b = step.step.owner; + + // We could use our config.target/optimize fields here but its more + // correct to always match our step. + const target = step.root_module.resolved_target.?; + const optimize = step.root_module.optimize.?; + + // We maintain a list of our static libraries and return it so that + // we can build a single fat static library for the final app. + var static_libs = LazyPathList.init(b.allocator); + errdefer static_libs.deinit(); + + // Every exe gets build options populated + step.root_module.addOptions("build_options", self.options); + + // Freetype + _ = b.systemIntegrationOption("freetype", .{}); // Shows it in help + if (self.config.font_backend.hasFreetype()) { + const freetype_dep = b.dependency("freetype", .{ + .target = target, + .optimize = optimize, + .@"enable-libpng" = true, + }); + step.root_module.addImport("freetype", freetype_dep.module("freetype")); + + if (b.systemIntegrationOption("freetype", .{})) { + step.linkSystemLibrary2("bzip2", dynamic_link_opts); + step.linkSystemLibrary2("freetype2", dynamic_link_opts); + } else { + step.linkLibrary(freetype_dep.artifact("freetype")); + try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin()); + } + } + + // Harfbuzz + _ = b.systemIntegrationOption("harfbuzz", .{}); // Shows it in help + if (self.config.font_backend.hasHarfbuzz()) { + const harfbuzz_dep = b.dependency("harfbuzz", .{ + .target = target, + .optimize = optimize, + .@"enable-freetype" = true, + .@"enable-coretext" = self.config.font_backend.hasCoretext(), + }); + + step.root_module.addImport( + "harfbuzz", + harfbuzz_dep.module("harfbuzz"), + ); + if (b.systemIntegrationOption("harfbuzz", .{})) { + step.linkSystemLibrary2("harfbuzz", dynamic_link_opts); + } else { + step.linkLibrary(harfbuzz_dep.artifact("harfbuzz")); + try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin()); + } + } + + // Fontconfig + _ = b.systemIntegrationOption("fontconfig", .{}); // Shows it in help + if (self.config.font_backend.hasFontconfig()) { + const fontconfig_dep = b.dependency("fontconfig", .{ + .target = target, + .optimize = optimize, + }); + step.root_module.addImport( + "fontconfig", + fontconfig_dep.module("fontconfig"), + ); + + if (b.systemIntegrationOption("fontconfig", .{})) { + step.linkSystemLibrary2("fontconfig", dynamic_link_opts); + } else { + step.linkLibrary(fontconfig_dep.artifact("fontconfig")); + try static_libs.append(fontconfig_dep.artifact("fontconfig").getEmittedBin()); + } + } + + // Libpng - Ghostty doesn't actually use this directly, its only used + // through dependencies, so we only need to add it to our static + // libs list if we're not using system integration. The dependencies + // will handle linking it. + if (!b.systemIntegrationOption("libpng", .{})) { + const libpng_dep = b.dependency("libpng", .{ + .target = target, + .optimize = optimize, + }); + step.linkLibrary(libpng_dep.artifact("png")); + try static_libs.append(libpng_dep.artifact("png").getEmittedBin()); + } + + // Zlib - same as libpng, only used through dependencies. + if (!b.systemIntegrationOption("zlib", .{})) { + const zlib_dep = b.dependency("zlib", .{ + .target = target, + .optimize = optimize, + }); + step.linkLibrary(zlib_dep.artifact("z")); + try static_libs.append(zlib_dep.artifact("z").getEmittedBin()); + } + + // Oniguruma + const oniguruma_dep = b.dependency("oniguruma", .{ + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma")); + if (b.systemIntegrationOption("oniguruma", .{})) { + step.linkSystemLibrary2("oniguruma", dynamic_link_opts); + } else { + step.linkLibrary(oniguruma_dep.artifact("oniguruma")); + try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin()); + } + + // Glslang + const glslang_dep = b.dependency("glslang", .{ + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("glslang", glslang_dep.module("glslang")); + if (b.systemIntegrationOption("glslang", .{})) { + step.linkSystemLibrary2("glslang", dynamic_link_opts); + step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts); + } else { + step.linkLibrary(glslang_dep.artifact("glslang")); + try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); + } + + // Spirv-cross + const spirv_cross_dep = b.dependency("spirv_cross", .{ + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("spirv_cross", spirv_cross_dep.module("spirv_cross")); + if (b.systemIntegrationOption("spirv-cross", .{})) { + step.linkSystemLibrary2("spirv-cross", dynamic_link_opts); + } else { + step.linkLibrary(spirv_cross_dep.artifact("spirv_cross")); + try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin()); + } + + // Simdutf + if (b.systemIntegrationOption("simdutf", .{})) { + step.linkSystemLibrary2("simdutf", dynamic_link_opts); + } else { + const simdutf_dep = b.dependency("simdutf", .{ + .target = target, + .optimize = optimize, + }); + step.linkLibrary(simdutf_dep.artifact("simdutf")); + try static_libs.append(simdutf_dep.artifact("simdutf").getEmittedBin()); + } + + // Sentry + if (self.config.sentry) { + const sentry_dep = b.dependency("sentry", .{ + .target = target, + .optimize = optimize, + .backend = .breakpad, + }); + + step.root_module.addImport("sentry", sentry_dep.module("sentry")); + + // Sentry + step.linkLibrary(sentry_dep.artifact("sentry")); + try static_libs.append(sentry_dep.artifact("sentry").getEmittedBin()); + + // We also need to include breakpad in the static libs. + const breakpad_dep = sentry_dep.builder.dependency("breakpad", .{ + .target = target, + .optimize = optimize, + }); + try static_libs.append(breakpad_dep.artifact("breakpad").getEmittedBin()); + } + + // Wasm we do manually since it is such a different build. + if (step.rootModuleTarget().cpu.arch == .wasm32) { + const js_dep = b.dependency("zig_js", .{ + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("zig-js", js_dep.module("zig-js")); + + return static_libs; + } + + // On Linux, we need to add a couple common library paths that aren't + // on the standard search list. i.e. GTK is often in /usr/lib/x86_64-linux-gnu + // on x86_64. + if (step.rootModuleTarget().os.tag == .linux) { + const triple = try step.rootModuleTarget().linuxTriple(b.allocator); + step.addLibraryPath(.{ .cwd_relative = b.fmt("/usr/lib/{s}", .{triple}) }); + } + + // C files + step.linkLibC(); + step.addIncludePath(b.path("src/stb")); + step.addCSourceFiles(.{ .files = &.{"src/stb/stb.c"} }); + if (step.rootModuleTarget().os.tag == .linux) { + step.addIncludePath(b.path("src/apprt/gtk")); + } + + // C++ files + step.linkLibCpp(); + step.addIncludePath(b.path("src")); + { + // From hwy/detect_targets.h + const HWY_AVX3_SPR: c_int = 1 << 4; + const HWY_AVX3_ZEN4: c_int = 1 << 6; + const HWY_AVX3_DL: c_int = 1 << 7; + const HWY_AVX3: c_int = 1 << 8; + + // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 + // To workaround this we just disable AVX512 support completely. + // The performance difference between AVX2 and AVX512 is not + // significant for our use case and AVX512 is very rare on consumer + // hardware anyways. + const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3; + + step.addCSourceFiles(.{ + .files = &.{ + "src/simd/base64.cpp", + "src/simd/codepoint_width.cpp", + "src/simd/index_of.cpp", + "src/simd/vt.cpp", + }, + .flags = if (step.rootModuleTarget().cpu.arch == .x86_64) &.{ + b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}), + } else &.{}, + }); + } + + // We always require the system SDK so that our system headers are available. + // This makes things like `os/log.h` available for cross-compiling. + if (step.rootModuleTarget().isDarwin()) { + try @import("apple_sdk").addPaths(b, &step.root_module); + + const metallib = self.metallib.?; + metallib.output.addStepDependencies(&step.step); + step.root_module.addAnonymousImport("ghostty_metallib", .{ + .root_source_file = metallib.output, + }); + } + + // Other dependencies, mostly pure Zig + step.root_module.addImport("opengl", b.dependency( + "opengl", + .{}, + ).module("opengl")); + step.root_module.addImport("vaxis", b.dependency("vaxis", .{ + .target = target, + .optimize = optimize, + }).module("vaxis")); + step.root_module.addImport("wuffs", b.dependency("wuffs", .{ + .target = target, + .optimize = optimize, + }).module("wuffs")); + step.root_module.addImport("xev", b.dependency("libxev", .{ + .target = target, + .optimize = optimize, + }).module("xev")); + step.root_module.addImport("z2d", b.addModule("z2d", .{ + .root_source_file = b.dependency("z2d", .{}).path("src/z2d.zig"), + .target = target, + .optimize = optimize, + })); + step.root_module.addImport("ziglyph", b.dependency("ziglyph", .{ + .target = target, + .optimize = optimize, + }).module("ziglyph")); + step.root_module.addImport("zf", b.dependency("zf", .{ + .target = target, + .optimize = optimize, + .with_tui = false, + }).module("zf")); + + // Mac Stuff + if (step.rootModuleTarget().isDarwin()) { + const objc_dep = b.dependency("zig_objc", .{ + .target = target, + .optimize = optimize, + }); + const macos_dep = b.dependency("macos", .{ + .target = target, + .optimize = optimize, + }); + + step.root_module.addImport("objc", objc_dep.module("objc")); + step.root_module.addImport("macos", macos_dep.module("macos")); + step.linkLibrary(macos_dep.artifact("macos")); + try static_libs.append(macos_dep.artifact("macos").getEmittedBin()); + + if (self.config.renderer == .opengl) { + step.linkFramework("OpenGL"); + } + } + + // cimgui + const cimgui_dep = b.dependency("cimgui", .{ + .target = target, + .optimize = optimize, + }); + step.root_module.addImport("cimgui", cimgui_dep.module("cimgui")); + step.linkLibrary(cimgui_dep.artifact("cimgui")); + try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin()); + + // Highway + const highway_dep = b.dependency("highway", .{ + .target = target, + .optimize = optimize, + }); + step.linkLibrary(highway_dep.artifact("highway")); + try static_libs.append(highway_dep.artifact("highway").getEmittedBin()); + + // utfcpp - This is used as a dependency on our hand-written C++ code + const utfcpp_dep = b.dependency("utfcpp", .{ + .target = target, + .optimize = optimize, + }); + step.linkLibrary(utfcpp_dep.artifact("utfcpp")); + try static_libs.append(utfcpp_dep.artifact("utfcpp").getEmittedBin()); + + // If we're building an exe then we have additional dependencies. + if (step.kind != .lib) { + // We always statically compile glad + step.addIncludePath(b.path("vendor/glad/include/")); + step.addCSourceFile(.{ + .file = b.path("vendor/glad/src/gl.c"), + .flags = &.{}, + }); + + // When we're targeting flatpak we ALWAYS link GTK so we + // get access to glib for dbus. + if (self.config.flatpak) step.linkSystemLibrary2("gtk4", dynamic_link_opts); + + switch (self.config.app_runtime) { + .none => {}, + + .glfw => glfw: { + const mach_glfw_dep = b.lazyDependency("mach_glfw", .{ + .target = target, + .optimize = optimize, + }) orelse break :glfw; + step.root_module.addImport("glfw", mach_glfw_dep.module("mach-glfw")); + }, + + .gtk => { + step.linkSystemLibrary2("gtk4", dynamic_link_opts); + if (self.config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); + if (self.config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts); + + if (self.config.wayland) { + const scanner = Scanner.create(b, .{ + // We shouldn't be using getPath but we need to for now + // https://codeberg.org/ifreund/zig-wayland/issues/66 + .wayland_xml_path = b.dependency("wayland", .{}) + .path("protocol/wayland.xml") + .getPath(b), + .wayland_protocols_path = b.dependency("wayland_protocols", .{}) + .path("") + .getPath(b), + }); + + const wayland = b.createModule(.{ .root_source_file = scanner.result }); + + const plasma_wayland_protocols = b.dependency("plasma_wayland_protocols", .{ + .target = target, + .optimize = optimize, + }); + scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml")); + + scanner.generate("wl_compositor", 1); + scanner.generate("org_kde_kwin_blur_manager", 1); + + step.root_module.addImport("wayland", wayland); + step.linkSystemLibrary2("wayland-client", dynamic_link_opts); + } + + { + const gresource = @import("../apprt/gtk/gresource.zig"); + + const wf = b.addWriteFiles(); + const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml); + + const generate_resources_c = b.addSystemCommand(&.{ + "glib-compile-resources", + "--c-name", + "ghostty", + "--generate-source", + "--target", + }); + const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c"); + generate_resources_c.addFileArg(gresource_xml); + generate_resources_c.extra_file_dependencies = &gresource.dependencies; + step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} }); + + const generate_resources_h = b.addSystemCommand(&.{ + "glib-compile-resources", + "--c-name", + "ghostty", + "--generate-header", + "--target", + }); + const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h"); + generate_resources_h.addFileArg(gresource_xml); + generate_resources_h.extra_file_dependencies = &gresource.dependencies; + step.addIncludePath(ghostty_resources_h.dirname()); + } + }, + } + } + + self.help_strings.addImport(step); + self.unicode_tables.addImport(step); + + return static_libs; +} + +// For dynamic linking, we prefer dynamic linking and to search by +// mode first. Mode first will search all paths for a dynamic library +// before falling back to static. +const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ + .preferred_link_mode = .dynamic, + .search_strategy = .mode_first, +}; diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig new file mode 100644 index 000000000..0159de442 --- /dev/null +++ b/src/build/UnicodeTables.zig @@ -0,0 +1,43 @@ +const UnicodeTables = @This(); + +const std = @import("std"); +const Config = @import("Config.zig"); + +/// The exe. +exe: *std.Build.Step.Compile, + +/// The output path for the unicode tables +output: std.Build.LazyPath, + +pub fn init(b: *std.Build) !UnicodeTables { + const exe = b.addExecutable(.{ + .name = "unigen", + .root_source_file = b.path("src/unicode/props.zig"), + .target = b.host, + }); + exe.linkLibC(); + + const ziglyph_dep = b.dependency("ziglyph", .{ + .target = b.host, + }); + exe.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph")); + + const run = b.addRunArtifact(exe); + return .{ + .exe = exe, + .output = run.captureStdOut(), + }; +} + +/// Add the "unicode_tables" import. +pub fn addImport(self: *const UnicodeTables, step: *std.Build.Step.Compile) void { + self.output.addStepDependencies(&step.step); + step.root_module.addAnonymousImport("unicode_tables", .{ + .root_source_file = self.output, + }); +} + +/// Install the exe +pub fn install(self: *const UnicodeTables, b: *std.Build) void { + b.installArtifact(self.exe); +} diff --git a/src/build/bash_completions.zig b/src/build/bash_completions.zig index 6649bcb01..86c2dc3cf 100644 --- a/src/build/bash_completions.zig +++ b/src/build/bash_completions.zig @@ -14,7 +14,7 @@ const Action = @import("../cli/action.zig").Action; /// it's part of an on going completion like --=. Working around this requires looking /// backward in the command line args to pretend the = is an empty string /// see: https://www.gnu.org/software/gnuastro/manual/html_node/Bash-TAB-completion-tutorial.html -pub const bash_completions = comptimeGenerateBashCompletions(); +pub const completions = comptimeGenerateBashCompletions(); fn comptimeGenerateBashCompletions() []const u8 { comptime { @@ -319,7 +319,7 @@ fn writeBashCompletions(writer: anytype) !void { \\ # clear out prev so we don't run any of the key specific completions \\ prev="" \\ fi - \\ + \\ \\ case "${COMP_WORDS[1]}" in \\ --*) _handle_config ;; \\ +*) _handle_actions ;; diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index b75c4dd16..dca119c6f 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -5,7 +5,7 @@ const Action = @import("../cli/action.zig").Action; /// A fish completions configuration that contains all the available commands /// and options. -pub const fish_completions = comptimeGenerateFishCompletions(); +pub const completions = comptimeGenerateFishCompletions(); fn comptimeGenerateFishCompletions() []const u8 { comptime { diff --git a/src/build/gtk.zig b/src/build/gtk.zig new file mode 100644 index 000000000..f33219988 --- /dev/null +++ b/src/build/gtk.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub const Targets = packed struct { + x11: bool = false, + wayland: bool = false, +}; + +/// Returns the targets that GTK4 was compiled with. +pub fn targets(b: *std.Build) Targets { + // Run pkg-config. We allow it to fail so that zig build --help + // works without all dependencies. The build will fail later when + // GTK isn't found anyways. + var code: u8 = undefined; + const output = b.runAllowFail( + &.{ "pkg-config", "--variable=targets", "gtk4" }, + &code, + .Ignore, + ) catch return .{}; + + return .{ + .x11 = std.mem.indexOf(u8, output, "x11") != null, + .wayland = std.mem.indexOf(u8, output, "wayland") != null, + }; +} diff --git a/src/build/main.zig b/src/build/main.zig new file mode 100644 index 000000000..8228abfbf --- /dev/null +++ b/src/build/main.zig @@ -0,0 +1,30 @@ +//! Build logic for Ghostty. A single "build.zig" file became far too complex +//! and spaghetti, so this package extracts the build logic into smaller, +//! more manageable pieces. + +pub const gtk = @import("gtk.zig"); +pub const Config = @import("Config.zig"); +pub const GitVersion = @import("GitVersion.zig"); + +// Artifacts +pub const GhosttyBench = @import("GhosttyBench.zig"); +pub const GhosttyDocs = @import("GhosttyDocs.zig"); +pub const GhosttyExe = @import("GhosttyExe.zig"); +pub const GhosttyLib = @import("GhosttyLib.zig"); +pub const GhosttyResources = @import("GhosttyResources.zig"); +pub const GhosttyXCFramework = @import("GhosttyXCFramework.zig"); +pub const GhosttyWebdata = @import("GhosttyWebdata.zig"); +pub const HelpStrings = @import("HelpStrings.zig"); +pub const SharedDeps = @import("SharedDeps.zig"); +pub const UnicodeTables = @import("UnicodeTables.zig"); + +// Steps +pub const LibtoolStep = @import("LibtoolStep.zig"); +pub const LipoStep = @import("LipoStep.zig"); +pub const MetallibStep = @import("MetallibStep.zig"); +pub const XCFrameworkStep = @import("XCFrameworkStep.zig"); + +// Shell completions +pub const fish_completions = @import("fish_completions.zig").completions; +pub const zsh_completions = @import("zsh_completions.zig").completions; +pub const bash_completions = @import("bash_completions.zig").completions; diff --git a/src/build/zsh_completions.zig b/src/build/zsh_completions.zig index 5c42ea5ab..4114abc63 100644 --- a/src/build/zsh_completions.zig +++ b/src/build/zsh_completions.zig @@ -5,7 +5,7 @@ const Action = @import("../cli/action.zig").Action; /// A zsh completions configuration that contains all the available commands /// and options. -pub const zsh_completions = comptimeGenerateZshCompletions(); +pub const completions = comptimeGenerateZshCompletions(); const equals_required = "=-:::"; diff --git a/src/build_config.zig b/src/build_config.zig index 13131c132..b80247aab 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -10,88 +10,9 @@ const apprt = @import("apprt.zig"); const font = @import("font/main.zig"); const rendererpkg = @import("renderer.zig"); const WasmTarget = @import("os/wasm/target.zig").Target; +const BuildConfig = @import("build/Config.zig"); -/// The build configurations options. This may not be all available options -/// to `zig build` but it contains all the options that the Ghostty source -/// needs to know about at comptime. -/// -/// We put this all in a single struct so that we can check compatibility -/// between options, make it easy to copy and mutate options for different -/// build types, etc. -pub const BuildConfig = struct { - version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, - flatpak: bool = false, - adwaita: bool = false, - x11: bool = false, - wayland: bool = false, - sentry: bool = true, - app_runtime: apprt.Runtime = .none, - renderer: rendererpkg.Impl = .opengl, - font_backend: font.Backend = .freetype, - - /// The entrypoint for exe targets. - exe_entrypoint: ExeEntrypoint = .ghostty, - - /// The target runtime for the wasm build and whether to use wasm shared - /// memory or not. These are both legacy wasm-specific options that we - /// will probably have to revisit when we get back to work on wasm. - wasm_target: WasmTarget = .browser, - wasm_shared: bool = true, - - /// Configure the build options with our values. - pub fn addOptions(self: BuildConfig, step: *std.Build.Step.Options) !void { - // We need to break these down individual because addOption doesn't - // support all types. - step.addOption(bool, "flatpak", self.flatpak); - step.addOption(bool, "adwaita", self.adwaita); - step.addOption(bool, "x11", self.x11); - step.addOption(bool, "wayland", self.wayland); - step.addOption(bool, "sentry", self.sentry); - step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); - step.addOption(font.Backend, "font_backend", self.font_backend); - step.addOption(rendererpkg.Impl, "renderer", self.renderer); - step.addOption(ExeEntrypoint, "exe_entrypoint", self.exe_entrypoint); - step.addOption(WasmTarget, "wasm_target", self.wasm_target); - step.addOption(bool, "wasm_shared", self.wasm_shared); - - // Our version. We also add the string version so we don't need - // to do any allocations at runtime. This has to be long enough to - // accommodate realistic large branch names for dev versions. - var buf: [1024]u8 = undefined; - step.addOption(std.SemanticVersion, "app_version", self.version); - step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ( - &buf, - "{}", - .{self.version}, - )); - step.addOption( - ReleaseChannel, - "release_channel", - channel: { - const pre = self.version.pre orelse break :channel .stable; - if (pre.len == 0) break :channel .stable; - break :channel .tip; - }, - ); - } - - /// Rehydrate our BuildConfig from the comptime options. Note that not all - /// options are available at comptime, so look closely at this implementation - /// to see what is and isn't available. - pub fn fromOptions() BuildConfig { - return .{ - .version = options.app_version, - .flatpak = options.flatpak, - .adwaita = options.adwaita, - .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, - .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, - .renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?, - .exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?, - .wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?, - .wasm_shared = options.wasm_shared, - }; - } -}; +pub const ReleaseChannel = BuildConfig.ReleaseChannel; /// The semantic version of this build. pub const version = options.app_version; @@ -114,7 +35,7 @@ pub const artifact = Artifact.detect(); /// Our build configuration. We re-export a lot of these back at the /// top-level so its a bit cleaner to use throughout the code. See the doc /// comments in BuildConfig for details on each. -pub const config = BuildConfig.fromOptions(); +const config = BuildConfig.fromOptions(); pub const exe_entrypoint = config.exe_entrypoint; pub const flatpak = options.flatpak; pub const app_runtime: apprt.Runtime = config.app_runtime; @@ -175,35 +96,3 @@ pub const Artifact = enum { }; } }; - -/// The possible entrypoints for the exe artifact. This has no effect on -/// other artifact types (i.e. lib, wasm_module). -/// -/// The whole existence of this enum is to workaround the fact that Zig -/// doesn't allow the main function to be in a file in a subdirctory -/// from the "root" of the module, and I don't want to pollute our root -/// directory with a bunch of individual zig files for each entrypoint. -/// -/// Therefore, main.zig uses this to switch between the different entrypoints. -pub const ExeEntrypoint = enum { - ghostty, - helpgen, - mdgen_ghostty_1, - mdgen_ghostty_5, - webgen_config, - webgen_actions, - bench_parser, - bench_stream, - bench_codepoint_width, - bench_grapheme_break, - bench_page_init, -}; - -/// The release channel for the build. -pub const ReleaseChannel = enum { - /// Unstable builds on every commit. - tip, - - /// Stable tagged releases. - stable, -};