Skip to content

Upgrade to Zig 0.17 - #1163

Merged
fazxes merged 36 commits into
mainfrom
fazxes/zig-0.17-upgrade
Oct 12, 2026
Merged

fazxes merged 36 commits into
mainfrom
fazxes/zig-0.17-upgrade

Conversation

@fazxes

@fazxes fazxes commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Build fx with Zig 0.17.0. build.zig.zon requires it, and every CI, release, benchmark, and libfx workflow installs it.

  • Require macOS 15 or later for the macOS release binaries, which is Zig 0.17's default minimum. Binaries built with Zig 0.16 ran on macOS 13.

  • Accept MCP authorization documents, gateway generation responses, and provider error bodies that are exactly at their size limit, because Zig 0.17's allocRemaining treats the limit as inclusive. readFileToEnd still rejects a file of exactly max_bytes.

  • Use Zig's memset in Linux binaries in place of fx's replacement, now that Zig's version stores a full vector per iteration.

  • Qualify PGSO builds with LLVM 22.1.8, the LLVM version in Zig 0.17.

  • Run allocation-failure sweeps and allocation-count tests on an allocator that never resizes in place, so they make the same allocations on every run under Zig 0.17's SafeAllocator.

  • Read and set terminal attributes with the kernel ioctls on Linux. Zig 0.17 sizes std.posix.termios to the kernel struct, but std.posix.tcgetattr calls glibc when libc is linked, and glibc writes its larger struct past the end of it. ReleaseSafe Linux builds crashed in the terminal launcher and on interactive startup.

  • Build the MCP stdio dispatcher driver once before its E2E tests. Zig 0.17 spends about 50 seconds compiling its build runner against an empty cache, which is longer than the first test's timeout in the uncached macOS platform job.

  • Stop using every standard library declaration that Zig 0.17 deprecates, such as std.fmt.allocPrint, the std.mem.indexOf family, std.fs.path, and @import("builtin").os. Most are exact renames. std.mem.copyForwards became @memmove, with each destination sliced to its source's length where it was longer, and the trace report maps builtin.optimize back to the ReleaseSafe style labels it printed before. The one deprecated call left is b.addTranslateC, because its replacement is the external translate-c package.

  • Decode single UTF-8 sequences with a helper that checks the lead byte against the slice length. The deprecated std.unicode.utf8Decode reached an assertion when they disagreed, so inline rendering crashed when a truncated sequence preceded an emphasis delimiter.

  • Profile the seed's verified bitcode in the distributed PGSO candidate job instead of rebuilding it. Zig 0.17 can number anonymous symbols differently on another runner, so a rebuild of the same commit no longer matches byte for byte.

  • Link the PGSO candidate against a copy of Zig's macOS 27 libSystem.tbd without its arm64e.x1 targets. The Xcode 16.4 linker on the macos-15 runners rejects that architecture, and the arm64 link never uses it.

  • Unroll and fold constant-trip loops before PGSO instrumentation and profile use. Zig 0.17 emits array bitcasts as per-element loops, so the training profile counted every element and mem.eqlBytes dominated it, which pushed nearly all other code below the cold cutoff.

fazxes added 2 commits October 5, 2026 19:44
Build fx with Zig 0.17.0. build.zig.zon requires it, every workflow
installs it, and PGSO qualification uses LLVM 22.1.8 to match.

Zig 0.17 removes `**`, `errdefer |err|`, `@cImport`, and the old type
info field lists. The source moves to `@splat`, a comptime
`text_utils.repeat` helper, split cleanup functions, and the new
field_names and field_types arrays. The Node-API addon translates
node_api.h with a build step. Test fakes checked with `@hasDecl` become
pub, because `@hasDecl` no longer sees private declarations.

`zig build test` gives the test binary its fx and source paths through
a generated test_paths module, since the build no longer exposes install
paths for environment variables.

Zig 0.17's SafeAllocator grows a small allocation in place only when it
sits at the end of a bucket, and buckets are never reused, so an
allocation-failure sweep could count a different number of allocations
on each run. Sweeps and allocation-count tests now use
testing_allocator.no_resize, which never resizes in place.

allocRemaining now treats its limit as inclusive. readFileToEnd keeps
rejecting a file of exactly max_bytes by reading through
appendRemaining. Direct allocRemaining callers for remote responses now
accept a body exactly at their limit.

