Basic knowledge of Python 3 is assumed before using Drake. This section stays above the hood to give a quick functional overview of drake. If anything seems magical, it really isn't, we are just using all the available shortcut to keep things as simple as possible. Further section dive deeper into the actual interfaces.
The simplest setup to build with drake is a drakefile
, which is a
python file, that contains a configure
function. This function
defines all the buildable things, the structure of you project. Here
is the empty drakefile
:
def configure():
pass
Invoking drake
in this directory will build everything - which is,
exactly nothing.
$ drake
drake: Entering directory ...
drake: Leaving directory ...
In drake, anything that can be built is called a node. This is often a file, the produce of a compilation for instance, but it can really be anything: a Docker image, a database, a remote upload ...
The simplest way to create a node is through the drake.node
function, passing it the path to the node.
import drake
def configure():
n = drake.node('file')
print('this is my node:', n)
Since this is plain Python, we can do pretty much anything, including
printing to see what it looks like. While drakefiles tend to be mostly
declarative, this can be useful. If we run drake
now:
$ drake
this is my node: file
drake: Entering directory ...
drake: Leaving directory ...
drake: error: no builder to make file
[1]
Drake first ran the configure, showing our print
statement. It then
went on to build everything, which is, our file
node. Unfortunately
we never specified how to do so, hence the "no builder to make file"
error.
In drake, a builder is an operations that takes zero or more nodes as
an input, and generate zero or more node as an output. Typically for
instance, a builder would compile some C++ source files into a binary
object file. This shed some light on the previous error message: we
asked to build file
, but there is no builder for it and Drake can't
fulfill our request.
To fix this, we can use one of the simplest builder there is: the
touch builder. It has no source and simply create its target as empty
files, similarly to the touch (1)
Unix utility. The drake.touch
convenience function is the fastest way to declare a touch builder:
import drake
def configure():
n = drake.node('file')
drake.touch(n)
print('this is my node:', n)
$ drake
print('this is my node:', n)
drake: Entering directory ...
Touch file
drake: Leaving directory ...
$ ls
drakefile file
$ cat file
$
Running drake now creates file
as expected. It is important to
understand that the configure
function itself did not touch the
file. While touch(n)
sounds imperative for concision, what it really
means is "the node n
can be created by touching it", not "touch n
right now". The print statement was left to illustrate this: we first
run the configure
entirely, as the output proves, and it's when we
actually build our project that drake will use the information
provided to actually run some commands. This is important because we
could, for instance, build some specific nodes instead of all of them,
and file
would thus not necessarily be touched. More on that pretty
soon.
Being a build system, Drake tracks dependencies to rerun builders only
if needed. Since the creation of file
is not dependent on anything,
and it is already created, re-running Drake will do nothing:
$ drake
drake: Entering directory ...
drake: Leaving directory ...
Another simple builder is the copy builder. The drake.copy
helper
let you instantiate it to copy file(s) in a directory:
import drake
def configure():
n = drake.node('file')
drake.touch(n)
drake.copy(n, 'subdir')
$ drake
drake: Entering directory ...
Copy subdir/file
drake: Leaving directory ...
$ cat subdir/file
$
If we remove the created files, we can see that drake runs all the actions in the correct order to recreate everything:
$ rm file subdir/file
$ drake
drake: Entering directory ...
Touch file
Copy subdir/file
drake: Leaving directory ...
So far we have only created files out of thin air, but real project
start with some files that get transformed. Drake calls files provided
to it "source nodes", and nodes produces by some builder "built
nodes". As the name indicate though, both are nodes, the only
difference being whether they have a builder or not. Let us now
provide file
as a source node with some content instead of touching
it.
import drake
def configure():
n = drake.node('file')
drake.copy(n, 'subdir')
In this setup, file
must be present or drake will complain once more
that it has no builder. If we now run drake:
$ ls
drakefile file
$ cat file
contents
$ drake
drake: Entering directory ...
Copy subdir/file
drake: Leaving directory ...
$ cat subdir/file
contents
$
As a rule of thumb, any (useful) node will always either have a builder, either exist before running drake. Now, it might seem a bit strange to insist on this difference, while the result is pretty much the same. It has however its importance as the upcoming points will show.
$ drake --clean
We've said so far that invoking drake
builds every node it knows
about. That's however just a default. Drake is in "build" mode by
default, but other exist. For instance, the "clean" mode is the
reciprocate, where any built file will be removed.
$ drake --build # this is the same as plain "drake"
drake: Entering directory ...
Copy subdir/file
drake: Leaving directory ...
$ drake --clean
drake: Entering directory ...
Remove subdir/file
drake: Leaving directory ...
$
We can see here a first difference between source nodes and built
nodes: file
was not removed as it is a source node - we wouldn't
want drake to erase our source code.
Source tree and build tree are a common concept, where built files go in a different folder -- the build tree -- that the source directories -- the source tree. This has several advantages, notably:
- Keeping the sources clean.
- Enable to have several build tree for the same source tree - eg. compiled with different options.
- Avoid mistakes where built files could overwrite source files.
While Drake does support "source = build" -- what we used so far -- it
is generally considered good practice to separate them. This can be
achieved with the --workdir
option:
$ drake --workdir _build
drake: Entering directory ...
Copy subdir/file
drake: Leaving directory ...
While the output of drake stays neutral no matter what the build directory is set to, the copy really was made in "_build":
$ ls
_build drakefile file
$ ls _build/subdir
file
$
This underline another key difference between source nodes and built nodes: the first one live in the source tree, while the latter live in the build tree.
So far we have always asked Drake to build "everything", it is however possible to instead pass the list of node we want to build. If we consider the following drakefile:
import drake
def configure():
n = drake.node('file')
drake.touch(n)
drake.copy(n, 'foo')
drake.copy(n, 'bar')
We can build only one those files specifically:
$ drake --workdir _build foo/file
drake: Entering directory ...
Touch file
Copy foo/file
drake: Leaving directory ...
$ ls _build/
file foo
$
Drake only built the node we asked for -- and its dependencies as
prerequisites of course. Note that the name of the nodes are
independent from the potential build tree, i.e. we built foo/file
,
not _build/foo/file
. If we ask Drake to build the rest of our
project it will now run only the missing operations to do so:
$ drake --workdir _build
drake: Entering directory ...
Copy bar/file
drake: Leaving directory ...
$
As previously hinted, most nodes are files, but they can really be
anything that can be built. The most basic example of this is the
Rule
node, which represents nothing and takes no action to
build. Its only use is to fit it with dependencies, so that building
it will build all of the dependencies. This is really about grouping
nodes under a common convenience shortcut as per the make
concept of
rules, e.g. make all
, make check
, ...
import drake
def configure():
# drake.nodes is like drake.node, but with multiple arguments
a, b, c = drake.nodes('a', 'b', 'c')
drake.touch(a)
drake.touch(b)
drake.touch(c)
drake.Rule('myrule', [a, c])
There's a trick to invoking the rule : any node not representing a
file in Drake has its name prefixed with "//". This is to ensure there
can never be a collision between such a "virtual" node and file on
disk (avoiding .PHONY
, if you catch my drift). That explains the
"//install" to install Drake in the first place !
$ drake --workdir _build //myrule
a b c
drake: Entering directory ...
Touch a
Touch c
drake: Leaving directory ...
$
Rules are often used to //install, //check the project, ...
Some builds being parametric, Drake support passing arguments to configure. These parameters are then reflected in your drakefile help message, and can be passed from the command line. A common usage is for instance to define an installation prefix.
import drake
def configure(prefix = '/usr/local'):
n = drake.node('f')
drake.Rule('install', drake.copy(n, prefix))
$ drake
drake: Entering directory ...
Copy /usr/local/f
copy of /usr/local/f: [Errno 13] Permission denied: '/usr/local/f'
drake: *** copy of /usr/local/f failed: [Errno 13] Permission denied: '/usr/local/f'
drake: Leaving directory ...
$ drake --prefix=/tmp
drake: Entering directory ...
Copy /tmp/f
drake: Leaving directory ...
$
We didn't build anything complex yet, but that's what drake is though : a framework, a library really, providing a model to describe builds and an engine to run them. Anything else is defined one layer above and not technically core Drake. In a sense, even the touch and copy builders we've seen are not core, and simply part of the "standard Drake library" of common builders.
Drake does however come with subpackages to build specific stuff, like
drake.cxx
to build C/C++ projects. If a library exist for you
purpose, you may proceed to its quickstart below, or you may refer to
the drake API guide if you want to implement your own specific
commands.