Zig build specifying install path for WASM library

Hi everyone :wave: - until recently (i.e. prior to this commit and the related issue #14951), the std.build.CompileStep had the convenient .setOutputDir() function which now has been removed and needs to be replaced with a “FileSource abstraction rather than
telling the compiler directly where to output files” (as per @andrewrk’s commit msg). I’ve been trying to find examples of this new approach on Github and tried using .addInstallDirectory() and have the compile step depend on it. Also tried a few other routes which sounded promising, but no matter what the output always ends up under /zig-out… For the seeming lack of any docs about this, would somebody be able to explain/illustrate how this is done in recent v0.11-dev versions? Thank you very much!

FWIW my particular project setup is a hybrid JS/Zig project with this structure:

├── build.zig
├── src
    └── wasm
└── zig

All Zig source files are under /zig and the built static library file should go to /src/wasm

“src” stands for “source” and is for source files that are committed to version control. Your wasm binary which is outputted by the compiler is, by definition, not source. Are you committing it to version control?

I’m happy to help more if I can understand the goal of your file hierarchy.

Thanks, @andrewrk - I realize the folder structure makes not much sense without further explanation, but I just would like to know how the supposed workaround would look like for the previously simple & easy-to-grok:

var lib = b.addSharedLibrary(.{ ... });

With most of the build system types and functions still completely undocumented (no criticism & with everything still in flux, I totally understand why!) — I’m just rather missing something fundamental in my understanding of the various installation-related functions, options and build steps… I’ve tried finding my way through various issues/PRs and other projects in the wild, but so far still stumped…

Ps. As for the folder structure: The /src folder is for JS/TS source files and placing the WASM artifact there makes imports easier with my bundler, but the actual structure is irrelevant for this general issue…

Are you committing the wasm binary to version control? If so, then you can use std:Build.WriteFileStep.addCopyFileToSource.

If not, then presumably, it is being used as an intermediate artifact, both as the output of one process and the input of another. Putting it in src is not only unorganized in this case, it potentially will cause a race condition when executing the build graph in parallel. Instead of putting it in src, use std:Build.CompileStep.getOutputSource and pass that as the input to another step.

Thanks, Andrew, but my question is really about the recommended general workaround for the now removed std.Build.Step.Compile.setOutputDir(). In other words: How does one get the build system to place a library artifact not into the default /zig-out/lib, but in another location of one’s choosing?

Please do ignore the fact that I chose to place the WASM into /src/wasm (it really could be anywhere on the disk!). In my previous posts I tried to explain that in this project template all Zig sources are only located under /zig and the /src dir is only used for the parent JS app, so the latter is completely out of scope for this issue here (and no race conditions… :slight_smile: )

I still don’t understand your question either, but zig build -p /path/to/root replaces the prefix ./zig-out with whatever you’d like. you might need sudo if writing to a directory the user does not own :slight_smile:

Essentially I’m asking for the new build API equivalent to the CLI --prefix arg. I don’t want to use the CLI approach, since I’m having a custom wrapper function around .addDynamicLibrary() which has its own set of options and one of them is (was) to specify an output directory for the compiled WASM library file. Now that the related build API method .setOutputDir() has been removed, I’m merely asking what the new approach/workaround is. I’m not sure why this is confusing or a seemingly strange question to ask, especially since it’s about a feature which existed until 2 months ago… Does this make more sense now? :sweat_smile:

To provide additional context (also about the project structure), please see the readme’s for the parent project(s):

A workaround is a short-term solution to avoid a bug or a design limitation. Neither of these two things exists for this use case, so there is nothing to be worked around. The two functions that I linked you above have the capability to put files anywhere in the source tree.

I think you did not recognize that I did indeed answer your question, because I served the answer along with an uncomfortable piece of advice.

Perhaps it would be useful to leave a small paragraph in the source code describing why was something deprecated.

It looks like zig-gamedev had similar issue to this one. Perhaps tracking down their solution could help?

1 Like

@andrewrk, I’m truly super thankful you’re taking the time to engage with this and apologies this (what I thought was a really basic question) takes so much back and forth, as well as seemingly causing plenty of misunderstanding and talking past each other. I will try one last time with an even more super simple example and hope that you can better see that all references about source folders, project structures and/or version control are entirely unrelated to my original question…

I’m just after the recommended new way to compile some Zig source files as dynamic WASM library/module and have the result placed/installed directly into a custom location/subdir (outside of any Zig-related source tree).

I did take a look at the two functions you linked earlier, but a) without any toplevel docs or a small usage example, I unfortunately couldn’t yet make sense of what to do with them and b) from their naming I also just don’t think either of them even applies…

In principle, here’s the old code I’m trying to find an equivalent replacement for:

