Previously ghostty_terminal_set required all values to be passed as
pointers to the value, even when the value itself was already a
pointer (userdata, function pointer callbacks). This forced callers
into awkward patterns like compound literals or intermediate
variables just to take the address of a pointer.
Now pointer-typed options (userdata and all callbacks) are passed
directly as the value parameter. Only non-pointer types like
GhosttyString still require a pointer to the value. This
simplifies InType to return the actual stored type for each option
and lets setTyped work with those types directly.
Add title and pwd as both gettable data keys
(GHOSTTY_TERMINAL_DATA_TITLE/PWD) and settable options
(GHOSTTY_TERMINAL_OPT_TITLE/PWD) in the C terminal API. Getting
returns a borrowed GhosttyString; setting copies the data into the
terminal via setTitle/setPwd.
The underlying Terminal.setTitle/setPwd now append a null sentinel so
that getTitle/getPwd can return sentinel-terminated slices ([:0]const
u8), which is useful for downstream consumers that need C strings.
Change ghostty_terminal_set to return GhosttyResult instead of void
so that the new title/pwd options can report allocation failures.
Existing option-setting calls cannot fail so the return value is
backwards-compatible for callers that discard it.
### This is it! This one (and the other stacked PRs) and #11782 should
finally give a clean test run on Windows!
## Summary
- Increase `@setEvalBranchQuota` from 1M to 10M (too much? how much is
too much?) in `checkGhosttyHEnum` (src/lib/enum.zig)
- Fixes the only remaining test failure on Windows MSVC: `ghostty.h
MouseShape`
## Context
This one was fun! Claude started blabbering, diminishing returns it
said. It couldn't figure out. So I called Dario and it worked.
Nah, much easier than that.
On MSVC, the translate-c output for `ghostty.h` is ~360KB with ~2173
declarations (vs ~112KB / ~1502 on Linux/Mac) because `<sys/types.h>`
and `<BaseTsd.h>` pull in Windows SDK headers. The `checkGhosttyHEnum`
function uses a nested `inline for` (enum fields x declarations) with
comptime string comparisons. For MouseShape (34 variants), this
generates roughly 34 x 2173 x ~20 = ~1.5M comptime branches, exceeding
the 1M quota.
The failure was confusing because it presented as a runtime error
("ghostty.h is missing value for GHOSTTY_MOUSE_SHAPE_DEFAULT") rather
than a compile error. The constants exist in the translate-c output and
the test compiles, but the comptime loop silently stops matching when it
hits the branch limit, so `set.remove` is never called and the set
reports all entries as missing at runtime.
## How we found it
The translate-c output clearly had all 34 GHOSTTY_MOUSE_SHAPE_*
constants, yet the test reported all of them as missing. I asked Claude
to list 5 hypotheses (decl truncation, branch quota, string comparison
bug, declaration ordering, field access failure) and to write 7 targeted
POC tests in enum.zig to isolate each step of `checkGhosttyHEnum`:
1. POC1-2: Module access and declaration count (both passed)
2. POC3: `@hasDecl` for the constant (passed)
3. POC4: Direct field value access (passed)
4. POC5: `inline for` over decls with string comparison - **compile
error: "evaluation exceeded 1000 backwards branches"**
5. POC6: Same but with 10M quota (passed)
6. POC7: Full `checkGhosttyHEnum` clone with 10M quota - **passed,
confirming the fix**
POC5 was the key: the default 1000 branch limit for test code confirmed
the comptime budget mechanism. The existing 1M quota in
`checkGhosttyHEnum` was enough for Linux/Mac (1502 declarations) but not
for MSVC (2173 declarations) with larger enums.
## Stack
Stacked on 016-windows/fix-libcxx-msvc.
## Test plan
### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)
| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (016, ce9930051) | FAIL - 49/51, 2630/2654, 1 test failed,
23 skipped | PASS - 86/86, 2655/2678, 23 skipped | PASS - 160/160,
2655/2662, 7 skipped |
| **AFTER** (017, 68378a0bb) | FAIL - 49/51, 2631/2654, 23 skipped |
PASS - 86/86, 2655/2678, 23 skipped | PASS - 160/160, 2655/2662, 7
skipped |
### Windows: what changed (2630 -> 2631 tests, MouseShape fixed)
**Fixed by this PR:**
- `ghostty.h MouseShape` test - was failing because comptime branch
quota exhaustion silently prevented the inline for loop from matching
any constants
**Remaining failure (pre-existing, unrelated):**
- `config.Config.test.clone can then change conditional state` -
segfaults (exit code 3) on Windows. We investigated this and it looked
familiar.. cherry-picking the `CommaSplitter `fix from PR #11782
resolved it! The backslash path handling in `CommaSplitter `breaks theme
path parsing on Windows, which is exactly what that PR addresses. So
once that lands, we should be in a good place... ready to ship to
Windows users! (just kidding)
### Linux/macOS: no regressions
Identical pass counts and test results before and after.
## What I Learnt
- Comptime branch quota exhaustion in Zig does not always surface as a
clean compile error. When it happens inside an `inline for` loop with
`comptime` string comparisons that gate runtime code (like
`set.remove`), the effect is that matching code is silently not
generated. The test compiles and runs, but the runtime behavior is wrong
because the matching branches were never emitted. This makes the failure
look like a data issue (missing declarations) rather than a compile
budget issue.
- When debugging comptime issues, writing small isolated POC tests that
exercise each step of the failing function independently is very
effective. It took 7 targeted tests to pinpoint the exact failure point.
- Cross-platform translate-c outputs can vary significantly in size. On
MSVC, system headers are much larger than on Linux/Mac, which affects
comptime budgets for any code that iterates over translated module
declarations.
Replace the O(N×M) nested inline for loop with direct @hasDecl lookups.
The old approach iterated over all translate-c declarations for each enum
field, which required a 10M comptime branch quota on MSVC (2173 decls ×
138 fields × ~20 branches). The new approach constructs the expected
declaration name and checks directly, reducing to O(N) and needing only
100K quota on all platforms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DA1 trampoline was converting C feature codes into a local
stack buffer and returning a slice pointing into it. This is
unsound because the slice outlives the stack frame once the
trampoline returns, leaving reportDeviceAttributes reading
invalid memory.
Move the scratch buffer into the wrapper effects struct so that
its lifetime extends beyond the trampoline call, keeping the
returned slice valid for the caller.
Assign handler.effects as a struct literal instead of setting fields
individually. This lets the compiler catch missing fields if new
effects are added to the Effects struct.
Also sort the callback function typedefs in vt/terminal.h
alphabetically (Bell, ColorScheme, DeviceAttributes, Enquiry, Size,
TitleChanged, WritePty, Xtversion).
Rename device_status.h to device.h and add C-compatible structs for
device attributes (DA1/DA2/DA3) responses. The new header includes
defines for all known conformance levels, DA1 feature codes, and DA2
device type identifiers.
Add a GhosttyTerminalDeviceAttributesFn callback that C consumers can
set via GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES. The callback follows
the existing bool + out-pointer pattern used by color_scheme and size
callbacks. When the callback is unset or returns false, the trampoline
returns a default VT220 response (conformance level 62, ANSI color).
The DA1 primary features use a fixed [64]uint16_t inline array with a
num_features count rather than a pointer, so the entire struct is
value-typed and can be safely copied without lifetime concerns.
Change device_status.ColorScheme from a plain Zig enum to
lib.Enum so it uses c_int backing when targeting the C ABI.
Add a color_scheme callback to the C terminal effects, following
the bool + out-pointer pattern used by the size callback. The
trampoline converts between the C calling convention and the
internal stream handler color_scheme effect, returning null when
no callback is set.
Add device_status.h header with GhosttyColorScheme enum and wire
it through terminal.h as GHOSTTY_TERMINAL_OPT_COLOR_SCHEME (= 7)
with GhosttyTerminalColorSchemeFn.
Add GHOSTTY_TERMINAL_OPT_SIZE so C consumers can respond to
XTWINOPS size queries (CSI 14/16/18 t). The callback receives a
GhosttySizeReportSize out-pointer and returns true if the size is
available, or false to silently ignore the query. The trampoline
converts the bool + out-pointer pattern to the optional that the
Zig handler expects.
Add GHOSTTY_TERMINAL_OPT_TITLE_CHANGED so C consumers are notified
when the terminal title changes via OSC 0 or OSC 2 sequences. The
callback has the same fire-and-forget shape as bell.
Add GHOSTTY_TERMINAL_OPT_ENQUIRY and GHOSTTY_TERMINAL_OPT_XTVERSION
so C consumers can respond to ENQ (0x05) and XTVERSION (CSI > q)
queries. Both callbacks return a GhosttyString rather than using
out-pointers.
Introduce GhosttyString in types.h as a borrowed byte string
(ptr + len) backed by lib.String on the Zig side. This will be
reusable for future callbacks that need to return string data.
Without an xtversion callback the trampoline returns an empty
string, which causes the handler to report the default
"libghostty" version. Without an enquiry callback no response
is sent.
Test that the write_pty callback receives correct DECRQM response
data and userdata, that queries are silently ignored without a
callback, and that setting null clears the callback. Test that
the bell callback fires on single and multiple BEL characters
with correct userdata, and that BEL without a callback is safe.
Add GHOSTTY_TERMINAL_OPT_BELL so C consumers can receive bell
notifications during VT processing. The bell trampoline follows
the same pattern as write_pty.
Move the C function pointer typedefs (WritePtyFn, BellFn) into
the Effects struct namespace to keep callback types co-located
with their storage and trampolines.
Add a typed option setter ghostty_terminal_set() following the
existing setopt pattern used by the key encoder and render state
APIs. This is the first step toward exposing stream_terminal
Handler.Effects through the C API.
The initial implementation includes a write_pty callback and a
shared userdata pointer. The write_pty callback is invoked
synchronously during ghostty_terminal_vt_write() when the terminal
needs to send a response back to the pty, such as DECRQM mode
reports or device status responses.
Trampolines are always installed at terminal creation time and
no-op when no C callback is set, so callers can configure
callbacks at any point without reinitializing the stream. The C
callback state is grouped into an internal Effects struct on the
TerminalWrapper to simplify adding more callbacks in the future.
The MSVC translate-c output includes Windows SDK declarations,
bringing the total to ~2173 declarations (vs ~1502 on Linux/Mac).
The nested inline for in checkGhosttyHEnum (enum fields x declarations)
exceeds the 1M comptime branch quota for larger enums like MouseShape
(34 variants). Increase to 10M to accommodate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MSVC translates C enums as signed int, while GCC/Clang uses unsigned
int. The freetype Zig bindings hardcode c_uint for enum backing types,
causing type mismatches when compiling with MSVC target.
Fix by adding @intCast at call sites where enum values are passed to
C functions, and @bitCast for the glyph format tag extraction where
bit-shift operations require unsigned integers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use writerStreaming() instead of writer() for stdout in helpgen and
main_build_data. The positional writer calls setEndPos/ftruncate in
end(), which fails on Windows because ftruncate on pipes maps
INVALID_PARAMETER to FileTooBig.
Replace scandir with opendir/readdir plus qsort in framegen since
scandir is a POSIX extension not available on Windows.
This was previously applied and reverted upstream (f4998c6ab, 0fdddd5bc)
as collateral from an unrelated example-execution hang that has since
been resolved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
On Windows, calling `free()` on memory allocated by libghostty crashes
because Zig and MSVC use separate heaps.
This adds `ghostty_free()` so consumers can free library-allocated
memory safely on all platforms.
## Why
When Zig builds a DLL on Windows with `link_libc = true`, it does not
link the Windows C runtime (`ucrtbase.dll`). Instead it uses its own
libc built on top of `KERNEL32.dll`. So `builtin.link_libc` is true and
`c_allocator` is selected, but Zig's `malloc` and MSVC's `malloc` are
different implementations with different heaps. 💥
On Linux/macOS this is not a problem because Zig links the system libc
and everyone shares the same heap. On Windows, `free(buf)` from MSVC
tries to free memory from Zig's heap and you get a debug assertion
failure or undefined behavior.
The `format_alloc` docs said "the buffer can be freed with `free()`" but
that is only true when the library and consumer share the same C
runtime, which is not the case on Windows.
## How
- Add `ghostty_free(allocator, ptr, len)` that frees through the same
allocator that did the allocation
- Update `format_alloc` docs to point to `ghostty_free()` instead of
`free()`
- Update all 3 examples to use `ghostty_free(NULL, buf, len)`
The signature takes an allocator because raw buffers (unlike objects
like terminals or formatters) do not store their allocator internally.
The caller already has all three values: the allocator they passed, the
pointer, and the length they got back.
I went back and forth on the naming. Other options I considered:
`ghostty_alloc_free(allocator, ptr, len)` or returning a `GhosttyBuffer`
wrapper with its own `_free`. Happy to change the naming if there is a
preference.
No impact on Linux/macOS. `ghostty_free()` works correctly there too, it
just happens to call the same `free()` the consumer would have called
anyway.
## Verified
- `zig build test-lib-vt` passes on Windows, macOS arm64, Linux x86_64
(exit 0)
- `zig build test` passes on Windows (2575/2619 passed, 1 pre-existing
font sprite failure) and macOS (exit 0)
- cmake shared example builds, links, and runs correctly on Windows with
`ghostty_free()` (no more heap crash)
## What I Learnt
- What I wrote in Why
- Zig allocators require the length to free (no hidden metadata headers
like C's malloc). This is a deliberate design choice for explicit
control.
- The standard pattern for C libraries on Windows is "whoever allocates,
frees" (like `curl_free()`, `SDL_free()`). This avoids cross-runtime
heap issues entirely.
Add a ghostty_alloc function that pairs with the existing
ghostty_free, giving embedders a symmetric malloc/free-style
API for buffer allocation through the libghostty allocator
interface. Returns NULL on allocation failure.
Extract the inline free_alloc function from main.zig into a new
allocator.zig module in the C API layer. The function is renamed
to alloc_free in main.zig (and free in allocator.zig) for
consistency with the other C API naming conventions. Add tests
for null pointer, allocated memory, and null allocator fallback.
Renames `ReadonlyStream` to `TerminalStream` and introduces an
effects-based callback system so that the stream handler can optionally
respond to queries and side effects (bell, title changes, device
attributes, device status, size reports, XTVERSION, ENQ, DECRQM, kitty
keyboard queries).
The default behavior is still read-only, callers have to opt-in to
setting callbacks to function pointers.
This doesn't handle every possible side effect yet, e.g. this doesn't
include clipboards, pwd reporting, and others. But this covers the
important ones.
This PR is Zig only, the C version of this will come later.
The default firmware_version for Secondary device attributes is 0,
but the test expected a value of 10. Update the test expectation to
match the actual default.
Add a device_attributes effect callback to the stream_terminal
Handler. The callback returns a device_attributes.Attributes
struct which the handler encodes and writes back to the pty.
Add Attributes.encode which dispatches to the correct sub-type
encoder based on the request type (primary, secondary, tertiary).
In readonly mode the callback is null so all DA queries are
silently ignored, matching the previous behavior where
device_attributes was in the ignored actions list.
Tests cover all three DA types with default attributes, custom
attributes, and readonly mode.
Introduce a dedicated device_attributes.zig module that consolidates
all device attribute types and encoding logic. This moves
DeviceAttributeReq out of ansi.zig and adds structured response
types for DA1 (primary), DA2 (secondary), and DA3 (tertiary) with
self-encoding methods.
Primary DA uses a ConformanceLevel enum covering VT100-series
per-model values and VT200+ conformance levels, plus a Feature
enum with all known xterm DA1 attribute codes (132-col, printer,
sixel, color, clipboard, etc.) as a simple slice. Secondary DA
uses a DeviceType enum matching the xterm decTerminalID values.
Tertiary DA encodes the DECRPTUI unit ID as a u32 formatted to
8 hex digits.
This is preparatory work for exposing device attributes through
the stream_terminal Effects callback system.
Previously device_status was in the ignored "no terminal-modifying
effect" group in stream_terminal.zig. This ports it to use the
Effects pattern, handling all three DSR request types.
Operating status and cursor position are handled entirely within
stream_terminal since they only need terminal state and write_pty.
Cursor position respects origin mode and scrolling region offsets.
Color scheme adds a new color_scheme effect callback that returns
a ColorScheme enum (light/dark). The handler encodes the response
internally, keeping protocol knowledge in the terminal layer. A
new ColorScheme type is added to device_status.zig so the terminal
layer does not depend on apprt.
Previously the ENQ (0x05) action was ignored in stream_terminal,
listed in the no-op group alongside other unhandled queries. The
real implementation in termio/stream_handler writes a configurable
response string back to the pty.
Add an enquiry callback to Effects following the same query-style
pattern as xtversion: the callback returns the raw response bytes
and the handler owns writing them to the pty via writePty. When no
callback is set (readonly mode), ENQ is silently ignored. Empty
responses are also ignored. The response is capped at 256 bytes
using a stack buffer with sentinel conversion for writePty.
## What
Two fixes for tests that fail on Windows due to Unix-specific
assumptions.
1. The "cache directory paths" test in xdg.zig hardcodes Unix paths like
`/Users/test/.cache` in expected values. The function under test uses
`std.fs.path.join` which produces native separators, so the expectations
need to match. Fixed by using `std.fs.path.join` for expected values
too, with a platform-appropriate mock home path.
2. Two shell integration tests for `setupXdgDataDirs` hardcode Unix path
separators (`:`) and Unix default paths (`/usr/local/share:/usr/share`).
These are not applicable on Windows where the delimiter is `;` and
`XDG_DATA_DIRS` is not a standard concept. Skipped on Windows with
`SkipZigTest`.
## Why skip instead of fix for the shell integration tests?
`setupXdgDataDirs` is used by fish, elvish, and nushell. On Windows,
`XDG_DATA_DIRS` is not standard. The equivalent would be `%ProgramData%`
(what Go's `adrg/xdg`, Python's `platformdirs`, and others map to).
Fixing this properly means adding a Windows-appropriate default, which
is a separate change. (How do you guys deal with these situations? Do
you create issues on the spot as reminders or do you wait for the
requirement to emerge by itself when the time comes?
Worth noting: the production code on line 664 of `shell_integration.zig`
hardcodes the fallback to `"/usr/local/share:/usr/share"` with `:`
separators, while `prependEnv` correctly uses `std.fs.path.delimiter`
(`;` on Windows). If a shell that uses this runs on Windows, you would
get mixed separators. Tracked separately.
## Verified
- `zig build test-lib-vt` passes on Windows (exit 0)
- No behavior change on Linux/macOS (xdg.zig fix produces same paths,
shell_integration skip only triggers on Windows)
## What I Learnt
- `std.fs.path.join` uses the native path separator, so tests that
hardcode `/` in expected paths will fail on Windows even if the
production code is correct. Better to use `path.join` in test
expectations too.
- The XDG Base Directory spec is Unix-only but cross-platform libraries
have converged on mappings. Ghostty maps to `%LOCALAPPDATA%` which
matches common conventions. The missing piece is `XDG_DATA_DIRS` which
has no Windows default and falls through to Unix paths.
Add a `size` callback to the stream_terminal Effects struct that
returns a size_report.Size geometry snapshot for XTWINOPS size
queries (CSI 14/16/18 t). The handler owns all protocol encoding
using the existing size_report.encode, keeping VT knowledge out
of effect consumers. This follows the same pattern as the xtversion
effect: the callback supplies data, the handler formats the reply
and calls write_pty.
CSI 21 t (title report) is handled internally from terminal state
since the title is already available via terminal.getTitle() and
does not require an external callback.
Add an xtversion callback to the Effects struct so that
stream_terminal can respond to XTVERSION queries. The callback
returns the version string to embed in the DCS response. If the
callback is unset or returns an empty string, the response defaults
to "libghostty". The response is formatted and written back via the
existing write_pty effect.
Previously kitty_keyboard_query was listed as a no-op in the
readonly stream handler. This implements it using the write_pty
effect callback so that the current kitty keyboard flags are
reported back via the pty, matching the behavior in the full
stream handler.
The effect callback no longer receives the title string directly.
Instead, the handler stores the title in terminal state via setTitle
before invoking the callback, so consumers query it through
handler.terminal.getTitle(). This removes the redundant parameter
and keeps the effect signature consistent with the new terminal
title field. Tests now verify terminal state directly rather than
tracking the title through the callback.
Add a title field to Terminal, mirroring the existing pwd field.
The title is set via setTitle/getTitle and tracks the most recent
value written by OSC 0/2 sequences. The stream handler now persists
the title in terminal state in addition to forwarding it to the
surface. The field is cleared on full reset.
Previously the window_title action was silently ignored in the
readonly stream handler. Add a set_window_title callback to the
Effects struct so callers can be notified when a window title is
set via OSC 2. Follows the same pattern as bell and write_pty
where the callback is optional and defaults to null in readonly
mode.
Add a generic write_pty effect callback to the stream terminal
handler, allowing callers to receive pty response data. Use it to
implement request_mode and request_mode_unknown (DECRQM), which
encode the mode state as a DECRPM response and write it back
through the callback. Previously these were silently ignored.
The write_pty data is stack-allocated and only valid for the
duration of the call.
Rename stream_readonly.zig to stream_terminal.zig and its exported
types from ReadonlyStream/ReadonlyHandler to TerminalStream. The
"readonly" name is now wrong since the handler now supports
settable effects callbacks. The new name better reflects that this
is a stream handler for updating terminal state.
Add an Effects struct to the readonly stream Handler that allows
callers to provide optional callbacks for side effects like bell.
Previously, the bell action was silently ignored along with other
query/response actions. Now it is handled separately and dispatched
through the effects callback if one is provided.
Add a test that verifies bell with a null callback (default readonly
behavior) does not crash, and that a provided callback is invoked
the correct number of times.
## What
Skip the `expandHomeUnix` test on Windows with `SkipZigTest`.
`expandHomeUnix` is a Unix-internal function that is never called on
Windows. The public `expandHome` already returns the path unchanged on
Windows (added upstream in cccdb0d2a). But the unit test calls
`expandHomeUnix` directly, which invokes `home()` and expects Unix-style
forward-slash separators, so it fails on Windows.
## How
Two lines:
```zig
if (builtin.os.tag == .windows) return error.SkipZigTest;
```
## Verified
- `zig build test-lib-vt` passes on Windows (exit 0)
- No behavior change on Linux/macOS
## What I Learnt
- When upstream adds a platform dispatch for production code (like
`expandHome` returning unchanged on Windows), the unit tests for
internal platform-specific functions (like `expandHomeUnix`) may still
need a skip guard.
- Zig doesn't have something like Go's `//go:build` but damn... comptime
is insane, like supercharged C# `#if`
On Windows, Zig's built-in libc and MSVC's CRT maintain separate
heaps, so calling free() on memory allocated by the library causes
undefined behavior. Add ghostty_free() that frees through the same
allocator that performed the allocation, making it safe on all
platforms.
Update format_alloc docs and all examples to use ghostty_free()
instead of free().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SIMD C++ files reference __ubsan_handle_* symbols when compiled
in debug mode, but we do not link or bundle the ubsan runtime on
MSVC. This matches what the highway and simdutf packages already do
in their own build files.
expandHomeUnix is a Unix-internal function that is never called on
Windows. The public expandHome function returns the path unchanged
on Windows since ~/ is not a standard Windows idiom. The test calls
expandHomeUnix directly, which invokes home() and expects Unix-style
forward-slash separators.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SIMD C++ files use C++17 features (std::optional, std::size).
With Zig's bundled libc++ these are available implicitly, but MSVC
headers guard C++17 features behind the standard version
(_HAS_CXX17). Without an explicit -std=c++17 flag, clang defaults
to a lower standard and the MSVC <optional> header does not define
std::optional.
When compiling C++ files, Zig unconditionally passes -nostdinc++ and,
if link_libcpp is set, adds its bundled libc++/libc++abi include paths
as replacements (see Compilation.zig). On MSVC targets this conflicts
with the MSVC C++ runtime headers (vcruntime_typeinfo.h,
vcruntime_exception.h, etc.), causing compilation failures in SIMD
C++ code.
The fix is to use linkLibC instead of linkLibCpp on MSVC. Zig always
passes -nostdinc to strip default search paths, but LibCDirs.detect
re-adds the MSVC SDK include directories, which contain both C and
C++ standard library headers. This gives us proper access to MSVC's
own <optional>, <iterator>, <cstddef>, etc. without the libc++
conflicts.
For the package builds (highway, simdutf, utfcpp) this means
switching from linkLibCpp to linkLibC on MSVC. For SharedDeps and
GhosttyZig, linkLibC is already called separately, so we just skip
linkLibCpp.
Make the "cache directory paths" test cross-platform by using
std.fs.path.join for expected values and a platform-appropriate
mock home path, since the function under test uses native path
separators.
Skip the two shell integration XDG_DATA_DIRS tests on Windows.
These tests use hardcoded Unix path separators (:) and Unix default
paths (/usr/local/share:/usr/share) which are not applicable on
Windows where the path delimiter is ; and XDG_DATA_DIRS is not a
standard concept.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zig's bundled libc++/libc++abi conflicts with the MSVC C++ runtime
headers (vcruntime_typeinfo.h, vcruntime_exception.h, etc.) when
targeting native-native-msvc. This caused compilation failures in
the SIMD C++ code due to -nostdinc++ suppressing MSVC headers and
libc++ types clashing with MSVC runtime types.
Skip linkLibCpp() for MSVC targets across all packages (highway,
simdutf, utfcpp) and the main build (SharedDeps, GhosttyZig) since
MSVC provides its own C++ standard library natively. Also add
missing <iterator> and <cstddef> includes that were previously
pulled in transitively through libc++ headers but are not
guaranteed by MSVC's headers.
Zig defaults to the GNU ABI on Windows, which produces COFF objects
with invalid COMDAT sections in compiler_rt that the MSVC linker
rejects (LNK1143), and uses GNU conventions like ___chkstk_ms that
are unavailable in the MSVC CRT.
Default to the MSVC ABI when no explicit ABI is requested, following
the same pattern as the existing macOS target override. This ensures
compiler_rt produces valid COFF and the generated code uses
MSVC-compatible symbols. Users can still explicitly request the GNU
ABI via -Dtarget.
Also disable bundling ubsan_rt on Windows (its /exclude-symbols
directives are MSVC-incompatible) and add ntdll and kernel32 as
transitive link dependencies for the static library.
Three issues when linking the static library with the MSVC linker:
Use the LLVM backend on Windows to produce valid COFF objects.
The self-hosted backend generates compiler_rt objects with invalid
COMDAT sections that the MSVC linker rejects (LNK1143).
Disable bundling ubsan_rt on Windows. Zig's ubsan runtime emits
/exclude-symbols linker directives that MSVC does not understand
(LNK4229).
Add ntdll and kernel32 as transitive link dependencies for the
static library on Windows. The Zig standard library uses NT API
functions (NtClose, NtCreateSection, etc.) that consumers must
link.
Zig's compiler_rt produces COFF objects with invalid COMDAT
sections that the MSVC linker rejects (LNK1143), and its ubsan_rt
emits /exclude-symbols directives that MSVC does not understand
(LNK4229). Skip bundling both in the static library on Windows
since the MSVC CRT provides the needed builtins (memcpy, memset,
etc.). The shared library continues to bundle compiler_rt as it
needs to be self-contained.
Zig's ubsan runtime emits /exclude-symbols linker directives that
are incompatible with the MSVC linker, causing LNK4229 warnings and
LNK1143 errors. Disable bundling ubsan_rt on Windows while keeping
compiler_rt which provides essential symbols like memcpy, memset,
memmove, and ___chkstk_ms.
The previous check used target.result.abi == .msvc which never
matched because Zig defaults to the gnu ABI on Windows.
Zig's ubsan instrumentation emits ELF-style /exclude-symbols linker
directives into the compiled object files, causing LNK4229 warnings
with the MSVC linker. The bundled compiler_rt also produces COMDAT
sections that are incompatible with MSVC, causing fatal LNK1143.
Disable sanitize_c entirely on the root module for MSVC targets and
skip bundling both compiler_rt and ubsan_rt since MSVC provides its
own runtime.