How to change the default location of the output binary produced on running `zig build`?

On building an application using zig build, the output binary is produced in the ./zig-out/bin/ directory.

My build.zig is:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const executable = b.addExecutable(.{
        .name = "cow",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(executable);

    const executable_step = b.step("run", "Run the app");
    executable_step.dependOn(&executable.step);

    const install_executable = b.addRunArtifact(executable);
    install_executable.step.dependOn(b.getInstallStep());
}

I would like to update build.zig so that the binary is produced in the project root - as the project’s config file is also at the project root.

Appreciate any help !

You can only output files to the prefix, which is by default ./zig-out relative to the build root. You can override that with --prefix.

you cannot change that within the build system, this is intentional, it makes the output predictable, and easily configurable.

By default, executables will be put into a bin subdir and libraries in a lib subdir, etc. This is so you can easily install on unix systems by just changing the prefix to either the local root (usually ~/.local) or system root.

That substructure of the prefix can be changed through the build system by using addInstallArtifact or other addInstall* variants.
Note that you will have to add the result step of those functions as dependencies to a step, to make them run by default add them as dependencies to b.getInstallStep().
The non add versions of the install functions will do that for you, though are less customisable.


TLDR you cant do it within the build script. but you can set the prefix to the build root instead of the default zig-out, and install files at the root of the prefix, instead of their default subdirs.

1 Like

(a bunch of related thoughts, not necessary the answer to the question)

FWIW, one thing I really appreciate about Zig is how it, in general, prevents specific “middle layers” accidentally influence the task at hand. E.g., it doesn’t assume that you are using glibc on Linux, and sticks to the Linux’s actual interface — syscalls. Or how zig init doesn’t create a .gitignore for you, because there are many different VCSes (some of which are yet to be invented!)

And the --prefix story I think is the opposite, it over-indexes on the traditional UNIX way of doing things, but not everything is UNIX. Not having sudo make install and building into a local folder is good. But assuming that the user wants /bin, /lib, and /include is going too far, it feels more orthogonal to put stuff just in ./zig-out (the -Dflat option of the Zig compiler’s own build).

I understand that the motivation here is to make Zig software minimally annoying for Linux distributions to package, but the cost here is annoying anyone who doesn’t really care about the packaging, by first making them type an extra bin all over the place, and then, once they understand the motivation better, forcing refactoring existing setup to a “flat” structure, or living on as is, with a nagging feeling of imperfection (:waving_hand:, that’s me :D).

I’d maybe just put stuff in ./out/my-program by default (no zig-, because the output of the build system is a part of projects “public API”, and shouldn’t disclose implementation details. If my project contains both Zig and TypeScript code, I want the results to end at the same place).

Note though, that it is always possible to “fork” any built-in step you don’t like, and write your own custom step with just the logic you want. The InstallFile in particular is simple:

1 Like

Note though, that it is always possible to “fork” any built-in step you don’t like, and write your own custom step with just the logic you want. The InstallFile in particular is simple:

Instead of forking InstallFile, why not writing a custom script to do the job?

Custom script will have to copy files from somewhere. If it copies them from zig-out/bin, you’ll end up with both zig-out/bin and a binary in the root of the project, which is not great. If it copies them from ./.zig-cache, it needs to know the hash the files are stored under, which require some amount of build.zig custom logic to convert an appropriate LazyPath to a location on the physical file system.

Oh, I forgot the actual horrible hack we use for this:

b.addInstallFile(lazy_path, b.pathJoin(&.{ "../", filename })

That is, you could install in ../ and that installs into project root, assuming default location of zig-out

I was assuming the case of installing on a prefix directory accessible by PATH. As an example, /usr.

In this case you can run the script with admin privilege, using the correct tool for the specified OS.

In my case i usually install in ~/.local/

1 Like

Ah, got it! Yeah, I was specifically thinking about the use case of

1 Like

By default, executables will be put into a bin subdir and libraries in a lib subdir, etc. This is so you can easily install on unix systems by just changing the prefix to either the local root (usually ~/.local ) or system root.

This behavior of the build system conflicts with another Zig feature, which is using a ZON file to store configuration.