Zig: Build System Reworked
Recorded: May 30, 2026, 10 a.m.
| Original | Summarized |
Devlog Devlog Download Download Devlog Also available as an This page contains entries for the year 2026. Other years are available in May 26, 2026 Author: Andrew KelleyBig branch just landed: separate the maker process from the configurer processThis devlog entry is essentially a preview of the upcoming release notes, but serves as an advanced notice to those who want to help test out the new features and provide feedback that will guide the Zig project moving forward.Before, build.zig files plus the build system implementation were all compiled into one bloated process, in Debug mode. After build.zig logic finished constructing a build graph in memory, the “build runner” code executed it.Now, build.zig files are compiled into a small process (the “configurer”) in debug mode. After this logic finishes constructing a build graph in memory, it is serialized to a binary configuration file. The parent zig build process is aware of this file and caches it for next time. While waiting for all that, it asynchronously compiles the build graph execution process (the “maker”) in release mode. Once the configuration file is available and the maker process is finished compiling, the maker process is executed, passing it the configuration file. The maker process only needs to be compiled once per zig version thanks to the global cache. The maker process then executes the build graph, which is contained within the serialized configuration file.The primary motivation of this change was to make zig build faster, in three ways:Only the user’s build.zig logic will be compiled with each change, rather than the entire build system along with it. This is starting to become more valuable now that we have introduced --watch, --fuzz and --webui. The build system can grow more features without making zig build take longer.Now the build system can skip rerunning the build.zig logic entirely when it knows nothing will change, for example if you add -freference-trace to your zig build command line, it now avoids re-running your build.zig logic redundantly, using the same configuration as last time.Now the process that actually executes the build graph is compiled with optimizations enabled.To demonstrate points 2 and 3, here is the difference between running zig build --help before and after:Benchmark 1 (34 runs): master/zig build -h ⬇️run_cmd.addPassthruArgs(); This removes a capability from build scripts since they can no longer observe those arguments. In exchange, it means that when changing those arguments, build scripts no longer must be rebuilt from source.If you’re someone who wants to influence the direction of Zig, this is a good time to upgrade your projects to the development version and try out these changes. We’ll be releasing 0.17.0 within a couple weeks from now. However, if you don’t have time, and you find out that 0.17.0 broke your build, don’t worry, there will be plenty of opportunity to get fixes in for the 0.17.1 tag as well. April 08, 2026 Author: Matthew LuggI’ve been spending a bit of time working on personal projects after merging my type resolution changes last month, but I did find the time recently to make some improvements to the LLVM codegen backend. This involved a few different enhancements with various goals, but one nice user-facing change was that I managed to get incremental compilation working with the LLVM backend.Sadly this can’t do anything to speed up the dreaded LLVM Emit Object: that time is entirely down to LLVM. However, what incremental compilation does help with is minimizing the time spent in the actual Zig compiler code, which means that if your code has compile errors (so “LLVM Emit Object” will be skipped), you’ll usually get those errors very quickly. (Of course, it does still give you a slight speed-up in successful builds too.)This support is available in master branch builds right now, and will be in the 0.16.0 release (which we’ll be tagging very soon).For anyone who still hasn’t tried it, especially if you’re using Zig’s master branch, please do try out incremental compilation by passing -fincremental --watch to zig build! The Zig core team have benefited from incremental compilation in our workflows for a good year now, and we’re also hearing good things from users. The feature is relatively stable at this point, and people are often surprised how much time they can save just by getting up-to-date compile errors in milliseconds rather than seconds.I haven’t really personally used incremental compilation with the LLVM backend, but all of the incremental test coverage in CI is now enabled for the LLVM backend, and I’ve had positive feedback from users, so it’s definitely worth giving a shot. As always, if you encounter bugs in incremental compilation, please report them if you can!Thank you, and I hope you find this useful :) March 10, 2026 Author: Matthew LuggToday, I merged a 30,000 line PR after two (arguably three) months of work. The goal of this branch was to rework the Zig compiler’s internal type resolution logic to a more logical and straightforward design. It’s a quite exciting change for me personally, because it allowed me to clean up a bunch of the compiler guts, but it also has some nice user-facing changes which you might be interested in!For one thing, the Zig compiler is now lazier about analyzing the fields of types: if the type is never initialized, then there’s no need for Zig to care what that type “looks like”. This is important when you have a type which doubles as a namespace, a common pattern in modern Zig. For instance, when using std.Io.Writer, you don’t want the compiler to also pull in a bunch of code in std.Io! Here’s a straightforward example:const Foo = struct { Previously, this code emitted a compile error. Now, it compiles just fine, because Zig never actually looks at the @compileError call.Another improvement we’ve made is in the “dependency loop” experience. Anyone who has encountered a dependency loop compile error in Zig before knows that the error messages for them are entirely unhelpful—but that’s now changed! If you encounter one (which is also a bit less likely now than it used to be), you’ll get a detailed error message telling you exactly where the dependency loop comes from. Check it out:const Foo = struct { inner: Bar }; $ zig build-obj repro.zig February 13, 2026 Author: Andrew KelleyAs we approach the end of the 0.16.0 release cycle, Jacob has been hard at work, bringing std.Io.Evented up to speed with all the latest API changes:io_uring implementationGrand Central Dispatch implementationBoth of these are based on userspace stack switching, sometimes called “fibers”, “stackful coroutines”, or “green threads”.They are now available to tinker with, by constructing one’s application using std.Io.Evented. They should be considered experimental because there is important followup work to be done before they can be used reliably and robustly:better error handlingremove the loggingdiagnose the unexpected performance degradation when using IoMode.evented for the compilera couple functions still unimplementedmore test coverage is neededbuiltin function to tell you the maximum stack size of a given function to make these implementations practical to use when overcommit is off.With those caveats in mind, it seems we are indeed reaching the Promised Land, where Zig code can have Io implementations effortlessly swapped out:const std = @import("std"); pub fn main(init: std.process.Init.Minimal) !void { var threaded: std.Io.Threaded = .init(gpa, .{ return app(io); fn app(io: std.Io) !void { $ strace ./hello_threaded pub fn main(init: std.process.Init.Minimal) !void { var evented: std.Io.Evented = undefined; return app(io); fn app(io: std.Io) !void { execve("./hello_evented", ["./hello_evented"], 0x7fff368894f0 /* 98 vars */) = 0 February 06, 2026 Author: Andrew KelleyIf you have a Zig project with dependencies, two big changes just landed which I think you will be interested to learn about.Fetched packages are now stored locally in the zig-pkg directory of the project root (next to your build.zig file).For example here are a few results from awebo after running zig build:$ du -sh zig-pkg/* February 03, 2026 Author: Andrew KelleyThe Windows operating system provides a large ABI surface area for doing things in the kernel. However, not all ABIs are created equally. As Casey Muratori points out in his lecture, The Only Unbreakable Law, the organizational structure of software development teams has a direct impact on the structure of the software they produce.The DLLs on Windows are organized into a heirarchy, with some of the APIs being high-level wrappers around lower-level ones. For example, whenever you call functions of kernel32.dll, ultimately, the actual work is done by ntdll.dll. You can observe this directly by using ProcMon.exe and examining stack traces.What we’ve learned empirically is that the ntdll APIs are generally well-engineered, reasonable, and powerful, but the kernel32 wrappers introduce unnecessary heap allocations, additional failure modes, unintentional CPU usage, and bloat.This is why the Zig standard library policy is to Prefer the Native API over Win32. We’re not quite there yet - we have plenty of calls into kernel32 remaining - but we’ve taken great strides recently. I’ll give you two examples.Example 1: EntropyAccording to the official documentation, Windows does not have a straightforward way to get random bytes.Many projects including Chromium, boringssl, Firefox, and Rust call SystemFunction036 from advapi32.dll because it worked on versions older than Windows 8.Unfortunately, starting with Windows 8, the first time you call this function, it dynamically loads bcryptprimitives.dll and calls ProcessPrng. If loading the DLL fails (for example due to an overloaded system, which we have observed on Zig CI several times), it returns error 38 (from a function that has void return type and is documented to never fail).The first thing ProcessPrng does is heap allocate a small, constant number of bytes. If this fails it returns NO_MEMORY in a BOOL (documented behavior is to never fail, and always return TRUE).bcryptprimitives.dll apparently also runs a test suite every time you load it.All that ProcessPrng is really doing is NtOpenFile on "\\Device\\CNG" and reading 48 bytes with NtDeviceIoControlFile to get a seed, and then initializing a per-CPU AES-based CSPRNG.So the dependency on bcryptprimitives.dll and advapi32.dll can both be avoided, and the nondeterministic failure and latencies on first RNG read can also be avoided.Example 2: NtReadFile and NtWriteFileReadFile looks like this:pub extern "kernel32" fn ReadFile( NtReadFile looks like this:pub extern "ntdll" fn NtReadFile( As a reminder, the above function is implemented by calling the below function.Already we can see some nice things about using the lower level API. For instance, the real API simply gives us the error code as the return value, while the kernel32 wrapper hides the status code somewhere, returns a BOOL and then requires you to call GetLastError to find out what went wrong. Imagine! Returning a value from a function 🌈Furthermore, OVERLAPPED is a fake type. The Windows kernel doesn’t actually know or care about it at all! The actual primitives here are events, APCs, and IO_STATUS_BLOCK.If you have a synchronous file handle, then Event and ApcRoutine must be null. You get the answer in the IO_STATUS_BLOCK immediately. If you pass an APC routine here then some old bitrotted 32-bit code runs and you get garbage results.On the other hand if you have an asynchronous file handle, then you need to either use an Event or an ApcRoutine. kernel32.dll uses events, which means that it’s doing extra, unnecessary resource allocation and management just to read from a file. Instead, Zig now passes an APC routine and then calls NtDelayExecution. This integrates seamlessly with cancelation, making it possible to cancel tasks while they perform file I/O, regardless of whether the file was opened in synchronous mode or asynchronous mode.For a deeper dive into this topic, please refer to this issue:Windows: Prefer the Native API over Win32 January 31, 2026 Author: Andrew KelleyOver the past month or so, several enterprising contributors have taken an interest in the zig libc subproject. The idea here is to incrementally delete redundant code, by providing libc functions as Zig standard library wrappers rather than as vendored C source files. In many cases, these functions are one-to-one mappings, such as memcpy or atan2, or trivially wrap a generic function, like strnlen:fn strnlen(str: [*:0]const c_char, max: usize) callconv(.c) usize { So far, roughly 250 C source files have been deleted from the Zig repository, with 2032 remaining.With each function that makes the transition, Zig gains independence from third party projects and from the C programming language, compilation speed improves, Zig’s installation size is simplified and reduced, and user applications which statically link libc enjoy reduced binary size.Additionally, a recent enhancement now makes zig libc share the Zig Compilation Unit with other Zig code rather than being a separate static archive, linked together later. This is one of the advantages of Zig having an integrated compiler and linker. When the exported libc functions share the ZCU, redundant code is eliminated because functions can be optimized together. It’s kind of like enabling LTO (Link-Time Optimization) across the libc boundary, except it’s done properly in the frontend instead of too late, in the linker.Furthermore, when this work is combined with the recent std.Io changes, there is potential for users to seamlessly control how libc performs I/O - for example forcing all calls to read and write to participate in an io_uring event loop, even though that code was not written with such use case in mind. Or, resource leak detection could be enabled for third-party C code. For now this is only a vaporware idea which has not been experimented with, but the idea intrigues me.Big thanks to Szabolcs Nagy for libc-test. This project has been a huge help in making sure that we don’t regress any math functions.As a reminder to our users, now that Zig is transitioning to being the static libc provider, if you encounter issues with the musl, mingw-w64, or wasi-libc libc functionality provided by Zig, please file bug reports in Zig first so we don’t annoy maintainers for bugs that are in Zig, and no longer vendored by independent libc implementation projects.The very same day I sat at home writing this devlog like a coward, less than five miles away, armed forces who are in my city against the will of our elected officials shot tear gas, unprovoked, at peaceful protestors. Next time I hope to have the courage to join my neighbors, and I hope to not get shot like Alex Pretti and Renée Good. |
Recent developments in the Zig project focus heavily on performance enhancements across the build system and compiler internals, alongside significant overhauls in standard library I/O implementations and package management workflows. The build system underwent a major rework to improve build speed. This involved separating the maker process, which executes the build graph, from the configurer process, which handles configuration serialization. This change allows the build system to asynchronously compile the maker process in release mode while serializing the build graph configuration. The motivation behind this separation is to limit the compilation of build logic only when necessary, allowing the system to skip redundant execution when build arguments remain unchanged. This optimization, combined with compiling the execution process with optimizations and enabling skips for unchanged logic, results in substantial performance gains, as demonstrated by benchmark results showing reduced wall time for operations. Furthermore, the change introduced an API modification where build scripts can no longer directly observe certain arguments, necessitating a different mechanism for passing arguments, which can influence how build scripts interact with dependency management. The Zig compiler itself saw deep internal restructuring, particularly concerning type resolution. The compiler now adopts a lazier approach to analyzing type fields; if a type is not initialized, the compiler avoids unnecessary analysis of its structure, which is particularly beneficial for modern patterns involving types that function as namespaces. Additionally, error reporting for complex issues like dependency loops has been significantly improved, providing more detailed and actionable error messages to help developers trace dependencies. This effort was coupled with enhancements to incremental compilation, where improvements were made to the LLVM codegen backend to minimize the time spent executing Zig compiler code. This feature aims to provide faster error detection by ensuring that incremental updates minimize the amount of work performed, significantly speeding up the feedback loop during compilation. In the realm of standard library implementations, significant progress was made regarding input/output operations. New implementations for io_uring and Grand Central Dispatch were introduced via std.Io.Evented, leveraging user-space stack switching, often referred to as fibers or green threads. These implementations are marked as experimental, and further work is required for robust error handling, diagnostics, and additional functionality. This effort allows for flexible swapping of I/O backends, enabling developers to interact with system calls through a unified interface. The package management workflow was also enhanced for greater flexibility and offline capability. Packages are now stored locally in a dedicated zig-pkg directory within the project root, which facilitates easier local manipulation of dependencies. This local storage, alongside a global cache of compressed dependency files, supports offline building and archival. Furthermore, a new command line flag, --fork, was introduced to allow users to override specific packages across the entire dependency tree during development. This feature enables fluid iteration on dependency issues by allowing users to temporarily use alternative source checkouts while working on fixes, promoting a more resilient development workflow. Finally, efforts focused on refining the underlying C standard library integration. Contributors have worked to incrementally remove redundant C source files from the repository, providing libc functionality as Zig standard library wrappers rather than vendored C source files. This transition aims to increase independence from third-party projects and the C language, leading to improved compilation speed, a reduced installation size, and better opportunities for compiler-level optimizations that mirror link-time optimization across the libc boundary. These changes also open potential avenues for users to integrate advanced features, such as forcing I/O operations to participate in event loops. |