diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 000000000..fb7a37e2b --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,152 @@ +name: Build Windows + +on: + push: + pull_request: + branches: [main] + +env: + ZIG_VERSION: 0.15.2 + +jobs: + build-native-windows: + name: Windows - Native Build and Test + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + + - name: Install dependencies + run: bun install + + # Workaround for Zig 0.15.2 bug on Windows (https://github.com/ziglang/zig/issues/25805) + # When cwd and cache path are on different drives, build fails + # Setting cache dir explicitly to be on the same drive as the source + - name: Build native with Zig (Windows only) + working-directory: packages/core/src/zig + run: zig build -Doptimize=ReleaseFast --cache-dir .zig-cache --global-cache-dir .zig-cache + + - name: Copy native binaries to node_modules + working-directory: packages/core + run: bun scripts/build.ts --native --skip-zig-build + + - name: Verify Windows binary exists + shell: pwsh + run: | + if (!(Test-Path "packages/core/node_modules/@opentui/core-win32-x64/opentui.dll")) { + Write-Error "Windows x64 binary missing!" + exit 1 + } + Write-Host "Windows x64 binary exists" + Get-ChildItem "packages/core/node_modules/@opentui/core-win32-x64/" + + - name: Run native tests + working-directory: packages/core/src/zig + run: zig build test --summary all --cache-dir .zig-cache --global-cache-dir .zig-cache + + - name: Build lib + working-directory: packages/core + run: bun run build:lib + + - name: Run JS tests + working-directory: packages/core + run: bun run test:js + + build-on-macos: + name: macOS - Cross-compile for Windows + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + + - name: Install dependencies + run: bun install + + - name: Build for Windows (cross-compile) + working-directory: packages/core/src/zig + run: zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast + + - name: Package Windows binaries + run: | + mkdir -p artifacts + cp packages/core/src/zig/lib/x86_64-windows/opentui.dll artifacts/ + ls -la artifacts/ + + - name: Upload cross-compiled Windows binaries + uses: actions/upload-artifact@v4 + with: + name: windows-binaries-cross-compiled + path: artifacts/opentui.dll + if-no-files-found: error + retention-days: 1 + + test-cross-compiled-on-windows: + name: Windows - Test macOS Cross-Compiled Binaries + runs-on: windows-latest + needs: build-on-macos + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Download cross-compiled Windows binaries + uses: actions/download-artifact@v4 + with: + name: windows-binaries-cross-compiled + path: artifacts/ + + - name: Install dependencies + run: bun install + + - name: Setup cross-compiled binaries + shell: bash + run: | + # Create the target directory + targetDir="packages/core/node_modules/@opentui/core-win32-x64" + mkdir -p "$targetDir" + + # Copy the cross-compiled DLL + cp artifacts/opentui.dll "$targetDir/" + + # Create index.ts that exports the path to the DLL + echo 'const module = await import("./opentui.dll", { with: { type: "file" } })' > "$targetDir/index.ts" + echo 'const path = module.default' >> "$targetDir/index.ts" + echo 'export default path;' >> "$targetDir/index.ts" + + # Create package.json + echo '{"name":"@opentui/core-win32-x64","version":"0.0.0","main":"index.ts","types":"index.ts"}' > "$targetDir/package.json" + + echo "Cross-compiled binary placed at:" + ls -la "$targetDir" + + - name: Build lib (uses cross-compiled binary) + working-directory: packages/core + run: bun run build:lib + + - name: Run JS tests with cross-compiled binary + working-directory: packages/core + run: bun run test:js diff --git a/packages/core/scripts/build.ts b/packages/core/scripts/build.ts index 45c8b98da..46d552c4a 100644 --- a/packages/core/scripts/build.ts +++ b/packages/core/scripts/build.ts @@ -41,6 +41,7 @@ const buildLib = args.find((arg) => arg === "--lib") const buildNative = args.find((arg) => arg === "--native") const isDev = args.includes("--dev") const buildAll = args.includes("--all") // Build for all platforms (requires macOS or cross-compilation setup) +const skipZigBuild = args.includes("--skip-zig-build") // Skip zig build, just copy existing binaries const variants: Variant[] = [ { platform: "darwin", arch: "x64" }, @@ -79,26 +80,30 @@ if (missingRequired.length > 0) { } if (buildNative) { - console.log(`Building native ${isDev ? "dev" : "prod"} binaries${buildAll ? " for all platforms" : ""}...`) + if (skipZigBuild) { + console.log("Skipping zig build, copying existing binaries...") + } else { + console.log(`Building native ${isDev ? "dev" : "prod"} binaries${buildAll ? " for all platforms" : ""}...`) - const zigArgs = ["build", `-Doptimize=${isDev ? "Debug" : "ReleaseFast"}`] - if (buildAll) { - zigArgs.push("-Dall") - } + const zigArgs = ["build", `-Doptimize=${isDev ? "Debug" : "ReleaseFast"}`] + if (buildAll) { + zigArgs.push("-Dall") + } - const zigBuild: SpawnSyncReturns = spawnSync("zig", zigArgs, { - cwd: join(rootDir, "src", "zig"), - stdio: "inherit", - }) + const zigBuild: SpawnSyncReturns = spawnSync("zig", zigArgs, { + cwd: join(rootDir, "src", "zig"), + stdio: "inherit", + }) - if (zigBuild.error) { - console.error("Error: Zig is not installed or not in PATH") - process.exit(1) - } + if (zigBuild.error) { + console.error("Error: Zig is not installed or not in PATH") + process.exit(1) + } - if (zigBuild.status !== 0) { - console.error("Error: Zig build failed") - process.exit(1) + if (zigBuild.status !== 0) { + console.error("Error: Zig build failed") + process.exit(1) + } } for (const { platform, arch } of variants) { diff --git a/packages/core/src/zig/bench-utils.zig b/packages/core/src/zig/bench-utils.zig index b81fdc5d1..6029d9296 100644 --- a/packages/core/src/zig/bench-utils.zig +++ b/packages/core/src/zig/bench-utils.zig @@ -61,7 +61,9 @@ pub fn formatBytes(bytes: usize) struct { value: f64, unit: []const u8 } { pub fn printResults(writer: anytype, results: []const BenchResult) !void { if (results.len == 0) return; - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + var arena = std.heap.ArenaAllocator.init(gpa.allocator()); defer arena.deinit(); const allocator = arena.allocator(); diff --git a/packages/core/src/zig/build.zig.zon b/packages/core/src/zig/build.zig.zon index 061e6ae3d..7853c645c 100644 --- a/packages/core/src/zig/build.zig.zon +++ b/packages/core/src/zig/build.zig.zon @@ -9,8 +9,9 @@ .hash = "uucode-0.1.0-ZZjBPvoGQACkgWDKIrtI8CQcSXIufU3Kvty-pIfh02i2", }, .ghostty = .{ - .url = "git+https://github.com/ghostty-org/ghostty.git#fbed63b0474ce0fff66859c1563a0359589ef179", - .hash = "ghostty-1.3.0-dev-5UdBC_dRRAS-mGtQa1JGeQPgcubWBeell0hGr_47fW75", + // Using fork with Windows fix: https://github.com/ghostty-org/ghostty/issues/10147 + .url = "git+https://github.com/remorses/ghostty.git#fd725490a14e4500b6676d9f4d67f6a566006bd0", + .hash = "ghostty-1.3.0-dev-5UdBC1YFPgTu_jyuC0G4FGOhjsKgATa1Eqi5MY02ycYh", }, }, .paths = .{ diff --git a/packages/core/src/zig/lib.zig b/packages/core/src/zig/lib.zig index bc74a3c6e..6d9258779 100644 --- a/packages/core/src/zig/lib.zig +++ b/packages/core/src/zig/lib.zig @@ -31,7 +31,10 @@ export fn setEventCallback(callback: ?*const fn (namePtr: [*]const u8, nameLen: event_bus.setEventCallback(callback); } -var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const gpa_allocator = gpa.allocator(); + +var arena = std.heap.ArenaAllocator.init(gpa_allocator); const globalArena = arena.allocator(); export fn getArenaAllocatedBytes() usize { @@ -46,7 +49,7 @@ export fn createRenderer(width: u32, height: u32, testing: bool) ?*renderer.CliR const pool = gp.initGlobalPool(globalArena); _ = link.initGlobalLinkPool(globalArena); - return renderer.CliRenderer.create(std.heap.page_allocator, width, height, pool, testing) catch |err| { + return renderer.CliRenderer.create(gpa_allocator, width, height, pool, testing) catch |err| { logger.err("Failed to create renderer: {}", .{err}); return null; }; @@ -107,7 +110,7 @@ export fn createOptimizedBuffer(width: u32, height: u32, respectAlpha: bool, wid const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode; const id = idPtr[0..idLen]; - return buffer.OptimizedBuffer.init(std.heap.page_allocator, width, height, .{ + return buffer.OptimizedBuffer.init(gpa_allocator, width, height, .{ .respectAlpha = respectAlpha, .pool = pool, .width_method = wMethod, @@ -505,7 +508,7 @@ export fn createTextBuffer(widthMethod: u8) ?*text_buffer.UnifiedTextBuffer { const pool = gp.initGlobalPool(globalArena); const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode; - const tb = text_buffer.UnifiedTextBuffer.init(std.heap.page_allocator, pool, wMethod) catch { + const tb = text_buffer.UnifiedTextBuffer.init(gpa_allocator, pool, wMethod) catch { return null; }; @@ -615,7 +618,7 @@ export fn textBufferGetPlainText(tb: *text_buffer.UnifiedTextBuffer, outPtr: [*] // TextBufferView functions (Array-based for backward compatibility) export fn createTextBufferView(tb: *text_buffer.UnifiedTextBuffer) ?*text_buffer_view.UnifiedTextBufferView { - const view = text_buffer_view.UnifiedTextBufferView.init(std.heap.page_allocator, tb) catch { + const view = text_buffer_view.UnifiedTextBufferView.init(gpa_allocator, tb) catch { return null; }; return view; @@ -763,7 +766,7 @@ export fn createEditBuffer(widthMethod: u8) ?*edit_buffer_mod.EditBuffer { const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode; return edit_buffer_mod.EditBuffer.init( - std.heap.page_allocator, + gpa_allocator, pool, wMethod, ) catch null; @@ -1323,8 +1326,7 @@ export fn textBufferGetLineHighlightsPtr( return null; } - const alloc = std.heap.page_allocator; - var slice = alloc.alloc(ExternalHighlight, highs.len) catch return null; + var slice = gpa_allocator.alloc(ExternalHighlight, highs.len) catch return null; for (highs, 0..) |hl, i| { slice[i] = .{ @@ -1341,8 +1343,7 @@ export fn textBufferGetLineHighlightsPtr( } export fn textBufferFreeLineHighlights(ptr: [*]const ExternalHighlight, count: usize) void { - const alloc = std.heap.page_allocator; - alloc.free(@constCast(ptr)[0..count]); + gpa_allocator.free(@constCast(ptr)[0..count]); } export fn textBufferGetHighlightCount(tb: *text_buffer.UnifiedTextBuffer) u32 { @@ -1361,7 +1362,7 @@ export fn textBufferGetTextRangeByCoords(tb: *text_buffer.UnifiedTextBuffer, sta // SyntaxStyle functions export fn createSyntaxStyle() ?*syntax_style.SyntaxStyle { - return syntax_style.SyntaxStyle.init(std.heap.page_allocator) catch |err| { + return syntax_style.SyntaxStyle.init(gpa_allocator) catch |err| { logger.err("Failed to create SyntaxStyle: {}", .{err}); return null; }; @@ -1410,15 +1411,15 @@ export fn encodeUnicode( // Find grapheme info var grapheme_list: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{}; - defer grapheme_list.deinit(std.heap.page_allocator); + defer grapheme_list.deinit(gpa_allocator); const tab_width: u8 = 2; - utf8.findGraphemeInfo(text, tab_width, is_ascii_only, wMethod, std.heap.page_allocator, &grapheme_list) catch return false; + utf8.findGraphemeInfo(text, tab_width, is_ascii_only, wMethod, gpa_allocator, &grapheme_list) catch return false; const specials = grapheme_list.items; // Allocate output array const estimated_count = if (is_ascii_only) text.len else text.len * 2; - var result = std.heap.page_allocator.alloc(EncodedChar, estimated_count) catch return false; + var result = gpa_allocator.alloc(EncodedChar, estimated_count) catch return false; var result_idx: usize = 0; var success = false; var pending_gid: ?u32 = null; // Track grapheme allocated but not yet stored in result @@ -1441,7 +1442,7 @@ export fn encodeUnicode( pool.decref(gid) catch {}; } } - std.heap.page_allocator.free(result); + gpa_allocator.free(result); } } @@ -1495,7 +1496,7 @@ export fn encodeUnicode( // Ensure we have space if (result_idx >= result.len) { const new_len = result.len * 2; - result = std.heap.page_allocator.realloc(result, new_len) catch return false; + result = gpa_allocator.realloc(result, new_len) catch return false; } result[result_idx] = EncodedChar{ @@ -1508,7 +1509,7 @@ export fn encodeUnicode( } // Trim to actual size - result = std.heap.page_allocator.realloc(result, result_idx) catch result; + result = gpa_allocator.realloc(result, result_idx) catch result; outPtr.* = result.ptr; outLenPtr.* = result_idx; @@ -1531,7 +1532,7 @@ export fn freeUnicode(charsPtr: [*]const EncodedChar, charsLen: usize) void { } // Free the array itself - std.heap.page_allocator.free(chars); + gpa_allocator.free(chars); } export fn bufferDrawChar( diff --git a/packages/core/src/zig/terminal.zig b/packages/core/src/zig/terminal.zig index f9fe1f8bc..31de592a1 100644 --- a/packages/core/src/zig/terminal.zig +++ b/packages/core/src/zig/terminal.zig @@ -223,7 +223,9 @@ pub fn enableDetectedFeatures(self: *Terminal, tty: anytype, use_kitty_keyboard: } fn checkEnvironmentOverrides(self: *Terminal) void { - var env_map = std.process.getEnvMap(std.heap.page_allocator) catch return; + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + var env_map = std.process.getEnvMap(gpa.allocator()) catch return; defer env_map.deinit(); // Always just try to enable bracketed paste, even if it was reported as not supported