Introducing toolchains_msvc: a hermetic MSVC toolchain for Bazel
I created toolchains_msvc, a Bazel module that enables users to fetch MSVC and define rules_cc toolchains for building native Windows applications targeting the MSVC ABI — in other words, standard Windows binaries.
Motivation
I come from the video game industry, where I believe Bazel could be a great fit — complex builds, cross-compilation, custom tooling for shaders and assets. But game dev is a Windows-first ecosystem; even cross-compiling for consoles is done from Windows. Improving that story is a big part of what motivated this project. There's also no standard build system in the industry: Unreal Engine uses Unreal Build Tool, Unity uses Bee, Godot uses SCons — and those are just the public ones; I'm aware of more I can't discuss here. Each reinvented the wheel because no ready solution existed. Today I believe Bazel's core is capable enough to satisfy the same needs — and with solid Windows support, the barrier to entry for new players would be lower: no need to develop your own build system.
The Bazel ecosystem also lacks a Windows toolchain that fully unlocks what makes Bazel great — and I believe that's a friction point for adoption. Bazel adoption isn't limited by how hard it is to use, but by how much configuration it takes to make it work with your ecosystem. Bzlmod addresses that by enabling ready-made solutions for mainstream languages and platforms — but Windows is missing one. A Bzlmod module where you can git clone && bazel build, up and running in seconds with no extra setup, lowers the bar for people evaluating Bazel on Windows and serves as a modern reference for those who need to customize it for their own ecosystem. At least, I wished it existed — so I set out to build one.
Design Goals
Going in, I had four goals:
Native to the Windows ecosystem: MSVC ABI, fully compatible with the standard Windows developer experience.
Idiomatic Bazel: modern rules-based architecture, platform constraints, build settings, and preferring Bazel's feature mechanism over raw flags.
Full Bazel capabilities without compromise: hermetic through Bzlmod-based installation, reproducible builds (enabling a shared build cache), and remote execution support.
Customizable: choose your compiler frontend (cl.exe, clang-cl.exe, or clang.exe) and linker (link.exe or lld-link.exe), customize flags per compilation mode (dbg, fastbuild, opt), with defaults matching Visual Studio settings.
Fetching a Proprietary Tool: MSVC
Targeting the MSVC ABI requires more than a compiler. Even when using Clang, you still need MSVC headers, libraries, and the Windows SDK — the equivalent of a sysroot for Windows. This dependency is mandatory.
MSVC is not open source. It is distributed by Microsoft under a license that prohibits repackaging and redistribution. Any Bzlmod module integrating MSVC must therefore acquire it through official Microsoft channels.
The license also explicitly states:
you may not: work around any technical limitations in the software;
Beyond the legal reading, there is a practical principle here: the Microsoft installer has always required the user to acknowledge the license before installation. A tool that automates this process should not silently bypass that step — users need to be aware of what they are agreeing to.
toolchains_msvc addresses this as follows:
It fetches Visual Studio Build Tools from the official Microsoft distribution channel.
It programmatically filters packages to ensure only those belonging to Visual Studio Build Tools can be installed.
It refuses to proceed unless the user has explicitly agreed to the license. That agreement is made by setting a specific environment variable to the value documented in the module; if they have not, it prints the license URL and the variable name so they can do so deliberately.
The check only triggers when Bazel actually fetches MSVC. A project that lists toolchains_msvc as a dependency but does not run any build action using MSVC will never fetch it and will never be prompted to agree to the license.
This approach was inspired by portablemsvc and PortableBuildTools.
Toolchain Definition
Toolchains are defined via a toolchain set. A toolchain set produces a set of toolchains from the cross-product of: host platform, target platform, MSVC version, Windows SDK version, LLVM version, and compiler frontend.
Per toolchain set, you can configure default flags and features, as well as flags and features specific to each compilation mode (dbg, fastbuild, opt). You can define as many toolchain sets as needed.
All toolchains from all toolchain sets are declared in @msvc_toolchains//BUILD.bazel and registered with register_toolchains("@msvc_toolchains//:all"). Bazel selects the right toolchain using its standard selection mechanism based on platform constraints and config_setting constraints.
The active toolchain can be controlled with the following build settings:
@msvc_toolchains//msvc=<version>@msvc_toolchains//winsdk=<version>@msvc_toolchains//llvm=<version>@msvc_toolchains//compiler=[msvc-cl|clang-cl|clang]@msvc_toolchains//toolchain_set=<toolchain_set_name>
All settings have defaults, so bazel build //my_target works with no extra configuration.
Fine-grained Customization
A toolchain that works out of the box is valuable only if it can also be customized without forking.
You can select any officially available version of MSVC and Windows SDK, and any version of LLVM that has a published SHA-256 digest.
You customize the toolchain by supplying default compile and link flag lists (per compiler and per compilation mode: dbg, fastbuild, opt). You can replace the built-in defaults entirely or layer additions on top of them.
A few behaviors are not “free-form flags” in those lists: they are implemented as standard Bazel features so cc_library, cc_binary, and feature toggles stay consistent and work out of the box — for example generate_debug_symbols, treat_warnings_as_errors, static_runtime, debug_runtime, and LTO (thinlto, fulllto). For those, you enable or disable the feature (per target or globally), rather than duplicating the same MSVC switches in toolchain defaults. Everything else you care to pass — typical examples include /Od, /O2, /W3, /Zc:__cplusplus — remains ordinary toolchain flag customization.
Reproducibility and Hermeticity
Reproducibility unlocks shared build caches — eliminating "works on my machine" and making every build verifiable. It requires two things: hermetic toolchain acquisition and deterministic compiler output.
For hermetic acquisition, toolchains_msvc fetches MSVC, Windows SDK, and optionally LLVM from their official distribution channels via Bzlmod repository rules. As an optional hardening step, you can record the SHA-256 of each package in a lock file; the repository rule will then fail if the downloaded package does not match.
For deterministic output, the toolchain passes flags to remove timestamps and absolute paths from build outputs.
Reproducibility is validated in GitHub Actions: a project and its copy are built independently, then the hashes of all source inputs and build outputs are compared to confirm they are identical.
For a concrete example, see toolchains_msvc_example: a small DirectX 12 GUI application built with toolchains_msvc. The repo includes sha256_manifest.py, which prints SHA-256 digests for build artifacts such as .obj, .lib, .pdb, and .exe. Run it after a local build and compare the output to the same step in GitHub Actions to confirm your artifacts match.
One caveat: link.exe cannot produce deterministic PDB files — its internal stream IDs are not stable across executions. lld-link.exe (from LLVM) does not have this limitation. By default, cl.exe-based toolchains use link.exe, while clang-cl.exe- and clang.exe-based toolchains use lld-link.exe. You can force cl.exe-based toolchains to use lld-link.exe via the cl_with_lld_version option in your toolchain set definition — at the cost of requiring LLVM to be fetched even when compiling with cl.exe.
600MB Saved with On-demand System Libraries
In a typical toolchain, system libraries are listed as dependencies of the cc_tool definition for the linker. On Windows, MSVC and Windows SDK libraries together weigh around 600 MB — compared to roughly 60 MB for executables and 120 MB for headers. For remote execution, this means 75% of what gets uploaded is system libraries, even though most targets only need two or three of them (e.g. kernel32.lib, user32.lib).
In toolchains_msvc, system libraries are not dependencies of cc_tool. They are regular cc_import rules. To use kernel32.lib, you declare a dependency on @msvc_toolchains//lib:kernel32.lib, which is an alias that resolves to the correct library for the Windows SDK version of the selected toolchain.
Conclusion
I built this project for myself first. It was my own evaluation of Bazel — I wanted to know if it was possible, and I am the first user of the result. I did this on my personal time, with the goal of building enough knowledge and confidence to invest in Bazel professionally. That goal is now confirmed: I will start transitioning my work projects to Bazel, which involves making it work with game consoles. That part will likely stay private.
The toolchain has not been used in production yet. Some design decisions are probably naive, and I expect bugs to surface once real projects start using it. The CI covers what it can, but automated tests cannot replace real-world usage. I will maintain and improve it as issues emerge — but that work depends on user feedback.
I am curious to see if this resonates. It could eventually become a BCR module. This is a modest contribution, and mostly a way to find out whether I am the only one with this need, or whether it can create traction and opportunity from there.
I am not attached to this repository as the final artifact. If the right outcome is a rename, a rearchitecture, or a different canonical module that learns from this one, I am fine with that. What I care about is that hermetic MSVC toolchains for Bazel exist and improve in the ecosystem — not that this particular project stays the one.
The natural next step would be rules_visualstudio. On Windows, the default debugger is Visual Studio, and a tight debugging workflow matters for adoption. Using Bazel's aspect mechanism, it should be possible to generate .sln and .vcxproj files that delegate the actual build to Bazel — a pattern already proven by Unreal Engine 5 with UnrealBuildTool. Not a big problem technically, but still some work.


