The best tool for the Bazel job might be older than you
Mature tooling on top of the mtree specification

A lot of engineering is tribal: you know a few languages, use the technology that they enable, and follow the ideas and opinions of that community. Bazel is a cross-language build tool, and that means as Bazel experts, we end up cross-pollinating a lot of ideas between tribes. We don’t even make fun of COBOL or Fortran!
Some tribes are dismissive of old technology. However, a lot of old projects are not abandoned: they are stable, and in many cases are the bedrock on which a whole stack of tools have been developed. So in this article I’ll take you back to the turn of the decade - and not the most recent one. I suggest you listen to 🎶 “Poison” by Bell Biv DeVoe while you read this article — here’s the music video https://www.youtube.com/watch?v=hgnhVcyLy1I. That’s because Poison came out in March 1990, a couple months before 4.3BSD-Reno, which introduced a tool called mtree(5).
From the archive:
https://man.archlinux.org/man/mtree.5.en
The
mtreeformat is a textual format that describes a collection of filesystem objects. Such files are typically used to create or verify directory hierarchies.
Well, that sounds useful to use in a build system like Bazel. We love textual formats for the ease of manipulation, and Bazel is obsessed with small files produced in one place being inputs to various tools.
The mtree format
Here’s a simple mtree file:
usr/bin uid=0 gid=0 mode=0755 time=1672560000 type=dir
usr/bin/bazelisk uid=0 gid=0 mode=0755 time=1672560000 type=file contents=/path/to/download
usr/bin/bazel uid=0 gid=0 mode=0755 time=1672560000 type=link link=bazelisk
The format includes everything we need for reproducible builds, especially that time attribute. We can describe any arbitrary filesystem structure along with permissions, symlinks, and a bunch more.
Now, following the Unix philosophy, Bazel enables you to compose tools or random bash one-liners with ease. So let’s say we need /usr/bin/bazel to be owned by the right user since we’ll stick this filesystem into a Docker container image. It can be as simple as sed:
genrule(
name = "change_owner",
cmd = "sed 's/uid=0/uid=1000/;s/gid=0/gid=500/' <$< >$@",
...
)
Where mtree’s are used
Since the mtree format is 35 years old, it can run for President of the United States! And with age comes wisdom — it also has a lot of useful utilities, for example https://github.com/vbatts/go-mtree is a Go utility to interact with manifests. The most useful one, however is tar - at least BSD tar (the GNU tar is not very reproducible and doesn’t have an intermediate representation of the filesystem being archived)
We wrote a Bazel rule, https://registry.bazel.build/modules/tar.bzl, which is a simple starlark wrapper around a hermetic BSD tar toolchain and the mtree format. This rule doesn’t have to be smart, because it just takes an mtree to describe how the inputs should be laid out into the archive.
I gave that sed example earlier - it turns out another Very Old tool is a great way to make more general edits to mtrees - awk. We built this into tar.bzl to provide various mutations of the filesystem layout:
https://github.com/bazel-contrib/tar.bzl/blob/main/tar/private/modify_mtree.awk
And wrapping that with a minimal mutate API lets us replace most of rules_pkg in a simple way:
https://github.com/bazel-contrib/tar.bzl/blob/main/examples/migrate-rules_pkg/BUILD
The philosophy
This post isn’t just about mtree. It turns out many problems were solved long ago in compilers, optimization, packaging, verification, and so on. It’s easy to be tribal and only look for solutions in the ecosystem you’re familiar with, like GitHub repos with a bunch of social followers and recent commits. Often the best tool for the job is on a website that looks like the Wayback Machine, and will serve you a lot better than The Latest Rewrite in Rust.
Shoutout to http://github.com/thesayyn for finding all the clever ideas I’ve described in this post!
What’s next
Meet the Aspect Build team at BazelCon 2025.





