Buildsystem Rundown
Looking back from 2026. This one was always a “living document” - I posted it half-finished in 2016 with a couple of
TODOsections I swore I’d get back to, and then didn’t for a decade. So this is the finished version. The original tool-by-tool writeups (Make, Ant, Maven, Gradle, QBT) stand as the 2016 period record with only typos fixed. What’s new: I actually wrote the Go and Docker sections I left stubbed, filled in Bazel, and added a whole section on the tools that showed up after 2016 - Buck2, Pants v2, Nx, Turborepo - plus a timeline so you can see the shape of the last fifty years at a glance. The through-line hasn’t changed: the interesting problem is still strict, reproducible, cross-project dependency management, and it’s still mostly solved only inside company walls.
Note from the editor (2016): This blog post is a living document. In it, I will try to discuss popular (and unpopular) build tools, and their benefits and drawbacks. In the interest of not delaying it forever, I am posting it in this not-yet-complete form, and will fill in more details for more build tools as I find time to do so.
There are many specializations in the field of software engineering. There are
those who specialize in machine learning, in computational linguistics, in
distributed systems, in reliable networks, or in massive-scale systems. These
areas are often seen as “sexy”, “desirable”, and “on the bleeding edge of
computer science”. There are some critically important fields that don’t come
to mind quite so easily, however. Devops is one that is every bit as much a
specialty as those I listed, and every bit as important, if not moreso.
Here’s another: Build Systems. Chances are, if you are developing software to
deliver value and solve difficult problems, you are not talking about software
simple enough that you can run -classpath Foo.java by hand.
How do you solve a problem in software that is too big to run by hand? You
write code to do it, of course. It is from this realization that Make was
born.
The last fifty years at a glance
Before diving in, here is the shape of the whole thing. Every one of these tools was somebody’s answer to “the previous tool is driving me insane”, and you can watch the center of gravity move from single-machine DAG runners, to the JVM’s XML dark age, to language-native tooling, and finally to the graph-aware monorepo tools that dominate today.
GNU Make
Make was originally created by Stuart Feldman at Bell Labs, with an early version completed in April 1976 (Wikipedia) - work that later earned him the 2003 ACM Software System Award. Today many derivatives exist, most notably GNU Make. Even after almost five decades, a plethora of software builds using GNU Make or some other Make variant. The key concepts for Make are simple - you have inputs and outputs, outputs are targets you want to build, inputs are the thing you need to build those outputs, and the output of one process can be the input of some other process. This alone is complex enough to describe a “Directed, Acyclic Graph” (DAG) of tasks and their dependencies, sort them into a topological ordering, and then execute them, possibly in parallel. Make can use filesystem modification times to detect whether any inputs are “newer” than an output, meaning the output needs to be rebuilt, or if it can skip building, allowing incremental builds. Yes, people were doing incremental builds fifty years ago. No shit. Really.
The Good
- Fast
- Supports incremental builds
- Excellent support on practically every platform
- How many pieces of software do YOU use that have five decades of stability and bugfixes?
The Bad
- With so many variants floating around, whether a given Makefile will work with a given Make is anybody’s guess
- Because it is so simple, it is very difficult to do more powerful things, there is no plugin system
- There is no easy way to test make code, it either works or it doesn’t.
- There is no external dependency management for depending upon projects built by other tools or outside the current build
- Make does literally nothing for you, even a relatively simple build could require hundreds or thousands of lines of make
The Ugly
- Whitespace sensitive. Requires hard tabs. EWWWW!
- Have you ever seen a Makefile that actually made it clear what was happening? Me neither.
Known Offenders
- The Linux Kernel and probably 99% of the GNU Userspace tools build using GNU Make
- Amazon’s massive proprietary, internal build system used Perl scripts to generate makefiles which actually ran the build circa 2009. My understanding is that this is no longer true, but I have no firsthand knowledge anymore
Apache Ant
Released in July of 2000, Ant (“Another Neat Tool”) was birthed out of the need to replace a proprietary make flavor with something “platform independent” (Wikipedia: Ant 1.1 shipped July 19, 2000, out of the effort to open-source Sun’s servlet engine, later Tomcat). In other words, a bunch of Java guys saw a tool not written in Java and decided to write a “better” version with the exact same (or worse) features, but it’ll be better because it was written in Java. Unsurprisingly, Ant (now part of the Apache project, and often called Apache Ant) is little more than a shitty make rewrite in Java, except replace eye-bleeding hard tabs with even-more-eye-bleeding XML. What would possess a person to design such a tool in the year of our lord 2000? I genuinely do not know.
The Good
- …Written in Java? I guess? We’ll call that good?
- Supports “modules” (sorta - it’s terrible, but you basically shove jars onto ant’s classpath and then you can use extended or custom tasks. It’s completely manual, and completely terrible)
The Bad
- No support for incremental builds - filesystem mtime stuff was untenable to do in a cross-platform compatible way
- Lazy property evaluation is not supported - in fact, property values can only even be changed using “ant contrib” extensions
- Creating new “rules” a la Make cannot be done inline, you need to write a Java class and compile it.
- Again, like Make, there is no easy way to test xml “code”, it either works or it doesn’t.
- Again, like Make, there is no external dependency management for depending upon projects built by other tools or outside the current build
The Ugly
- XML is a travesty. From the wikipedia page: “The complex structure (hierarchical, partly ordered, and pervasively cross-linked) of Ant documents can be a barrier to learning. The build files of large or complex projects can become unmanageably large. Good design and modularization of build files can improve readability but not necessarily reduce size.” That doesn’t even begin to describe the real difficulties of using Ant for large, complex, and intertwined projects.
- If you thought Make was verbose, wait till you see Ant build files. The simple example on wikipedia is 21 lines long to build a single jar file plus a clean target.
- You haven’t known true pain until you found a “portable” ant build.xml that wouldn’t work because you needed to somehow get some ant contrib jar on your build tool’s classpath (which is different from your buildtime classpath, of course).
- Undefined properties do not raise an error (!!!!!) but instead are left alone, so you get the string “${foo.property}” instead, which is just about the only output you can be SURE is not what is intended. Ever seen this?
<if><equals first="${foo}" second="$${foo}"/>...
Known Offenders
- Many large Java projects are built using ANT, and little else. The original case it was written for was to build Tomcat after its open-sourcing.
- Apache Ivy - this external dependency manager can be shoved into Ant (like ant contrib, see other comments about classpath disasters) to give you Real Dependency Management (TM). Sorta. It also builds using Ant, of course.
Maven
Maven (“accumulator of knowledge” or “expert” in Yiddish) started as a Turbine sub-project in 2002, became a top-level Apache project in 2003, and shipped 1.0 in 2004 (Maven history). It took the Java world by storm on account of its undisputed ability to “suck less than Ant”. That said, a far more appropriate moniker would have been Nudnik (Yiddish for “pest” or “pain in the neck”). Like Ant (and in a move that makes no contribution whatsoever to that “sucking less than ant” thing), Maven uses an XML format for its build files (called pom.xml). Contrary to Make and Ant, Maven uses “convention over configuration” and assumes everything it isn’t explicitly told is some (presumably sane) default. Despite having to say fewer things explicitly, the pom format is so insane that even the simplest pom file is usually much larger than any build.xml would be, though because Maven has so many more features than Ant it is difficult to make a fair comparison. Today, many other build tools have adopted Maven’s default layouts, so you have Maven to thank for any project that has the classic directory layout of:
- src/ ** main/ *** java/ *** resources/ ** test/ *** java/ *** resources/
Also contrary to Make and Ant, Maven has a built-in dependency manager. This is Maven’s greatest strength and a large reason why it has become the defacto build tool for a majority of modern Java projects. Maven may even be credited with causing the explosion of quality, easy-to-depend-upon libraries in the Java ecosystem which made it such a popular and productive enterprise software stack. Even as a self-described Maven hater, I have to admit it is likely responsible for much of the success Java has enjoyed between 2004 and now.
The Good
- Because of its built-in dependency management and plugin architecture, Maven is the first popular build system where you can specify your build tool dependencies and use them, and have it “just work”
- Maven gets an extra “good” line for dependency management, because you can not only specify your dependencies but you can specify if they are “test” or “compile” or “provided” - it supports classpath separation and publishing of a strict, consistent classpath (if you use it correctly)
- Maven has excellent built-in support for interactive development environments (IDEs) such as IntelliJ and Eclipse
- Maven can be used to build things besides Java (but is very Java-centric and still mostly only used for Java)
- Maven has first-class support of hierarchical projects in a structured way, making building very large monolithic repositories easier
- Maven has clear support workflows which makes supported things very easy, such as running tests, publishing artifacts, etc., and plugins make adding new goals easier than forking Maven (if only slightly)
The Bad
- Maven’s error messages are TERRIBLE - It lies to you
- Again, Creating new “rules” a la Make cannot be done inline, you need to write a Maven plugin and compile it
- Again, like Make, there is no easy way to test xml “code”, it either works or it doesn’t
The Ugly
- Like Ant, Maven pom.xml files are a travesty
- If you can fathom it, properties are almost WORSE than Ant. You cannot easily set properties dynamically from environment, they have to be passed in via command line parameters
- Newer versions of Maven have had features intentionally removed because “that is the wrong way to do it” or “you shouldn’t be doing that in your build”
- Both Maven 2 and Maven 3 are in common use across many projects but most projects only work with one, or the other - Maven does not bootstrap itself or control the dependency on itself at all
- People have written extensively on the value of Internal Reprogrammability, frequently using Maven as the canonical example of how to fail at it
Known Offenders
- A vast majority of jars you will find on search.maven.org were built using maven (but not all - maven.org is a sort of artifact repository which both Maven and Ivy a la Apache Ant can use)
- Many SDKs bundle a version of Maven to build java-based plugins, such as Atlassian’s Plugin SDK
Gradle
Gradle released version 0.1 on April 21, 2008 (Wikipedia; the project was conceived in 2007). It was finally a build tool that doesn’t completely suck. I mean, it did in the early days, probably, but Gradle was created with a mission and supported by a services company and an open source community together, and it has accomplished more than any other open source build tool in popular use. Hans Dockter is the CEO of Gradleware, Gradle’s strongest voice of support, and a very smart guy behind the demand that we as an industry deserve better build tools, a stance I agree with and care deeply about.
Gradle’s mission is to be a reliable, powerful, platform-agnostic tool for
building and testing projects large and small, using any language/technology
stack, with easily-extended and easily-tested build code in a domain specific
language (DSL) based on Groovy. There is a lot to unpack there. First off,
Gradle is “unopinionated”. It doesn’t assume anything about your build (that
it is in java or C/C++, that it has dependencies, etc). Furthermore, because
it uses a Groovy-based DSL, the build files themselves can be very short and
incredibly descriptive. Because Gradle allows both inline and external plugin
modules, you can write your build code inline for ease-of-use, or in a module
for easy testing. You can write shared code which doesn’t make any assumptions
(in, say, a module called “java-base”), and then an “opinionated” module called
“java”. If you include “java-base”, you get the targets you can manually call
to create jars and so on. If you include “java”, you get “java-base” but also
the assumption that your source code lives in src/main/java and your
tests in src/test/java and so on, making both “configuration over
convention” and “convention over configuration” possible when appropriate.
A Gradle project to build a simple java library following the standard Maven layout looks like this:
apply plugin: 'java'
Yeah, you aren’t missing anything, that’s it! That alone gives you test targets, compile targets, publish targets, the works. It gives you incremental builds (doing an up-to-date check of your source files) and understands that source and resources goes into your jar. With a few more lines, you can pull in dependencies from search.maven.org just like you would with Maven or Ant + Ivy. But wait, there’s more!
Gradle, unlike any other build tool we have thus far discussed, has a “bootstrap” step. Using a minimal jar you check in next to your project, gradle can download itself - and not just any version of itself, but a specific version of itself you tell it to use - and it uses that to run the build. In this way, Gradle is the first build tool in common use (TODO: apologies to SBT?) to properly bootstrap itself and control its own dependency. A new version of Gradle will never be released causing hundreds or thousands of projects to break, because their bootstrapper will continue to fetch the known-working version until someone tells it to upgrade. This is how build tools should work, ladies and gentlemen. The only dependency Gradle still has is on Java itself (you need a compatible JDK around to invoke gradle, even the bootstrapper). Breaks can still be caused by incompatible Java versions, but that is a much less common problem, and one which is much easier to fix.
The Good
- Dependency management “as good as it gets” for Maven-compatible repositories (and compatible with maven and ivy repositories)
- Because of its superior dependency management and plugin architecture, you can write build code inline or in modules, external or internal to your project, with or without tests of your build code
- The Groovy DSL is concise, expressive, and powerful - you can implement build code in it, in raw Groovy, or even in Java if you prefer
- Like Maven, Gradle supports classpath separation between test and compile targets, but unlike Maven it is even more powerful as you can create arbitrary independent classpaths / targets (i.e. an “api only” classpath, etc)
- Like Maven, Gradle has excellent built-in support for interactive development environments (IDEs) such as IntelliJ and Eclipse
- Unlike Maven, Gradle is actually used to build many non-java projects, and there is a huge ecosystem of plugins for building non-java projects - Maven says it does it, but Gradle is what people actually use to accomplish that
- Gradle’s consistency, reproducibility, and flexibility are unmatched by any other open source tool in common use I am aware of
The Bad
- Groovy, the main language build.gradle files tend to be in, is a duck-typed and interpreted language so errors are often non-obvious and difficult for the compiler to report - and the groovy compiler/interpreter is pretty shitty at error reporting.
- Gradle is sloooow - because build.gradle files must be interpreted, and for large projects, this might mean reading in and building the configuration data structures for hundreds of projects - Gradle can be very slow on startup. This is partially mitigated by the “Gradle Daemon”, which caches configuration information and avoids JVM startup time at the expense of sometimes serving stale or incorrect configuration or artifacts (up to you if that is an acceptable tradeoff or not)
The Ugly
- Gradle’s incremental builds are not perfect, especially if you use janky plugins which were written incorrectly. There are known bugs even with core plugins such as “java” though
- Gradle can put crazy things on your classpath, or even execute code you might prefer not to trust, simply by misspelling “apply plugin ‘jaav’”. The plugins are automatically searched and downloaded from Gradle.org, and who knows what degree of validation happens before code gets posted there?
Known Offenders
- My understanding is that nearly everything built at Netflix is built using Gradle, including their large and generous open-source contributions - they are frequent presenters at Gradle Conferences
- Lots of Android app development happens with Gradle, there are plugins to assist with this
2026 note on Gradle. Everything above still holds, and Gradle grew up. The Groovy DSL got a statically-typed sibling in the Kotlin DSL (
build.gradle.kts), which fixes most of my “the interpreter is shitty at error reporting” complaint because your editor can actually type-check the build now. Remote build caching and configuration caching landed too, which mostly answers the “sloooow” gripe for big projects. Theapply plugin: 'java'magic is exactly the same.
Go (Google’s golang)
While Gradle was still in its infancy in 2007, somewhere in the Googleplex, pioneers of computer science were stirring. Google engineers Robert Griesemer, Rob Pike, and Ken Thompson (the latter two of Bell Labs fame) birthed the Go programming language, which was announced and open-sourced on November 10, 2009 (go.dev). With it came the go build system, which has some interesting features.
Unsurprisingly similar to the Google build system, the go get command
originally fetched a module’s dependencies directly and built them “from tip” (i.e. the
newest version). For more discussion of “the Google way” versus alternatives,
see my earlier post A
Tale of Two Build Systems. The standard operating procedure then, for
external or open source projects which need to control their dependencies, was
to fetch and check in static versions of those dependencies. Updating then
became a terrible mess. Some folks in the Go community decided this was a
problem and tried to create alternative tools, like godep.
The Good
- The Go toolchain is a single static binary and the build is blisteringly fast - Go’s designers treated slow builds as a bug in the language, not a fact of life
go build,go test,go fmt,go vetship in the box - no plugin zoo, nobuild.xml, nopom.xml, no DSL to learn- Cross-compilation is a one-liner (
GOOS=linux GOARCH=arm64 go build), which is genuinely magical the first time you use it
The Bad
- In 2016 the dependency story was a disaster (see above) - “build from tip and pray, or vendor everything by hand”
- It is deeply opinionated about directory layout and (originally) the
GOPATH, which fought you if your world didn’t look like Google’s - It builds Go, and only Go - there’s no pretense of being a general build tool
The Ugly
- The vendoring-vs-GOPATH-vs-third-party-tool churn of 2013-2018 broke a lot of builds and produced a small graveyard of dependency managers (godep, glide, dep) before the official answer arrived
2026 note - Go fixed the one thing I dinged it for. Go modules landed as experimental opt-in support in Go 1.11 (August 2018), grew out of Russ Cox’s
vgoprototype, and became the default in Go 1.16 (2021) (Go Modules wiki). Versions are now pinned ingo.mod, builds are reproducible, and Minimal Version Selection decides which versions you actually compile against - you upgrade on purpose instead of getting swept along with tip. godep itself was archived. This is squarely “the Amazon way” and it’s a strict improvement over what I described in 2016.
Docker and the container as a build artifact
Docker deserves a mention here even though it isn’t strictly a build tool,
because after Solomon Hykes first demoed it at PyCon US on March 15, 2013 and
open-sourced it that month
(Wikipedia), it quietly
became part of nearly everyone’s build. A Dockerfile is, whether its authors
admit it or not, a little imperative build script that produces an artifact (an
image), and docker build is a crappy, non-incremental, layer-cached build tool
bolted onto that.
This matters for the themes of this post because Docker made “the environment” into a versioned, shippable artifact for the first time in the mainstream. Before Docker, “works on my machine” was a build-reproducibility problem you solved with documentation and prayer. After Docker, you could at least pin the whole userland.
The Good
- The build environment is now itself an artifact you can version, cache, and ship - a genuine step toward reproducibility
- Layer caching gives you a crude form of incremental builds for free
- It composes with real build tools - the modern pattern is a proper build tool (Bazel, Gradle, Go) producing an artifact, and Docker packaging it
The Bad
docker buildcache invalidation is coarse and famously surprising - reorder two lines in your Dockerfile and watch everything rebuild- Naive Dockerfiles are the opposite of reproducible:
apt-get updateorpip install foowith no pin fetches whatever is newest today, so the “same” build produces different images on different days - It encourages people to conflate “build” and “package”, which are different jobs
The Ugly
- An entire industry sprang up to make container builds reproducible and hermetic after the fact - Bazel’s
rules_docker, Google’s distroless, and daemonless builders like Buildah and Kaniko - which tells you the base tool never really solved it
Bazel
Bazel is Google’s build system, open-sourced in March 2015 (Wikipedia), reaching 1.0 in October 2019. It is not a fork or a clone of Google’s internal tool - it is that tool, or most of it. Google’s internal build system is called Blaze (“Bazel” is an anagram), and the two share the bulk of their implementation and co-evolve. When people talk about “the Google way” of building software (see A Tale of Two Build Systems), Bazel is the closest thing to that experience you can run outside Google.
The central idea is hermeticity: per Bazel’s own docs, “when given the same input source code and product configuration, a hermetic build system always returns the same output by isolating the build from changes to the host system” (bazel.build). Builds declare their inputs explicitly, run in a sandbox that hides everything they didn’t declare, and are therefore reproducible and safely cacheable - including across a whole team via remote caching, and across a fleet via remote execution. Build rules are written in Starlark, a deliberately restricted Python dialect with no I/O and no clocks, precisely so that evaluating a build file can’t do anything non-deterministic.
The Good
- Genuinely hermetic, reproducible builds with correct, fine-grained incrementality - the gold standard for “only rebuild what actually changed”
- Remote caching and remote execution let a whole team share build outputs and fan a build out across hundreds of machines
- Language-agnostic by design, with rulesets for Java, C++, Go, Python, Rust, and more
- Starlark makes build logic testable and, crucially, prevents the “my build reaches out and does something weird” class of bug
The Bad
- The on-ramp is brutal. To get hermeticity you have to declare every dependency and tool explicitly, and the ecosystem outside Google fights you (a decade on, “how do I get my third-party deps into Bazel” is still where teams burn weeks)
- You are signing up to maintain a lot of
BUILDfiles, and the graph is only as correct as your least-careful rule author
The Ugly
- It very much wants your whole repo to play by its rules. Adopting Bazel in a big existing polyglot repo is a migration project, not an afternoon
QBT - QBT Build Tool
QBT began its life in December of 2014 as a very small prototype, written in Perl by Keith Amling, an engineer at Palantir Technologies. The tool’s original name was DBT, which stood for “Distributed Build Tool” but was really backronymed from Keith’s manager’s nickname as an inside joke. When the joke got old, and his manager couldn’t stand his IRC client going nuts every time someone talked about DBT, the project was renamed to QBT. Keith worked the prototype into a fully functional self-building system written in Java before having a few other engineers (myself included) added to the team, but he very much played the part of the “Benevolent Dictator for Life” for this project. Keith insisted from the beginning that the tool not compromise its goals for any reason.
QBT was created to solve the very specific problem of many projects, from many different technology stacks, needing to depend upon each other in a way that can be kept consistent, changed atomically, and versioned strictly. Reproducibility and technology agnosticism were both baked in from the beginning, as well as a strong plan to eventually go open source. Inspired heavily by the proprietary build system in use at Amazon.com, QBT was meant to be a decentralized, zero-infrastructure feature-equivalent (though its design for accomplishing this was so different from Amazon’s build system, the link is barely recognizable).
Like godep, QBT has a “manifest” of sorts which contains the versions of every package to be built, which is itself checked into source control and thus can be updated “atomically”. QBT can be thought of as a build framework, a “repository stitcher and dependency manager”, as it uses the manifest to locate packages in various repositories, stitch together the correct versions of all of those repositories, and invoke each package’s build in topological order, providing each with its dependencies. Unlike other build systems, not only are the versions stored in the manifest but also the complete dependency graph, allowing QBT to do interesting things like only check out the packages needed to perform a certain build.
QBT does much of its magic by being coupled with the Git source control manager. Though written through an abstraction layer, making writing adapters to other similar DVCS systems like mercurial possible, QBT utilizes Git for performing many of its necessary tasks. QBT analyzes not just the repository revision, but the actual “tree hash” of the root directory of a package, and combines that with the tree hashes of its complete transitive closure of dependencies to calculate a “cumulative version” (CV) for each package. If you made a commit to a repository which didn’t change a particular package, its CV would stay the same, therefore QBT doesn’t have to rebuild it. QBT therefore has package-granularity incremental builds. Furthermore, since classpath isolation is generally done at the package level, packages are encouraged to be small and numerous. Adding a package is as easy as adding a few lines to the manifest file and creating a new directory in an existing repository, so packages are very lightweight.
Unlike most dependency managers, QBT does not assume artifacts will be
published. Similar to go get, qbt must fetch the complete source
(and in fact, grabs the entire source control history for each dependent
repository) in order to build things. With an extremely flexible configuration
(specified in Groovy), you can configure QBT to consult an artifact server, and
even publish there. Cache hits (based on package + CV) are never rebuilt if
the artifact is already present, so in that way, QBT can have artifacts,
but it doesn’t need to. Because only changed packages, and other packages that
depend upon those changed packages, need to be rebuilt, even very large graphs
can be built very quickly and efficiently.
Also unique compared to many build tools, QBT can easily function in a
disconnected environment. Inspired by Git itself, QBT has only two commands
that invoke the network - qbt fetchPins and qbt pushPins.
If you run a git fetch in your manifest repository, and a qbt fetchPins, you
can guarantee you have every repository of every package in your manifest
cached locally, and ready to build or check out any version in the complete
history of your manifest file. Similarly, you can make commits in satellite
repositories, and in meta, as much as you please, working on many different
branches or projects. No network is needed until you are ready to push your
changes, which you would do by first doing a git push of your
manifest file followed by a qbt pushPins to get your satellite
versions pushed.
QBT may be a repository stitcher, and dependency manager, but it is not actually a compile tool beyond that. If you want to build java, you need to use ant, maven, or gradle inside your package. If you want to build C/C++, you should invoke make, or gcc. All QBT does is assemble your package and its dependencies, place them in a known location, and invoke the shell script “qbt-make” in the root of your package. From there, what you do is up to you, so there are many ways to do it. In this way, QBT is more of a framework or toolkit with which you can produce incremental, reproducible, strict-consistency builds. Today, QBT builds itself using Gradle, but we could use a different tool, or even just a shell script that invoked javac, if we wanted to.
Finally, one of the most important things QBT can do for you that other build systems cannot is QBT gives us a way to ensure consistency. Because all changes occur in the manifest file of the meta repository, we can have a continuous build that ensures the entire manifest is always consistent. Furthermore, if someone makes a pull request, before merging that pull request in, we can perform a build of that manifest to ensure the new versions are also consistent. This means nobody is ever broken without knowing it. Because there is only a single version of every dependency in the manifest, any change someone makes is tested throughout the entire development ecosystem.
The Good
- Strict dependency management with atomic changes enables strong consistency guarantees when used correctly
- Tight coupling with Git allows package-granularity incremental building which is the safest incremental building available
- Technology agnosticism allows QBT to build anything from Java to C/C++ to Node.js to lua to LaTeX to copying around text configuration files or static webpages (this webpage is built using QBT)
- The “submanifest” command lets you keep a “private manifest” which is a superset of a public manifest, make changes “in private”, and vend only the changes to the public repositories to certain parties (ideal for an enterprise writing proprietary software which depends upon open-source software)
- QBT automatically parallelizes your build at the package granularity and can build very quickly when packages are appropriately granular
- QBT includes a “third party importer” and “link checker” which can import jars from search.maven.org and even examine their bytecode to ensure their dependencies were properly specified in their pom.xmls to guarantee they will work in QBT - this is the next best thing to compiling them from source
- If using a build tool like Bazel, it should theoretically be possible to produce byte-for-byte reproducible artifacts.
- QBT has a “runArtifact” command which lets you compile and run your programs in a single step, making the develop/test cycle tighter
- QBT supports “dev protocols”, allowing packages to “opt in” to certain protocols (such as eclipse-gen, which generates eclipse project files, help wanted writing protocols for IntelliJ and other IDEs)
- QBT versions itself internally - while you do need to get a binary version of QBT first in order to build QBT, you can always use that to build the version of QBT specified in a given manifest and use that to build the software, guaranteeing that future QBT changes do not break you until you merge them into your manifest
- QBT supports mechanisms for versioning external tools as well (such as JDKs, which are too large and platform-specific to include as a regular package in your manifest). These external dependencies impact CVs correctly and don’t interfere with reproducibility or dependency management
The Bad
- QBT has only been well-tested on Linux so far
- QBT is still immature and not widely used
- QBT does not yet have a good answer for “realms” (how will different organizations vend manifests to each other?)
The Ugly
- Not unlike Git, QBT has a steep learning curve and can be very difficult to use correctly, especially at first
Known Offenders
- www.cmyers.org is built using QBT
- QBT builds itself
- A large set of useful java libraries called “misc1” were developed for use by QBT and are available via QBT - Libraries include argument parsing, concurrent programming, and immutable data structures and struct libraries of immensely high quality
There is a lot more to write about QBT, but hopefully this quick look at QBT and many other build systems will put into context what QBT can and cannot do for you.
2026 note on QBT. Full disclosure that I’ve since left Palantir and have public disagreements with the company’s direction - see A Tale of Two Build Systems for more on that. None of that changes the engineering: QBT solved cross-project strict dependency management in the open, and a decade later that is still the part almost nobody else solves outside company walls. The graph-aware monorepo tools below (Bazel, Buck2, Pants) get most of the way there when you control all the code in one repo. QBT’s bet was that you often don’t, and that bet has aged well.
The 2026 landscape
Here’s the section I couldn’t have written in 2016, because most of these tools didn’t exist yet or weren’t open. The big shift of the last decade is that “graph-aware, cache-everything, only-rebuild-what-changed” stopped being exotic and became the default expectation for any large repo. Bazel (above) kicked it off; here’s the rest of the field.
Buck2
Facebook’s original Buck was open-sourced April 17, 2013, in the same Bazel-like “declare your inputs, get hermetic caching” family. In April 2023, Meta open-sourced Buck2, a from-scratch rewrite with its core written in Rust and its build rules written in Starlark (Meta Engineering). Meta reports it builds roughly twice as fast as Buck1 on their monorepo. It is the most credible open competitor to Bazel, with a cleaner rule API and a very fast core, though its ecosystem and docs are still thinner than Bazel’s.
Pants
Pants started around 2010-2011 as an internal Twitter tool - the name is
literally “Python + Ant” - and was open-sourced in 2011, later picked up at
Foursquare and Square
(Twitter Engineering).
The version worth caring about now is Pants v2, a ground-up rewrite released
October 27, 2020
(pantsbuild.org).
Pants v2 is aimed squarely at the pain point Bazel gets criticized for: it does
a lot of dependency inference automatically (it reads your imports) so you write
far fewer BUILD files by hand. It’s the friendliest of the hermetic tools for
Python-heavy shops.
Nx and Turborepo
The JavaScript world got its own take on all of this, because npm (first
released January 12, 2010, Wikipedia) never
had a story for “I have 40 packages in one repo and only want to rebuild and
retest the ones that changed.”
Nx (from Nrwl) first appeared in 2017 with 1.0 landing at ng-conf in April 2018 (Nrwl announcement). It builds a dependency graph of your workspace, caches task results locally and remotely, and runs only the affected projects. It started in the Angular world and has since gone framework-agnostic.
Turborepo (created by Jared Palmer) was acquired by Vercel and open-sourced on December 9, 2021 (Vercel). Same core idea as Nx - content-addressed task caching and only-build-what-changed - with a smaller, simpler surface area aimed at JS/TS monorepos.
If you squint, Nx and Turborepo are Bazel’s ideas (the build graph, hermetic-ish
task caching, remote cache) repackaged for people who were never going to write
a BUILD file and just want npm/pnpm to stop rebuilding the world. That’s a
good thing. The ideas won.
Sir Not Appearing in this Blog
Honorable mentions I haven’t given a full writeup:
- CMake (first released 2000, cmake.org/history) - the de facto standard for C/C++ meta-builds
- SBT, the Scala Build Tool (first released December 2008, Wikipedia)
- Grunt, Rake, and the rest of the language-specific task-runner crowd
- Cargo, which quietly became the build tool everyone else wishes theirs was as pleasant as