Merge pull request #1461 from mitchellh/build-change

build: support multiple entrypoints, get bench exes building again, per-object build options
This commit is contained in:
Mitchell Hashimoto
2024-02-04 20:45:23 -08:00
committed by GitHub
9 changed files with 434 additions and 374 deletions

106
build.zig
View File

@ -9,7 +9,8 @@ const font = @import("src/font/main.zig");
const renderer = @import("src/renderer.zig"); const renderer = @import("src/renderer.zig");
const terminfo = @import("src/terminfo/main.zig"); const terminfo = @import("src/terminfo/main.zig");
const config_vim = @import("src/config/vim.zig"); const config_vim = @import("src/config/vim.zig");
const BuildConfig = @import("src/build_config.zig").BuildConfig; const build_config = @import("src/build_config.zig");
const BuildConfig = build_config.BuildConfig;
const WasmTarget = @import("src/os/wasm/target.zig").Target; const WasmTarget = @import("src/os/wasm/target.zig").Target;
const LibtoolStep = @import("src/build/LibtoolStep.zig"); const LibtoolStep = @import("src/build/LibtoolStep.zig");
const LipoStep = @import("src/build/LipoStep.zig"); const LipoStep = @import("src/build/LipoStep.zig");
@ -159,7 +160,7 @@ pub fn build(b: *std.Build) !void {
"If not specified, git will be used. This must be a semantic version.", "If not specified, git will be used. This must be a semantic version.",
); );
const version: std.SemanticVersion = if (version_string) |v| config.version = if (version_string) |v|
try std.SemanticVersion.parse(v) try std.SemanticVersion.parse(v)
else version: { else version: {
const vsn = try Version.detect(b); const vsn = try Version.detect(b);
@ -198,7 +199,7 @@ pub fn build(b: *std.Build) !void {
// Help exe. This must be run before any dependent executables because // Help exe. This must be run before any dependent executables because
// otherwise the build will be cached without emit. That's clunky but meh. // otherwise the build will be cached without emit. That's clunky but meh.
if (emit_helpgen) addHelp(b, null); if (emit_helpgen) try addHelp(b, null, config);
// Add our benchmarks // Add our benchmarks
try benchSteps(b, target, optimize, config, emit_bench); try benchSteps(b, target, optimize, config, emit_bench);
@ -211,19 +212,8 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize, .optimize = optimize,
}) else null; }) else null;
const exe_options = b.addOptions();
config.addOptions(exe_options);
exe_options.addOption(std.SemanticVersion, "app_version", version);
exe_options.addOption([:0]const u8, "app_version_string", try std.fmt.allocPrintZ(
b.allocator,
"{}",
.{version},
));
// Exe // Exe
if (exe_) |exe| { if (exe_) |exe| {
exe.root_module.addOptions("build_options", exe_options);
// Add the shared dependencies // Add the shared dependencies
_ = try addDeps(b, exe, config); _ = try addDeps(b, exe, config);
@ -428,7 +418,7 @@ pub fn build(b: *std.Build) !void {
} }
// Documenation // Documenation
if (emit_docs) buildDocumentation(b, version); if (emit_docs) try buildDocumentation(b, config);
// App (Linux) // App (Linux)
if (target.result.os.tag == .linux and config.app_runtime != .none) { if (target.result.os.tag == .linux and config.app_runtime != .none) {
@ -464,7 +454,6 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
}); });
lib.root_module.addOptions("build_options", exe_options);
_ = try addDeps(b, lib, config); _ = try addDeps(b, lib, config);
const lib_install = b.addInstallLibFile( const lib_install = b.addInstallLibFile(
@ -482,7 +471,6 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
}); });
lib.root_module.addOptions("build_options", exe_options);
_ = try addDeps(b, lib, config); _ = try addDeps(b, lib, config);
const lib_install = b.addInstallLibFile( const lib_install = b.addInstallLibFile(
@ -510,7 +498,6 @@ pub fn build(b: *std.Build) !void {
b, b,
optimize, optimize,
config, config,
exe_options,
); );
// Add our library to zig-out // Add our library to zig-out
@ -526,7 +513,6 @@ pub fn build(b: *std.Build) !void {
null, null,
optimize, optimize,
config, config,
exe_options,
); );
// Add our library to zig-out // Add our library to zig-out
@ -542,7 +528,6 @@ pub fn build(b: *std.Build) !void {
.simulator, .simulator,
optimize, optimize,
config, config,
exe_options,
); );
// Add our library to zig-out // Add our library to zig-out
@ -605,6 +590,11 @@ pub fn build(b: *std.Build) !void {
}), }),
}; };
// 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. // Modify our build configuration for wasm builds.
const wasm_config: BuildConfig = config: { const wasm_config: BuildConfig = config: {
var copy = config; var copy = config;
@ -616,26 +606,19 @@ pub fn build(b: *std.Build) !void {
// Backends that are fixed for wasm // Backends that are fixed for wasm
copy.font_backend = .web_canvas; copy.font_backend = .web_canvas;
// Wasm-specific options
copy.wasm_shared = wasm_shared;
copy.wasm_target = wasm_target;
break :config copy; break :config copy;
}; };
// 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;
exe_options.addOption(bool, "wasm_shared", wasm_shared);
// We want to support alternate wasm targets in the future (i.e.
// server side) so we have this now although its hardcoded.
exe_options.addOption(WasmTarget, "wasm_target", wasm_target);
const wasm = b.addSharedLibrary(.{ const wasm = b.addSharedLibrary(.{
.name = "ghostty-wasm", .name = "ghostty-wasm",
.root_source_file = .{ .path = "src/main_wasm.zig" }, .root_source_file = .{ .path = "src/main_wasm.zig" },
.target = b.resolveTargetQuery(wasm_crosstarget), .target = b.resolveTargetQuery(wasm_crosstarget),
.optimize = optimize, .optimize = optimize,
}); });
wasm.root_module.addOptions("build_options", exe_options);
// So that we can use web workers with our wasm binary // So that we can use web workers with our wasm binary
wasm.import_memory = true; wasm.import_memory = true;
@ -666,7 +649,6 @@ pub fn build(b: *std.Build) !void {
.target = b.resolveTargetQuery(wasm_crosstarget), .target = b.resolveTargetQuery(wasm_crosstarget),
}); });
main_test.root_module.addOptions("build_options", exe_options);
_ = try addDeps(b, main_test, wasm_config); _ = try addDeps(b, main_test, wasm_config);
test_step.dependOn(&main_test.step); test_step.dependOn(&main_test.step);
} }
@ -709,7 +691,6 @@ pub fn build(b: *std.Build) !void {
copy.static = true; copy.static = true;
break :config copy; break :config copy;
}); });
main_test.root_module.addOptions("build_options", exe_options);
const test_run = b.addRunArtifact(main_test); const test_run = b.addRunArtifact(main_test);
test_step.dependOn(&test_run.step); test_step.dependOn(&test_run.step);
@ -755,7 +736,6 @@ fn createMacOSLib(
b: *std.Build, b: *std.Build,
optimize: std.builtin.OptimizeMode, optimize: std.builtin.OptimizeMode,
config: BuildConfig, config: BuildConfig,
exe_options: *std.Build.Step.Options,
) !struct { *std.Build.Step, std.Build.LazyPath } { ) !struct { *std.Build.Step, std.Build.LazyPath } {
// Modify our build configuration for macOS builds. // Modify our build configuration for macOS builds.
const macos_config: BuildConfig = config: { const macos_config: BuildConfig = config: {
@ -781,7 +761,6 @@ fn createMacOSLib(
}); });
lib.bundle_compiler_rt = true; lib.bundle_compiler_rt = true;
lib.linkLibC(); lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged // Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, macos_config); var lib_list = try addDeps(b, lib, macos_config);
@ -810,7 +789,6 @@ fn createMacOSLib(
}); });
lib.bundle_compiler_rt = true; lib.bundle_compiler_rt = true;
lib.linkLibC(); lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged // Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, macos_config); var lib_list = try addDeps(b, lib, macos_config);
@ -847,7 +825,6 @@ fn createIOSLib(
abi: ?std.Target.Abi, abi: ?std.Target.Abi,
optimize: std.builtin.OptimizeMode, optimize: std.builtin.OptimizeMode,
config: BuildConfig, config: BuildConfig,
exe_options: *std.Build.Step.Options,
) !struct { *std.Build.Step, std.Build.LazyPath } { ) !struct { *std.Build.Step, std.Build.LazyPath } {
const ios_config: BuildConfig = config: { const ios_config: BuildConfig = config: {
var copy = config; var copy = config;
@ -868,7 +845,6 @@ fn createIOSLib(
}); });
lib.bundle_compiler_rt = true; lib.bundle_compiler_rt = true;
lib.linkLibC(); lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged // Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, ios_config); var lib_list = try addDeps(b, lib, ios_config);
@ -895,6 +871,11 @@ fn addDeps(
step: *std.Build.Step.Compile, step: *std.Build.Step.Compile,
config: BuildConfig, config: BuildConfig,
) !LazyPathList { ) !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);
var static_libs = LazyPathList.init(b.allocator); var static_libs = LazyPathList.init(b.allocator);
errdefer static_libs.deinit(); errdefer static_libs.deinit();
@ -1126,7 +1107,7 @@ fn addDeps(
} }
} }
addHelp(b, step); try addHelp(b, step, config);
return static_libs; return static_libs;
} }
@ -1135,7 +1116,8 @@ fn addDeps(
fn addHelp( fn addHelp(
b: *std.Build, b: *std.Build,
step_: ?*std.Build.Step.Compile, step_: ?*std.Build.Step.Compile,
) void { config: BuildConfig,
) !void {
// Our static state between runs. We memoize our help strings // Our static state between runs. We memoize our help strings
// so that we only execute the help generation once. // so that we only execute the help generation once.
const HelpState = struct { const HelpState = struct {
@ -1150,6 +1132,15 @@ fn addHelp(
}); });
if (step_ == null) b.installArtifact(help_exe); 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); const help_run = b.addRunArtifact(help_exe);
HelpState.generated = help_run.captureStdOut(); HelpState.generated = help_run.captureStdOut();
break :strings HelpState.generated.?; break :strings HelpState.generated.?;
@ -1166,8 +1157,8 @@ fn addHelp(
/// Generate documentation (manpages, etc.) from help strings /// Generate documentation (manpages, etc.) from help strings
fn buildDocumentation( fn buildDocumentation(
b: *std.Build, b: *std.Build,
version: std.SemanticVersion, config: BuildConfig,
) void { ) !void {
const manpages = [_]struct { const manpages = [_]struct {
name: []const u8, name: []const u8,
section: []const u8, section: []const u8,
@ -1179,15 +1170,22 @@ fn buildDocumentation(
inline for (manpages) |manpage| { inline for (manpages) |manpage| {
const generate_markdown = b.addExecutable(.{ const generate_markdown = b.addExecutable(.{
.name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, .name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section,
.root_source_file = .{ .root_source_file = .{ .path = "src/main.zig" },
.path = "src/mdgen_" ++ manpage.name ++ "_" ++ manpage.section ++ ".zig",
},
.target = b.host, .target = b.host,
}); });
addHelp(b, generate_markdown); 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(); const generate_markdown_options = b.addOptions();
generate_markdown_options.addOption(std.SemanticVersion, "version", version); try gen_config.addOptions(generate_markdown_options);
generate_markdown.root_module.addOptions("build_options", generate_markdown_options); generate_markdown.root_module.addOptions("build_options", generate_markdown_options);
const generate_markdown_step = b.addRunArtifact(generate_markdown); const generate_markdown_step = b.addRunArtifact(generate_markdown);
@ -1254,24 +1252,26 @@ fn benchSteps(
// Name of the conformance app and full path to the entrypoint. // Name of the conformance app and full path to the entrypoint.
const name = entry.name[0..index]; const name = entry.name[0..index];
const path = try fs.path.join(b.allocator, &[_][]const u8{
c_dir_path,
entry.name,
});
// Executable builder. // Executable builder.
const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name}); const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name});
const c_exe = b.addExecutable(.{ const c_exe = b.addExecutable(.{
.name = bin_name, .name = bin_name,
.root_source_file = .{ .path = path }, .root_source_file = .{ .path = "src/main.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
// .main_pkg_path = .{ .path = "./src" },
}); });
if (install) b.installArtifact(c_exe); if (install) b.installArtifact(c_exe);
_ = try addDeps(b, c_exe, config: { _ = try addDeps(b, c_exe, config: {
var copy = config; var copy = config;
copy.static = true; copy.static = true;
var buf: [64]u8 = undefined;
copy.exe_entrypoint = std.meta.stringToEnum(
build_config.ExeEntrypoint,
try std.fmt.bufPrint(&buf, "bench_{s}", .{name}),
).?;
break :config copy; break :config copy;
}); });
} }

View File

@ -13,7 +13,7 @@
const std = @import("std"); const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const cli_args = @import("../cli_args.zig"); const cli = @import("../cli.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
pub fn main() !void { pub fn main() !void {
@ -29,7 +29,7 @@ pub fn main() !void {
errdefer args.deinit(); errdefer args.deinit();
var iter = try std.process.argsWithAllocator(alloc); var iter = try std.process.argsWithAllocator(alloc);
defer iter.deinit(); defer iter.deinit();
try cli_args.parse(Args, alloc, &args, &iter); try cli.args.parse(Args, alloc, &args, &iter);
break :args args; break :args args;
}; };
defer args.deinit(); defer args.deinit();

View File

@ -1,13 +1,13 @@
const std = @import("std"); const std = @import("std");
const gen = @import("build/mdgen/mdgen.zig"); const gen = @import("mdgen.zig");
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator(); const alloc = gpa.allocator();
const writer = std.io.getStdOut().writer(); const writer = std.io.getStdOut().writer();
try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_1_header.md"), writer); try gen.substitute(alloc, @embedFile("ghostty_1_header.md"), writer);
try gen.genActions(writer); try gen.genActions(writer);
try gen.genConfig(writer, true); try gen.genConfig(writer, true);
try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_1_footer.md"), writer); try gen.substitute(alloc, @embedFile("ghostty_1_footer.md"), writer);
} }

View File

@ -1,13 +1,13 @@
const std = @import("std"); const std = @import("std");
const gen = @import("build/mdgen/mdgen.zig"); const gen = @import("mdgen.zig");
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator(); const alloc = gpa.allocator();
const output = std.io.getStdOut().writer(); const output = std.io.getStdOut().writer();
try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_5_header.md"), output); try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), output);
try gen.genConfig(output, false); try gen.genConfig(output, false);
try gen.genKeybindActions(output); try gen.genKeybindActions(output);
try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_5_footer.md"), output); try gen.substitute(alloc, @embedFile("ghostty_5_footer.md"), output);
} }

View File

@ -1,23 +1,20 @@
const std = @import("std"); const std = @import("std");
const help_strings = @import("help_strings"); const help_strings = @import("help_strings");
const build_options = @import("build_options"); const build_config = @import("../../build_config.zig");
const Config = @import("../../config/Config.zig"); const Config = @import("../../config/Config.zig");
const Action = @import("../../cli/action.zig").Action; const Action = @import("../../cli/action.zig").Action;
const KeybindAction = @import("../../input/Binding.zig").Action; const KeybindAction = @import("../../input/Binding.zig").Action;
pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void {
const version_string = try std.fmt.allocPrint(alloc, "{}", .{build_options.version});
defer alloc.free(version_string);
const output = try alloc.alloc(u8, std.mem.replacementSize( const output = try alloc.alloc(u8, std.mem.replacementSize(
u8, u8,
input, input,
"@@VERSION@@", "@@VERSION@@",
version_string, build_config.version_string,
)); ));
defer alloc.free(output); defer alloc.free(output);
_ = std.mem.replace(u8, input, "@@VERSION@@", version_string, output); _ = std.mem.replace(u8, input, "@@VERSION@@", build_config.version_string, output);
try writer.writeAll(output); try writer.writeAll(output);
} }

View File

@ -9,6 +9,7 @@ const assert = std.debug.assert;
const apprt = @import("apprt.zig"); const apprt = @import("apprt.zig");
const font = @import("font/main.zig"); const font = @import("font/main.zig");
const rendererpkg = @import("renderer.zig"); const rendererpkg = @import("renderer.zig");
const WasmTarget = @import("os/wasm/target.zig").Target;
/// The build configuratin options. This may not be all available options /// The build configuratin options. This may not be all available options
/// to `zig build` but it contains all the options that the Ghostty source /// to `zig build` but it contains all the options that the Ghostty source
@ -18,6 +19,7 @@ const rendererpkg = @import("renderer.zig");
/// between options, make it easy to copy and mutate options for different /// between options, make it easy to copy and mutate options for different
/// build types, etc. /// build types, etc.
pub const BuildConfig = struct { pub const BuildConfig = struct {
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
static: bool = false, static: bool = false,
flatpak: bool = false, flatpak: bool = false,
libadwaita: bool = false, libadwaita: bool = false,
@ -25,8 +27,17 @@ pub const BuildConfig = struct {
renderer: rendererpkg.Impl = .opengl, renderer: rendererpkg.Impl = .opengl,
font_backend: font.Backend = .freetype, 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. /// Configure the build options with our values.
pub fn addOptions(self: BuildConfig, step: *std.Build.Step.Options) void { pub fn addOptions(self: BuildConfig, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't // We need to break these down individual because addOption doesn't
// support all types. // support all types.
step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "flatpak", self.flatpak);
@ -34,6 +45,19 @@ pub const BuildConfig = struct {
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(font.Backend, "font_backend", self.font_backend);
step.addOption(rendererpkg.Impl, "renderer", self.renderer); 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.
var buf: [64]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},
));
} }
/// Rehydrate our BuildConfig from the comptime options. Note that not all /// Rehydrate our BuildConfig from the comptime options. Note that not all
@ -41,11 +65,15 @@ pub const BuildConfig = struct {
/// to see what is and isn't available. /// to see what is and isn't available.
pub fn fromOptions() BuildConfig { pub fn fromOptions() BuildConfig {
return .{ return .{
.version = options.app_version,
.flatpak = options.flatpak, .flatpak = options.flatpak,
.libadwaita = options.libadwaita, .libadwaita = options.libadwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?, .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,
}; };
} }
}; };
@ -62,6 +90,7 @@ pub const artifact = Artifact.detect();
/// top-level so its a bit cleaner to use throughout the code. See the doc /// top-level so its a bit cleaner to use throughout the code. See the doc
/// comments in BuildConfig for details on each. /// comments in BuildConfig for details on each.
pub const config = BuildConfig.fromOptions(); pub const config = BuildConfig.fromOptions();
pub const exe_entrypoint = config.exe_entrypoint;
pub const flatpak = options.flatpak; pub const flatpak = options.flatpak;
pub const app_runtime: apprt.Runtime = config.app_runtime; pub const app_runtime: apprt.Runtime = config.app_runtime;
pub const font_backend: font.Backend = config.font_backend; pub const font_backend: font.Backend = config.font_backend;
@ -94,3 +123,20 @@ 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,
bench_parser,
};

