Skip to content

9. Build System Basics

In this lesson, we want to learn about the basics of the build2 build system. Everything from this lesson and even more can also be read in the build2 build system manual.

Starting with the Simple Project

It is understandable that always starting with a bare-bones project becomes boring over time. But one has to consider the benefits of a completely understood procedure when adding external tools to the workflow. The typical hello-world program is not that exciting but this gives us the opportunity to concentrate on the scripting of our build system.

$ pwd
/home/lyrahgames/projects/cpp-course
$ ls
01-hello  02-input  03-fibonacci  04-files  05-rational  06-graphics
$ mkdir 07-build
$ ls
01-hello  02-input  03-fibonacci  04-files  05-rational  06-graphics  07-build
$ cd 07-build
$ pwd
/home/lyrahgames/projects/cpp-course/07-build
$ touch main.cpp
$ ls
main.cpp

So, write the code which prints "Hello, World!".

// main.cpp
#include <iostream>
int main(){
  std::cout << "Hello, World!\n";
}

Typically, we would compile and run the given code by using the command lines below.

$ g++ -o hello main.cpp
$ ./hello
Hello, World!

Now, we would like to use the build system. At first, create a file, called buildfile.

$ touch buildfile
$ ls
buildfile  main.cpp

Open the buildfile and add the lines written below.

using cxx
exe{hello}: cxx{main.cpp}

The line using cxx in the buildfile tells build2 that we would like to compile C++ code and that it should use its C++ module to be able to do this. The second line exe{hello}: cxx{main.cpp} defines a so-called target exe{hello} to the left of the colons with its prerequisites cxx{main.cpp} to the right of the colons. Targets and prerequisites are defined by first using a type, like exe for executable or cxx for C++ source files, and then appending the file name in curly braces. The build2 build system then knows: To generate the executable file hello, it needs to compile the C++ source file main.cpp by using a C++ compiler and then link it. Comments can be started by using #. The beginning of a newline ends a comment.

At first, the extra effort of writing a buildfile seems to be unnecessary complicated. But please note that the buildfile expresses the same information as the command-line approach.

At this point, the build2 build system is able to find the buildfile. So your code should be compiled successfully when calling b.

$ b
c++ cxx{main}
ld exe{hello}
$ ./hello
Hello, World!

b is the command in the shell which calls the build2 build system. The build2 toolchain mainly consists of three tools, the build system b, also known as build2, the package manager bpkg, and the dependency manager bdep. For this lesson, we will only need b.

Running the build system again without changing anything will not recompile the code. build2 automatically checks if the prerequisites of any target have changed over time.

$ b
info: dir{./} is up to date

To force a recompile, we first clean up all files generated by build2 by calling b clean. Then we can run b again. For the advanced user, this can also be achieved in one step by calling b {clean update}.

$ ls
buildfile  main.cpp  main.o  main.d  main.o.d  hello
$ b clean
rm exe{hello}
rm obje{main}
$ ls
buildfile  main.cpp
$ b
c++ cxx{main}
ld exe{hello}

Adding a Header File

We will now add a header file hello.hpp to our project which will provide the inline definition of the function void say_hello(). Please remember, every header file should start with #pragma once and the function has to be marked by inline because its definition is positioned in the header file.

// hello.hpp
#pragma once
#include <iostream>

inline void say_hello(){
  std::cout << "Hello, World!\n";
}

Of course, the new routine should be called in the main.cpp file.

// main.cpp
#include <iostream>
#include "hello.hpp"

int main(){
  std::cout << "Hello, World!\n";
  say_hello();
}

Compiling the program by using the command line does not change in comparison to the last attempt. Every header file is processed by the preprocessor and therefore not given as an actual argument to the compiler.

$ g++ -o hello main.cpp
$ ./hello
Hello, World!
Hello, World!

This also is true for the buildfile. But build2 offers us a more consistent approach. We can directly state the dependence on a specific header file with the type hxx like we would do it for every other C++ source file with the type cxx. Besides the improved consistency, we will also gain other advantages and features which will be discussed later. The current buildfile is now slightly changed

using cxx
exe{hello}: cxx{main.cpp} hxx{hello.hpp}

Again, compiling and running the program has not changed and keeps simple.

$ b
c++ cxx{main}
ld exe{hello}
$ ./hello
Hello, World!
Hello, World!

Using Standard Include

In the last projects, we always wanted to include other header files by using tags. So, we tweak the content of our main.cpp to reflect that decision.

// main.cpp
#include <iostream>
#include <hello.hpp>

int main(){
  std::cout << "Hello, World!\n";
  say_hello();
}

For this small change, the command line must be altered to able to compile the program.

$ g++ -o hello main.cpp -I.
$ ./hello
Hello, World!
Hello, World!

