build: fix windows build to properly run tests and build of libghostty-vt (#11781)

Our Windows build has been broken for a _long_ time. It hasn't actually
worked and our CI was falsely passing when it was actually failing to
build/test. This PR fixes that and fixes the issues it found so
`libghostty-vt` can build and pass tests.

**This is only for libghostty!** I'd still like to expand our _test_
coverage to all of Ghostty for Windows but libghostty is more important
for that platform in the short term and it's an incremental piece of
work.

A couple windows compatibility issues fixed:

- `terminal.Page` uses `VirtualAlloc` on Windows (thanks @deblasis)
- Our rgb.txt loading was not resilient to CRLF endings
This commit is contained in:
Mitchell Hashimoto
2026-03-23 10:29:45 -07:00
committed by GitHub
4 changed files with 92 additions and 98 deletions

1
.gitattributes vendored
View File

@@ -12,3 +12,4 @@ src/font/nerd_font_attributes.zig linguist-generated=true
src/font/nerd_font_codepoint_tables.py linguist-generated=true
src/font/res/** linguist-vendored
src/terminal/res/** linguist-vendored
src/terminal/res/rgb.txt -text

View File

@@ -92,13 +92,13 @@ jobs:
- build-libghostty-vt
- build-libghostty-vt-android
- build-libghostty-vt-macos
- build-libghostty-vt-windows
- build-linux
- build-linux-libghostty
- build-nix
- build-macos
- build-macos-freetype
- build-snap
- build-windows
- test
- test-simd
- test-gtk
@@ -500,6 +500,25 @@ jobs:
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
build-libghostty-vt-windows:
runs-on: windows-2025
continue-on-error: true
timeout-minutes: 45
needs: test
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
# TODO: Work towards passing the full test suite on Windows.
- name: Test libghostty-vt
run: zig build test-lib-vt
- name: Build libghostty-vt
run: zig build -Demit-lib-vt
build-linux:
strategy:
fail-fast: false
@@ -795,79 +814,6 @@ jobs:
run: |
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_freetype
build-windows:
runs-on: windows-2022
# this will not stop other jobs from running
continue-on-error: true
timeout-minutes: 45
needs: test
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# This could be from a script if we wanted to but inlining here for now
# in one place.
# Using powershell so that we do not need to install WSL components. Also,
# WSLv1 is only installed on Github runners.
- name: Install zig
shell: pwsh
run: |
# Get the zig version from build.zig.zon so that it only needs to be updated
$fileContent = Get-Content -Path "build.zig.zon" -Raw
$pattern = 'minimum_zig_version\s*=\s*"([^"]+)"'
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
$version = "zig-x86_64-windows-$zigVersion"
Write-Output $version
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
Expand-Archive -Path ".\zig-windows.zip" -DestinationPath ".\" -Force
Remove-Item -Path ".\zig-windows.zip"
Rename-Item -Path ".\$version" -NewName ".\zig"
Write-Host "Zig installed."
.\zig\zig.exe version
- name: Compile build
shell: pwsh
run: .\zig\zig.exe build --help
- name: Generate build testing script
shell: pwsh
run: |
# Generate a script so that we can swallow the errors
$scriptContent = @"
.\zig\zig.exe build test 2>&1 | Out-File -FilePath "build.log" -Append
exit 0
"@
$scriptPath = "zigbuild.ps1"
# Write the script content to a file
$scriptContent | Set-Content -Path $scriptPath
Write-Host "Script generated at: $scriptPath"
- name: Test Windows
shell: pwsh
run: .\zigbuild.ps1 -ErrorAction SilentlyContinue
- name: Generate build script
shell: pwsh
run: |
# Generate a script so that we can swallow the errors
$scriptContent = @"
.\zig\zig.exe build 2>&1 | Out-File -FilePath "build.log" -Append
exit 0
"@
$scriptPath = "zigbuild.ps1"
# Write the script content to a file
$scriptContent | Set-Content -Path $scriptPath
Write-Host "Script generated at: $scriptPath"
- name: Build Windows
shell: pwsh
run: .\zigbuild.ps1 -ErrorAction SilentlyContinue
- name: Dump logs
shell: pwsh
run: Get-Content -Path ".\build.log"
test:
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip

View File

@@ -6,6 +6,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const assert = @import("../quirks.zig").inlineAssert;
const testing = std.testing;
const posix = std.posix;
const windows = std.os.windows;
const fastmem = @import("../fastmem.zig");
const color = @import("color.zig");
const hyperlink = @import("hyperlink.zig");
@@ -26,6 +27,60 @@ const alignBackward = std.mem.alignBackward;
const log = std.log.scoped(.page);
/// Page-aligned allocator used for terminal page backing memory. Pages
/// require page-aligned, zeroed memory obtained directly from the OS
/// (not the Zig allocator) because the allocation fast-path is
/// performance-critical and the OS guarantees zeroed pages.
const PageAlloc = switch (builtin.os.tag) {
.windows => AllocWindows,
else => AllocPosix,
};
/// Allocate page-aligned, zeroed backing memory using mmap with
/// MAP_PRIVATE | MAP_ANONYMOUS which guarantees zeroed pages.
const AllocPosix = struct {
pub fn alloc(n: usize) ![]align(std.heap.page_size_min) u8 {
return try posix.mmap(
null,
n,
posix.PROT.READ | posix.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
}
pub fn free(mem: []align(std.heap.page_size_min) u8) void {
posix.munmap(mem);
}
};
/// Allocate page-aligned, zeroed backing memory using VirtualAlloc with
/// MEM_COMMIT | MEM_RESERVE which guarantees zeroed pages.
const AllocWindows = struct {
pub fn alloc(n: usize) error{OutOfMemory}![]align(std.heap.page_size_min) u8 {
const addr = windows.VirtualAlloc(
null,
n,
windows.MEM_COMMIT | windows.MEM_RESERVE,
windows.PAGE_READWRITE,
) catch return error.OutOfMemory;
return @as(
[*]align(std.heap.page_size_min) u8,
@ptrCast(@alignCast(addr)),
)[0..n];
}
pub fn free(mem: []align(std.heap.page_size_min) u8) void {
windows.VirtualFree(
@ptrCast(@alignCast(mem.ptr)),
0,
windows.MEM_RELEASE,
);
}
};
/// The allocator to use for multi-codepoint grapheme data. We use
/// a chunk size of 4 codepoints. It'd be best to set this empirically
/// but it is currently set based on vibes. My thinking around 4 codepoints
@@ -167,20 +222,13 @@ pub const Page = struct {
pub inline fn init(cap: Capacity) !Page {
const l = layout(cap);
// We use mmap directly to avoid Zig allocator overhead
// (small but meaningful for this path) and because a private
// anonymous mmap is guaranteed on Linux and macOS to be zeroed,
// We allocate page-aligned zeroed memory directly to avoid Zig
// allocator overhead (small but meaningful for this path). Both
// mmap (POSIX) and VirtualAlloc (Windows) guarantee zeroed pages,
// which is a critical property for us.
assert(l.total_size % std.heap.page_size_min == 0);
const backing = try posix.mmap(
null,
l.total_size,
posix.PROT.READ | posix.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
errdefer posix.munmap(backing);
const backing = try PageAlloc.alloc(l.total_size);
errdefer PageAlloc.free(backing);
const buf = OffsetBuf.init(backing);
return initBuf(buf, l);
@@ -245,7 +293,7 @@ pub const Page = struct {
/// this if you allocated the backing memory yourself (i.e. you used
/// initBuf).
pub inline fn deinit(self: *Page) void {
posix.munmap(self.memory);
PageAlloc.free(self.memory);
self.* = undefined;
}
@@ -578,15 +626,8 @@ pub const Page = struct {
/// using the page allocator. If you want to manage memory manually,
/// use cloneBuf.
pub inline fn clone(self: *const Page) !Page {
const backing = try posix.mmap(
null,
self.memory.len,
posix.PROT.READ | posix.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
errdefer posix.munmap(backing);
const backing = try PageAlloc.alloc(self.memory.len);
errdefer PageAlloc.free(backing);
return self.cloneBuf(backing);
}

View File

@@ -25,12 +25,18 @@ fn colorMap() ColorMap {
// of our unit tests will catch it.
var iter = std.mem.splitScalar(u8, data, '\n');
var i: usize = 0;
while (iter.next()) |line| {
while (iter.next()) |raw_line| {
// Trim \r so this works with both LF and CRLF line endings,
// since git may convert rgb.txt to CRLF on Windows checkouts.
const line = if (raw_line.len > 0 and raw_line[raw_line.len - 1] == '\r')
raw_line[0 .. raw_line.len - 1]
else
raw_line;
if (line.len == 0) continue;
const r = try std.fmt.parseInt(u8, std.mem.trim(u8, line[0..3], " "), 10);
const g = try std.fmt.parseInt(u8, std.mem.trim(u8, line[4..7], " "), 10);
const b = try std.fmt.parseInt(u8, std.mem.trim(u8, line[8..11], " "), 10);
const name = std.mem.trim(u8, line[12..], " \t\n");
const name = std.mem.trim(u8, line[12..], " \t");
kvs[i] = .{ name, .{ .r = r, .g = g, .b = b } };
i += 1;
}