From a5423592cde24222c0d1719f780c54434b5b5d34 Mon Sep 17 00:00:00 2001 From: Uzair Aftab Date: Sat, 29 Aug 2026 21:13:46 +0200 Subject: [PATCH 1/6] libghostty: use caller allocation on native freestanding Native freestanding targets have neither an OS page allocator nor a usable default SMP allocator. Use the allocator supplied through libghostty for terminal page storage and make a missing C allocator fail with out-of-memory instead of instantiating hosted allocation machinery. Document that native freestanding C callers must supply an allocator for allocating operations. --- include/ghostty/vt/allocator.h | 13 +++++++++---- src/lib/allocator.zig | 4 ++++ src/terminal/PageList.zig | 17 +++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/include/ghostty/vt/allocator.h b/include/ghostty/vt/allocator.h index 2e8685e84..e12d10e47 100644 --- a/include/ghostty/vt/allocator.h +++ b/include/ghostty/vt/allocator.h @@ -31,14 +31,17 @@ * function that accepts one,** and libghostty will use a default allocator. * The default allocator will be libc malloc/free if libc is linked. * Otherwise, a custom allocator is used (currently Zig's SMP allocator) - * that doesn't require any external dependencies. + * that doesn't require any external dependencies. On native freestanding + * targets, the default allocator always fails instead. * * ## Basic Usage * * For simple use cases, you can ignore this interface entirely by passing NULL * as the allocator parameter to functions that accept one. This will use the * default allocator (typically libc malloc/free, if libc is linked, but - * we provide our own default allocator if libc isn't linked). + * we provide our own default allocator if libc isn't linked). Native + * freestanding builds must provide a custom allocator for operations that + * allocate memory. * * To use a custom allocator: * 1. Implement the GhosttyAllocatorVtable function pointers @@ -76,7 +79,8 @@ * * If you're not going to use a custom allocator, you can ignore all of * this. All functions that take an allocator pointer allow NULL to use a - * default allocator. + * default allocator. Native freestanding builds must provide an allocator + * for operations that allocate memory. * * The interface is based on the Zig allocator interface. I'll say up front * that it is easy to look at this interface and think "wow, this is really @@ -183,7 +187,8 @@ typedef struct { * For functions that take an allocator pointer, a NULL pointer indicates * that the default allocator should be used. The default allocator will * be libc malloc/free if we're linking to libc. If libc isn't linked, - * a custom allocator is used (currently Zig's SMP allocator). + * a custom allocator is used (currently Zig's SMP allocator). On native + * freestanding targets, the default allocator always fails instead. * * @ingroup allocator * diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index 375a09e98..de343dab3 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -34,6 +34,10 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { // Wasm if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator; + // Freestanding targets don't have a default heap. Using the failing + // allocator makes a missing allocator show up as out-of-memory. + if (comptime builtin.os.tag == .freestanding) return std.mem.Allocator.failing; + // No libc, use the preferred allocator for releases which is the // Zig SMP allocator. return std.heap.smp_allocator; diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index d33eff00a..9f013608b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -29,6 +29,8 @@ const Page = pagepkg.Page; const Row = pagepkg.Row; const log = std.log.scoped(.page_list); +const native_freestanding = builtin.os.tag == .freestanding and + !builtin.target.cpu.arch.isWasm(); /// The number of PageList.Nodes we preheat the pool with. A node is /// a very small struct so we can afford to preheat many, but the exact @@ -538,12 +540,15 @@ fn initialCapacity(cols: size.CellCountInt) Capacity { return cap; } -/// This is the page allocator we'll use for all our underlying -/// VM page allocations. -inline fn pageAllocator() Allocator { +/// This is the allocator we'll use for all our underlying page allocations. +inline fn pageAllocator(alloc: Allocator) Allocator { // In tests we use our testing allocator so we can detect leaks. if (builtin.is_test) return std.testing.allocator; + // Native freestanding targets don't have an OS page allocator, so use + // the allocator provided by the embedder. + if (native_freestanding) return alloc; + // On non-macOS we use our standard Zig page allocator. if (!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator; @@ -614,7 +619,7 @@ pub fn init( try tw.check(.init_memory_pool); var pool = try MemoryPool.init( alloc, - pageAllocator(), + pageAllocator(alloc), page_preheat, ); errdefer pool.deinit(); @@ -1117,7 +1122,7 @@ pub fn clone( // Setup our pool var pool: MemoryPool = try .init( alloc, - pageAllocator(), + pageAllocator(alloc), page_count, ); errdefer pool.deinit(); @@ -7499,7 +7504,7 @@ pub const Builder = struct { return .{ .pool = try MemoryPool.init( alloc, - pageAllocator(), + pageAllocator(alloc), page_preheat, ), .options = options, From 3376153a44e5269b9aee5e6d2524954c171e717b Mon Sep 17 00:00:00 2001 From: Uzair Aftab Date: Sat, 29 Aug 2026 21:13:56 +0200 Subject: [PATCH 2/6] build: support native freestanding libghostty-vt Native freestanding targets cannot emit shared libraries and do not provide an OS page size, stack unwinder, hosted SIMD dependencies, or filesystem-backed Kitty graphics. Build only the static artifact for those targets, define the minimum alignment used for terminal pages, disable hosted-only defaults, and install the public headers with the archive. --- build.zig | 33 ++++++++++++++++++++++++++------- src/build/Config.zig | 7 ++++--- src/build/GhosttyLibVt.zig | 4 +++- src/lib_vt.zig | 21 +++++++++++++++++---- src/terminal/build_options.zig | 3 +-- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index 41ac5f04c..5204b3d87 100644 --- a/build.zig +++ b/build.zig @@ -125,26 +125,36 @@ pub fn build(b: *std.Build) !void { } // libghostty-vt - const libghostty_vt_shared = shared: { + const native_freestanding = config.target.result.os.tag == .freestanding and + !config.target.result.cpu.arch.isWasm(); + const libghostty_vt_shared: ?buildpkg.GhosttyLibVt = shared: { if (config.target.result.cpu.arch.isWasm()) { break :shared try buildpkg.GhosttyLibVt.initWasm( b, &mod, ); } + if (native_freestanding) break :shared null; break :shared try buildpkg.GhosttyLibVt.initShared( b, &mod, ); }; - libghostty_vt_shared.install(b.getInstallStep()); + if (libghostty_vt_shared) |shared| { + shared.install(b.getInstallStep()); - const type_schema_test = b.addSystemCommand(&.{"python3"}); - type_schema_test.addFileArg(b.path("src/terminal/c/types-schema-verify.py")); - type_schema_test.addFileArg(b.path("src/terminal/c/types.schema.json")); - type_schema_test.addFileArg(libghostty_vt_shared.output); - test_lib_vt_schema_step.dependOn(&type_schema_test.step); + const type_schema_test = b.addSystemCommand(&.{"python3"}); + type_schema_test.addFileArg(b.path("src/terminal/c/types-schema-verify.py")); + type_schema_test.addFileArg(b.path("src/terminal/c/types.schema.json")); + type_schema_test.addFileArg(shared.output); + test_lib_vt_schema_step.dependOn(&type_schema_test.step); + } else { + try test_lib_vt_schema_step.addError( + "cannot execute the ABI manifest for a native freestanding target", + .{}, + ); + } // libghostty-vt static lib const libghostty_vt_static = try buildpkg.GhosttyLibVt.initStatic( @@ -167,6 +177,15 @@ pub fn build(b: *std.Build) !void { libghostty_vt_static.output, static_lib_name, ).step); + + if (native_freestanding) { + b.getInstallStep().dependOn(&b.addInstallDirectory(.{ + .source_dir = b.path("include/ghostty"), + .install_dir = .header, + .install_subdir = "ghostty", + .include_extensions = &.{".h"}, + }).step); + } } // libghostty-vt xcframework (Apple only, universal binary). diff --git a/src/build/Config.zig b/src/build/Config.zig index c90f3781b..71fa4d289 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -240,9 +240,10 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf "simd", "Build with SIMD-accelerated code paths. Results in significant performance improvements.", ) orelse simd: { - // We can't build our SIMD dependencies for Wasm. Note that we may - // still use SIMD features in the Wasm-builds. - if (target.result.cpu.arch.isWasm()) break :simd false; + // We can't build our SIMD dependencies for Wasm or freestanding + // targets. Note that we may still use SIMD features in the Wasm-builds. + if (target.result.cpu.arch.isWasm() or + target.result.os.tag == .freestanding) break :simd false; break :simd true; }; diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index a97fdf78f..690f864c5 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -246,7 +246,9 @@ fn initLib( // Enable PIC so the static library can be linked into PIE // executables, which is the default on most Linux distributions. - lib.root_module.pic = true; + // Native freestanding targets don't have a dynamic loader and some, + // such as Xtensa, don't support PIC relocations. + lib.root_module.pic = target.result.os.tag != .freestanding; } if (target.result.os.tag == .windows) { diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 325f0e138..2a45d1cd4 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -424,6 +424,15 @@ comptime { pub const std_options: std.Options = opts: { var options: std.Options = .{}; + if (native_freestanding) { + // Freestanding targets don't have an OS page size. We still need an + // alignment for terminal page allocations, and 16 covers everything + // stored in a page without requiring 4 KiB-aligned embedded heaps. + options.page_size_min = 16; + options.page_size_max = 16; + options.allow_stack_tracing = false; + } + if (builtin.target.cpu.arch.isWasm()) { // In non-debug modes, we want to ship effectively no logging // warn and lower add ~200KB at the time of this comment. @@ -464,10 +473,14 @@ pub const std_options: std.Options = opts: { /// traces on panic, std.debug.print, etc.). These builds are for /// development, where the roughly 160KB of binary size it costs is /// worth it. -const debug_machinery: bool = builtin.is_test or switch (builtin.mode) { - .Debug, .ReleaseSafe => true, - .ReleaseFast, .ReleaseSmall => false, -}; +const native_freestanding = builtin.target.os.tag == .freestanding and + !builtin.target.cpu.arch.isWasm(); + +const debug_machinery: bool = !native_freestanding and + (builtin.is_test or switch (builtin.mode) { + .Debug, .ReleaseSafe => true, + .ReleaseFast, .ReleaseSmall => false, + }); /// The panic handler for when this file is the root module. /// diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 725fccfd0..8fc2c3e90 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -263,8 +263,7 @@ pub const Options = struct { /// targets, so it is always disabled there regardless of the /// feature setting. pub fn kittyGraphics(self: Options, target: std.Target) bool { - if (target.cpu.arch == .wasm32 and target.os.tag == .freestanding) - return false; + if (target.os.tag == .freestanding) return false; return self.features.kitty_graphics; } From 10b7158e8cd57f21f7327c73a6b2cdb2f498fdae Mon Sep 17 00:00:00 2001 From: Uzair Aftab Date: Sun, 30 Aug 2026 16:53:31 +0200 Subject: [PATCH 3/6] ci: cross-compile freestanding libghostty-vt --- .github/workflows/freestanding.yml | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/freestanding.yml diff --git a/.github/workflows/freestanding.yml b/.github/workflows/freestanding.yml new file mode 100644 index 000000000..b2bc3a674 --- /dev/null +++ b/.github/workflows/freestanding.yml @@ -0,0 +1,51 @@ +on: + push: {} + pull_request: {} + workflow_dispatch: {} + +name: Freestanding + +# We only want the latest commit to test for any non-main ref. +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + build-libghostty-vt: + if: github.repository == 'ghostty-org/ghostty' + strategy: + matrix: + include: + - target: riscv32-freestanding-eabi + cpu: baseline + - target: thumb-freestanding-eabi + cpu: cortex_m4 + runs-on: namespace-profile-ghostty-sm + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 + with: + path: | + /nix + /zig + + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build + run: | + nix develop -c zig build -Demit-lib-vt \ + -Dtarget=${{ matrix.target }} \ + -Dcpu=${{ matrix.cpu }} \ + -Doptimize=ReleaseSafe From fc7bfa60b4b5b40183387e52b782612f417812f3 Mon Sep 17 00:00:00 2001 From: Uzair Aftab Date: Mon, 31 Aug 2026 16:26:40 +0200 Subject: [PATCH 4/6] ci: move freestanding build into test workflow --- .github/workflows/freestanding.yml | 51 ------------------------------ .github/workflows/test.yml | 39 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/freestanding.yml diff --git a/.github/workflows/freestanding.yml b/.github/workflows/freestanding.yml deleted file mode 100644 index b2bc3a674..000000000 --- a/.github/workflows/freestanding.yml +++ /dev/null @@ -1,51 +0,0 @@ -on: - push: {} - pull_request: {} - workflow_dispatch: {} - -name: Freestanding - -# We only want the latest commit to test for any non-main ref. -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }} - cancel-in-progress: true - -jobs: - build-libghostty-vt: - if: github.repository == 'ghostty-org/ghostty' - strategy: - matrix: - include: - - target: riscv32-freestanding-eabi - cpu: baseline - - target: thumb-freestanding-eabi - cpu: cortex_m4 - runs-on: namespace-profile-ghostty-sm - env: - ZIG_LOCAL_CACHE_DIR: /zig/local-cache - ZIG_GLOBAL_CACHE_DIR: /zig/global-cache - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 - with: - path: | - /nix - /zig - - - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Build - run: | - nix develop -c zig build -Demit-lib-vt \ - -Dtarget=${{ matrix.target }} \ - -Dcpu=${{ matrix.cpu }} \ - -Doptimize=ReleaseSafe diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ff2462ce..b377abb0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -649,6 +649,45 @@ jobs: nix develop -c zig build -Demit-lib-vt \ -Dtarget=${{ matrix.target }} + build-libghostty-vt-freestanding: + strategy: + matrix: + include: + - target: riscv32-freestanding-eabi + cpu: baseline + - target: thumb-freestanding-eabi + cpu: cortex_m4 + runs-on: namespace-profile-ghostty-sm + needs: test + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 + with: + path: | + /nix + /zig + + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build + run: | + nix develop -c zig build -Demit-lib-vt \ + -Dtarget=${{ matrix.target }} \ + -Dcpu=${{ matrix.cpu }} \ + -Doptimize=ReleaseSafe + build-libghostty-vt-wasm: runs-on: namespace-profile-ghostty-sm needs: test From 028af92ce3876ce1627163ce777f516b8218b410 Mon Sep 17 00:00:00 2001 From: Uzair Aftab Date: Mon, 31 Aug 2026 16:27:38 +0200 Subject: [PATCH 5/6] terminal: clarify page allocator fallback --- src/terminal/PageList.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 9f013608b..1e818efcf 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -540,7 +540,11 @@ fn initialCapacity(cols: size.CellCountInt) Capacity { return cap; } -/// This is the allocator we'll use for all our underlying page allocations. +/// Returns the allocator used for underlying page allocations. +/// +/// `alloc` is the caller-provided allocator. It is used on native freestanding +/// targets, where no OS page allocator is available. Other targets select a +/// platform-specific allocator below. inline fn pageAllocator(alloc: Allocator) Allocator { // In tests we use our testing allocator so we can detect leaks. if (builtin.is_test) return std.testing.allocator; From 5e02d0014de36ab776ac947e60ae5641c9674ca4 Mon Sep 17 00:00:00 2001 From: Uzair Aftab Date: Mon, 31 Aug 2026 21:47:45 +0200 Subject: [PATCH 6/6] ci: require freestanding libghostty-vt builds --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b377abb0a..cf43c6799 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,6 +94,7 @@ jobs: - build-cmake - build-flatpak - build-libghostty-vt + - build-libghostty-vt-freestanding - build-libghostty-vt-wasm - build-libghostty-vt-features - build-libghostty-vt-android