Frequently asked questions

Why another build tool?

A common answer to a common question: Because none of the available tools met the requirements of the author, especially for the development of embedded software with cross-compiler toolchains and generated source code.

(Desirable) property dlb Make SCons Ninja
Resource effectiveness [1] Speed of full build (huge project) ⊕⊕ ⊕⊕
Speed of partial or “empty” build ⊕⊕ ⊕⊕
Avoidance of unnecessary execution of tools ⊕⊕ ⊖⊖
Small RAM footprint (during run) ⊕⊕ ⊖⊖ ⊕⊕
Small disk footprint (between runs) ⊖⊖ ⊕⊕ ⊕⊕
Usability [2] Expressiveness of build description ⊕⊕ ⊖⊖
Self-containedness ⊕⊕ ⊖⊖ ⊕⊕ ⊖⊖
Simplicity of debugging a build ⊖⊖ ⊖⊖
Correctness, reliability [3] Safety of use of paths with “special” characters (' ', '$', '\\', …) ⊕⊕ ⊖⊖
Accuracy of automatically detected input dependencies ⊕⊕ - -
Robustness to system time jumps ⊖⊖ ⊕⊕ ⊖⊖
Robustness to changes during build ⊕⊕ ⊖⊖ ⊕⊕ ⊖⊖
Robustness to manual changes of output files - ⊕⊕
Reproducibility of builds ⊕⊕ ⊖⊖ ⊖⊖ ⊖⊖
Powerfulness, scalability [4] Portability of build description ⊕⊕ ⊖⊖
Modularity ⊕⊕ ⊖⊖
Simplicity of separation between input and output directories ⊕⊕ ⊖⊖
Abstraction of tools ⊕⊕ ⊖⊖
Ability to deal with circular dependencies ⊕⊕ - - -
Fundamental objects contexts, tools, paths, dependencies strings environments, builders, nodes strings, string lists

Legend

[1]What amount of resources is required to perform a task?
[2]How easy is it to read a build description and run a build?
[3]How easy is it to develop a correct and reliable build description?
[4]How unconstrained is the future direction of the project by the build system?

dlb has the unique ability to enforce requirements instead of just assuming them. This makes dlb not only a build tool but also a tool for quality assurance. For example: You can check the design decision that library A in your project must not depend on library B every time library A is built and use this guarantee (not just assumption!) to reduce the build time (see example/c-huge-libraries/).

There is a plethora of other build tools:

Of these, SCons and doit are closest to the goals of dlb. See the following questions for a comparison to Make, Ninja and SCons.

The other tools fall into two large categories which both have major limitation in the view of the author.

Tools based on declared dependency rules

Most of them implement the functionality of Make in a more readable descriptive language and improve the modularity and the ability to split large projects into smaller ones.

Examples are:

See here why a descriptive language is not the best approach to describe a build process.

Tools based on directory structure

Some build tools are specialized in organizing large projects, fetching files from different sources, packaging and publishing the build products. They usually do so by imposing a certain directory structure and assign files a given meaning based on this structure.

These tools are heavy, complex and restrictive. A build tool should be simple and flexible.

Examples are:

Who should use dlb?

You should not use dlb if:

  • You are proud to have mastered a flavour of Make + sh + makedepend + automake + autoconf + find + xargs + grep + sed + awk + … and happily run a make clean-all now and then — because that’s what you do. You cannot understand the need for an alternative.
  • You are happy with your IDE and build all your software with Ctrl+Shift+B.
  • You do not know Python and are not willing to learn it.
  • You do not have to build your code on several machines (i.e. you work alone).
  • You do not care about portability and quality.
  • Your project is huge, everything depends on everything, and its individual parts cannot be tested in isolation (i.e. it is poorly designed).
  • Your project is mostly built from scratch and extremely often (like FreeBSD ports or Linux kernels built by users for installation). Even a build time increase as low as 5% would — cumulated — mean a massive waste of (life) time and energy.

Otherwise, you should give dlb a try. Especially if:

  • You build the same project on several platforms (e.g. GNU/Linux and MS Windows).
  • You need to generate source code (e.g. a header file with version information).
  • You love the elegance and power of Python.
  • You use a not-so-common compiler toolchain and want to control every aspect of code generation (e.g. for embedded software where code size matters).
  • You have to make sure your software can be reproduced exactly in 10 years.
  • Your build process consists of sequential steps whose dependencies cannot easily be described by an acyclic graph (you currently use a shell script to combine them).
  • You lost enough time with missing dependencies: You just tested a bunch of small code modification based on each other, carefully tested the output for each of them and committed them to the repository. Only to realize that the first of them introduced a bug (you did not notice it because your build tool did not regenerate all the dependent files).

How fast is dlb?

There is a lot of controversy in comparing the speed of build tools in general and SCons in particular.

Raw speed for a single build in an ideal and static environment is hardly the most important benchmark for productivity; the necessary total effort to develop and maintain a trustworthy and complete build description is far from negligible. Spending hours to find subtle flaws in the build process and doing complete rebuilds out of mistrust in the completeness of the dependency information costs more than a few seconds per — otherwise perfect — partial build. [9]

