Skip to content
WSLDeep Dive Published Updated 7 min readViews unavailable

Bazel in WSL: Hermetic Builds, Output Bases, and Linux Paths

Use Bazel in WSL with a pinned Bazelisk toolchain, Linux-side workspaces, reproducible tests, and deliberate separation from Windows build artifacts.

Bazel is a build and test system that aims to make actions reproducible by declaring dependencies and build inputs rather than relying on incidental files in a developer’s shell. Running it in WSL can align local builds with Linux CI and avoid mixing Windows and Linux compilers, linkers, interpreters, and generated artifacts. It does not make a build hermetic automatically: workspace rules, downloaded toolchains, environment variables, host tools, and undeclared inputs still matter.

The central WSL rule is to keep the Bazel workspace, output base, repository caches, and toolchains on the Linux filesystem. Microsoft’s WSL filesystem guidance recommends the distro filesystem for Linux command-line workloads. A build tree under /mnt/c can involve file-event and I/O behavior unlike a Linux CI runner. Use a Windows-mounted workspace only when testing that exact interoperability path.

Pin Bazel through Bazelisk

The Bazel project recommends Bazelisk as a version manager for Bazel. Bazelisk can select the version requested by a repository’s .bazelversion, reducing “works on my machine” drift between developers. Check the official install guide for the supported method on the WSL distribution and architecture. Record both Bazelisk and the resolved Bazel version rather than only noting that a bazel command exists.

Use one checkout and inspect version selection before an expensive build:

cd "$HOME/src/example"
bazelisk version
cat .bazelversion
git status --short

If the project uses another wrapper or a pinned Bazel binary, follow that convention instead of installing a competing global version. A repository may also pin language toolchains and rules separately from the Bazel executable. Review changes to .bazelversion, module files, lockfiles, and MODULE.bazel as build-system changes, not incidental formatting.

Understand workspace inputs and generated outputs

Bazel’s analysis and execution depend on declared targets and inputs. Source files, rule definitions, external repositories, toolchains, and configuration determine the action graph. An action that reads a file only present on one developer’s machine is not reproducible just because it succeeds locally. Inspect the rule that owns a dependency and declare it through the supported module or repository mechanism rather than reaching into an arbitrary absolute path.

Start with bounded targets. bazel query can help discover labels, while bazel test //path:target runs a known test target. Avoid beginning with //... in a large monorepo when a specific target provides a useful signal. Record command-line flags from .bazelrc and user rc files; two shells may invoke the same label with different configurations, platforms, or remote-cache settings.

Bazel commonly creates convenience symlinks such as bazel-bin and stores its larger output tree separately. Inspect bazel info output_base and bazel info execution_root to understand where state is located. Don’t move or delete the output base while a build is active. bazel clean affects cached build outputs; stronger cleanup modes can remove more cached state and require a full rebuild. Use them only when a measured cache problem justifies the cost.

Keep Linux and Windows artifacts separate

A Linux build generally emits Linux binaries and uses Linux toolchains. A Windows build uses different compilers, path conventions, executable formats, and often different rules. Do not share one generated output base between Windows-native Bazel and WSL Bazel. Keep separate checkouts or separate output roots for operating-system-specific builds and use version control to compare source changes.

When a toolchain invokes a compiler or SDK, inspect its resolved path inside WSL. which gcc, which clang, environment variables, and Bazel’s configured platforms can reveal accidental use of a Windows executable or a host binary from an unexpected path. Use bazel info and verbose action output selectively; it can reveal environment values and command lines, so avoid pasting logs containing secrets into public tickets.

WSL’s Linux filesystem and Windows filesystem have different case, symlink, executable, and permission behavior. A target can pass on /home and fail under /mnt/c because a source path or generated link is treated differently. Treat that as a filesystem boundary issue to isolate, not proof that the build graph is wrong. For IDE integration, open the WSL workspace through a Linux-aware remote environment rather than maintaining an unsynchronized second copy.

Use local cache behavior intentionally

Bazel caches action results and downloaded external dependencies. A warm local build can be much faster than a clean build, but it can hide undeclared-input defects. Compare a clean, bounded target in a disposable workspace if reproducibility is in question. Do not wipe a developer’s entire output base as a routine fix; first capture the failing target, Bazel version, configuration, and logs.

Remote cache configuration is a separate system boundary. WSL may reach a cache service through a Windows proxy or host network path, but that does not make all developer environments equivalent. Verify authentication, TLS, cache namespace, and whether CI and local builds use compatible platform keys. A cache hit should not be accepted as proof that the action can execute correctly on a clean worker.

For incremental correctness, change a declared input and verify only affected actions rerun. Then run a clean test target or CI job on the target OS. Use checksums or artifact metadata for important outputs. Reproducibility is a property of declared inputs and execution environment, not just an identical command string.

Platform selection deserves explicit tests. A target that builds for the host platform may not exercise a cross-compilation toolchain or the execution platform used by CI. Record the target platform and relevant Bazel flags, and inspect the action graph when a build unexpectedly uses a host tool. If a rule fetches an SDK or toolchain during analysis, declare and pin that repository input so a fresh Linux runner can reproduce it.

For a failure that appears only after an incremental build, compare the failing result with a clean build of the same target in a separate output base or disposable worktree. If the clean build also fails, focus on declared inputs and toolchain setup; if only the incremental build fails, capture the cache state and action logs before cleaning. This keeps bazel clean --expunge from becoming a ritual that removes the evidence and masks an action-cache correctness problem.

For cache investigations, compare action keys and declared inputs instead of treating a cache hit as a correctness oracle. A local result may have been produced by a previous configuration, and remote cache entries may be scoped by platform or repository policy. Record the Bazel version, command, relevant .bazelrc files, target platform, and whether execution was local or remote. Use a clean checkout or a separate --output_user_root only for a bounded reproduction, because a second output root downloads dependencies and consumes additional disk. Never share an output tree between native Windows and Linux invocations.

Before changing cache flags, establish a baseline on one deterministic target: run it once, run it again without source changes, then change a declared input and verify the expected action is invalidated. This distinguishes normal incremental reuse from a stale or incomplete dependency edge. If only an undeclared environment value changes the result, make that value an explicit build input or toolchain setting. The goal is not to disable caching but to prove that the declared action graph captures every relevant input.

Troubleshoot by layer

If Bazelisk cannot resolve the repository version, inspect .bazelversion, network access to the release source, and the tool’s cache. If analysis fails, inspect target labels, module resolution, rule versions, and platform constraints. If an action fails, inspect the specific command and environment, then reproduce with the same target and flags. If a test passes only when a file exists outside the repository, identify and declare that dependency.

If builds are slow, separate dependency fetching, analysis, execution, filesystem latency, CPU pressure, and remote-cache misses. Compare Linux filesystem and /mnt/c only with the same target and cache state. Do not change cache, toolchain, and filesystem placement simultaneously. If disk pressure grows, identify Bazel’s output base before removing state.

Acceptance criteria

Accept the WSL build environment when the Bazel version is pinned and recorded, the workspace and output base resolve under the intended Linux path, one representative build and test target succeed from a clean shell, and the same target can be rerun with understandable cache behavior. Document how Windows-native and Linux outputs remain separate.

Bazel in WSL is a strong Linux build-control environment. It is not automatically hermetic, and a successful laptop build does not replace clean CI execution on the target platform.

Related:

Sources:

Comments