pub fn build(b: *std.Build) void {
    var lib = b.addSharedLibrary(.{
        .name = "main",
        .root_source_file = .{ .path = "zig/main.zig" },
        .target = .{
            .cpu_arch = .wasm32,
            .os_tag = .freestanding,
        .optimize = .ReleaseSmall,

    // could be anything outside the ./zig source folder
    // here to obtain: ./foo/bar/main.wasm


Updated new code for latest Zig version (minus custom output dir, which is my question: how?):

pub fn build2(b: *std.Build) void {
    var lib = b.addSharedLibrary(.{
        // ...

    // how to achieve WASM output to the same custom ./foo/bar subdir as in the old version?
    // ???


Is this any more clear? If not, then I’ll just have to throw the towel and find another solution involving external tools… Thank you in any way!

Remove this code:


Add this code:

    const wf = b.addWriteFiles();
    // Copies to a path relative to the package root.
    // Be careful with this because it updates source files. This should not be
    // used as part of the normal build process, but as a utility occasionally
    // run by a developer with intent to modify source files and then commit
    // those changes to version control.
    wf.addCopyFileToSource(lib.getOutputSource(), "foo/bar/main.wasm");

    const update_wasm_step = b.step("update-wasm", "Update foo/bar/main.wasm");

I have included the documentation comment of addCopyFileToSource for the benefit of someone arriving on this thread on a search engine to caution them from blindly following a bad practice.

1 Like

:tada: Thank you, this helps a lot!

Btw. I also just realized that part of the misunderstanding (on my side) of your replies is the use of the word Source: I think the way it’s used here (also in the function names) is somewhat overloaded as I understood it to refer to “source files” rather than the FileSource type…

Also, last question: Do you consider the above build procedure a “bad practice” in general, even if the file is guaranteed to be written to a location outside the Zig-related source tree (as in this case)? Not all subdirs in a project/workspace are source folders, related to Zig or under version control — so why should this be a problem?

I could be wrong but it seems that one core tenet of the Zig build system philosophy is that the build.zig file should not statically dictate the output directory of artifacts, but rather that the user invoking zig build should always be in full control of the output directory by passing the --prefix option. The build.zig file only decides where files are installed relative to the passed root install directory, not relative to the cwd or the build.zig file location itself.

You can control where the wasm file is installed relative to the root install directory by setting lib.override_dest_dir. See std:Build.InstallDir for available options; use .prefix if you want it to end up in the root install directory.

Could you explain why you need to specify a static output directory? Looking at your projects you seem to be using npm scripts to orchestrate builds, so specifying the output directory using --prefix seems like it would still work for your use case. If you add lib.override_dest_dir = .prefix to your build.zig file and update your npm scripts to run ... zig build --prefix src/wasm && some-other-tool src/wasm/main.wasm ... you should end up with something like what you seem to ask for without the need for workarounds.

1 Like

Yes, I agree with this as being confusing, and I share your preference on how I want the word “source” to be used. Very likely I will rename that type to LazyPath. Please feel free to counter-propose a different name if you have a better one.

I think @castholm’s response above is a nice response to this. Happy to continue the discussion further, but let’s start there.

1 Like

I also just realized that part of the misunderstanding … is the use of the word Source

oohhhh now I get it. It updates the file you want to copy as if you ran touch command on it, which can mess with the build system, right?

Thank you @castholm - this all makes sense to me and I largely agree with the overall philosophy. The hybrid Zig/JS project setup and custom build script was created almost a year ago, when the build API provided a very convenient way to specify the output dir programmatically rather than via CLI args only. My OP was to see if I can keep that setup in place for the users (of my projects) or if there will have to be breaking changes (for them too).

The reason for not using the --prefix CLI arg was about keeping all Zig related build options in a single place, i.e. in the config options given to my custom build wrapper (example invocation, list of all options).

Before posting here, I’ve played around (for hours) with various permutations of .addInstallDir(), .override_dest_dir = { .custom = "xxx" } , .install_prefix, .install_path and anything else which seemed relevant/useful, but just couldn’t get any of them to work… I will try your above recommendation tomorrow, looks v.simple & promising!

I love Zig, but the build system has always been the most cryptic part to me (options & API only, I do get most of the overall concepts & reasoning). Again, zero criticism, but some brief top-level docs would be absolutely splendid (wish I could contribute myself, but not there yet…)

1 Like

I’m apologyzing for offtopic.

I just thought the better de-abbreviation for WASM would be not “WebAssembly”,
but something like “Web App Stack Machine”. It is a Stack Machine, isn’t it? :upside_down_face:

1 Like

Yeah, sticking Web in front of a new technology these days is like sticking Java in the olden years. lol

1 Like

I meant “Stack Machine” in ASM abbrv.