Introduction to meson build system
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 are usually 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:
Right now all these files are empty. I’m gonna start with some basic code just to get things going.
… and the corresponding implementation file:
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 executable and I’m building a library so, I’ll adjust it accordingly:
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
meson setup bld meson compile -C bld
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
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
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
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:
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:
Great! I’m gonna create a new empty
meson.build file under
to be able to build my test:
I’m gonna need to add this subdirectory to the top-level meson file as well:
My project tree looks like so now:
It seems like the puzzle is coming along together. My
tests is using
gtest subproject and obtains the declarations of
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
library as a dependency, just as I mentioned before. In the top-level
meson.build, I’m gonna add the following declarations:
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:
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:
The contents of
subprojects directory changed:
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
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:
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:
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
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
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.