Zig 0.17's memset stores a full vector per iteration, so the Linux
memset replacement is removed. macOS release binaries now require macOS
15, Zig 0.17's default minimum.
The test only needs a current model and filler entries that share its
prefix, so it no longer names a real model.
@fazxes fazxes added the type: maintenance Changes internal tooling, dependencies, CI, or implementation structure label Oct 5, 2026
fazxes added 11 commits October 5, 2026 19:54
The compactor's allocation-failure test now backs its sweep with
testing_allocator.no_resize. That file imports only std, so it belongs
with the shared basics the boundary check already allows.
Zig 0.17's `zig build` no longer accepts --global-cache-dir, so the
binary size builds and the PGSO pipeline set ZIG_GLOBAL_CACHE_DIR for
the same directories.

The binary size check now builds the base commit with the Zig version
that commit pins in build.zig.zon. A pull request that changes the
toolchain could not build its base with the new compiler, and the
comparison now reports the size change across the toolchain bump.
Zig 0.17 sizes std.posix.termios on Linux to the kernel struct, but with
libc linked std.posix.tcgetattr calls glibc, which writes its larger
struct and overruns the caller's stack. ReleaseSafe builds crashed in
the terminal launcher and on interactive startup.

Use the TCGETS and TCSETS ioctls on Linux so the struct and the call
share one layout. Other platforms keep the std.posix functions.
# Conflicts:
#	src/core/images/png_downscale.zig
Zig 0.17 compiles its build runner the first time zig build runs
against an empty cache, which takes about 50 seconds. The macOS
platform E2E job starts without a Zig cache, so the first MCP test
that ran the dispatcher driver timed out and was killed before the
cache was written, leaving every later test cold as well.

Build the driver once in a beforeAll with a longer timeout through a
new build-only step. The MCP auth tests also keep Zig's global cache
when they isolate HOME, so local runs stop rebuilding it per test.
Zig 0.17 can number anonymous symbols differently on another runner,
so the same commit emits bitcode that differs in a few symbol names.
The candidate job rebuilt the bitcode and required it to match the
seed byte for byte, which failed on every run.

Copy the seed's hash-verified bitcode instead, the same way the job
already reuses the seed's control binary and merged profile.
Zig 0.17 bundles the macOS 27 libSystem stub, which lists arm64e.x1
targets. The Xcode 16.4 linker on the macos-15 runners rejects that
architecture, so the temporal candidate link failed. Link against a copy
of the pinned stub with only those target entries removed. The arm64
link never resolves against them, and the evidence records the hashes
of both stubs.
Zig 0.17 emits array bitcasts, such as the [4]u8 reads in std.mem.eql,
as per-element loops that the optimizer later folds into single loads.
Instrumentation runs before loop unrolling, so the training profile
counted every element. On the e2e-cli and e2e-tui-resize shard,
mem.eqlBytes rose from 1.5 billion counts to 22 billion while every
other function stayed level. That one function then held most of the
profile, so the cold cutoff marked nearly all other code minsize and a
supplemented function landed exactly on the cutoff.

Run sroa and loop-unroll-full ahead of the default pipeline when
generating and using profiles. Both sides see the same IR at the
instrumentation point, so CFG hashes still match, and eqlBytes gets the
same counter layout it had on Zig 0.16.
Unrolling constant-trip loops ahead of the default pipeline removed the
per-element counts, but the raw unrolled code also made the optimized
benchmarks slower. The ui-activity candidate ran about 19% slower than
its plain O2 control, and an unprofiled build with the same preparation
was 11% slower on its own. Running instcombine before and after the
unroll and simplifycfg after it brings that build within about 1% of
plain O2, while eqlBytes keeps its Zig 0.16 counter layout.
Open src/main.zig relative to the build root handle instead of joining
the root path by hand, so reading the version needs no separate
allocation failure path.
pathForReexec already resolves the installed test binary from test_paths,
so the test-only branch in foregroundSessionExecutable repeated it with
an error arm that could no longer run. Drop it and keep testProductExe
private to self_exe.
@fazxes
fazxes marked this pull request as ready for review October 6, 2026 13:15
fazxes added 2 commits October 6, 2026 21:31
Zig 0.17 removed the ** operator. The tall pasted prompt test that
landed on main used it, so build its body with text_utils.repeat like
the rest of the file.
fazxes added 2 commits October 7, 2026 00:57
Zig 0.17's @hasDecl only finds pub declarations. The host workspace test
app that landed on main declared workspaceHostInfo without pub, so the
agent runtime ignored its host root and two tests failed.
@fazxes
fazxes marked this pull request as draft October 7, 2026 05:03
@fazxes
fazxes marked this pull request as ready for review October 7, 2026 06:17
fazxes added 2 commits October 9, 2026 02:34
# Conflicts:
#	src/core/agent/runtime/parallel_execution.zig
The libfx durability work and the other changes that just landed on main
were written for Zig 0.16.

