diff --git a/build.zig b/build.zig index 037b46d71..e267fe79a 100644 --- a/build.zig +++ b/build.zig @@ -106,6 +106,29 @@ pub fn build(b: *std.Build) !void { }; libghostty_vt_shared.install(b.getInstallStep()); + // libghostty-vt static lib. We don't build this for wasm since wasm has + // no concept of static vs shared and we put the wasm binary up in + // our shared handling. + if (!config.target.result.cpu.arch.isWasm()) { + const libghostty_vt_static = try buildpkg.GhosttyLibVt.initStatic( + b, + &mod, + ); + + if (config.is_dep) { + // If we're a dependency, we need to install everything as-is + // so that dep.artifact("ghostty-vt-static") works. + libghostty_vt_static.install(b.getInstallStep()); + } else { + // If we're not a dependency, we rename the static lib to + // be idiomatic. + b.getInstallStep().dependOn(&b.addInstallLibFile( + libghostty_vt_static.output, + "libghostty-vt.a", + ).step); + } + } + // Helpgen if (config.emit_helpgen) deps.help_strings.install(); diff --git a/example/c-vt-static/README.md b/example/c-vt-static/README.md new file mode 100644 index 000000000..52da4ddb0 --- /dev/null +++ b/example/c-vt-static/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Static Linking + +This contains a simple example of how to statically link the `ghostty-vt` +C library with a C program using the `ghostty-vt-static` artifact. It is +otherwise identical to the `c-vt` example. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-static/build.zig b/example/c-vt-static/build.zig new file mode 100644 index 000000000..0e53d69c5 --- /dev/null +++ b/example/c-vt-static/build.zig @@ -0,0 +1,44 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + // Use "ghostty-vt-static" for static linking instead of + // "ghostty-vt" which provides a shared library. + exe_mod.linkLibrary(dep.artifact("ghostty-vt-static")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_static", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-static/build.zig.zon b/example/c-vt-static/build.zig.zon new file mode 100644 index 000000000..413bf66fb --- /dev/null +++ b/example/c-vt-static/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_static, + .version = "0.0.0", + .fingerprint = 0xa592a9fdd5d87ed2, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-static/src/main.c b/example/c-vt-static/src/main.c new file mode 100644 index 000000000..b1297d7a7 --- /dev/null +++ b/example/c-vt-static/src/main.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +int main() { + GhosttyOscParser parser; + if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { + return 1; + } + + // Setup change window title command to change the title to "hello" + ghostty_osc_next(parser, '0'); + ghostty_osc_next(parser, ';'); + const char *title = "hello"; + for (size_t i = 0; i < strlen(title); i++) { + ghostty_osc_next(parser, title[i]); + } + + // End parsing and get command + GhosttyOscCommand command = ghostty_osc_end(parser, 0); + + // Get and print command type + GhosttyOscCommandType type = ghostty_osc_command_type(command); + printf("Command type: %d\n", type); + + // Extract and print the title + if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) { + printf("Extracted title: %s\n", title); + } else { + printf("Failed to extract title\n"); + } + + ghostty_osc_free(parser); + return 0; +} diff --git a/src/build/Config.zig b/src/build/Config.zig index 5789e9f92..53d3c737b 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -61,6 +61,10 @@ emit_xcframework: bool = false, emit_webdata: bool = false, emit_unicode_table_gen: bool = false, +/// True when Ghostty is being built as a dependency of another project +/// rather than as the root project. +is_dep: bool = false, + /// Environmental properties env: std.process.EnvMap, @@ -88,6 +92,10 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { break :target result; }; + // Detect if Ghostty is a dependency of another project. + // dep_prefix is non-empty when this build is running as a dependency. + const is_dep = b.dep_prefix.len > 0; + // 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. @@ -110,6 +118,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { .optimize = optimize, .target = target, .wasm_target = wasm_target, + .is_dep = is_dep, .env = env, }; @@ -221,9 +230,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { const app_version = try std.SemanticVersion.parse(appVersion); // Is ghostty a dependency? If so, skip git detection. - // @src().file won't resolve from b.build_root unless ghostty - // is the project being built. - b.build_root.handle.access(@src().file, .{}) catch break :version .{ + if (is_dep) break :version .{ .major = app_version.major, .minor = app_version.minor, .patch = app_version.patch, diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 6d44c62b6..ba3881496 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -12,11 +12,22 @@ step: *std.Build.Step, /// The artifact result artifact: *std.Build.Step.InstallArtifact, +/// The kind of library +kind: Kind, + /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, pkg_config: ?std.Build.LazyPath, +/// The kind of library being built. This is similar to LinkMode but +/// also includes wasm which is an executable, not a library. +const Kind = enum { + wasm, + shared, + static, +}; + pub fn initWasm( b: *std.Build, zig: *const GhosttyZig, @@ -39,20 +50,40 @@ pub fn initWasm( return .{ .step = &exe.step, .artifact = b.addInstallArtifact(exe, .{}), + .kind = .wasm, .output = exe.getEmittedBin(), .dsym = null, .pkg_config = null, }; } +pub fn initStatic( + b: *std.Build, + zig: *const GhosttyZig, +) !GhosttyLibVt { + return initLib(b, zig, .static); +} + pub fn initShared( b: *std.Build, zig: *const GhosttyZig, ) !GhosttyLibVt { + return initLib(b, zig, .dynamic); +} + +fn initLib( + b: *std.Build, + zig: *const GhosttyZig, + linkage: std.builtin.LinkMode, +) !GhosttyLibVt { + const kind: Kind = switch (linkage) { + .static => .static, + .dynamic => .shared, + }; const target = zig.vt.resolved_target.?; const lib = b.addLibrary(.{ - .name = "ghostty-vt", - .linkage = .dynamic, + .name = if (kind == .static) "ghostty-vt-static" else "ghostty-vt", + .linkage = linkage, .root_module = zig.vt_c, .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, }); @@ -62,6 +93,15 @@ pub fn initShared( .{ .include_extensions = &.{".h"} }, ); + if (kind == .static) { + // These must be bundled since we're compiling into a static lib. + // Otherwise, you get undefined symbol errors. This could cause + // problems if you're linking multiple static Zig libraries but + // we'll cross that bridge when we get to it. + lib.bundle_compiler_rt = true; + lib.bundle_ubsan_rt = true; + } + if (lib.rootModuleTarget().abi.isAndroid()) { // Support 16kb page sizes, required for Android 15+. lib.link_z_max_page_size = 16384; // 16kb @@ -82,11 +122,10 @@ pub fn initShared( if (builtin.os.tag.isDarwin()) try @import("apple_sdk").addPaths(b, lib); } - // Get our debug symbols + // Get our debug symbols (only for shared libs; static libs aren't linked) const dsymutil: ?std.Build.LazyPath = dsymutil: { - if (!target.result.os.tag.isDarwin()) { - break :dsymutil null; - } + if (kind != .shared) break :dsymutil null; + if (!target.result.os.tag.isDarwin()) break :dsymutil null; const dsymutil = RunStep.create(b, "dsymutil"); dsymutil.addArgs(&.{"dsymutil"}); @@ -116,6 +155,7 @@ pub fn initShared( return .{ .step = &lib.step, .artifact = b.addInstallArtifact(lib, .{}), + .kind = kind, .output = lib.getEmittedBin(), .dsym = dsymutil, .pkg_config = pc,