Device management: tools on your developers PATH


“Device Management”, or MDM, is that thing which forces your work computer to have security software installed. It has the ability to push tools to developer machines too - however it’s owned by the security team at your company. While it would be convenient, I’ve found it’s difficult for the Developer Platform team to use that to distribute the "canonical developer environment”.
Some folks use a devcontainer or VDI (Virtual Desktop Infrastructure) to describe the tooling you need installed, but that’s heavy and slow. If we’re using Bazel, it already has features to give a hermetic environment, right?
A year ago, I wrote an article describing how to get tools on your developers local machine using Bazel, and today I have an update.
In the technique from my original post (https://blog.aspect.build/run-tools-installed-by-bazel), users have to change their behavior, typing ./tools/my-tool
rather than just my-tool
. Retraining developers to have a different command under their fingers is hard, especially for things they run all the time.
Shortly after I wrote that post, Fabian Meumertzheim created https://github.com/buildbuddy-io/bazel_env.bzl. This is an alternative technique I’ll write about today. It puts the tools on the $PATH
instead, fixing the ergonomic issue from the first technique — however there are always trade-offs! This one requires engineers manually install the direnv
tool before they get setup.
How direnv works
direnv is a command-line tool that automatically sets and unsets environment variables when you cd
into or out of a directory. It’s especially useful for managing project-specific environment variables, such as secrets, configuration settings, or language versions (like Python or Node.js versions).
You place a
.envrc
file in your project directory.This
.envrc
file contains shell commands to export environment variables or run setup scripts.When you enter the directory,
direnv
loads the.envrc
file and applies the environment changes.When you leave the directory, it automatically reverts those changes.
Here’s how it looks when I enter Aspect’s monorepo (named “silo”):
alexeagle@aspect-build ~ % cd Projects/silo
direnv: loading ~/Projects/silo/.envrc
direnv: export ~PATH
Installing direnv
isn’t just a matter of getting the program on your machine. It also needs to hook into your shell (it supports bash
, zsh
and many others). And finally, you must explicitly allow each .envrc
to be trusted.
To follow this pattern, you’ll need to instruct your developers to install this first tool manually. Fortunately they’ll get a reminder of the instructions in the next step.
bazel_env creates a .envrc
After installing bazel_env.bzl
you’ll have a runnable Bazel target, typically bazel run //:_bazel_env
or bazel run //tools:bazel_env
.
The bazel_env
target is defined with a dictionary that maps a tool name to put on the PATH, to some other target that provides it. What kinds of targets can those be? Take a look at the example: https://github.com/buildbuddy-io/bazel_env.bzl/blob/main/examples/BUILD.bazel
Binary targets for programs you author yourself in the monorepo
Tools provided by a toolchain, like
go
,node
,pnpm
,cargo
, etcWith https://github.com/theoremlp/rules_multitool you can run
multitool
to update atools.lock.json
file, and all of these tools are installed.CLI utilities distributed by a package manager, like console scripts from PyPI,
bin
entries from thepackage.json
of NPM packages, Go utilities (seescaffold
example here), and so on.
% bazel run //tools:bazel_env
INFO: Analyzed target //tools:bazel_env (580 packages loaded, 72963 targets configured).
INFO: Found 1 target...
Target //tools:bazel_env up-to-date:
bazel-bin/tools/bazel_env_all_tools
INFO: Elapsed time: 9.399s, Critical Path: 0.45s
INFO: Running command line: bazel-bin/tools/bazel_env.sh
====== bazel_env ======
✅ direnv is installed
✅ direnv added bazel-out/bazel_env-opt/bin/tools/bazel_env/bin to PATH
Tools available in PATH:
* aws: @aws
* pnpm: @pnpm
* gofumpt: @@rules_multitool~~multitool~multitool//tools/gofumpt:gofumpt
* jsonnetfmt: @@rules_multitool~~multitool~multitool//tools/jsonnetfmt:jsonnetfmt
* shfmt: @@rules_multitool~~multitool~multitool//tools/shfmt:shfmt
* terraform: //tools:terraform
* yamlfmt: @@rules_multitool~~multitool~multitool//tools/yamlfmt:yamlfmt
* ruff: @@rules_multitool~~multitool~multitool//tools/ruff:ruff
* shellcheck: @@rules_multitool~~multitool~multitool//tools/shellcheck:shellcheck
* buf: @@rules_multitool~~multitool~multitool//tools/buf:buf
* buildozer: @@rules_multitool~~multitool~multitool//tools/buildozer:buildozer
* docker-compose: @@rules_multitool~~multitool~multitool//tools/docker-compose:docker-compose
* diesel-cli: @@rules_multitool~~multitool~multitool//tools/diesel-cli:diesel-cli
* etcdctl: @@rules_multitool~~multitool~multitool//tools/etcdctl:etcdctl
* grpcurl: @@rules_multitool~~multitool~multitool//tools/grpcurl:grpcurl
* ibazel: @@rules_multitool~~multitool~multitool//tools/ibazel:ibazel
* multitool: @@rules_multitool~~multitool~multitool//tools/multitool:multitool
* otel-cli: @@rules_multitool~~multitool~multitool//tools/otel-cli:otel-cli
* otelcol-contrib: @@rules_multitool~~multitool~multitool//tools/otelcol-contrib:otelcol-contrib
* promtool: @@rules_multitool~~multitool~multitool//tools/promtool:promtool
* pyrra: @@rules_multitool~~multitool~multitool//tools/pyrra:pyrra
* tflint: @@rules_multitool~~multitool~multitool//tools/tflint:tflint
* tfsec: @@rules_multitool~~multitool~multitool//tools/tfsec:tfsec
* buildifier: @buildifier_prebuilt//:buildifier
* scaffold: @com_github_hay_kot_scaffold//:scaffold
* node: $(NODE_PATH)
* cargo: $(CARGO)
* rustfmt: $(RUSTFMT)
Toolchains available at stable relative paths:
* nodejs: bazel-out/bazel_env-opt/bin/tools/bazel_env/toolchains/nodejs
* rust: bazel-out/bazel_env-opt/bin/tools/bazel_env/toolchains/rust
direnv: loading ~/Projects/silo/.envrc
direnv: export ~PATH
Guardrails
There are a few things that can go wrong, which are worth pointing out.
You’ll note that the tools are actually installed under a named output folder, bazel-out/bazel_env-opt
- what if the user runs a bazel clean
? This case is well handled, since direnv
is able to report errors in the .envrc
file with a custom message:
alexeagle@aspect-build silo % bazel clean
INFO: Starting clean (this may take a while). Consider using --async if the clean takes more than several minutes.
direnv: loading ~/Projects/silo/.envrc
direnv: ERROR[bazel_env.bzl]: Run 'bazel run //tools:bazel_env' to regenerate bazel-out/bazel_env-opt/bin/tools/bazel_env/bin
direnv: export ~PATH
Any time the tools change, everyone has to run it again.
Not lazy (yet)
When you run bazel_env
it needs to fetch all the tools, even those you never plan to run. See https://github.com/buildbuddy-io/bazel_env.bzl/issues/14
As a workaround, you can have multiple bazel_env
targets, but users have to choose the right one for the work they intend to do.
Subscribe to my newsletter
Read articles from Alex Eagle directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Alex Eagle
Alex Eagle
Fixing Bazel!