View File

@ -1,309 +1,10 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const build_config = @import("build_config.zig"); const build_config = @import("build_config.zig");
const options = @import("build_options");
const glfw = @import("glfw");
const glslang = @import("glslang");
const macos = @import("macos");
const oni = @import("oniguruma");
const cli = @import("cli.zig");
const internal_os = @import("os/main.zig");
const xev = @import("xev");
const fontconfig = @import("fontconfig");
const harfbuzz = @import("harfbuzz");
const renderer = @import("renderer.zig");
const apprt = @import("apprt.zig");
const App = @import("App.zig"); // See build_config.ExeEntrypoint for why we do this.
const Ghostty = @import("main_c.zig").Ghostty; pub usingnamespace switch (build_config.exe_entrypoint) {
.ghostty => @import("main_ghostty.zig"),
/// Global process state. This is initialized in main() for exe artifacts .helpgen => @import("helpgen.zig"),
/// and by ghostty_init() for lib artifacts. This should ONLY be used by .mdgen_ghostty_1 => @import("build/mdgen/main_ghostty_1.zig"),
/// the C API. The Zig API should NOT use any global state and should .mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"),
/// rely on allocators being passed in as parameters. .bench_parser => @import("bench/parser.zig"),
pub var state: GlobalState = undefined;
/// The return type for main() depends on the build artifact.
const MainReturn = switch (build_config.artifact) {
.lib => noreturn,
else => void,
}; };
pub fn main() !MainReturn {
// We first start by initializing our global state. This will setup
// process-level state we need to run the terminal. The reason we use
// a global is because the C API needs to be able to access this state;
// no other Zig code should EVER access the global state.
state.init() catch |err| {
const stderr = std.io.getStdErr().writer();
defer std.os.exit(1);
const ErrSet = @TypeOf(err) || error{Unknown};
switch (@as(ErrSet, @errorCast(err))) {
error.MultipleActions => try stderr.print(
"Error: multiple CLI actions specified. You must specify only one\n" ++
"action starting with the `+` character.\n",
.{},
),
error.InvalidAction => try stderr.print(
"Error: unknown CLI action specified. CLI actions are specified with\n" ++
"the '+' character.\n",
.{},
),
else => try stderr.print("invalid CLI invocation err={}\n", .{err}),
}
};
defer state.deinit();
const alloc = state.alloc;
if (comptime builtin.mode == .Debug) {
std.log.warn("This is a debug build. Performance will be very poor.", .{});
std.log.warn("You should only use a debug build for developing Ghostty.", .{});
std.log.warn("Otherwise, please rebuild in a release mode.", .{});
}
// Execute our action if we have one
if (state.action) |action| {
std.log.info("executing CLI action={}", .{action});
std.os.exit(action.run(alloc) catch |err| err: {
std.log.err("CLI action failed error={}", .{err});
break :err 1;
});
return;
}
if (comptime build_config.app_runtime == .none) {
const stdout = std.io.getStdOut().writer();
try stdout.print("Usage: ghostty +<action> [flags]\n\n", .{});
try stdout.print(
\\This is the Ghostty helper CLI that accompanies the graphical Ghostty app.
\\To launch the terminal directly, please launch the graphical app
\\(i.e. Ghostty.app on macOS). This CLI can be used to perform various
\\actions such as inspecting the version, listing fonts, etc.
\\
\\We don't have proper help output yet, sorry! Please refer to the
\\source code or Discord community for help for now. We'll fix this in time.
,
.{},
);
std.os.exit(0);
}
// Create our app state
var app = try App.create(alloc);
defer app.destroy();
// Create our runtime app
var app_runtime = try apprt.App.init(app, .{});
defer app_runtime.terminate();
// Run the GUI event loop
try app_runtime.run();
}
pub const std_options = struct {
// Our log level is always at least info in every build mode.
pub const log_level: std.log.Level = switch (builtin.mode) {
.Debug => .debug,
else => .info,
};
// The function std.log will call.
pub fn logFn(
comptime level: std.log.Level,
comptime scope: @TypeOf(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
// Stuff we can do before the lock
const level_txt = comptime level.asText();
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
// Lock so we are thread-safe
std.debug.getStderrMutex().lock();
defer std.debug.getStderrMutex().unlock();
// On Mac, we use unified logging. To view this:
//
// sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
//
if (builtin.target.isDarwin()) {
// Convert our levels to Mac levels
const mac_level: macos.os.LogType = switch (level) {
.debug => .debug,
.info => .info,
.warn => .err,
.err => .fault,
};
// Initialize a logger. This is slow to do on every operation
// but we shouldn't be logging too much.
const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope));
defer logger.release();
logger.log(std.heap.c_allocator, mac_level, format, args);
}
switch (state.logging) {
.disabled => {},
.stderr => {
// Always try default to send to stderr
const stderr = std.io.getStdErr().writer();
nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return;
},
}
}
};
/// This represents the global process state. There should only
/// be one of these at any given moment. This is extracted into a dedicated
/// struct because it is reused by main and the static C lib.
pub const GlobalState = struct {
const GPA = std.heap.GeneralPurposeAllocator(.{});
gpa: ?GPA,
alloc: std.mem.Allocator,
action: ?cli.Action,
logging: Logging,
/// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it.
resources_dir: ?[]const u8,
/// Where logging should go
pub const Logging = union(enum) {
disabled: void,
stderr: void,
};
/// Initialize the global state.
pub fn init(self: *GlobalState) !void {
// Initialize ourself to nothing so we don't have any extra state.
// IMPORTANT: this MUST be initialized before any log output because
// the log function uses the global state.
self.* = .{
.gpa = null,
.alloc = undefined,
.action = null,
.logging = .{ .stderr = {} },
.resources_dir = null,
};
errdefer self.deinit();
self.gpa = gpa: {
// Use the libc allocator if it is available because it is WAY
// faster than GPA. We only do this in release modes so that we
// can get easy memory leak detection in debug modes.
if (builtin.link_libc) {
if (switch (builtin.mode) {
.ReleaseSafe, .ReleaseFast => true,
// We also use it if we can detect we're running under
// Valgrind since Valgrind only instruments the C allocator
else => std.valgrind.runningOnValgrind() > 0,
}) break :gpa null;
}
break :gpa GPA{};
};
self.alloc = if (self.gpa) |*value|
value.allocator()
else if (builtin.link_libc)
std.heap.c_allocator
else
unreachable;
// We first try to parse any action that we may be executing.
self.action = try cli.Action.detectCLI(self.alloc);
// If we have an action executing, we disable logging by default
// since we write to stderr we don't want logs messing up our
// output.
if (self.action != null) self.logging = .{ .disabled = {} };
// For lib mode we always disable stderr logging by default.
if (comptime build_config.app_runtime == .none) {
self.logging = .{ .disabled = {} };
}
// I don't love the env var name but I don't have it in my heart
// to parse CLI args 3 times (once for actions, once for config,
// maybe once for logging) so for now this is an easy way to do
// this. Env vars are useful for logging too because they are
// easy to set.
if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| {
defer v.deinit(self.alloc);
if (v.value.len > 0) {
self.logging = .{ .stderr = {} };
}
}
// Output some debug information right away
std.log.info("ghostty version={s}", .{build_config.version_string});
std.log.info("runtime={}", .{build_config.app_runtime});
std.log.info("font_backend={}", .{build_config.font_backend});
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
if (comptime build_config.font_backend.hasFontconfig()) {
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
}
std.log.info("renderer={}", .{renderer.Renderer});
std.log.info("libxev backend={}", .{xev.backend});
// First things first, we fix our file descriptors
internal_os.fixMaxFiles();
// We need to make sure the process locale is set properly. Locale
// affects a lot of behaviors in a shell.
try internal_os.ensureLocale(self.alloc);
// Initialize glslang for shader compilation
try glslang.init();
// Initialize oniguruma for regex
try oni.init(&.{oni.Encoding.utf8});
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
self.resources_dir = try internal_os.resourcesDir(self.alloc);
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
}
/// Cleans up the global state. This doesn't _need_ to be called but
/// doing so in dev modes will check for memory leaks.
pub fn deinit(self: *GlobalState) void {
if (self.resources_dir) |dir| self.alloc.free(dir);
if (self.gpa) |*value| {
// We want to ensure that we deinit the GPA because this is
// the point at which it will output if there were safety violations.
_ = value.deinit();
}
}
};
test {
_ = @import("circ_buf.zig");
_ = @import("pty.zig");
_ = @import("Command.zig");
_ = @import("font/main.zig");
_ = @import("apprt.zig");
_ = @import("renderer.zig");
_ = @import("termio.zig");
_ = @import("input.zig");
_ = @import("cli.zig");
_ = @import("surface_mouse.zig");
// Libraries
_ = @import("segmented_pool.zig");
_ = @import("inspector/main.zig");
_ = @import("terminal/main.zig");
_ = @import("terminfo/main.zig");
// TODO
_ = @import("blocking_queue.zig");
_ = @import("config.zig");
_ = @import("lru.zig");
}

