2. Tutorial

2.1. Hello, Bakefile

After complaining about the lack of examples of how “Hello, World” program can be built in a portable way in the introduction, we’d be amiss to not provide an example of doing this here. So, assuming the text of the program is in the file hello.c, here is the corresponding hello.bkl bakefile to build it:

toolsets = gnu vs2010;
program hello {
    sources { hello.cpp } // could also have been "hello.c"
}

To produce something interesting from it we need to run bakefile simply passing it this file name as argument:

$ bkl hello.bkl

or maybe:

C:\> bkl hello.bkl

In the grand tradition of Unix tools, Bakefile doesn’t produce any output after running successfully. If this is too closemouthed for your taste, you can try adding -v option which will indicate all the files read and, more importantly, written by the tool. Using this option or just examining the directory you ran Bakefile in you can see that it created several new files.

In our case, they will be GNUmakefile for “gnu” toolset (the one using standard GNU development tools) or hello.sln and its supporting files for “vs2010” toolset (Microsoft Visual Studio 2010) one. These files then can be used to build the project in the usual way, i.e. by running “make” or opening the file in the IDE.

Please check that you can run Bakefile and generate a working make or project file appropriate for your platform (“gnu” for Unix systems including OS X, “vs2010” for Microsoft Windows ones).

2.2. Example Explained

Bakefile input language is C-like, with braces and semicolons playing their usual roles. Additionally, both C and C++ style comments can be used. It however also borrows some simple and uncontroversial elements [*] of make syntax. Notably, there is no need to quote literal strings and, because of this, Bakefile variables – such as toolsets above – need to be explicitly dereferenced using make-file $(toolsets) expression when needed.

Knowing this we can see that the first line of the hello bakefile above simply sets a variable toolsets to the list of two strings. This variable is special for Bakefile – and hence is called “property” rather than a simple “variable” – and indicates which make or project files should be generated when it is ran. It must be present in all bakefiles as no output would be created without it.

The next block – program hello { ... } – defines an executable target with the name “hello”. All the declarations until the closing bracket affect this target only. In this simple example the only thing that we have here is the definition of the sources which should be compiled to build the target. Source files can be of any type but currently Bakefile only handles C (extension .c) and C++ (extension .cpp, .cxx or .C) files automatically and custom compilation rules need to be defined for the other ones. In any real project there are going to be more than one source file, of course. In this case they can just be listed all together inside the same block like this:

sources {
    foo.cpp subdir/bar.cpp
    // Not necessarily on the same line
    baz.cpp
}

or they can be separated in several blocks:

sources { foo.cpp }
sources { subdir/bar.cpp }
sources { baz.cpp }

The two fragments have exactly the same meaning.

2.3. Customizing Your Project

2.3.1. Compilation And Linking Options

A realistic project needs to specify not only the list of files to compile but also the options to use for compiling them. The most common options needed for C/C++ programs are the include paths allowing to find the header file used and the preprocessor definitions and Bakefile provides convenient way to set both of them by assigning to defines and includedirs properties inside a target:

program hello {
    defines = FOO "DEBUG=1";    // Quotes are needed if value is given
    includedirs = ../include;
    ...
}

These properties can be set more than once but each subsequent assignment overrides the previous value which is not particular useful. It can be more helpful to append another value to the property instead, for example:

program hello {
    defines = FOO;
    ...
    defines += BAR;
    ...
    defines += "VERSION=17";
    defines += "VERSION_STR=\"v17\"";
}

will define “FOO” and “BAR” symbols (without value) as well as “VERSION” with the value of 17 and “VERSION_STR” with the value as a C string during compilation. This is still not very exciting as all these values could have been set at once, but the possibility of conditional assignment is more interesting:

program hello {
    if ( $(toolset) == gnu )
        defines += LINUX;
    if ( $(toolset) == vs2010 )
        defines += MSW;
}

would define LINUX only for makefile-based build and MSW for the project files.

