dlb — explicit is better than implicit

dlb is a Pythonic build tool that does not try to mimic Make but to leverage the power of object-oriented languages. It is free software.

dlb is inspired by djb’s redo but takes a more dynamic approach.

A build system generates files in a filesystem, mostly with the help of external tools. Its most important tasks are:

  • collect and transform filesystem paths
  • find and execute tool executables (e.g. compilers) with context-dependent command line arguments
  • generate files (e.g. some program source files or configuration files)
  • make the build fast by omitting unnecessary redos

These are the strengths of dlb. dlb performs its tasks in a precisely specified way and with emphasis on correctness, reliability, and robustness.

dlb does not try to hide its Python personality. dlb build scripts are just Python scripts importing the dlb module and using its functionality. There is no magic code before or after the script. Since dlb build scripts are Python scripts, you can easily analyse, run, or debug them in your favorite Python IDE.

Tools (e.g. compiler/linker toolchains) are represented as classes. Adapting tools means adapting classes by subclassing.

Example:

import dlb.di
import dlb.fs
import dlb.ex
import dlb_contrib.gcc

class Path(dlb.fs.PosixPath, dlb.fs.WindowsPath, dlb.fs.NoSpacePath): pass    # (a)

class Compiler(dlb_contrib.gcc.CplusplusCompilerGcc): DIALECT = 'c++14'       # (b)
class Linker(dlb_contrib.gcc.CplusplusLinkerGcc): pass

with dlb.ex.Context():                                                        # (c)
    output_directory = Path('build/out/')

    object_files = [                                                          # (d)
       Compiler(
           source_files=[p],
           object_files=[output_directory / p.with_appended_suffix('.o')]
       ).start().object_files[0]
       for p in Path('src/X/').iterdir(name_filter=r'.+\.cpp', is_dir=False)
    ]

    application_file = Linker(
        object_and_archive_files=object_files,
        linked_file=output_directory / 'example'                              # (e)
    ).start().linked_file

dlb.di.inform(f'size: {application_file.native.raw.stat().st_size} B')        # (f)

Explanation:

  1. Restrict paths to ones without spaces, usable on Windows and POSIX systems. The attempt to construct such a Path object for a path violating these restrictions leads to an exception (helps to enforce portability).

  2. Configure some tools of the toolchain by subclassing and redefining attributes.

  3. Create a context. A context determines how subprocesses (e.g. of the compiler) are executed.

  4. Compile all .cpp files in directory src/X/ and its subdirectories into object files.

    Compiling also means: automatically find all included files and remember them as input dependencies for future runs of dlb. start() executes the compiler only when a redo is necessary (e.g. because one of its include files has changed). Otherwise is does almost nothing.

  5. Link these object files into an executable file.

  6. Output the size of the executable file.

Indices and tables