Infinit's build system: Drake

In April 2016, we started our open-sourcing process by releasing a fork of Infinit's technical leader's (Quentin mefyl Hocquet) build system: Drake.

Drake is a language agnostic build-system, officially adopted and supported by Infinit since July 2012. At Infinit, we use Drake to build our products (C++ and Objective-C), our servers (Python), our websites (HTML, CSS and JS), to run tests and to package applications, etc.

We moved from CMake to Drake for several reasons, the main one being that Drake configuration files (known as a drakefile) are plain Python3 files; it's easy to read and benefits from the rich Python3 ecosystem, making most tasks trival.

Drake is a bottom-up designed build system. It relies on 3 simple parts: nodes (files: sources or targets), builders (that creates nodes) and dependencies (conditions for builders); builders transforms a list of source nodes into a list of target nodes when all its dependencies have been fulfilled.

The drakefile (comparable to both a CMakefile and the resulting Makefile) is a Python3 file listing nodes and defining builders, in order to create a dependency graph. Once defined, the dependency graph is explored and Drake creates target nodes by running the associated builder. Greenlets allow for parallelizing builders and a cache (based on mtime and sources, targets and builder hashing) prevents unnecessary operations.

Drake and C++

Even though Drake is language agnostic, it includes a module for C++ designed to ease integration, including:

  • Header exploration: Drake recursively explores includes and adds headers to the builder's sources; editing one, even a system header, will cause Drake to recompile.
  • Configuration composition: C++ configurations describe include paths, library paths, flags, etc and can be summed up together.
  • Tookits: Drake determines tools related to your compiler and detects source and target architectures, thereby, ease cross-compilation.

Here is a drakefile for a dummy hello_world.cc example:

import drake  
import drake.cxx

def configure(cxx_toolkit = None,  
              cxx_config = drake.cxx.Config(),
              path = '.'):

  # Create a default C++ toolkit if none is provided.
  # This will use the default system compiler.
  cxx_toolkit = drake.cxx.GccToolkit(cxx_toolkit)

  # List the sources required.
  sources = drake.nodes(
    'hello_world.cc',
  )

  # Declare a builder to create our 'hello_world' executable.
  hello_world = drake.cxx.Executable(
    # Path in the build directory where the executable will be
    # output to.
    path = drake.Path('%s/hello_world' % path),
    sources = sources, # Sources on which the executable depends.
    tk = cxx_toolkit,  # C++ toolkit to use.
    cfg = cxx_config,  # C++ compiler configuration to use.
  )

  # Create a 'build' rule.
  # Invoking this rule using //build will build all targets
  # added to the rule (and their dependencies).
  build = drake.Rule('build')

  # Add the 'hello_world' executable to the rule's targets.
  build << hello_world

Assuming the following hierarchy for the current working directory:

$ tree . --noreport
.
├── drakefile
└── hello_world.cc

Here is an example showing how easy Drake makes changing configuration. Assuming both the PYTHONPATH and PATH are correctly set:

$ drake . --cxx_toolkit="g++" --path=linux //build
<...>  
Compile hello_world.o  
Link linux/hello_world  
<...>  
$ ./linux/hello_world
Hello, world!  
$ drake . --cxx_toolkit="x86_64-w64-mingw32-g++" --path=windows //build
<...>  
Compile hello_world.o  
Link windows/hello_world.exe  
<...>  
$ wine64 windows/hello_world.exe
Hello, world!  

Easy!

Simple as a brick building game

Drake has been designed to be extensible! Because it relies on simple bricks, writing new modules is a simple task.

So, feel free to extend it: write modules for your favorite language, for documents, pictures, sound or video manipulation (why not!).

For example, here are the basics to add Go support to Drake. This can be used with a simple drakefile to build Docker machine:

import drake.go  
import drake.git

def configure(goconfig = drake.go.Configuration(),  
              gotoolkit = drake.go.Toolkit()):

  config = drake.go.Configuration(goconfig)
  toolkit = drake.go.Toolkit(gotoolkit)
  build = drake.Rule('build')
  machine = drake.go.Source('cmd/machine.go')
  git = drake.git.Git()

  config.add_ldflags(
    ['-X',
     '%s.GitCommit=%s' % (toolkit.list('version'),
                          git.rev_parse('HEAD', short = True)),
     '-w',
     '-s'
   ])
  build << drake.go.Executable(source = machine,
                               toolkit = toolkit,
                               config = config)

Get started with Infinit in less than 10 minutes! Test it!

Antony Méchin

Software engineer at Docker (from the former Infinit team)

Paris

Subscribe to Infinit's Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!