- Six tests built strings with the ** operator, which 0.17 removed. Build
  them with text_utils.repeat.
- journal.zig built a std.json.Array from its fields. 0.17 added a
  pointer_stability field to that type, so use fromOwnedSlice, which sets
  the same fields.
- A new allocation-failure sweep used std.testing.allocator. Use
  testing_allocator.no_resize like the other sweeps, so the allocation
  sequence is the same on every run.
@fazxes
fazxes marked this pull request as draft October 9, 2026 06:58
@fazxes
fazxes marked this pull request as ready for review October 9, 2026 07:59
fazxes added 3 commits October 9, 2026 06:02
Zig 0.17 deprecates standard library names that now forward to new
declarations. Replace each use with the declaration it forwards to, so
behavior does not change:

- std.fmt.allocPrint(alloc, ...) becomes alloc.print(...), and
  std.fmt.bufPrint becomes std.mem.print.
- The std.mem.indexOf and lastIndexOf families become std.mem.find,
  findScalar, findLast, and the rest of the find family.
- std.fs.path and std.fs.max_path_bytes become std.Io.Dir.path and
  std.Io.Dir.max_path_bytes, and path.resolve and path.relative become
  resolveAlloc and relativeAlloc.
- @import("builtin").os and .cpu become .target.os and .target.cpu.
- std.builtin becomes std.lang, and the Unmanaged container aliases
  become std.ArrayList, std.array_hash_map.String, and
  std.bit_set.Dynamic.

A script made these edits outside strings and comments and resolved
each file's local aliases, so this commit contains only renames.
These replacements change more than a name, so each call site was
checked by hand:

- std.mem.copyForwards and copyBackwards become @memmove. @memmove
  requires equal lengths, so the six sites that copied into a longer
  destination now slice it to the source length.
- std.unicode.utf8Decode becomes display_width.decodeUtf8Sequence,
  which checks the lead byte against the slice length before decoding.
  The deprecated decoder reached an assertion when they disagreed, so
  inline rendering crashed when a truncated UTF-8 sequence preceded an
  emphasis delimiter, on Zig 0.16 as well. A regression test covers
  that input, and inline_render.zig joins the unit test list.
- builtin.mode becomes builtin.optimize. Zig 0.17 renamed its tags to
  debug, safe, fast, and small, so the trace report maps them back to
  the Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall labels it
  printed before.
- std.debug.runtime_safety becomes builtin.optimize.runtimeSafety(),
  std.math.divCeil becomes @divCeil, and ArrayList.getLast becomes
  last().?.
AGENTS.md lists the Zig 0.17 standard library forms that fx uses in
place of the declarations 0.17 deprecates, and its I/O section no
longer refers to 0.16. Comments that described Zig 0.16 behavior now
describe 0.17: Response.reader is unchanged, Writer.Allocating still
reports allocation failure as WriteFailed, and Darwin preserve-rename
calls renameatx_np but still needs fx's own error mapping. The web
research eval prompt names Zig 0.17.
@fazxes
fazxes marked this pull request as draft October 9, 2026 10:04
fazxes added 7 commits October 9, 2026 06:40
Rename the two std.mem.lastIndexOfScalar calls that follow a slice
range, which the earlier rename missed, to std.mem.findScalarLast.

Correct the preserve-rename comment: Zig's errnoBug panics only in
Debug builds and returns error.Unexpected otherwise, so ENOTEMPTY does
not panic in ReleaseSafe. fx still needs its own error mapping.
# Conflicts:
#	src/builtins/mcp.zig
#	src/core/mcp/mcp_runtime.zig
#	src/core/mcp/project_config.zig
#	src/core/mcp/tool_search.zig
The MCP-v2 engine and the other changes that just landed on main were
written for Zig 0.16.

