Contents

Introduction to meson build system

Intro

I started using meson exclusively for any new C or C++ project I create. It’s much more convenient and less cumbersome than CMake. In this post, I’ll try to give a short introduction to meson and the reasons I like it.

My typical meson project layout

My projects usually contain small libraries with a set of tests or executables relying on a bunch of libraries. For the purpose of presentation, I’ll start with a demo project, let’s call it libmagick. I’m gonna start with my typical project layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.
├── include
│   └── libmagick
│       └── magic.h
├── src
│   └── magic.cpp
└── tests
    └── magick_test.cpp

4 directories, 3 files

Right now, all these files are empty. I’m gonna start with some basic code just to get things going.

magic.h
1
2
3
4
5
6
7
8
9
#ifndef MAGIC_H
#define MAGIC_H

namespace magick {
    /// magick sum
    std::size_t sum(std::size_t a, std::size_t b);
}

#endif /* MAGIC_H */

… and the corresponding implementation file:

magic.cpp
1
2
3
4
5
6
7
8
#include <libmagick/magic.h>

namespace magick {
    std::size_t sum(std::size_t a, std::size_t b) {
        // really complex stuff!
        return a + b;
    }
}

It’s time to establish the build system. Usually, meson is able to autodetect most of the details but I like to give it a little extra nudge by specifying the language explicitly:

meson init -l cpp .

Great. Now, I’ve got a sample project file. The generated file is for an executable and I’m building a library so, I’ll adjust it accordingly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
project('libmagick', 'cpp',
  version : '0.1',
  default_options : ['warning_level=3', 'cpp_std=c++14'])

libmagick_inc = include_directories('include')

libmagick_lib = shared_library('libmagick',
                               'src/magic.cpp',
                               include_directories : [ libmagick_inc ],
                               install : true)

Everything is nice and simple. Meson is a declarative language so, libmagick_inc defines an include directory path which is later used when declaring a target; a shared library. It’s possible to build the project now:

meson setup bld
meson compile -C bld

That’s it!

3rdParty dependencies and testing

Usually, you’ll want to write some tests with your code. To do that, some testing framework is preferable. This is simple with meson. Meson manages dependencies as subprojects. Subproject has to live under subprojects directory so, I’m gonna create it now:

mkdir subprojects

Before I continue, I’m gonna try to clarify some meson parlance first.

What is a dependency?

This is described in meson documentation thoroughly but in simple terms, in meson, you declare a dependency just as like any other target (executable or library), the difference is that the dependency wraps the target link paths, include paths and all other necessary details under a single declaration - which makes it super convenient to use later on. I’m gonna declare my library libmagick_lib as a meson dependency to demonstrate what I mean.

What is a subproject?

It can be anything really, it can be a git submodule, a separate project under subprojects directory a tar archive or a so called wrap. Meson wrap files define subprojects as a set of dependencies they provide and a way to obtain the project - it can be fetched from external git repo, a tarball, svn.

Meson comes with a so called WrapDB which is simply a collection of wrap files for most popular projects. WrapDB is searchable. I’m gonna use it to install google test:

$ meson wrap search gtest
gtest
$ meson wrap install gtest
Installed gtest version 1.11.0 revision 2

I’ve got gtest installed now. But what exactly happened? meson downloaded a wrap file for me:

$ ls -l subprojects/
total 8
-rw-r--r--  1 tomasz  staff  541  1 Oct 12:30 gtest.wrap

Here’s what’s in the file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[wrap-file]
directory = googletest-release-1.11.0
source_url = https://github.com/google/googletest/archive/release-1.11.0.tar.gz
source_filename = gtest-1.11.0.tar.gz
source_hash = b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5
patch_filename = gtest_1.11.0-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.11.0-2/get_patch
patch_hash = 764530d812ac161c9eab02a8cfaec67c871fcfc5548e29fd3d488070913d4e94

[provide]
gtest = gtest_dep
gtest_main = gtest_main_dep
gmock = gmock_dep
gmock_main = gmock_main_dep

It contains an URL to gtest, which will be used to obtain the package and a set of dependencies that this library provides. I can now write some tests:

magick_test.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <gtest/gtest.h>

#include <libmagick/magic.h>


class MagickTest : public ::testing::Test {
public:
    void SetUp() {
    }

    void TearDown() {
    }
};


TEST_F(MagickTest, test_ifNumbersAddUp) {
    ASSERT_EQ(5, magick::sum(2, 3));
}

Great! I’m gonna create a new empty meson.build file under tests directory to be able to build my test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
gtest_proj = subproject('gtest')

gtest_dep = gtest_proj.get_variable('gtest_dep')
gtest_main_dep = gtest_proj.get_variable('gtest_main_dep')

magick_test_exe = executable('magicktest',
                             'magick_test.cpp',
                             dependencies : [ gtest_dep, gtest_main_dep ],
                             include_directories : libmagick_inc,
                             link_with : [ libmagick_lib ])

test('magick test', magick_test_exe)

I’m gonna need to add this subdirectory to the top-level meson file as well:

1
subdir('tests')

My project tree looks like so now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.
├── include
│   └── libmagick
│       └── magic.h
├── meson.build
├── src
│   └── magic.cpp
├── subprojects
│   └── gtest.wrap
└── tests
    ├── magick_test.cpp
    └── meson.build

5 directories, 6 files

It seems like the puzzle is coming along together. My meson.build for tests is using gtest subproject and obtains the declarations of gtest and gtest_main dependencies from it. These are the same dependencies that are visible in the wrap file. There’s one more improvement to be made to that file. You might have noticed that my test is just linking directly with my library and is referring directly to library’s include paths. Now, there’s nothing wrong with it but it can be done better by defining the libmagick library as a dependency, just as I mentioned before. In the top-level meson.build, I’m gonna add the following declarations:

