Developers fed up with cryptic Makefiles should take a look at the new Meson build system, which is simple to operate, offers scripting capabilities, integrates external test tools, and supports Linux, Windows, and Mac OS X.

A Better Builder

Finnish developer Jussi Pakkanen was frustrated by existing build systems with foolish syntax in configuration files and unexpected behavior. During the Christmas season of 2012, he therefore decided to develop his own build system that would run fast, be reliable and easy to use, run on all major operating systems, and integrate important test tools like Valgrind.

Just two months later, Pakkanen published a first version of his build system, which as a trained physicist he named for the Meson particle. Since then, the technology has advanced quickly. Version 0.17.0 – the latest version when this article was written – is already extremely stable despite its low version number and contains all the features planned by the program author.

Meson builds executables and libraries and supports multiple directories for different builds from the same source. The flexible configuration language is easy to learn, opens many possibilities to the developer, and supports if statements.

Presentable Features

Meson is written entirely in Python 3 and is released under Apache License 2.0. One minor drawback, however, is that currently, Meson can only handle source code in the programming languages, C, C++, Java, and Vala. Meson does not bundle the source code into the appropriate compiler itself; rather, it simply generates configuration files for an existing build system. Under Linux, Meson generates build files for the relatively unknown but quite nimble Ninja mini-build system. Alternatively, Meson outputs project files for Visual Studio 2010 or Xcode. Meson refers to all of these external tools as back ends.

If you would like to use the build system on Linux, you therefore need to install Ninja 3 and Python via the package manager. On Ubuntu this means typing,

sudo apt-get install python3 ninja-build

then downloading the current Meson version and extracting the archive.

The installation is handled by the install_meson script, which you launch as the root user. By default, it copies the build tool into subdirectories below /usr/local/. However, the --prefix <path> parameter lets you choose a different target.

Rather than install Meson, users can call it directly from any directory. The short meson command then always needs to be replaced by <path>/meson.py.

Building Plan

The next thing you need to do is create a small configuration file named meson.build in the directory with your self-written source code. An example of such a file is given in Listing 1. Meson uses its own programming language within the meson.build file; it should look familiar to Python programmers. Meson somewhat inconsistently refers to the project() and executable() functions as commands. Each statement must be on a separate line.

Listing 1: Example meson.build File

project('Hello World', 'c')  # Name of project
src = ['main.c', 'file1.c', 'file2.c']
executable('hello', sources : src)

Starting a Project

A new project is defined by the project() keyword; it expects two arguments: the project name and the programming language used. In Listing 1, the project is called Hello World; its source code is in the C programming language. Armed with this knowledge, Meson can automatically select the correct compiler. C++ programmers would use cpp as the second parameter. Strings need to be framed in single quotes, and a “#” introduces a comment; Meson ignores everything that follows a hash mark until a newline occurs.

The second line in Listing 1 defines a new variable named src. This in turn expects an array with the names of the translatable source code files. The square brackets create the array here. In addition to arrays, variables will also accept integers (num = 123), logical values (right = true), and strings ( name = 'Peter'). You do not need to specify these values explicitly (dynamic typing); however, they cannot be converted from one type to another. Finally, the developer must always define a build target in meson.build. It determines whether Meson should output a library, a program, or both.

In Listing 1, the executable() function ensures that the compiler generates a program. As arguments it receives the program name and the list of compilable files.

The executable() function also supports putting the name of the corresponding parameter before each argument to increase readability. In Listing 1, this happened in sources : src. This concept is familiar from other languages as keyword arguments or named parameters.

Two other build targets besides execute() are static_library() and shared_library(), which generate a static or dynamic link library, respectively. The following call builds version 1.2.3 of a dynamic library named libhello from the files lib1.c and lib2.c:

shared_library('hello', ['lib1.c', 'lib2.c'],version : '1.2.3')

The version number is optional. The static_library() function is called in the same way, but you cannot specify the version number. Developers can specify several different build targets simultaneously; Meson then creates them automatically in sequence.

The three lines from Listing 1 are everything Meson needs. The tool itself selects the matching compiler, including the necessary parameters.

Divide and Conquer

To compile your source code, you first need to create a new subdirectory that stores all the output from the compiler. This build directory is mandatory: Meson insists on a separate directory and cannot be tricked into storing the object files in the source directory.

