# My Zig + VS Code Setup for multiple Zig versions

**URL:** <https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548>\
**Category:** Showcase\
**Created:** [February 13, 2025, 8:23pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548 "2025-02-13T20:23:14Z")\
**Posts on this page:** 15\
**Page:** 1

<div class="post-metadata">

**Author:** ![mtlynch](https://ziggit.dev/user_avatar/ziggit.dev/mtlynch/32/1254_2.png) [@mtlynch](https://ziggit.dev/u/mtlynch)\
**Post date:** [February 13, 2025, 8:23pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/1 "2025-02-13T20:23:14Z")

</div>

It took me a while to get Zig to play nicely with VS Code. I kept running into issues where the Zig VS Code extension couldn’t find the Zig binary or it would link to the wrong version of the standard library.

I finally got a solution that works consistently. I tested it on NixOS, and it also works with VS Code Remote SSH. It defaults to Zig 0.13.0, but you can change to the bleeding edge Zig compiler or any tagged release by [updating the config file](https://mtlynch.io/notes/zig-vscode-nix/#changing-zig-versions).

I wrote up an explanation of my setup here:

- [My Zig Configuration for VS Code](https://mtlynch.io/notes/zig-vscode-nix/)

It uses Nix and direnv, but you don’t need to know anything about Nix to use this solution.

If you already have Nix and direnv, you can try out my solution with these commands:

```bash
nix flake init \
  --template git+https://codeberg.org/mtlynch/zig-vscode-flake.git
direnv allow

```

Any questions or suggestions are welcome!

---

<div class="post-metadata">

**Author:** ![andrewrk](https://ziggit.dev/user_avatar/ziggit.dev/andrewrk/32/7011_2.png) [@andrewrk](https://ziggit.dev/u/andrewrk)\
**Post date:** [February 13, 2025, 9:10pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/2 "2025-02-13T21:10:34Z")

</div>

IMO the VS Code extension should manage the Zig installation. You should be able to choose which version you want from a convenient UI, and the default should be to use `minimum_zig_version` from the project’s `build.zig.zon`.

Edit: yeah looking at your blog post, that sounds like a total pain in the ass that should be unnecessary. It always makes me sad when people blog about complicated setups to use Zig.

---

<div class="post-metadata">

**Author:** ![matklad](https://ziggit.dev/user_avatar/ziggit.dev/matklad/32/2462_2.png) [@matklad](https://ziggit.dev/u/matklad)\
**Post date:** [February 13, 2025, 11:20pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/3 "2025-02-13T23:20:11Z")

</div>

It is a bit tricky to manage this from within VS Code, as one reasonable requirement here is that you should be able to pop open your terminal outside of VS Code, and get the “same” Zig.

Usually, this is achieved by installing some sort of version switcher, like rustup, user-globally, but that’s a _huge_ can of worms, and isnt’ really in scope for the extension, it shouldn’t be messing up with user’s PATH or some such.

I like the pattern we use in TigerBeetle, where we don’t put Zig into path at all, and say that `$PROJECT_ROOT/zig/zig` is the zig to use. So, both inside the editor and outside, in the terminal, you can consistently use `./zig/zig build`. _This_ pattern I think can be supported by an extension nicely, but than it needs to really be a common practice, as the overwhelming expectation today is that things are installed at least user-wide, and are in PATH.

---

<div class="post-metadata">

**Author:** ![Techatrix](https://ziggit.dev/user_avatar/ziggit.dev/techatrix/32/3225_2.png) [@Techatrix](https://ziggit.dev/u/Techatrix)\
**Post date:** [February 14, 2025, 1:58am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/4 "2025-02-14T01:58:18Z")

</div>

> [@andrewrk](#):
>
> IMO the VS Code extension should manage the Zig installation. You should be able to choose which version you want from a convenient UI, and the default should be to use `minimum_zig_version` from the project’s `build.zig.zon`.

This feature is already in the VS Code extension. See [ziglang/vscode-zig#230](https://github.com/ziglang/vscode-zig/pull/230)

> [@mtlynch](#):
>
> . I kept running into issues where the Zig VS Code extension couldn’t find the Zig binary or it would link to the wrong version of the standard library.

I would suggest to report this to the extension’s issue tracker. The problem that has been mentioned in the linked post is a bug that should be investigated and fixed.

---

<div class="post-metadata">

**Author:** ![floooh](https://ziggit.dev/user_avatar/ziggit.dev/floooh/32/2989_2.png) [@floooh](https://ziggit.dev/u/floooh)\
**Post date:** [February 14, 2025, 9:52am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/5 "2025-02-14T09:52:19Z")

</div>

> [@matklad](#):
>
> Usually, this is achieved by installing some sort of version switcher, like rustup, user-globally, but that’s a _huge_ can of worms

Is it though? I use zvm to quickly switch between versions (mainly the last stable and bleeding-edge version), and this also downloads the matching zls version (e.g. `zvm install --zls master`.

In the VSCode Zig extension I changed two settings fields, one for the Zig path and one for the ZLS path (which are `/Users/floh/.zvm/bin/zig` and `/Users/floh/.zvm/bin/zls` respectively).

This works fine, but then I’m also used to switching node versions all the time via nvm 😉

PS: I sometimes wonder if the zig executable should just be a slim ‘launcher proxy’ which manages the actual Zig toolchain as an automatic download into the global Zig cache and then picks whatever Zig toolchain version is required by the current project (but then I guess those version woes won’t be much of an issue after 1.0) - e.g. the Zig toolchain itself would become a build.zig.zon package including the actual Zig compiler (and the ‘zig frontend exe’ would only have the minimal required functionality to parse build.zig.zon, download packages, and then launch the actual zig compiler…)

---

<div class="post-metadata">

**Author:** ![matklad](https://ziggit.dev/user_avatar/ziggit.dev/matklad/32/2462_2.png) [@matklad](https://ziggit.dev/u/matklad)\
**Post date:** [February 14, 2025, 10:14am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/6 "2025-02-14T10:14:25Z")

</div>

> [@floooh](#):
>
> Is it though? I use zvm to quickly switch between versions (mainly the last stable and bleeding-edge version), and this also downloads the matching zls version (e.g. `zvm install --zls master`.

It is a can of worms, for two reasons:

- First, this whole dynamic proxying of executables with an extra `exec` in-between is fundamentally complex. It works reliably, but it isn’t simple, and is likely to induce complexity somewhere else. E.g., Cargo grew code to bypass the proxy recently: [Bypass rustup wrapper when invoking rustc · Issue #10986 · rust-lang/cargo · GitHub](https://github.com/rust-lang/cargo/issues/10986), in the name of performance (this _particular_ issue probably doesn’t apply to zig, as it doesn’t do separate compilation, but this _class_ of issues about latent complexity does apply).
- Second, you need to change your PATH. That can be surprisingly hard for many users. For example, on Linux, you usually have one script that affects your graphical environment, and one script that affects your shell, and it is easy to get in a situation where stuff works sometimes. Experienced users usually dodge these problems, but new users are hit hard. To solve this, Rustup actually _itself_ adds stuff to PATH on installation, but that’s obviously fraught and somewhat distasteful.

That being said, there _is_ an equilibrium point here where _everyone_ uses something like zvm, and than each editor plugin can assume that zvm is used and just use `zig` in PATH, suggesting to install `zvm` if that’s not the case.

---

<div class="post-metadata">

**Author:** ![invlpg](https://ziggit.dev/letter_avatar_proxy/v4/letter/i/13edae/32.png) [@invlpg](https://ziggit.dev/u/invlpg)\
**Post date:** [February 14, 2025, 10:33am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/7 "2025-02-14T10:33:39Z")

</div>

> [@matklad](#):
>
> First, this whole dynamic proxying of executables with an extra `exec` in-between is fundamentally complex. It works reliably, but it isn’t simple, and is likely to induce complexity somewhere else. E.g., Cargo grew code to bypass the proxy recently: [Bypass rustup wrapper when invoking rustc · Issue #10986 · rust-lang/cargo · GitHub](https://github.com/rust-lang/cargo/issues/10986), in the name of performance (this _particular_ issue probably doesn’t apply to zig, as it doesn’t do separate compilation, but this _class_ of issues about latent complexity does apply).

Just on this point: `zvm` doesn’t perform any proxying. `~/.zvm/bin` is just a symlink to the directory of the zig version currently in use:

```bash
$ ls -l ~/.zvm/bin
lrwxrwxrwx 1 joe users 21 Feb 14 09:46 /home/joe/.zvm/bin -> /home/joe/.zvm/master

```

---

<div class="post-metadata">

**Author:** ![matklad](https://ziggit.dev/user_avatar/ziggit.dev/matklad/32/2462_2.png) [@matklad](https://ziggit.dev/u/matklad)\
**Post date:** [February 14, 2025, 11:17am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/8 "2025-02-14T11:17:35Z")

</div>

Ah, I didn’t realize it works that way! Than, the problem is that “current version of zig” is a global state, which makes it hard for tools to support working with two projects needing different versions at the same time.

---

<div class="post-metadata">

**Author:** ![floooh](https://ziggit.dev/user_avatar/ziggit.dev/floooh/32/2989_2.png) [@floooh](https://ziggit.dev/u/floooh)\
**Post date:** [February 14, 2025, 2:46pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/9 "2025-02-14T14:46:08Z")

</div>

Yeah the path thing is unfortunate indeed, I guess because there’s no proper standard (maybe zvm could place a link into `/usr/local/bin` I guess, but I don’t actually know if this is ‘idiomatic’ on macOS for instance.

---

<div class="post-metadata">

**Author:** ![andrewrk](https://ziggit.dev/user_avatar/ziggit.dev/andrewrk/32/7011_2.png) [@andrewrk](https://ziggit.dev/u/andrewrk)\
**Post date:** [February 14, 2025, 4:46pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/10 "2025-02-14T16:46:11Z")

</div>

> [@floooh](#):
>
> PS: I sometimes wonder if the zig executable should just be a slim ‘launcher proxy’ which manages the actual Zig toolchain as an automatic download into the global Zig cache and then picks whatever Zig toolchain version is required by the current project (but then I guess those version woes won’t be much of an issue after 1.0) - e.g. the Zig toolchain itself would become a build.zig.zon package including the actual Zig compiler (and the ‘zig frontend exe’ would only have the minimal required functionality to parse build.zig.zon, download packages, and then launch the actual zig compiler…)

Here is the issue where I considered that idea:

> <https://github.com/ziglang/zig/issues/10204>
>
> With the upcoming package manager (#943) there is planned to be a declarative fi…le format, such as \`zig-package.json\`, which sits alongside build.zig and lists information such as the set of packages that are possibly depended on.
> 
> I propose that there are additionally these two fields in that declarative file. Example:
> 
> \`\`\`json
> "zig\_version\_min": "0.8.1",
> "zig\_version\_max": "0.9.0-dev.1703+6afcaf4a0",
> \`\`\`
> 
> Both fields would be optional. If min is omitted, it means there is no zig version that is too old. If a max is omitted, it means there is no zig version that is too new. The intent is that a maintainer would add a min if they discover that their package depends on an issue that has been resolved only after a particular zig version. They would add a max if they discover that a version of zig has done something backwards-incompatible that breaks their package.
> 
> When using the zig package manager (via the \`zig pkg\` subcommand), it would detect these fields, and, if found, acquire a suitable Zig compiler version.
> 
> Acquiring a zig compiler version would be done with different strategies:
> 1. (first priority) If the zig version used to run \`zig pkg\` is a compatible version for all dependencies recursively, use it.
> 2. If any tagged releases of zig are compatible with all dependencies recursively, then use the newest one. In this case it will download an official release from ziglang.org, or another mirror, as long as the checksum matches. Of course it will keep a cache and only download if not already cached.
> 3. If it's a recent master branch build, see if \`ziglang.org/builds/\*\` has a compatible version for the host system.
> 4. Finally, fall back to building from source, by downloading a zig-bootstrap tarball and using \`zig c++\` as the system C++ compiler. This allows any version of zig to be built from source. It will be important to communicate a sense of progress and estimated time remaining to the user for this because it could potentially take upwards of 1 hour to do this.
> 
> Regardless of which strategy is used, \`zig pkg\` should make it clear \*why\* the selected strategy has been chosen. For example it would print something like this, "package dependency a.b.c.d depends on zig version range 0.9.0-dev.1700+b644d4936 to 0.9.0-dev.1703+6afcaf4a0 which is not compatible with any tagged releases. Building zig 0.9.0-dev.1703+6afcaf4a0 from source...".
> 
> The \`zig pkg\` subcommand would have advanced CLI flags which could be used to override the behavior on how to acquire zig versions. It would also have a "--disallow-fetching-compiler-versions" flag, or similar, which would disable the ability to acquire alternate zig versions, emitting an error instead if the version of zig used was not compatible with all the dependencies recursively. Distro maintainers would likely want to always pass this flag when building zig projects. They would then be faced with the chore of getting older packages to add support for the (relatively newer) distro zig version, and newer packages to add support for the (relatively older) distro zig version.
> 
> After \`zig pkg\` is satisfied with the zig binary that it wants to use, it would call \`execve\` on the selected zig version, forwarding all the flags except for the ones having to do with this layer of abstraction, and passing an additional internal flag such as \`--already-resolved-zig-version\`. This would cause the \`zig pkg\` subcommand to skip this logic and proceed to business as usual.
> 
> The most useful command would be \`zig pkg build\`. This would be equivalent to \`zig build\` except that it would do this zig versioning layer first, possibly invoking a different zig version than the one used to invoke \`zig pkg\`. Most Zig projects in their README would have instructions to use \`zig pkg build\` rather than \`zig build\`, because it means that in practice they would not have to deal with users filing issues for using an incompatible zig version. However they would likely spend a fair amount of time using \`zig build\` directly, in order to test with exactly the version of zig that they have in their PATH.
> 
> Another subcommand would be \`zig pkg fetch-zig \[version\]\` which would expose the "acquiring a zig compiler version" feature above. For example it would acquire the specified version, and then print to stdout the path to the binary.
> 
> Building on top of this would be another subcommand \`zig pkg up \[version\]\` that would update a symlink pointer to the most recent version specified with the \`up\` sub-command. For example, assuming that the user runs the following commands:
> 
> \`\`\`
> zig pkg up 0.8.1
> zig pkg up 0.9.0
> zig pkg up
> \`\`\`
> 
> After this the cache directory would look something like this:
> 
> \`\`\`
> ~/.cache/zig/pkg/0.8.1/\*
> ~/.cache/zig/pkg/0.9.0/\*
> ~/.cache/zig/pkg/0.9.0-dev.1703+6afcaf4a0/\*
> ~/.cache/zig/pkg/up -\> ~/.cache/zig/pkg/0.9.0-dev.1703+6afcaf4a0/
> \`\`\`
> 
> If the user then ran \`zig pkg up 0.8.1\` then the \`up\` symlink would point to the 0.8.1 directory instead. This would allow the user to set up their PATH variable to include \`~/.cache/zig/pkg/up\` and have the \`zig pkg up\` command affect the default zig binary.
> 
> \------
> 
> Another motivation for this change is for the IDE experience. Here is a vision that I have:
> 
> \* Create a new VSCode extension that does both syntax as well as ZLS. Currently there are 2 separate extensions that you have to install. Deprecate those two extensions in favor of the One Extension To Rule Them All.
> \* This new VSCode extension would have zero configuration. Instead, when the user opens up a project, it would look at the project's declarative information about supported zig versions, and automatically fetch the appropriate zig compiler. It would build the corresponding ZLS version from source, and not ask silly questions such as "where is zig installed?" or "where is zls installed?".
> 
> VSCode is just an example; any IDE should be able to accomplish this user experience of simultaneously editing multiple zig different projects that depend on different versions of zig. And obviously the user should not have to install zig manually. If they install an IDE it should do it for them.

---

<div class="post-metadata">

**Author:** ![mtlynch](https://ziggit.dev/user_avatar/ziggit.dev/mtlynch/32/1254_2.png) [@mtlynch](https://ziggit.dev/u/mtlynch)\
**Post date:** [February 14, 2025, 7:05pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/11 "2025-02-14T19:05:31Z")

</div>

> [@Techatrix](#):
>
> I would suggest to report this to the extension’s issue tracker. The problem that has been mentioned in the linked post is a bug that should be investigated and fixed.

Thanks, done!

> <https://github.com/ziglang/vscode-zig/issues/398>
>
> When the Zig extension prompts me to install ZLS, if I say yes, it attempts to w…rite to \`~.confg/Code/user/settings.json\`, which is the system-wide config file for VS Code. 
> 
> On my system, this doesn't work because I manage my home directory with \[Home Manager\](https://nix-community.github.io/home-manager/), so the \`settings.json\` file is read-only.
> 
> \## Repro steps
> 
> 1. Open VS Code in an empty directory
> 1. Install ZIg VS Code extension
> 2. From command pallette, choose "Zig Setup: Install Zig"
> 3. Choose latest version of Zig
> 4. When prompted, choose to install ZLS
> 
> \### Expected behavior
> 
> Extension installs ZLS successfully and enables it within VS Code. 
> 
> I'd prefer that it write to the project-local \`.vscode/settings.json\` rather than the system-wide version, but failing that, I'd like it to at least fail over to the project-local settings one when it can't write the system-wide \`settings.json\`.
> 
> \### Actual behavior
> 
> Installation fails with this message:
> 
> \>Failed to install Zig 0.14.0-dev.3220: Unable to write into user settings because the file has unsaved changes. Please save the user settings file first and then try again.
> 
> \## Environment
> 
> \- OS: NixOS 24.11 (Linux 6.12.12)
> \- VS Code: 1.97.0
> \- Zig VS Code Extension: 0.6.4
> \- Architecture: AMD64

I was able to work around it and get it working with just the extension, so I’ve updated my post:

> **[My Zig Configuration for VS Code](https://mtlynch.io/notes/zig-vscode-nix/#update-the-simpler-non-nix-solution)**
>
> I finally found a solution that makes VS Code work consistently with Zig, so I’m sharing my setup in the hope that it saves someone else a headache.
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
> 
>   
> 
> 
> 
> 
> 
> 
> 
> 
>     
>     
>   
> 
>     
>     
>   
> 
>     
>     
>   
> 
> 
> 
> 
> 
> 
> 
> 
> 
>   
>     
>   
> 
> 
> 
> Zig extension...

---

<div class="post-metadata">

**Author:** ![andrewrk](https://ziggit.dev/user_avatar/ziggit.dev/andrewrk/32/7011_2.png) [@andrewrk](https://ziggit.dev/u/andrewrk)\
**Post date:** [February 14, 2025, 8:01pm UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/12 "2025-02-14T20:01:07Z")

</div>

Happy ending, thanks @Techatrix for clearing that up!

---

<div class="post-metadata">

**Author:** ![marler8997](https://ziggit.dev/user_avatar/ziggit.dev/marler8997/32/77_2.png) [@marler8997](https://ziggit.dev/u/marler8997)\
**Post date:** [February 15, 2025, 9:51am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/13 "2025-02-15T09:51:25Z")

</div>

I have similar concerns and came up with a relatively simple solution last weekend I’m calling “anyzig”. Curious to hear your thoughts on this idea.

> **[GitHub - marler8997/anyzig: One zig to rule them all.](https://github.com/marler8997/anyzig)**
>
> One zig to rule them all.

---

<div class="post-metadata">

**Author:** ![matklad](https://ziggit.dev/user_avatar/ziggit.dev/matklad/32/2462_2.png) [@matklad](https://ziggit.dev/u/matklad)\
**Post date:** [February 15, 2025, 11:21am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/14 "2025-02-15T11:21:59Z")

</div>

This looks like one of the better implementations of version switched concept! It is _like_ rustup, but also simpler&better in a few details:

- Rust has cargo&rustc. Zig unifies compiler and build system, by making the build system simply a library (and a part of standard library), so you only have to deal with one executable, `zig`, which is a nice simplification.
- There’s no overrides or special configuration files, fetching Zig version from build.zig.zon is as simple as it gets.
  - in particular, rustup has a design security bug, where it allows using paths to local toolchains, which of course is exploitable
  - But, when extracting version, I _think_ you should try to validate it more, and use `SemanticVersion`, rather than just `[]const u8`. I _don’t_ think that a maliciously crafted version can turn `allocPrint(allocator, "https://ziglang.org/builds/zig-" ++ url_platform ++ "-{0s}." ++ archive_ext, .{compiler_version}),` into an attacked controlled URL, but I am not entirely sure, so having `compiler_version` be not string, but structured data would inspire more confidence.

- You re-use zig-fetch, so I think everything _should_ just work in the presence of concurrency. A major pain with `rustup` is that it can wedge itself if several concurrent instances try to download stuff.
- Can you store `hashstore` in `~/.cache/zig/anyzig/hashstore`? It’s also a cache, in a sense that it should be possible to blew it away? And maybe you can trust the hashes from the [https://ziglang.org/](https://ziglang.org/)? Or I guess this requires that Zig actually publishes zig-fetch compatible hashes for the compiler itself?

Still, there’s fundamentally an extra process interposed in there. I can’t say that something will definitely break here, but probably there will be some weirdness around the edges.

Then, there’s also coordination problem — if you can convince the community that _most_ people should be use anyzig, than downstream tools can rely on that, teaching materials can assume that getting a particular version of zig is not a problem, etc.

Finally, there’s a messy problem of installing `anyzig` itself — you provide a pre-built binary, but there’s also the problem of adding to the `PATH`. For wider audience, you generally can’t assume that people are super-comfortable installing a raw binary from the internet, so _some_ guidance there would helpful (could be just an “add to path” copy-pastable script next to download commands?)

Overall, anyzig is definitely a great version switcher, as far as version switchers go, but I am still puzzled why, fundamentally, we need version switchers. Still holding onto my dear `./zig/zig` pattern 😛

---

<div class="post-metadata">

**Author:** ![marler8997](https://ziggit.dev/user_avatar/ziggit.dev/marler8997/32/77_2.png) [@marler8997](https://ziggit.dev/u/marler8997)\
**Post date:** [February 22, 2025, 2:54am UTC](https://ziggit.dev/t/my-zig-vs-code-setup-for-multiple-zig-versions/8548/15 "2025-02-22T02:54:33Z")

</div>

Thanks for the feedback/insights. I think your approach of downloading a specific version of zig for a project is valid and is actually the approach I’m using for Tuple. The only downsides I see is the extra code the project needs to manage it and extra disk space, but these both become negligible for sufficiently large projects. I think if anyzig ends up taking off the need for this will become less but it can still be advantageous to link to a specific version to avoid the extra complexity of the indirection like you say. Your approach avoids the main problem which is that there’s an “impedence mismatch” with having 1 version of zig installed system-wide at a time but having projects that require different versions. There’s only two solutions, you can either just not have a system-wide zig…or…you can make that system-wide zig support any version 🙂

I will say that now that I’ve been using anyzig for a couple weeks it feels very nice. I don’t have to manage zig at all any more. I can now download any project and immediately build it without having to muck around with which version of zig is currently enabled on my system. I also no longer need to remember to document which version of zig each project uses…if “anyzig” can build it, then it’s documented, otherwise, the build fails. I’m surprised how much easier this has made working with so many zig projects.