Having said that, here are the results of a simple benchmark used both against and in defense of SCons (which means it has some significance):

_images/benchmark-2.svg_images/benchmark-1.svg

Remarks:

  • Each source file defines one C++ class and includes 15 files from its own library as well as 5 files from other libraries. Each library depends on every other library (in other words: the benchmark scenario represents a very poorly designed project).
  • The generated simplistic GNU Makefiles contain static lists of files while and miss a lot of dependencies. dlb finds the files at run-time.
  • makedepend (used in a rule of the simplistic GNU Makefiles) crashes for very large numbers of classes.
  • In addition to the original simplistic GNU Makefiles, labeled GNU Make + makedepend (simplistic), a build with GNU Makefiles that describes the dependencies completely was added for comparison and labeled GNU Make.
  • The dlb performance is given for three styles of the dlb script (all describe the dependencies completely):
    • dlb: straight-forward
    • dlb (grouped): 5 source files per tool instance
    • dlb (hierarchical): assume a monotonic system time as Make does
  • The complete code of the benchmark is here: test/benchmark/.

Properties of tested builds (n: number of libraries, m: number of source files per library):

  GNU Make + makedepend (simplistic) GNU Make dlb dlb (grouped) dlb (hierarchical) SCons
Describes dependencies completely -
Can be aborted without corruption of output - -
Does not depend on monotonic system time - - -
Size of build description (number of non-trivial lines) 7 + n (m + 20) 188 + n 25 37 53 1 + n (m + 4)
Based on   example/c-minimal-gnumake/ example/c-minimal/ example/c-huge/ example/c-huge-libraries/  

How does dlb compare to Make?

The concept of Make originates from an era when running an interpreter like Python was too slow to be productive. Its authors sacrificed readability and correctness to speed.

It is very easy to write extremely fast, incomplete, unreproducible and unportable Makefiles. It is very hard to write complete (all dependencies are covered) and reproducible (the output is the same for the same input) Makefiles. It is impossible to write portable Makefiles. [5] It is possible but time-consuming to write Makefiles that clearly describe and check their requisites and assumptions.

There is a reason why there are so many flavours of Make and so many utilities that generate Makefiles.

In contrast, it is very easy to write fast, complete, reproducible and portable dlb scripts. dlb does not guess or assume but requires the explicit statement of information to be used by external tools (the expected content of environment variables, for example). This results in readable and self-documenting dlb scripts that concisely describe their prerequisites.

Make is significantly faster than dlb when only a small fraction of the output dependencies has to be generated (Make: only a few sources are newer than their targets). The available Make implementations have been carefully optimized for speed over the years. dlb is executed by an instance of a Python interpreter; starting a Python interpreter and importing some modules alone typically takes some 70 ms.

Make executes a rule’s command if one of the rule’s source has a later mtime than any of the the rule’s targets (or if one of the output dependencies does not exist). A Make build can therefore silently miss the update of a rule’s target, if one of the following (implicit) requirements is violated:

  1. The mtime of each involved filesystem object never decreases.
  2. The mtime of each involved filesystem object is in the past when the build starts.

Make requires that each output dependency (target) changes when one of its input dependencies (sources) has changed. Fixing a typo in a comment of a .c file necessarily leads to compilation, linking and all dependent actions, whereas in dlb the cascade stops with the first file that does not change. Since a typical dlb script describes the dependencies completely while a typical Makefile does not, you won’t so easily find yourself in the position with dlb where you have to remove all output dependencies and build from scratch.

Compare example/c-minimal/ and example/c-minimal-gnumake/.

How does dlb compare to Ninja?

Ninja’s mission statement reads:

Ninja is a small build system with a focus on speed. It differs from other build systems in two major respects: it is designed to have its input files generated by a higher-level build system, and it is designed to run builds as fast as possible.

This is a clever choice. Ninja files have an elegant and well-defined syntax. This means: Wherever Make is suitable, Ninja is better.

Despite its claim, Ninja has hardcoded support for compiler interfaces specific to GCC, Clang and MSVC as well as a file-based mechanism for dependency detection at build time.

Like Make, Ninja executes a rule’s command if one of the input dependencies have a later mtime than any of the output dependencies (or if one of the output dependencies does not exist). It therefore shares the risks of Make related to system time changes and file changes during a build.

Ninja (similar to dlb) detects outputs unchanged by a rule’s command.

As stated above, Ninja is meant to work as part of a higher-level build system that automatically generates Ninja files. [6] Rōnin is such a higher-level build system. It has a structure similar to dlb and can therefore be part of a dlb script. However, Rōnin shares the typical limitations of declarative build descriptions; it performs a lot of “magic” (with undocumented assumptions) and cannot be extended beyond the hardcoded and limited extension interfaces.

You can use dlb to generate Ninja files.

How does dlb compare to SCons?

SCons shares some goals with dlb. However, it approaches them differently.