We also have to adjust the buildfile. Think about processing the given flag -I.. The preprocessor makes sure to add the given directory to its standard include paths. Hence, in build2, we call this a C++ preprocessor option and as a result, we use the cxx.poptions variable to set the according flag. In the command line above, the given path is a relative directory pointing to the current folder. The build2 build system does not use relative paths but gives us a variable src_base which resolves to the absolute path of the current folder when evaluated by $. To prepend the flag to the variable, we write =+ .

using cxx
exe{hello}: cxx{main.cpp} hxx{hello.hpp}
cxx.poptions =+ "-I$src_base"

Compiling and running the code has not changed at all.

$ b
c++ cxx{main}
ld exe{hello}
$ ./hello
Hello, World!
Hello, World!

Adding another Source File

Let us add another function void say_bye() that will be defined in a separate source file hello.cpp. To be able to call this function from main.cpp, we first declare it inside our header file. This time an inline is not needed.

// hello.hpp
#pragma once
#include <iostream>

inline void say_hello(){
  std::cout << "Hello, World!\n";
}

void say_bye();

The function must be defined in the according source file. Remember that an according source file should include the appropriate header file, even if we are only dealing with functions.

// hello.cpp
#include <hello.hpp>

void say_bye(){
    std::cout << "Goodbye, World!\n";
}

We want to use the new function in the main.cpp file.

// main.cpp
#include <iostream>
#include <hello.hpp>

int main(){
  std::cout << "Hello, World!\n";
  say_hello();
  say_bye();
}

The command-line compilation has to add the new source file to the call.

$ g++ -o hello main.cpp hello.cpp -I.

For the buildfile, it looks similar. Another simple cxx{hello.cpp} as a prerequisite to exe{hello} will be added.

using cxx
exe{hello}: cxx{main.cpp} hxx{hello.hpp} cxx{hello.cpp}
cxx.poptions =+ "-I$src_base"

Running and compiling still keeps the same.

$ b
c++ cxx{main}
c++ cxx{hello}
ld exe{hello}
$ ./hello
Hello, World!
Hello, World!
Goodbye, World!

Tidying Up

At this point, we have written more code for our build system than we would have written in the terminal or shell to manually compile the code. The only advantage seems to be that we no longer have to remember the complicated commands for the manual compilation process. To further optimize the usage of the build system, let us tidy things up.

First of all, all C++ header and source files use the same file extensions by our convention. If we give build2 this information, we are allowed to omit all file extensions in the buildfile.

cxx{*}: extension = cpp
hxx{*}: extension = hpp

Now, take a look at the target definition in the code.

exe{hello}: cxx{main} hxx{hello} cxx{hello}

It still seems to be complicated. build2 offers us several alternative tweaks to optimize the writing of a buildfile. We can group multiple files of the same type by putting them in the same curly braces.

exe{hello}: cxx{main hello} hxx{hello}

If we have files with the same name of multiple types, we can even group the types of these files in some additional curly braces.

exe{hello}: cxx{main} {hxx cxx}{hello}

Furthermore, build2 allows the matching of arbitrary files in the current directory with a specific type by using the wildcard *.

exe{hello}: hxx{*} cxx{*}

Using the grouping of types, an even shorter version evolves.

exe{hello}: {hxx cxx}{*}

Moreover, this version catches new files in the source tree without explicitly adding them to the buildfile. This is a tremendous advantage in comparison to the manual compilation process.

Last but not least, for the future, we would always like to use the latest C++ standard the current compiler implementation is able to provide. In general, this is not the default. We want to learn the modern approach of programming C++ and not stick to the old methods and workarounds.

cxx.std = latest

The actual buildfile then looks like this.

# Set the C++ standard to latest version.
cxx.std = latest
# Tell the build system you want to compile C++ code.
using cxx

# Define the standard header and source file extensions of your project.
hxx{*}: extension = hpp
cxx{*}: extension = cpp

# Define the executable 'hello' which depends on all
# header and source files in the current directory.
exe{hello}: {hxx cxx}{*}

# Make sure, files can include themselves by using tags.
cxx.poptions =+ "-I$src_base"

Adding More Files to the Project

Adding more files to the project can be considered a piece of cake. Think of a routine void test() declared in test.hpp and defined in test.cpp.

// test.hpp
#pragma once

void test();
// test.cpp
#include <test.hpp>
#include <iostream>

void test(){
  std::cout << "This is a test message.\n";
}

This routine can simply be called in the main.cpp file.

// main.cpp
#include <iostream>
#include <hello.hpp>
#include <test.hpp>

int main(){
  std::cout << "Hello, World!\n";
  say_hello();
  say_bye();
  test();
}

Manual compilation involves changing the command line to the following expression.

$ g++ -o hello main.cpp hello.cpp test.cpp -I.

Compiling and running the code with the build2 build system looks again like the following.

$ b
c++ cxx{main}
c++ cxx{hello}
c++ cxx{test}
ld exe{hello}
$ ./hello
Hello, World!
Hello, World!
Goodbye, World!
This is a test message.

References


Last update: October 22, 2020