Build-On-Save

ZLS can be configured to build your project on save. If that results in errors, they will be displayed in your editor like any other diagnostic.

Configuration

There are two ways to opt into Build-On-Save:

  • The build.zig of your project defines a “check” step. (recommended)
  • The enable_build_on_save config option has been manually set to true.

The first method avoids dealing with config options and can report compile errors quicker.

Add a “check” step to your build.zig

This part is more deeply tied to your specific project but the gist is the following: whatever you do to define your main executable or library, you do it again in a new step named check.

I’ll use for this example the executable definition step you get generated automatically from zig init.

// Create a module just like in the `zig init` template.
const exe_mod = b.addModule("foo", .{
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});
// Any other code to define dependencies would probably be here.

// Create an executable from the given module.
// Still the same as the `zig init` template.
const exe = b.addExecutable(.{
    .name = "foo",
    .root_module = exe_mod,
});
b.installArtifact(exe);

// This is where the interesting part begins.
// As you can see we are re-defining the same executable but
// we're binding it to a dedicated build step.
const exe_check = b.addExecutable(.{
    .name = "foo",
    .root_module = exe_mod,
});
// There is no `b.installArtifact(exe_check);` here.

// Finally we add the "check" step which will be detected
// by ZLS and automatically enable Build-On-Save.
// If you copy this into your `build.zig`, make sure to rename 'foo'
const check = b.step("check", "Check if foo compiles");
check.dependOn(&exe_check.step);

The most important part about this second executable definition is that we ask to build it, but we never install it. If you look at the final line of the first section, you will see that we call b.installArtifact on the original executable, but for the executable bound to the “check” step, we don’t.

This one-line difference will have a big impact on the resulting behavior of the build as it will add the -fno-emit-bin flag to the compiler invocation which, in other words, means that Zig will analyze your code (and report any error) but it won’t bother calling into LLVM since you don’t plan to install the executable anyway.

The result is that you will get diagnostics pretty fast since you won’t have to go through the “LLVM Emit Code…” phase.

Once you’re done with this, restart your editor (or at least ZLS), save your file with an error in it, and enjoy your new spiffy diagnostics.

Customize build arguments

By default, ZLS will run the equivalent of the following command:

zig build check --watch

If no “check” step has been found and enable_build_on_save has been enabled, it will fall back to the “install” step.

To further customize the build arguments, the build_on_save_args config option can be used. Here are some arbitrary examples to showcase what could be done in theory:

  • Override which build step(s) should be run: "build_on_save_args": ["check", "test"]
  • Set project-specific build options: "build_on_save_args": ["-Dtarget=wasm32-wasi"]
  • Set additional build system flags: "build_on_save_args": ["-j4"]

Run zig build --help to find out what build steps and build options are available in your project.

Special Thanks

This Guide is based on the “Improving Your Zig Language Server Experience” Blog Post by Loris Cro.