This apparent tutelage has several advantages: for one, it keeps the files output by the compiler separate from the source code, improving understandability, especially of complex projects, and making sure the object files do not fall into the clutches of a version control system. On the other hand, you can simultaneously create several independent builds with different configurations. Thus, one build directory could contain the release version while a second houses a debug version.

How you name these build directories, and where they are, is up to you. In most cases, the default build directory is located at the top level of the source directory, and is simply called build:

cd helloworld/src
mkdir build

The developer has to let Meson set up the new build directory (see also Figure 1):

meson ~/helloworld/src ~/helloworld/src/build
Figure 1: If you call meson from the build directory, as shown here, you simply need to specify the source code directory.

The first parameter specifies the directory containing the source code, which also houses the meson.build file. The second parameter reveals the build directory. In it, Meson now creates all the necessary configuration files or build files for Ninja. From this, the developer can now build the actual program:

cd build
ninja

Ninja automatically integrates all available processor cores. If you later want to recompile your program, a call to ninja in the build directory is all it takes. Further commands are not required. Meson has set up Ninja so that it automatically detects source code file changes and only rebuilds these – even with a subsequent edit of the meson.build file.

By default, the compiler inserts debug information in the binaries and outputs all warnings. If the GCC C compiler is used, Meson enables the -g and -Wall parameters. If you need an optimized program for a release, you will want to call Meson with the --buildtype=release parameter. How to take further control over the compilation process is reveals in the “Full Control” boxout.

Full Control

Meson decides itself what compiler to invoke and with what parameters. However, developers occasionally want to control the compilation process themselves – for example, to put together a package for a specific distribution. Again, this is no problem. You can specify which compiler to use with environment variables. For example, to use Clang instead of GCC:

CC=clang meson ~/helloworld/src ~/helloworld/src/build

You can even specify or override compiler flags by setting the appropriate environment variables before calling meson. The --buildtype=plain also disables Meson’s own defaults:

CFLAGS="-o2" meson --buildtype=plain ~/helloworld/src ~/helloworld/build

If you want Ninja to install the compiled program, the target directory can be set in the DESTDIR environmental variable:

DESTDIR=/opt/helloworld ninja install

Alternatively, you can simply specify a different prefix: ninja install --prefix /usr.

On Linux, Meson always uses Ninja as the back end. A project for Visual Studio 2010 is created by the --backend=vs2010 parameter; Meson generates the equivalent for Xcode with --backend=xcode. Ninja can also install the compiled program directly. To this end, you just call ninja install.

Object of Desire

The programming language used by Meson in meson.build is object-oriented. Although developers cannot implement classes themselves, some functions do return an object. For example, shared_library() returns an object that represents the corresponding build target. This can be captured in a variable:

lib = static_library('hello', ['lib1.c','lib2.c'])

You then need to pass the object as an argument to another function – for example, to executable() which links the program directly against the library:

executable('hello', 'main.c', link_with : lib)

If you use the functions defined in lib2.c, you will probably want to check them with a test program first. You do not need to link this against the complete dynamic library – just against the lib2.c object file.

Luckily, the object returned by shared_library() itself has an extract_objects method, which in turn returns an object file. You need to invoke this method in dot notation as in many object-oriented languages:

obj = lib.extract_objects('lib2.c')

The returned object can now be linked with the program:

executable('hello', 'main.c', objects : obj)

Many other objects support methods that programmers can call and use in a similar way.

Walking the Tree

If the source code is stored in subdirectories, the subdir function changes to one of the subdirectories – in the following example, to the gui/ directory:

subdir('gui')

Meson then evaluates the configuration file meson.build in this directory. However, it must not use a project function, because the parent meson.build already defines a project. If the header files for C or C++ projects are stored in the include subdirectory, you can add them to the compiler’s search path as follows:

header = include_directories('include')
executable('hello', 'main.c',include_dirs : header)

Most major projects use external libraries like zlib or the Gtk toolkit. To include such a library, only two lines are needed:

gtk = dependency('gtk+-3.0')
executable('hello', 'main.c', deps : gtk)

The dependency() function first checks to see whether the library is installed on the system. If it is missing, it aborts the build process; otherwise, Meson links it with the hello program. This automatic mechanism works with all libraries that supply a pkg-config. Fortunately, Meson natively supports some particularly well-known frameworks without this file. Currently, these include Boost, Qt5, Google Test (gtest), and Google Mock (gmock). In the case of Boost, you can thus easily select the desired modules:

boost = dependency('boost', modules : ['thread', 'utility'])