View File

@ -6,6 +6,7 @@
// This currently isn't supported as a general purpose embedding API. // This currently isn't supported as a general purpose embedding API.
// This is currently used only to embed ghostty within a macOS app. However, // This is currently used only to embed ghostty within a macOS app. However,
// it could be expanded to be general purpose in the future. // it could be expanded to be general purpose in the future.
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const builtin = @import("builtin"); const builtin = @import("builtin");

315
src/main_ghostty.zig Normal file
View File

@ -0,0 +1,315 @@
//! The main entrypoint for the `ghostty` application. This also serves
//! as the process initialization code for the `libghostty` library.
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const build_config = @import("build_config.zig");
const options = @import("build_options");
const glfw = @import("glfw");
const glslang = @import("glslang");
const macos = @import("macos");
const oni = @import("oniguruma");
const cli = @import("cli.zig");
const internal_os = @import("os/main.zig");
const xev = @import("xev");
const fontconfig = @import("fontconfig");
const harfbuzz = @import("harfbuzz");
const renderer = @import("renderer.zig");
const apprt = @import("apprt.zig");
const App = @import("App.zig");
const Ghostty = @import("main_c.zig").Ghostty;
/// Global process state. This is initialized in main() for exe artifacts
/// and by ghostty_init() for lib artifacts. This should ONLY be used by
/// the C API. The Zig API should NOT use any global state and should
/// rely on allocators being passed in as parameters.
pub var state: GlobalState = undefined;
/// The return type for main() depends on the build artifact. The lib build
/// also calls "main" in order to run the CLI actions, but it calls it as
/// an API and not an entrypoint.
const MainReturn = switch (build_config.artifact) {
.lib => noreturn,
else => void,
};
pub fn main() !MainReturn {
// We first start by initializing our global state. This will setup
// process-level state we need to run the terminal. The reason we use
// a global is because the C API needs to be able to access this state;
// no other Zig code should EVER access the global state.
state.init() catch |err| {
const stderr = std.io.getStdErr().writer();
defer std.os.exit(1);
const ErrSet = @TypeOf(err) || error{Unknown};
switch (@as(ErrSet, @errorCast(err))) {
error.MultipleActions => try stderr.print(
"Error: multiple CLI actions specified. You must specify only one\n" ++
"action starting with the `+` character.\n",
.{},
),
error.InvalidAction => try stderr.print(
"Error: unknown CLI action specified. CLI actions are specified with\n" ++
"the '+' character.\n",
.{},
),
else => try stderr.print("invalid CLI invocation err={}\n", .{err}),
}
};
defer state.deinit();
const alloc = state.alloc;
if (comptime builtin.mode == .Debug) {
std.log.warn("This is a debug build. Performance will be very poor.", .{});
std.log.warn("You should only use a debug build for developing Ghostty.", .{});
std.log.warn("Otherwise, please rebuild in a release mode.", .{});
}
// Execute our action if we have one
if (state.action) |action| {
std.log.info("executing CLI action={}", .{action});
std.os.exit(action.run(alloc) catch |err| err: {
std.log.err("CLI action failed error={}", .{err});
break :err 1;
});
return;
}
if (comptime build_config.app_runtime == .none) {
const stdout = std.io.getStdOut().writer();
try stdout.print("Usage: ghostty +<action> [flags]\n\n", .{});
try stdout.print(
\\This is the Ghostty helper CLI that accompanies the graphical Ghostty app.
\\To launch the terminal directly, please launch the graphical app
\\(i.e. Ghostty.app on macOS). This CLI can be used to perform various
\\actions such as inspecting the version, listing fonts, etc.
\\
\\We don't have proper help output yet, sorry! Please refer to the
\\source code or Discord community for help for now. We'll fix this in time.
,
.{},
);
std.os.exit(0);
}
// Create our app state
var app = try App.create(alloc);
defer app.destroy();
// Create our runtime app
var app_runtime = try apprt.App.init(app, .{});
defer app_runtime.terminate();
// Run the GUI event loop
try app_runtime.run();
}
pub const std_options = struct {
// Our log level is always at least info in every build mode.
pub const log_level: std.log.Level = switch (builtin.mode) {
.Debug => .debug,
else => .info,
};
// The function std.log will call.
pub fn logFn(
comptime level: std.log.Level,
comptime scope: @TypeOf(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
// Stuff we can do before the lock
const level_txt = comptime level.asText();
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
// Lock so we are thread-safe
std.debug.getStderrMutex().lock();
defer std.debug.getStderrMutex().unlock();
// On Mac, we use unified logging. To view this:
//
// sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
//
if (builtin.target.isDarwin()) {
// Convert our levels to Mac levels
const mac_level: macos.os.LogType = switch (level) {
.debug => .debug,
.info => .info,
.warn => .err,
.err => .fault,
};
// Initialize a logger. This is slow to do on every operation
// but we shouldn't be logging too much.
const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope));
defer logger.release();
logger.log(std.heap.c_allocator, mac_level, format, args);
}
switch (state.logging) {
.disabled => {},
.stderr => {
// Always try default to send to stderr
const stderr = std.io.getStdErr().writer();
nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return;
},
}
}
};
/// This represents the global process state. There should only
/// be one of these at any given moment. This is extracted into a dedicated
/// struct because it is reused by main and the static C lib.
pub const GlobalState = struct {
const GPA = std.heap.GeneralPurposeAllocator(.{});
gpa: ?GPA,
alloc: std.mem.Allocator,
action: ?cli.Action,
logging: Logging,
/// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it.
resources_dir: ?[]const u8,
/// Where logging should go
pub const Logging = union(enum) {
disabled: void,
stderr: void,
};
/// Initialize the global state.
pub fn init(self: *GlobalState) !void {
// Initialize ourself to nothing so we don't have any extra state.
// IMPORTANT: this MUST be initialized before any log output because
// the log function uses the global state.
self.* = .{
.gpa = null,
.alloc = undefined,
.action = null,
.logging = .{ .stderr = {} },
.resources_dir = null,
};
errdefer self.deinit();
self.gpa = gpa: {
// Use the libc allocator if it is available because it is WAY
// faster than GPA. We only do this in release modes so that we
// can get easy memory leak detection in debug modes.
if (builtin.link_libc) {
if (switch (builtin.mode) {
.ReleaseSafe, .ReleaseFast => true,
// We also use it if we can detect we're running under
// Valgrind since Valgrind only instruments the C allocator
else => std.valgrind.runningOnValgrind() > 0,
}) break :gpa null;
}
break :gpa GPA{};
};
self.alloc = if (self.gpa) |*value|
value.allocator()
else if (builtin.link_libc)
std.heap.c_allocator
else
unreachable;
// We first try to parse any action that we may be executing.
self.action = try cli.Action.detectCLI(self.alloc);
// If we have an action executing, we disable logging by default
// since we write to stderr we don't want logs messing up our
// output.
if (self.action != null) self.logging = .{ .disabled = {} };
// For lib mode we always disable stderr logging by default.
if (comptime build_config.app_runtime == .none) {
self.logging = .{ .disabled = {} };
}
// I don't love the env var name but I don't have it in my heart
// to parse CLI args 3 times (once for actions, once for config,
// maybe once for logging) so for now this is an easy way to do
// this. Env vars are useful for logging too because they are
// easy to set.
if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| {
defer v.deinit(self.alloc);
if (v.value.len > 0) {
self.logging = .{ .stderr = {} };
}
}
// Output some debug information right away
std.log.info("ghostty version={s}", .{build_config.version_string});
std.log.info("runtime={}", .{build_config.app_runtime});
std.log.info("font_backend={}", .{build_config.font_backend});
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
if (comptime build_config.font_backend.hasFontconfig()) {
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
}
std.log.info("renderer={}", .{renderer.Renderer});
std.log.info("libxev backend={}", .{xev.backend});
// First things first, we fix our file descriptors
internal_os.fixMaxFiles();
// We need to make sure the process locale is set properly. Locale
// affects a lot of behaviors in a shell.
try internal_os.ensureLocale(self.alloc);
// Initialize glslang for shader compilation
try glslang.init();
// Initialize oniguruma for regex
try oni.init(&.{oni.Encoding.utf8});
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
self.resources_dir = try internal_os.resourcesDir(self.alloc);
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
}
/// Cleans up the global state. This doesn't _need_ to be called but
/// doing so in dev modes will check for memory leaks.
pub fn deinit(self: *GlobalState) void {
if (self.resources_dir) |dir| self.alloc.free(dir);
if (self.gpa) |*value| {
// We want to ensure that we deinit the GPA because this is
// the point at which it will output if there were safety violations.
_ = value.deinit();
}
}
};
test {
_ = @import("circ_buf.zig");
_ = @import("pty.zig");
_ = @import("Command.zig");
_ = @import("font/main.zig");
_ = @import("apprt.zig");
_ = @import("renderer.zig");
_ = @import("termio.zig");
_ = @import("input.zig");
_ = @import("cli.zig");
_ = @import("surface_mouse.zig");
// Libraries
_ = @import("segmented_pool.zig");
_ = @import("inspector/main.zig");
_ = @import("terminal/main.zig");
_ = @import("terminfo/main.zig");
// TODO
_ = @import("blocking_queue.zig");
_ = @import("config.zig");
_ = @import("lru.zig");
}