SCons is monolithic, string-oriented and describes dependencies by (implicit) rules; the order of the rules does not reflect the order of the actions. dlb is modular, object-oriented and describes dependencies by explicit statements. SCons contains a lot of predefined roles for typical tasks and environments and does a lot of guessing (e.g. it tries to detect toolchains). This makes SCons quite slow and intricate to extend in some aspects.

SCons relies on shell command-lines described as strings and tries to escape characters with special meaning only in a very simple manner (like putting '"' around paths with spaces). It is therefore risky to use characters in paths that have a special meaning in the shell (implicitly) used on any of the supported platforms. dlb does not use a shell. A relative path str(p.native) always starts with . if p is a dlb.fs.Path. As far as dlb is concerned, it is safe to use any character in paths (e.g. -o ~/.bashrc or ; sudo rm -rf /).

SCons detects dependencies before it executes a tool. It does so by scanning input files, roughly mimicking the tool to be executed potentially. dlb detects dependencies after a redo of a tool instance. It uses information provided by the tool itself (e.g. the list of include file directly from the compiler), which is much more accurate and also faster. When dlb detects a new dependency (after the execution of a tool instance), the next execution of this tool instance always performs a redo. SCons can avoid “redos” right after its first run.

SCons and dlb both use a database to store information of the last build. SCons builds a global dependency graph (in memory) on every run; it checks all input dependencies at the very beginning and stores their state at the beginning in the database. dlb does not rely on a global state in memory; it only checks the input dependencies per tool instance and stores their state for every tool instance (hence cycles and incremental modification of filesystem objects are possible). The database of dlb tends to be much bigger than the one maintained by SCons.

dlb is significantly faster and is designed for easy extension.

Why Python?

Building software with the help of external tools typically requires a lot of “glue logic” for generating files and manipulating files and program output. Python and its libraries are very well suited for this task. The language is clean and expressive and the community takes pride in elegance and simplicity.

Why is explicit better than implicit?

Some argue that restricting the expressiveness and power of the language to configure software is a good thing. For a tool whose developers have a different background than its users this is certainly true. As far as tools for developers are concerned, it is not. A build tool should be a powerful tool in the developer’s tool box that allows him to complete his tasks efficiently and without risking dead ends (caused by language restrictions).

A tailored DSL is a good thing exactly as long as you use it as foreseen by its creators. A two-line example may be impressive as a demonstration, but real-life projects look different.

If a certain task is repetitive enough to be described by static content (e.g. an XML file), there’s nothing wrong in doing so. But this situation does not call for a restriction of the language — it calls for an (optional) easy way to interpret the static content.

By restricting the language used to describe the build process instead, you usually lose first:

  • The possibility to debug the build process with powerful tools
  • The possibility to extend the build tool by aspects not anticipated by its creators
  • The possibility to adapt a certain behaviour of the build tool without replacing large parts of it

How do I control build scripts with command-line parameters?

When run with python3 -v or PYTHONVERBOSE is set, dlb does not suppress any messages. Aside from this, there is no command-line mechanism built into dlb.

Use argparse or Click, for example. But: Less is more.

Can I use dlb in closed-source projects?

dlb is licensed under LGPLv3 (which is a supplement to the GPLv3), dlb being “The Library” and each dlb script being a “Combined Work”. [8]

dlb scripts can be part of commercial closed-source software without the need to publish any of it. You may also add dlb to your source code repository (as dlb-*.zip, for example).

If you “convey” [7] a modified copy of dlb itself, however, you are required to convey your changes as free software too according to the terms of the LGPLv3 (see section 4 and 5 of the GPLv3). An easy way to do so is to fork dlb on GitHub. It is even better if you contribute to the original dlb by creating an issue.

Where are the sources?

Here: https://github.com/dlu-ch/dlb/.

Feel free to contribute.

Footnotes

[5]

POSIX (ISO 1003.1-2008) states:

Applications shall select target names from the set of characters consisting solely of periods, underscores, digits, and alphabetics from the portable character set […]. Implementations may allow other characters in target names as extensions. The interpretation of targets containing the characters ‘%’ and ‘”’ is implementation-defined.

Make implementations like GNU Make allow additional characters and limited quoting but treat paths differently on different platforms.

[6]https://github.com/ninja-build/ninja/wiki/List-of-generators-producing-ninja-build-files
[7]Propagating dlb to several developers in the same organization by the means of a source code repository does not qualify as conveying in the sense of GPLv3.
[8]“Inheritance creates derivative works in the same way as traditional linking, and the LGPL permits this type of derivative work in the same way as it permits ordinary function calls.” (https://www.gnu.org/licenses/lgpl-java.en.html)
[9]

The pitfalls of Make builds make up entire howto sections. For example (from http://linuxdocs.org/HOWTOs/Kernel-HOWTO-8.html, written in 2001 — 7 years after Linux 1.0.0 had been released):

  • “‘make mrproper’ will do a more extensive ‘clean’ing. It is sometimes necessary; you may wish to do it at every patch.”
  • “If your new kernel does really weird things after a routine kernel upgrade, chances are you forgot to make clean before compiling the new kernel. Symptoms can be anything from your system outright crashing, strange I/O problems, to crummy performance. Make sure you do a make dep, too.”