Meson supports if conditions that allow for more elaborate tests. Listing 2 shows an example: In a C project, it checks whether the sys/fstat.h header file is available. To be able to do this, Meson is handed the compiler in the first line. The second line then asks the compiler whether the header file exists (Figure 2).

Listing 2: Conditional Branching

compiler = meson.get_compiler('c')
if compiler.has_header('sys/fstat.h')
         # Header exists
else:
         # Header does not exist
endif
Figure 2: Ninja tells you the result of the if statement in its own output later on. In this case, this leads to an error in building the no_hello program.

Tests

Meson allows simple unit tests whose definition is demonstrated in Listing 3. Here, the test() function defines a new test case A test. It starts the hello program and passes the --debug and --encode parameters to it. Before this, Meson sets the environmental variable HELLOLANG to the value EN_us and the environmental variable HELLODIR to the value /opt/helloworld. If the hello program returns 0, the test is passed; any other value means a failure.

Listing 3: Unit Test

prg = executable('hello', 'main.c')
test('A Test', prg, args : ['--debug', '--encode'], env : ['HELLOLANG=EN_us', 'HELLODIR=/opt/helloworld'])

To perform the test, the developer compiles the program in the usual way with ninja and then calls ninja test (Figure 3). The output of the test run is stored in the meson-logs/testlog.txt file. If the developer has defined several tests, Meson executes them in parallel by default. If want or need to prevent this, you need to tell test like this:

test('A Test', prg, is_parallel : false)
Figure 3: Ninja can run automated tests. Here, the software has passed the test called A Test.

Additionally, Meson can embed additional debugging and testing tools. For example, if you pass in the --enable-gcov parameter to meson, it checks to see whether the code coverage tool Gcovr is installed. In this case, the new coverage-text build target is ready:

ninja coverage-xml

On seeing this line, Meson automatically runs Gcovr against the program; its output ends up in the coverage.xml file in the meson-logs/ directory. Based on the same principle, you can embed Valgrind and Cppcheck.

More Options

Meson offers many other possibilities. For example, the tool produces appropriate pkg-config files on request; it supports localization with Gettext and allows the use of cross compilers. Before compiling, the tool generates configuration files on demand, such as the common config.h. This template system built into Meson helps with this; for example, it automatically replaces the @version@ placeholder in the #define VERSION "@version@" line with the matching string (1.2.3), as defined in meson.build. You can also define completely new build targets. Enabling and using these features usually only involves adding a few lines to the meson.build file.

A very detailed and comprehensive guide can be found on SourceForge. Open questions are answered by the FAQ and on the official mailing list. Meson does not support IDEs yet, but the build system provides an API that any developer can use to integrate Meson into their favorite IDE. A simple GUI is in its infancy (Figure 4). If you want to accelerate its build, check out the two options in the “More Speed” box.

Figure 4: Meson includes the mesongui graphical front end; currently this only lets you change a few settings via mouse click.

More Speed

Parsing the header files from system libraries can take up a fair amount of time. Precompiled headers provide a remedy. The compiler then parses the header files once, and then saves its internal state in a file on your hard disk. During the next build, the compiler simply references the state in this file.

To use precompiled headers in Meson, you first need to put all #include statements in a new header file – call it hello_pch.h. This file is saved in a new subdirectory named pch. No source code files are allowed to include hello_pch.h, and the pch directory must not be in the search path. If these conditions are met, you just supplement executable() with the c_pch parameter:

executable('hello', sources: src, c_pch : 'pch/hello_pch.h')

To further optimize the build time, Meson supports unity builds. You first dump the contents of the source code files into one big file and then feed it to the compiler. This measure can reduce the compilation time by up to 50 percent depending on the project.

However, if you modify one of the files, the compiler has to recompile the complete source code. For this reason, unity builds are disabled by default in Meson. To enable them, to pass the --unity parameter to meson. The rest of the work is done by the build system; there is no need to glue the source code together yourself.

Conclusions

Although Meson is still quite young, the build system is remarkably stable and already meets all the requirements set by Pakkanen. In particular, it takes some work off the developer’s hands: If you previously had to deal with automake tools, you will probably not want to do without Meson.

The current version is already production-capable and well suited for medium-sized projects. However, Pakkanen expressly indicates that his implementation is still a proof of concept and that much could change in the course of further development. Pakkanen also announced that Meson would soon find its way into the Debian package repositories.