3. Language reference¶
3.1. Statements, blocks, literals etc.¶
Statements in Bakefile are separated by semicolon (;
) and code blocks are
marked up in with {
and }
, as in C. See an example:
toolsets = gnu vs2010;
program hello {
sources { hello.cpp }
}
In particular, expressions may span multiple lines without the need to escape newlines or enclose the expression in parenthesis:
os_files = foo.cpp
bar.cpp
;
3.2. Values, types and literals¶
Similarly to the make
syntax, quotes around literals are optional –
anything not a keyword or special character or otherwise specially marked is a
literal; specifically, a string literal.
Quoting is only needed when the literal contains whitespace or special
characters such as =
or quotes. Quoted strings are enclosed between "
(double quote) or '
(single quote) characters and may contain any
characters except for the quotes. Additionally, backslash (\
) can be used
inside quoted strings to escape any character. [1]
The two kinds of quoting differ:
- Double-quoted strings are interpolated. That is, variable references using
$(...)
(see below) are recognized and evaluated. If you want to use$
as a literal, you must escape it (\$
).- Single-quoted strings are literal,
$
doesn’t have any special meaning and is treated as any other character.
Values in Bakefile are typed: properties have types associated with them and only values that are valid for that type can be assigned to them. The language isn’t strongly-typed, though: conversions are performed whenever needed and possible, variables are untyped by default. Type checking primarily shows up when validating values assigned to properties.
The basic types are:
- Boolean properties can be assigned the result of a boolean expression or one of the
true
orfalse
literals.- Strings. Enough said.
- Lists are items delimited with whitespace. Lists are typed and the items must all be of the same type. In the reference documentation, list types are described as “list of string”, “list of path” etc.
- Paths are file or directory paths and are described in more detail in the next section.
- IDs are identifiers of targets.
- Enums are used for properties where only a few possible values exist; the property cannot be set to anything other than one of the listed strings.
AnyType
is the pseudo-type used for untyped variables or expressions with undetermined type.
3.3. Paths¶
File paths is a type that deserves more explanation. They are arguably the most important element in makefiles and project files both and any incorrectness in them would cause breakage.
All paths in bakefiles must be written using a notation similar to the Unix
one, using /
as the separator, and are always relative. By default, if you
don’t say otherwise and write the path as a normal Unix path (e.g.
src/main.cpp
), it’s relative to the source directory (or srcdir for
short). Srcdir is the implicitly assumed directory for the input files
specified using relative paths. By default, it is the directory containing the
bakefile itself but it can be changed as described below. Note that this may
be – and often is – different from the location where the generated output
files are written to.
This is usually the most convenient choice, but it’s sometimes not sufficient.
For such situations, Bakefile has the ability to anchor paths under a
different root. This is done by adding a prefix of the form of @<anchor>/
in front of the path. The following anchors are recognized:
@srcdir
, as described above.@top_srcdir
is the top level source directory, i.e. srcdir of the top-most bakefile of the project. This is only different from @srcdir if this bakefile was included from another one as a submodule.@builddir
is the directory where build files of the current target are placed. Note that this is not where the generated makefiles or projects go either. It’s often a dedicated directory just for the build artifacts and typically depends on make-time configuration. Visual Studio, for example, puts build files intoDebug/
andRelease/
subdirectories depending on the configuration selected.@builddir
points to these directories.
Here are some examples showing common uses for the anchors:
sources {
hello.cpp; // relative to srcdir
@builddir/generated_file.c;
}
includedirs += @top_srcdir/include;
3.3.1. Changing srcdir¶
As mentioned above, @srcdir
can be changed if its default value is
inconvenient, as, for example, is the case when the bakefile itself is in a
subdirectory of the source tree.
Take this for an example:
// build/bakefiles/foo.bkl
library foo {
includedirs += ../../include;
sources {
../../src/foo.cpp
../../src/bar.cpp
}
}
This can be made much nicer using scrdir
:
// build/bakefiles/foo.bkl
srcdir ../..;
library foo {
includedirs += include;
sources {
src/foo.cpp
src/bar.cpp
}
}
The srcdir
statement takes one argument, path to the new srcdir (relative
to the location of the bakefile). It affects all @srcdir
-anchored paths,
including implicitly anchored ones, i.e. those without any explicit anchor, in
the module (but not its submodules). Notably, (default) paths for generated
files are also affected, because these too are relative to @srcdir
.
Notice that because it affects the interpretation of all path expressions in
the file, it can only be used before any assignments, target definitions etc.
The only thing that can precede it is requires
.
3.4. Variables and properties¶
Bakefile allows you to set arbitrary variables on any part of the model. Additionally, there are properties, which are pre-defined variables with a set meaning. Syntactically, there’s no difference between the two. There’s semantical difference in that the properties are usually typed and only values compatible with their type can be assigned to them. For example, you cannot assign arbitrary string to a path property or overwrite a read-only property.
3.4.1. Setting variables¶
Variables don’t need to be declared; they are defined on first assignment. Assignment to variables is done in the usual way:
variable = value;
// Lists can be appended to, too:
main_sources = foo.cpp;
main_sources += bar.cpp third.cpp;
Occasionally, it is useful to set variables on other objects, not just in the current scope. For example, you may want to set per-file compilation flags, add custom build step for a particular source file or even modify a global variable. Bakefile uses operator :: for this purpose, with semantics reminiscent of C++: any number of scopes delimited by :: may precede the variable name, with leading :: indicating global (i.e. current module) scope. Here’s a simple example:
3.4.2. Referencing variables¶
Because literals aren’t quoted, variables are referenced using the make-like
$(<varname>)
syntax:
platform = windows;
sources { os/$(platform).cpp }
A shorthand form, where the brackets are omitted, is also allowed when such use is unambiguous: [2]
if ( $toolset == gnu ) { ... }
Note that the substitution isn’t done immediately. Instead, the reference is included in the object model of the bakefiles and is dereferenced at a later stage, when generating makefile and project files. Sometimes, they are kept in the generated files too.
This has two practical consequences:
It is possible to reference variables that are defined later in the bakefile without getting errors.
Definitions cannot be recursive, a variable must not reference itself. You cannot write this:
defines = $(defines) SOME_MOREUse operator
+=
instead:defines += SOME_MORE
3.5. Targets¶
Target definition consists of three things: the type of the target (an executable, a library etc.), its ID (the name, which usually corresponds to built file’s name, but doesn’t have to) and detailed specification of its properties:
type id {
property = value;
property = value;
...sources specification...
...more content...
}
(It’s a bit more complicated than that, the content may contain conditional statements too, but that’s the overall structure.)
3.5.1. Sources files¶
Source files are added to the target using the sources
keyword, followed by
the list of source files inside curly brackets. Note the sources list may
contain any valid expression; in particular, references to variables are
permitted.
It’s possible to have multiple sources
statements in the same target.
Another use of sources
appends the files to the list of sources, it doesn’t
overwrite it; the effect is the same as that of operator +=
.
See an example:
program hello {
sources {
hello.cpp
utils.cpp
}
// add some more sources later:
sources { $(EXTRA_SOURCES) }
}
3.5.2. Headers¶
Syntax for headers specification is identical to the one used for source files,
except that the headers
keyword is used instead. The difference between
sources and headers is that the latter may be used outside of the target (e.g.
a library installs headers that are then used by users of the library).
3.6. Templates¶
It is often useful to share common settings or even code among multiple
targets. This can be handled, to some degree, by setting properties such as
includedirs
globally, but more flexibility is often needed.
Bakefile provides a convenient way of doing just that: templates. A template is a named block of code that is applied and evaluated before target’s own body. In a way, it’s similar to C++ inheritance: targets correspond to derived classes and templates would be abstract base classes in this analogy.
Templates can be derived from another template; both targets and templates can be based on more than one template. They are applied in the order they are specified in, with base templates first and derived ones after them. Each template in the inheritance chain is applied exactly once, i.e. if a target uses the same template two or more times, its successive appearances are simply ignored.
Templates may contain any code that is valid inside target definition and may reference any variables defined in the target.
The syntax is similar to C++ inheritance syntax:
template common_stuff {
defines += BUILDING;
}
template with_logging : common_stuff {
defines += "LOGGING_ID=\"$(id)\"";
libs += logging;
}
program hello : with_logging {
sources {
hello.cpp
}
}
Or equivalently:
template common_stuff {
defines += BUILDING;
}
template with_logging {
defines += "LOGGING_ID=\"$(id)\"";
libs += logging;
}
program hello : common_stuff, with_logging {
sources {
hello.cpp
}
}
3.7. Conditional statements¶
Any part of a bakefile may be enclosed in a conditional if
statement.
The syntax is similar to C/C++’s one:
defines = BUILD;
if ( $(toolset) == gnu )
defines += LINUX;
In this example, the defines
list will contain two items, [BUILD,
LINUX]
when generating makefiles for the gnu
toolset and only one item,
BUILD
, for other toolsets.
The condition doesn’t have to be constant, it may reference e.g. options, where
the value isn’t known until make-time; Bakefile will correctly translate them into
generated code. [3]
A long form with curly brackets is accepted as well; unlike the short form, this one can contain more than one statement:
if ( $(toolset) == gnu ) {
defines += LINUX;
sources { os/linux.cpp }
}
Conditional statements may be nested, too:
if ( $(build_tests) ) {
program test {
sources { main.cpp }
if ( $(toolset) == gnu ) {
defines += LINUX;
sources { os/linux.cpp }
}
}
}
The expression that specifies the condition uses C-style boolean operators: &&
for and, ||
for or, !
for not and ==
and !=
for equality
and inequality tests respectively.
3.8. Build configurations¶
A feature common to many IDEs is support for different build configurations,
i.e. for building the same project using different compilation options.
Bakefile generates the two standard “Debug” and “Release” configurations by
default for the toolsets that usually use them (currently “vs*”) and also
supports the use of configurations with the makefile-based toolsets by
allowing to specify config=NameOfConfig
on make command line, e.g.
$ make config=Debug
# ... files are compiled with "-g" option and without optimizations ...
Notice that configuration names shouldn’t be case-sensitive as
config=debug
is handled in the same way as config=Debug
in make-based
toolsets.
In addition to these two standard configurations, it is also possible to define your own custom configurations, which is especially useful for the project files which can’t be customized as easily as the makefiles at build time.
Here is a step by step guide to doing this. First, you need to define the new configuration. This is done by using a configuration declaration in the global scope, i.e. outside of any target, e.g.:
configuration ExtraDebug : Debug {
}
The syntax for configuration definition is reminiscent of C++ class definition and, as could be expected, the identifier after the colon is the name of the base configuration. The new configuration inherits the variables defined in its base configuration.
Notice that all custom configurations must derive from another existing one, which can be either a standard “Debug” or “Release” configuration or a previously defined another custom configuration.
Defining a configuration doesn’t do anything on its own, it also needs to be
used by at least some targets. To do it, the custom configuration name must be
listed in an assignment to the special configurations
variable:
configurations = Debug ExtraDebug Release;
This statement can appear either in the global scope, like above, in which case it affects all the targets, or inside one or more targets, in which case the specified configuration is only used for these targets. So if you only wanted to enable extra debugging for “hello” executable you could do
program hello {
configurations = Debug ExtraDebug Release;
}
However even if the configuration is present in the generated project files after doing all this, it is still not very useful as no custom options are defined for it. To change this, you will usually also want to set some project options conditionally depending on the configuration being used, e.g.:
program hello {
if ( $(config) == ExtraDebug ) {
defines += EXTRA_DEBUG;
}
}
config
is a special variable automatically set by bakefile to the name of
the current configuration and may be used in conditional expressions as any
other variable.
For simple cases like the above, testing config
explicitly is usually all
you need but in more complex situations it might be preferable to define some
variables inside the configuration definition and then test these variables
instead. Here is a complete example doing the same thing as the above snippets
using this approach:
configuration ExtraDebug : Debug {
extra_debug = true;
}
configurations = Debug ExtraDebug Release;
program hello {
if ( $(extra_debug) ) {
defines += EXTRA_DEBUG;
}
}
Note
As mentioned above, it is often unnecessary (although still possible) to
define configurations for the makefile-based toolsets as it’s always
possible to just write make CPPFLAGS=-DEXTRA_DEBUG
instead of using an
“ExtraDebug” configuration from the example above with them. If you want to
avoid such unnnecessary configurations in your makefiles, you could define
them only conditionally, for example:
toolsets = gnu vs2010;
if ( $toolset == vs2010 && $config == ExtraDebug )
defines += EXTRA_DEBUG;
would work as before in Visual Studio but would generate a simpler makefile.
3.9. Build settings¶
Sometimes, configurability provided by configurations is not enough and more flexible settings are required; e.g. configurable paths to 3rdparty libraries, tools and so on. Bakefile handles this with settings: variable-like constructs that are, unlike Bakefile variables, preserved in the generated output and can be modified by the user at make-time.
Settings are part of the object model and as such have a name and additional properties that affect their behavior. Defining a setting is similar to defining a target:
setting JDK_HOME { help = "Path to the JDK"; default = /opt/jdk; }
Notice that the setting object has some properties. You will almost always want to set the two shown in the above example. help is used to explain the setting to the user and default provides the default value to use if the user of the makefile doesn’t specify anything else; both are optional. See Setting properties for the full list.
When you need to reference a setting, use the same syntax as when referencing variables:
includedirs += $(JDK_HOME)/include;
In fact, settings also act as variables defined at the highest (project) level. This means that they can be assigned to as well and some nice tricks are easily done:
setting LIBFOO_PATH { help = "Path to the Foo library"; default = /opt/libfoo; } // On Windows, just use our own copy: if ( $toolset == vs2010 ) LIBFOO_PATH = @top_srcdir/3rdparty/libfoo;
This removes the user setting for toolsets that don’t need it. Another handy use is to import some common code or use a submodule with configurable settings and just hard-code their values when you don’t need the flexibility.
Note
Settings are currently only fully supported by makefiles, they are always replaced with their default values in the project files.
3.10. Submodules¶
A bakefile file – a module – can include other modules as its children.
The submodule
keyword is used for that:
submodule samples/hello/hello.bkl;
submodule samples/advanced/adv.bkl;
They are useful for organizing larger projects into more manageable chunks, similarly to how makefiles are used with recursive make. The submodules get their own makefiles (automatically invoked from the parent module’s makefile) and a separate Visual Studio solution file is created for them by default as well. Typical uses include putting examples or tests into their own modules.
Submodules may only be included at the top level and cannot be included
conditionally (i.e. inside an if
statement).
3.11. Importing other files¶
There’s one more way to organize source bakefiles in addition to submodules:
direct import of another file’s content. The syntax is similar to submodules
one, using the import
keyword:
// define variables, templates etc:
import common-defs.bkl;
program myapp { ... }
Import doesn’t change the layout of output files, unlike submodule
.
Instead, it directly includes the content of the referenced file at the point
of import. Think of it as a variation on C’s #include
.
Imports help with organizing large bakefiles into more manageable files. You could, for example, put commonly used variables or templates, files lists etc. into their own reusable files.
Notice that there are some important differences to #include
:
- A file is only imported once in the current scope, further imports are ignored. Specifically:
- Second import of
foo.bkl
from the same module is ignored.- Import of
foo.bkl
from a submodule is ignored if it was already imported into its parent (or any of its ancestors).- If two sibling submodules both import
foo.bkl
and none of their ancestors does, then the file is imported into both. That’s because their local scopes are independent of each other, so it isn’t regarded as duplicate import.- An imported file may contain templates or configurations definitions and be included repeatedly (in the (1c) case above). This would normally result in errors, but Bakefile recognizes imported duplicates as identical and handles them gracefully.
The import
keyword can only be included at the top level and cannot be
done conditionally (i.e. inside an if
statement).
3.12. Version checking¶
If a bakefile depends on features (or even syntax) not available in older
versions, it is possible to declare this dependency using the requires
keyword.
// Feature XYZ was added in Bakefile 1.1:
requires 1.1;
This statement causes fatal error if Bakefile version is older than the specified one.
3.13. Loading plugins¶
Standard Bakefile plugins are loaded automatically. But sometimes a custom
plugin needed only for a specific project is needed and such plugins must be
loaded explicitly, using the plugin
keyword:
plugin my_compiler.py;
Its argument is a path to a valid Python file that will be loaded into the
bkl.plugins
module. You can also use full name of the module to make it
clear the file is a Bakefile plugin:
plugin bkl.plugins.my_compiler.py;
See the Writing Bakefile plugins chapter for more information about plugins.
3.14. Comments¶
Bakefile uses C-style comments, in both the single-line and multi-line variants. Single-line comments look like this:
// we only generate code for GNU format for now
toolsets = gnu;
Multi-line comments can span several lines:
/*
We only generate code for GNU format for now.
This will change later, when we add Visual C++ support.
*/
toolsets = gnu;
They can also be included in an expression:
program hello {
sources { hello.c /*main() impl*/ lib.c }
}
[1] | A string literal containing quotes can therefore be written as, say,
"VERSION=\"1.2\"" ; backslashes must be escaped as double backslashes
("\\" ). |
[2] | A typical example of ambiguous use is in a concatenation. You can’t
write $toolset.cpp because . is a valid part of a literal; it
must be written as $(toolset).cpp so that it’s clear which part is a
variable name and which is a literal appended to the reference. For
similar reasons, the shorthand form cannot be used in double-quoted strings. |
[3] | Although the syntax imposes few limits, it’s not always possible to generate makefiles or projects with complicated conditional content even though the syntax supports it. In that case, Bakefile will exit with an explanatory error message. |