While defines and includedirs are usually enough to cover 90% of your needs, sometimes some other compiler options may need to be specified and the compiler-options property can be used for this: you can simply any options you want to be passed to C or C++ compiler into it. If you need to be more precise and only use some options with a particular compiler in a project using more than one of them, you can also use c-compiler-options or cxx-compiler-options.

One aspect of using these properties is that different compilers use different format for their options, so it’s usually impossible to use the same value for all of them. Moreover, sometimes you may actually need to use custom options for a single toolset only. This can be done by explicitly testing for the toolset being used. For example, to use C++ 11 features with GNU compiler you could do

if ( $(toolset) == gnu )
    cxx-compiler-options = "-std=c++11"

Similarly, any non trivial project usually links with some external libraries. To specify these libraries, you need to assign to the libs property and also may need to set libdirs if these libraries are not present in the standard search path:

program hello {
    libdirs = ../3rdparty/somelib/lib;
    libs = somelib;
}

Notice that you only need to do the latter if the libraries are not built as part of the same project, otherwise you should simply list them as dependencies as shown in the next section.

And if you need to use any other linker option you can specify it using link-options property. As with compiler options, you would normally test for the toolkit before doing it as linker options are toolset-specific.

2.3.2. Multiple Modules

Larger projects typically consist in more than just a single executable but of several of them and some libraries. The same bakefile can contain definitions of all of them:

library network {
    sources { ... }
}

library gui {
    sources { ... }
}

program main {
    deps = network gui;
    ...
}

In this case, the libraries will be built before the main executable and will be linked with it. Bakefile is smart about linking and if a library has dependencies of its own, these will be linked in as well.

Alternatively, you can define each library or executable in its own bakefile. This is especially convenient if each of them is built in a separate directory. In this case you can use submodule keyword to include the sub-bakefiles.

2.3.3. Assorted Other Options

Under Windows, console and GUI programs are compiled differently. By default, Bakefile builds console executables. You can change this by setting the win32-subsystem property to windows.

Another Windows-specific peculiarity is that standard C run-time library headers as well as Platform SDK headers are compiled differently depending on whether _UNICODE and UNICODE macros, respectively, are defined or not. By default, Bakefile does define these macros but you can set win32-unicode target property to false to prevent it from doing it.

Finally, Windows projects generated by default only contain configurations for Win32 platform. To generate 64 bit configurations as well, you need to explicitly request them by defining the architectures to build for:

archs = x86 x86_64;

The name of 64 bit architecture is x86_64 and not x64 which is usually used under Windows because the architectures are also used under other platforms, notably OS X where universal binaries containing both 32 bit and 64 bit binaries would be built with the above value of archs.

[*]So no meaningful tabulations or backslashes for line continuation.

2.4. Advanced Stuff

2.4.1. Generated Source Files

Bakefile supports custom compilation steps; this can be used both for files generated with some script and for compilation of unsupported file types.

Compiling a custom file is as simple as setting the compile-commands property on it to the command (or several commands) to compile the file, outputs property with the list of created files and optionally filling in additional dependencies:

toolsets = gnu vs2010;
program hello {
    sources { hello.cpp mygen.cpp mygen.desc }

    mygen.desc::compile-commands = "tools/generator.py -o %(out) %(in)";
    mygen.desc::outputs = mygen.cpp;
    // add dependency on the generator script:
    mygen.desc::dependencies = tools/generator.py;
}

Notice that the generated files listed in outputs must be included in sources or headers section as well.

Additionally, any number of other dependency files can be added to the dependencies list. The command uses two placeholders, %(in) and %(out), that are replaced with the name of the source file (mygen.desc in our example) and outputs respectively; both placeholders are optional. If there are multiple output files, %(out0), %(out1), ... placeholders can be used to access individual items in the list.

Perhaps a better would be to demonstrate how to use this to generate a grammar parser with Bison:

sources {
    main.cpp
    parser.ypp              // Bison grammar file
    parser.cpp parser.hpp   // generated C++ parser
}

parser.ypp::compile-commands = "bison -o %(out0) %(in)"
parser.ypp::outputs = parser.cpp parser.hpp;