The Genode build system
The Genode OS Framework comes with a custom build system that is designed for the creation of highly modular and portable systems software. Understanding its basic concepts is pivotal for using the full potential of the framework. This document introduces those concepts and the best practises of putting them to good use. Beside building software components from source code, common and repetitive development tasks are the testing of individual components and the integration of those components into complex system scenarios. To streamline such tasks, the build system is accompanied with special tooling support. This document introduces those tools.
Build directories and repositories
The build system is supposed to never touch the source tree. The procedure of building components and integrating them into system scenarios is done at a distinct build directory. One build directory targets a specific platform, i.e., a kernel and hardware architecture. Because the source tree is decoupled from the build directory, one source tree can have many different build directories associated, each targeted at another platform.
The recommended way for creating a build directory is the use of the create_builddir tool located at <genode-dir>/tool/. By starting the tool without arguments, its usage information will be printed. For creating a new build directory, one of the listed target platforms must be specified. Furthermore, the location of the new build directory has to be specified via the BUILD_DIR= argument. For example:
cd <genode-dir> ./tool/create_builddir linux_x86 BUILD_DIR=/tmp/build.linux_x86
This command will create a new build directory for the Linux/x86 platform at /tmp/build.linux_x86/.
Build-directory configuration via build.conf
The fresh build directory will contain a Makefile, which is a symlink to tool/builddir/build.mk. This Makefile is the front end of the build system and not supposed to be edited. Beside the Makefile, there is a etc/ subdirectory that contains the build-directory configuration. For most platforms, there is only a single build.conf file, which defines the parts of the Genode source tree incorporated in the build process. Those parts are called repositories.
The repository concept allows for keeping the source code well separated for different concerns. For example, the platform-specific code for each target platform is located in a dedicated base-<platform> repository. Also, different abstraction levels and features of the system are residing in different repositories. The etc/build.conf file defines the set of repositories to consider in the build process. At build time, the build system overlays the directory structures of all repositories specified via the REPOSITORIES declaration to form a single logical source tree. By changing the list of REPOSITORIES, the view of the build system on the source tree can be altered. The etc/build.conf as found in a fresh created build directory will list the base-<platform> repository of the platform selected at the create_builddir command line as well as the base, os, and demo repositories needed for compiling Genode's default demonstration scenario. Furthermore, there are a number of commented-out lines that can be uncommented for enabling additional repositories.
Note that the order of the repositories listed in the REPOSITORIES declaration is important. Front-most repositories shadow subsequent repositories. This makes the repository mechanism a powerful tool for tweaking existing repositories: By adding a custom repository in front of another one, customized versions of single files (e.g., header files or target description files) can be supplied to the build system without changing the original repository.
To build all targets contained in the list of REPOSITORIES as defined in etc/build.conf, simply issue make. This way, all components that are compatible with the build directory's base platform will be built. In practice, however, only some of those components may be of interest. Hence, the build can be tailored to those components which are of actual interest by specifying source-code subtrees. For example, using the following command
make core server/nitpicker
the build system builds all targets found in the core and server/nitpicker source directories. You may specify any number of subtrees to the build system. As indicated by the build output, the build system revisits each library that is used by each target found in the specified subtrees. This is very handy for developing libraries because instead of re-building your library and then your library-using program, you just build your program and that's it. This concept even works recursively, which means that libraries may depend on other libraries.
In practice, you won't ever need to build the whole tree but only the targets that you are interested in.
Cleaning the build directory
To remove all but kernel-related generated files, use
To remove all generated files, use
Both clean and cleanall won't remove any files from the bin/ subdirectory. This makes the bin/ a safe place for files that are unrelated to the build process, yet required for the integration stage, e.g., binary data.
Controlling the verbosity of the build process
To understand the inner workings of the build process in more detail, you can tell the build system to display each directory change by specifying
If you are interested in the arguments that are passed to each invocation of make, you can make them visible via
Furthermore, you can observe each single shell-command invocation by specifying
Of course, you can combine these verboseness toggles for maximizing the noise.
Enabling parallel builds
To utilize multiple CPU cores during the build process, you may invoke make with the -j argument. If manually specifying this argument becomes an inconvenience, you may add the following line to your etc/build.conf file:
MAKE += -j<N>
This way, the build system will always use <N> CPUs for building.
Caching inter-library dependencies
The build system allows to repeat the last build without performing any library-dependency checks by using:
The use of this feature can significantly improve the work flow during development because in contrast to source-codes, library dependencies rarely change. So the time needed for re-creating inter-library dependencies at each build can be saved.
Repository directory layout
Each Genode repository has the following layout:
|doc/||Documentation, specific for the repository|
|etc/||Default configuration of the build process|
|mk/||The build system|
|include/||Globally visible header files|
|src/||Source codes and target build descriptions|
|lib/mk/||Library build descriptions|
For each custom source-code repository supplied to the build system, the following subdirectories are mandatory:
lib/mk/ src/ include/
Creating targets and libraries
A good starting point is to look at the init target. The source code of init is located at os/src/init/. In this directory, you will find a target description file named target.mk. This file contains the building instructions and it is usually very simple. The build process is controlled by defining the following variables.
Build variables to be defined by you
is the name of the binary to be created. This is the only mandatory variable to be defined in a target.mk file.
expresses the requirements that must be satisfied in order to build the target. You find more details about the underlying mechanism in Section Specializations.
is the list of libraries that are used by the target.
contains the list of .cc source files. The default search location for source codes is the directory, where the target.mk file resides.
contains the list of .c source files.
contains the list of assembly .s source files.
contains binary data files to be linked to the target.
is the list of include search locations. Directories should always be appended by using +=. Never use an assignment!
is a list of Genode-external objects or libraries. This variable is mostly used for interfacing Genode with legacy software components.
Rarely used variables
contains additional compiler options to be used for .c as well as for .cc files.
contains additional compiler options to be used for the C++ compiler only.
contains additional compiler options to be used for the C compiler only.
Specifying search locations
When specifying search locations for header files via the INC_DIR variable or for source files via vpath, relative pathnames are illegal to use. Instead, you can use the following variables to reference locations within the source-code repository, where your target lives:
is the base directory of the current source-code repository. Normally, specifying locations relative to the base of the repository is never used by target.mk files but needed by library descriptions.
is the directory, where your target.mk file resides. This variable is always to be used when specifying a relative path.
In contrast to target descriptions that are scattered across the whole source tree, library descriptions are located at the central place lib/mk. Each library corresponds to a <libname>.mk file. The base of the description file is the name of the library. Therefore, no TARGET variable needs to be set. The source-code locations are expressed as $(REP_DIR)-relative vpath commands.
Library-description files support the following additional declarations:
- SHARED_LIB = yes
declares that the library should be built as a shared object rather than a static library. The resulting object will be called <libname>.lib.so.
Building components for different platforms likely implicates portions of code that are tied to certain aspects of the target platform. For example, a target platform may be characterized by
A kernel API such as L4v2, Linux, L4.sec,
A hardware architecture such as x86, ARM, Coldfire,
A certain hardware facility such as a custom device, or
Other properties such as software license requirements.
Each of these attributes express a specialization of the build process. The build system provides a generic mechanism to handle such specializations.
The programmer of a software component knows the properties on which his software relies and thus, specifies these requirements in his build description file.
The user/customer/builder decides to build software for a specific platform and defines the platform specifics via the SPECS variable per build directory in etc/specs.conf. In addition to an (optional) etc/specs.conf file within the build directory, the build system incorporates the first etc/specs.conf file found in the repositories as configured for the build directory. For example, for a linux_x86 build directory, the base-linux/etc/specs.conf file is used by default. The build directory's specs.conf file can still be used to extend the SPECS declarations, for example to enable special features.
Each <specname> in the SPECS variable instructs the build system to
Include the make-rules of a corresponding base/mk/spec-<specname>.mk file. This enables the customization of the build process for each platform.
Search for <libname>.mk files in the lib/mk/<specname>/ subdirectory. This way, we can provide alternative implementations of one and the same library interface for different platforms.
Before a target or library gets built, the build system checks if the REQUIRES entries of the build description file are satisfied by entries of the SPECS variable. The compilation is executed only if each entry in the REQUIRES variable is present in the SPECS variable as supplied by the build directory configuration.
Automated integration and testing
Genode's cross-kernel portability is one of the prime features of the framework. However, each kernel takes a different route when it comes to configuring, integrating, and booting the system. Hence, for using a particular kernel, profound knowledge about the boot concept and the kernel-specific tools is required. To streamline the testing of Genode-based systems across the many different supported kernels, the framework comes equipped with tools that relieve you from these peculiarities.
Using so-called run scripts, complete Genode systems can be described in a concise and kernel-independent way. Once created, a run script can be used to integrate and test-drive a system scenario directly from the build directory. The best way to get acquainted with the concept is reviewing the run script for the hello_tutorial located at hello_tutorial/run/hello.run. Let's revisit each step expressed in the hello.run script:
Building the components needed for the system using the build command. This command instructs the build system to compile the targets listed in the brace block. It has the same effect as manually invoking make with the specified argument from within the build directory.
Creating a new boot directory using the create_boot_directory command. The integration of the scenario is performed in a dedicated directory at <build-dir>/var/run/<run-script-name>/. When the run script is finished, this directory will contain all components of the final system. In the following, we will refer to this directory as run directory.
Installing the Genode config file into the run directory using the install_config command. The argument to this command will be written to a file called config at the run directory picked up by Genode's init process.
Creating a bootable system image using the build_boot_image command. This command copies the specified list of files from the <build-dir>/bin/ directory to the run directory and executes the platform-specific steps needed to transform the content of the run directory into a bootable form. This form depends on the actual base platform and may be an ISO image or a bootable ELF image.
Executing the system image using the run_genode_until command. Depending on the base platform, the system image will be executed using an emulator. For most platforms, Qemu is the tool of choice used by default. On Linux, the scenario is executed by starting core directly from the run directory. The run_genode_until command takes a regular expression as argument. If the log output of the scenario matches the specified pattern, the run_genode_until command returns. If specifying forever as argument (as done in hello.run), this command will never return. If a regular expression is specified, an additional argument determines a timeout in seconds. If the regular expression does not match until the timeout is reached, the run script will abort.
Please note that the hello.run script does not contain kernel-specific information. Therefore it can be executed from the build directory of any base platform by using:
When invoking make with an argument of the form run/*, the build system will look in all repositories for a run script with the specified name. The run script must be located in one of the repositories run/ subdirectories and have the file extension .run.
For a more comprehensive run script, os/run/demo.run serves as a good example. This run script describes Genode's default demo scenario. As seen in demo.run, parts of init's configuration can be made dependent on the platform's properties expressed as spec values. For example, the PCI driver gets included in init's configuration only on platforms with a PCI bus. For appending conditional snippets to the config file, there exists the append_if command, which takes a condition as first and the snippet as second argument. To test for a SPEC value, the command have_spec <spec-value> is used as condition. Analogously to how append_if appends strings, there exists lappend_if to append list items. The latter command is used to conditionally include binaries to the list of boot modules passed to the build_boot_image command.
The run mechanism explained
Under the hood, run scripts are executed by an expect interpreter. When the user invokes a run script via make run/<run-script>, the build system invokes the run tool at <genode-dir>/tool/run with the run script as argument. The run tool is an expect script that has no other purpose than defining several commands used by run scripts, including a platform-specific script snippet called run environment (env), and finally including the actual run script. Whereas tool/run provides the implementations of generic and largely platform-independent commands, the env snippet included from the platform's respective base-<platform>/run/env file contains all platform-specific commands. For reference, the most simplistic run environment is the one at base-linux/run/env, which implements the create_boot_directory, install_config, build_boot_image, and run_genode_until commands for Linux as base platform. For the other platforms, the run environments are far more elaborative and document precisely how the integration and boot concept works on each platform. Hence, the base-<platform>/run/env files are not only necessary parts of Genode's tooling support but serve as resource for peculiarities of using each kernel.
Using run script to implement test cases
Because run scripts are actually expect scripts, the whole arsenal of language features of the Tcl scripting language is available to them. This turns run scripts into powerful tools for the automated execution of test cases. A good example is the run script at libports/run/lwip.run, which tests the lwIP stack by running a simple Genode-based HTTP server on Qemu. It fetches and validates a HTML page from this server. The run script makes use of a regular expression as argument to the run_genode_until command to detect the state when the web server becomes ready, subsequently executes the lynx shell command to fetch the web site, and employs Tcl's support for regular expressions to validate the result. The run script works across base platforms that use Qemu as execution environment.
To get the most out of the run mechanism, a basic understanding of the Tcl scripting language is required. Furthermore the functions provided by tool/run and base-<platform>/run/env should be studied.
Automated testing across base platforms
To execute one or multiple test cases on more than one base platform, there exists a dedicated tool at tool/autopilot. Its primary purpose is the nightly execution of test cases. The tool takes a list of platforms and of run scripts as arguments and executes each run script on each platform. The build directory for each platform is created at /tmp/autopilot.<username>/<platform> and the output of each run script is written to a file called <platform>.<run-script>.log. On stderr, autopilot prints the statistics about whether or not each run script executed successfully on each platform. If at least one run script failed, autopilot returns a non-zero exit code, which makes it straight forward to include autopilot into an automated build-and-test environment.