Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build systems

See also Make.

Build systems I have enjoyed

Build systems I gave up on

  • tup: can’t install via Nix
  • redo: interesting, but unmaintained

What to look for

Allow or require out-of-tree builds

Justification: Out-of-tree builds are occasionally necessary, for instance if your source tree is mounted on a read-only filesystem. More realistically, out-of-tree builds ensure that you and your build system are aware of all the files that get created and read during the build process, which aids in maintainability, source tree cleanliness, and accurate dependency tracking.

Do only necessary work

As much as your build system supports it, you should use dependency tracking to ensure that you don’t redo any build steps or other tasks.

Example: If package or file B depends on A and B changes, don’t re-compile, re-lint, or re-test A.

Pin external dependencies as precisely as possible

It’s nice to work with a range of versions of dependencies, but ensure that there is at least one known-good, fully-specified list of dependencies. This usually means a specific commit hash or tarball hash for each dependency.

Justification: This aids in reproducibility.

Example: Poetry and Cargo provides lock files that fully specify dependency versions. CMake and Pip do not. Cabal can be configured to provide and use a “freeze” file.

Use ecosystem-appropriate tooling

To the extent that it’s reasonable, use tooling that’s common in the ecosystem you’re targeting.

Justification: This aids in maintainability by making it easier for likely contributors (that is, people familiar with the ecosystem) to contribute. It can also aid in debugging, in that others may have seen the same issues you’re seeing before.

Example: When building C++, use Make, CMake, or Meson. Don’t use something super obscure.

Example: It’s not always reasonable to use pip or setuptools for Python, because they have notable flaws (e.g. pip install does extraneous work when the package hasn’t changed since the last call to pip install). When using Python on a project, it may be appropriate to consider a different build/packaging solution.

Utilize content-addressing for build products where possible

Justification: Content-addressing provides an extensional view of when dependencies change, leading to fewer rebuilds, i.e. speed.

Example: If package B depends on A and the README of A changes, but doesn’t affect the build products that B uses, don’t rebuild B.

Don’t modify the user’s system

The build system should only modify files inside the build directory. In particular, it shouldn’t install system-wide packages. There are certain exceptions, for instance it’s OK for Cabal’s Nix-style builds to install packages in ~/.cabal because they are hermetically built.

Justification: This aids portability, because your assumptions about developers’ (and CI systems’) machines will break.

Provide a source distribution

The build system should provide a target that packages all necessary source files into an archive so that that archive can be unpacked and built with just the files present plus those downloaded and managed by the build system.

Justification: This ensures all dependencies are accurately accounted for. For instance, a common unstated dependency is a bunch of git submodules. It also makes it easier to back up your sources.

Research