- Rename the APIs that 0.17 deprecates in the new code, with the same
  exact-alias renames as the rest of the branch: allocPrint and bufPrint,
  std.mem.indexOf* and lastIndexOf*, std.fs.path, and builtin.os. zig fmt
  rewrote @intFromEnum and @enumFromInt as @backingInt and @fromBackingInt.
- Three tests built strings with the ** operator, which 0.17 removed. The
  MCP host test uses text_utils.repeat. The src/mcp tests use @Splat,
  because that engine imports nothing outside its own directory.
- The MCP host menu filter called std.ascii.indexOfIgnoreCase, which 0.17
  removed. Call findIgnoreCase, which it aliased.
- Two event queues and a fuzz test shifted elements with copyForwards or
  copyBackwards. Their source and destination lengths already match, so
  use @memmove.

The nine @hasDecl probes in the new code all name pub declarations, so
the 0.17 rule that @hasDecl sees only pub declarations leaves them as
they were.
Six lines carried over from main formatted bounded values into fixed
buffers and asserted the result with catch unreachable.

- handleKernelCheckpoint, emitInlineLink, and a menu test already return
  errors, so they propagate the result with try.
- The MCP sign-in redirect URI is assembled from its parts with
  std.fmt.printInt. A comptime check proves the longest port fits the
  64-byte buffer.
- The pending blob name is built with std.fmt.bytesToHex, which cannot
  fail and produces the same name.
- failureDiagnosticValue copies the code and detail into its buffer. The
  detail is already capped so the joined text fits.

A new test checks the redirect URI for one-, four-, and five-digit ports.
# Conflicts:
#	README.md
#	src/gateway/host_stream_provider.zig
The Codex host stream that just landed on main converts the HTTP status
with @enumFromInt, which 0.17 deprecates. Use @fromBackingInt with the
same @intcast as the rest of the file, because the status code is a u16
and std.http.Status is backed by a narrower integer.

The rewritten README still asked for Zig 0.16.0 to build fx. Ask for
0.17.0.
@fazxes
fazxes marked this pull request as ready for review October 11, 2026 06:50
# Conflicts:
#	src/core/app/app_mcp_runtime.zig
#	src/core/mcp_host/login.zig
#	src/mcp/auth/oauth.zig
#	src/ui/footer/mcp_host_menu_presentation.zig
The MCP-v2 fixes that just landed on main format with std.fmt.allocPrint
and std.fmt.bufPrint, which 0.17 deprecates. Use alloc.print and
std.mem.print, as the rest of the code does.

A menu test builds a long name with **, which 0.17 removed. Build it with
text_utils.repeat.

The sign-in bridge formats its host header with bufPrint and catch
unreachable. answerForm already returns errors, so use try.
@fazxes
fazxes marked this pull request as draft October 11, 2026 21:39
@fazxes
fazxes marked this pull request as ready for review October 12, 2026 01:12
# Conflicts:
#	src/core/execution/command_runner.zig
#	src/gateway/host_model_catalog.zig
#	src/gateway/host_stream_provider.zig
#	src/gateway/js_host_model_catalog.zig
The command environment and direct-command permission changes that just
landed on main read builtin.os, which 0.17 deprecates. Read
builtin.target.os instead.

The new host HTTP status check converts with @enumFromInt, and its test
reads the status back with @intFromEnum. Use @fromBackingInt with
@intcast and @backingInt, as the rest of the gateway does.
# Conflicts:
#	src/core/mcp_host/login.zig
#	src/core/session_manager/log.zig
The request image budget that just landed on main formats with
std.fmt.allocPrint, which 0.17 deprecates, and one of its agent tests
builds a long model name with **, which 0.17 removed. Use the arena's
print and text_utils.repeat.

The catch unreachable cleanup changed the sign-in bridge's host header
to try std.fmt.bufPrint and gates the log's batch check on
std.debug.runtime_safety. Use std.mem.print and
builtin.optimize.runtimeSafety().
@fazxes
fazxes marked this pull request as draft October 12, 2026 03:26
The gateway request watch that just landed on main explains its design
in terms of Zig 0.16. Select and its queue are unchanged in 0.17, and
cancelling a still-waiting watch signals its thread the same way, so
name the version fx now builds with.
@fazxes
fazxes marked this pull request as ready for review October 12, 2026 04:38
@fazxes
fazxes merged commit f7d6cd1 into main Oct 12, 2026
93 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: maintenance Changes internal tooling, dependencies, CI, or implementation structure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant