Zig 0.17: what breaks in your build.zig and how to enable incremental compilation

Zig 0.17 is now available: your build.zig needs changes, incremental compilation works on x86_64-linux with a single flag, and ZLS still isn’t compatible. These three things matter if you maintain a Zig project. Additionally, @bitCast can change behavior without the compiler warning you. The release represents five months of work from 206 contributors in 925 commits, and nearly all that effort concentrated on the build system and linker.

What is Zig and how does it differ from Rust?

Zig is a general-purpose programming language and toolchain for systems programming, aimed at robust, optimal, and reusable software. It competes in the same space as C, C++, and Rust, but with a different philosophy.

Rust guarantees memory safety at compile time through its borrow checker. Zig doesn’t have a borrow checker: it gives you explicit allocators, compile-time code execution with comptime, and safety checks at runtime in safe compilation modes. In return, it offers a smaller language and direct C interoperability. Its toolchain also cross-compiles C and C++ with zig cc. If you’re wondering zig vs rust, the choice comes down to how much manual control you want and how many guarantees you prefer to delegate to the compiler.

Zig hasn’t reached 1.0 yet, so each minor version can break code. That’s why each update needs a migration guide. The project is funded by the Zig Software Foundation, a nonprofit organization, and its source code now lives on Codeberg instead of GitHub.

What changes in zig build with Zig 0.17?

zig build no longer runs your build.zig in the same process as the build. The old build runner separates into two pieces:

  • The configurer runs your build.zig and generates the build graph.
  • The maker handles package management and executes that graph.

In practice, this has three effects:

  • The maker compiles only once. It’s built with optimizations the first time you use Zig after installing it, and doesn’t need recompilation when you edit build.zig.
  • Configuration can be skipped. If nothing relevant changed, zig build can skip your build.zig entirely.
  • Configuration is now data. The build graph is serialized in a compact binary format that external tools can read. To see it, pass --print-configuration to zig build and it will print it as .zon to standard output.

That last point is the foundation of the new Build Server Protocol. If you pass --listen=-, the build system exposes a protocol that lets a connected client inspect the complete build graph, receive notifications when a step starts and ends, and request specific steps to run. It’s designed for IDEs and third-party tools. As a result, it’s no longer possible to replace the build runner: that use case is now covered by the protocol.

What do you need to change in build.zig?

The release notes explicitly list the build system’s API changes. Here are the ones most likely to affect a typical project.

Run step arguments. Build scripts can no longer inspect the arguments passed to the program. In return, modifying those arguments no longer forces recompilation of build.zig:

-if (b.args) |args| {
-    run_cmd.addArgs(args);
-}
+run_cmd.addPassthruArgs();

Fmt step paths. These are now lists of LazyPath, and are created with b.pathList:

-    const fmt_include_paths = &.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" };
+    const fmt_include_paths = b.pathList(&.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" });

Renames and removals:

  • b.build_root (a Directory) becomes b.root (a Path).
  • LazyPath.getDisplayName becomes format, used with "{f}".
  • LazyPath.basename is removed, because its value isn’t known until the make phase.
  • The Run step argument helpers are unified into a single variant ending in 2. For example, addArtifactArg and addPrefixedArtifactArg become addArtifactArg2, and addFileArg and addPrefixedFileArg become addFileArg2. The same pattern applies to output file arguments, file content, directory, output directory, and dep-file arguments.
  • ConfigHeader.Options.include_guard_override becomes include_guard.
  • Options that receive a path must now indicate what type of path it is: addOptionPath for a file, addOptionPathDirectory for a directory, or addOptionPathUntracked to exclude it from dependency tracking.

C translation. @cImport is removed; it was already deprecated in 0.16. std.Build.Step.TranslateC is deprecated in favor of the official translate-c package. First add the dependency:

zig fetch --save git+https://codeberg.org/ziglang/translate-c

Then replace b.addTranslateC(...) with the Translator from the package:

+const Translator = @import("translate_c").Translator;
 ...
-    const translate_c = b.addTranslateC(.{
-        .root_source_file = b.path("src/c.h"),
+    const translate_c = b.dependency("translate_c", .{});
+
+    const translator: Translator = .init(translate_c, .{
+        .c_source_file = b.path("src/c.h"),
         .target = target,
         .optimize = optimize,
     });
 ...
-                    .module = translate_c.createModule(),
+                    .module = translator.mod,

Side effects in configuration. Zig now detects when your configuration logic does something that the cache can’t track. Calling findProgram, for example, “poisons” the configuration cache, and that forces build.zig to run on every build. If you only need the program at build time, use findProgramLazy: it returns a LazyPath and leaves the cache intact. If your configuration reads files or directories, declare that dependency with std.Build.dependOnFileContents or one of its variants, and the cache will continue to work.

How do I enable incremental compilation in Zig?

Run the build with these two flags:

zig build -fincremental --watch

The build system watches your source files and does an incremental recompilation every time they change. According to the release notes, incremental compilation already works for most projects targeting x86_64-linux. It’s the result of many bug fixes and better support in the new ELF linker that arrived in the previous version.

The release notes set three conditions:

  • For now it requires --watch. Using incremental compilation without watch mode is listed as future work.
  • In practice it’s only for Linux x86_64. The new ELF linker is the piece that makes it possible. On Mac and aarch64 you have to wait for a new Mach-O linker and a native aarch64 backend, both planned for future versions.
  • The new linker remains disabled by default. It still doesn’t quite match the legacy ELF linker in functionality, but it turns on automatically when you use incremental compilation, just like in 0.16.

If you want to understand how it works under the hood, the release notes recommend this article by Matthew Lugg, a member of the Zig core team: Inside Zig's Incremental Compilation | mlugg.co.uk

Does ZLS work with Zig 0.17?

No: at the time this note was published (October 2026), ZLS doesn’t work with Zig 0.17. The release notes indicate that the separation between configurer and maker is an incompatible change that prevents ZLS, Zig’s language server, from working with this version. The Zig and ZLS teams are working together to extend the Build Server Protocol, so ZLS can regain its features and eventually surpass them.

If your editor workflow depends on ZLS for autocompletion, code navigation, or diagnostics, don’t update your main environment yet. Check the project status at ZLS language server - zigtools before switching.

What can break without the compiler warning you?

@bitCast is the most dangerous change in 0.17, because it can break code without generating any compilation errors. It’s now defined as a reinterpretation of the logical bit representation of a value.

  • What stays the same: casts between integers, and between integers and packed struct or packed union.
  • What changes: casts involving arrays or vectors have new semantics. The release notes warn that this can break existing code without compilation errors. The new definition is independent of endianness and largely matches the previous behavior on little-endian architectures.

Before updating, search your code for @bitCast calls involving arrays or vectors and review them one by one.

Casts with extern struct or extern union are now a compilation error. If what you’re looking for is type punning, the release notes recommend @ptrCast or an extern union.

What else changed in the language?

The following changes fail visibly at compile time, so the compiler will take you straight to them:

  • Array multiplication (a ** b) is removed. Use @splat, for example var result: [n]u8 = @splat(0);.
  • void{} is removed. Write {}.
  • Capture in errdefer |err| is removed. The notes suggest splitting the function into two and handling the error with catch in the outer function.
  • i0 is removed. You can almost certainly replace it with u0.
  • @backingInt and @fromBackingInt replace @intFromEnum and @enumFromInt. zig fmt does this update automatically.
  • @hasDecl now returns true only for public declarations, even when called from the same file.
  • New builtins: @divCeil, and @SpirvType for SPIR-V targets.

The standard library has its own list of changes:

  • std.builtin is deprecated in favor of std.lang.
  • OptimizeMode becomes Optimize, with tags debug, safe, fast, and small. The old names still exist, but comparisons with == against them no longer compile.
  • StackFallbackAllocator is now called BufferFirstAllocator.
  • std.heap.DebugAllocator is deprecated in favor of the new SafeAllocator, which is thread-safe.
  • std.fmt.allocPrint(arena, …) becomes arena.print(…).

Should you update now?

It depends on your setup:

  • Update if you compile mainly for x86_64-linux, don’t rely on ZLS, and want faster recompilations. Incremental compilation is the reward, and changes to build.zig are mechanical.
  • Wait if your editor workflow depends on ZLS, or if you use Run steps with response files. The release notes list it as a known regression in this version.
  • In any case, review your @bitCast calls before publishing any binary compiled with 0.17.

The version also brings Zig closer to 1.0. The team made decisions on around 150 language proposals in this cycle: it accepted around 25 and rejected around 125. The WebAssembly backend already passes 100% of behavior tests against the LLVM backend, but it’s not the default in debug mode yet because it lacks debugging information support.

You can download this version from Download ⚡ Zig Programming Language or install it with a package manager, following the project’s guide.


What about you? Which project are you going to test incremental compilation on first?

Are you going to update to Zig 0.17?
  • Yes, right now
  • When ZLS is compatible
  • I’m staying on 0.16 for now
  • I don’t use Zig yet (tell us what’s holding you back)
0 votantes

Comment below or, if this article reached you by email, reply directly to the email: your response will be published here.

Related: Lightpanda: The Headless Browser Made for AI Agents (Not for Humans)