diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ff2462ce..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 @@ -649,6 +650,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 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/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/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/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/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/PageList.zig b/src/terminal/PageList.zig index d33eff00a..1e818efcf 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,19 @@ 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 { +/// 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; + // 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 +623,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 +1126,7 @@ pub fn clone( // Setup our pool var pool: MemoryPool = try .init( alloc, - pageAllocator(), + pageAllocator(alloc), page_count, ); errdefer pool.deinit(); @@ -7499,7 +7508,7 @@ pub const Builder = struct { return .{ .pool = try MemoryPool.init( alloc, - pageAllocator(), + pageAllocator(alloc), page_preheat, ), .options = options, 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; }