1
2
libmagick_dep = declare_dependency(link_with : libmagick_lib,
                                   include_directories : libmagick_inc)

This creates libmagick_dep dependency. From now on, this can be used in exactly the same way as gtest, so the declaration of my test target can be modified the following way:

1
2
3
magick_test_exe = executable('magicktest',
                             'magick_test.cpp',
                             dependencies : [ libmagick_dep, gtest_dep, gtest_main_dep ])

Now, if I try to run

meson compile -C bld

… a lot of stuff will happen. First, meson will pull gtest and build it and then build my library and test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ninja: Entering directory `/Users/tomasz/libmagick/bld'
[0/1] Regenerating build files.
The Meson build system
Version: 0.63.2
Source dir: /Users/tomasz/libmagick
Build dir: /Users/tomasz/libmagick/bld
Build type: native build
Project name: libmagick
Project version: 0.1
C++ compiler for the host machine: c++ (clang 13.1.6 "Apple clang version 13.1.6 (clang-1316.0.21.2.5)")
C++ linker for the host machine: c++ ld64 764
Host machine cpu family: x86_64
Host machine cpu: x86_64
Downloading gtest source from https://github.com/google/googletest/archive/release-1.11.0.tar.gz
Download size: 886330
Downloading: ..........
Downloading gtest patch from https://wrapdb.mesonbuild.com/v2/gtest_1.11.0-2/get_patch
Download size: 2551
Downloading: ..........

Executing subproject gtest

gtest| Project name: gtest
gtest| Project version: 1.11.0
gtest| C++ compiler for the host machine: c++ (clang 13.1.6 "Apple clang version 13.1.6 (clang-1316.0.21.2.5)")
gtest| C++ linker for the host machine: c++ ld64 764
gtest| Run-time dependency threads found: YES
gtest| Dependency threads found: YES unknown (cached)
gtest| Dependency threads found: YES unknown (cached)
gtest| Dependency threads found: YES unknown (cached)
gtest| Build targets in project: 1
gtest| Subproject gtest finished.

Build targets in project: 2

libmagick 0.1

  Subprojects
    gtest  : YES

  User defined options
    backend: ninja

Found ninja-1.11.1 at /usr/local/bin/ninja
Cleaning... 0 files.
[7/7] Linking target tests/magicktest

The contents of subprojects directory changed:

1
2
3
4
5
$ ls -l subprojects/
total 8
drwxrwxr-x  19 tomasz  staff  608  1 Oct 12:59 googletest-release-1.11.0
-rw-r--r--   1 tomasz  staff  541  1 Oct 12:30 gtest.wrap
drwxr-xr-x   4 tomasz  staff  128  1 Oct 12:59 packagecache

This is where meson downloaded gtest and this is where it stores a pre-compiled, cached version of it. These files need not to be ever committed to git so, I like to add this simple rule to my .gitignore to prevent committing them:

1
2
subprojects/*
!subprojects/*.wrap

This works well if you only use wrap files. You may have to be more specific if you’ve got any submodules under subprojects or manage your dependencies in any other way.

There’s a convenient way to run all tests with meson as well:

meson test -C bld

… and here’s the output from my test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ninja: Entering directory `/Users/tomasz/libmagick/bld'
ninja: no work to do.
1/1 magick test        OK              0.01s

Ok:                 1
Expected Fail:      0
Fail:               0
Unexpected Pass:    0
Skipped:            0
Timeout:            0

Full log written to /Users/tomasz/libmagick/bld/meson-logs/testlog.txt

More on testing

Meson has a couple of cool features when it comes to testing. One of my favourites is:

meson test -C bld --gdb

This gets me directly to gdb. It’s not very useful on its own as most of the time I want to debug a specific test. To do that, I usually combine it with a couple of more flags:

meson test -C bld \
    --gdb "magick test" \
    --test-args "\-\-gtest_filter=MagickTest.test_ifNumbersAddUp"

Different types of builds

By default, meson builds in debug mode. This is clearly visible on binaries:

1
2
3
bld/liblibmagick.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, BuildID[sha1]=22d314fbbd9446b546fc3f45f746beb9a9660c3d,
with debug_info, not stripped

This can be easily changed using meson setup invocation:

# configure build type
meson setup --builttype release bld

… or on already existing build dir:

# clear the build directory
meson setup --wipe bld

# configure build type
meson configure --buildtype release bld

Now when building, it’s clearly visible that optimisations are being enabled:

meson compile -v -C bld
1
2
3
4
5
6
[1/7] ccache c++ -Iliblibmagick.so.p -I. -I.. -I../include
-fdiagnostics-color=always -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch
-Wnon-virtual-dtor -Wextra -Wpedantic -std=c++14 -O3 -fPIC -MD -MQ
liblibmagick.so.p/src_magic.cpp.o -MF liblibmagick.so.p/src_magic.cpp.o.d -o
liblibmagick.so.p/src_magic.cpp.o -c ../src/magic.cpp
...

In fact, meson configure gives control over a lot more options. Optimisations can be customised as well:

meson configure --optimization s bld

or cpp standard:

meson configure -Dcpp_std=c++20 bld

The configuration summary is presented when building:

meson compile --clean -C bld
1
2
3
4
5
  User defined options
    backend     : ninja
    buildtype   : release
    optimization: s
    cpp_std     : c++20

Conclusion

For me, meson is a perfect fit. It’s an ideal non-distracting solution for building. It’s feature rich and I don’t miss anything from CMake at all.

Example project discussed in this post can be found on twdev gitlab account.