Getting started with Meson (part 4)

German Diago Gomez
9 min readMay 24, 2018

Hello everyone again. After the former three posts about basic executables, basic dependency management and installing your binaries in Meson, today I want to focus on two things that were just ignored so far: the basic syntax for the language and the Meson command line. For the command line part I will also show how to use the test tool and will explain briefly about unit tests.

The language syntax

I have a strong tendency to compare Meson language with that of CMake. Why? Well, basically because if I am currently using Meson, one of my biggest frustrations has been, by far, the CMake language.

Coming to Meson running away from those endless experiments with space handling, variable interpolation and absolutely insane conditionals is a godsend and one of the big reasons why I decided to settle on Meson.

So, how does Meson syntax look like? Well, for anyone used to any “regular” (as in popular) programming language, Meson does not present any difficulties.

In Meson variables can hold any type, but that type is strong. This means that Meson can detect errors at configure time easily. The most approximate thing you can think of is a strong dynamic programming language such as Python.

Here there is a teaser of the basic data types:

  • numbers
  • booleans
  • strings
  • arrays
  • custom objects returned by Meson
  • functions
  • methods
  • control flow
  • looping
  • logical operations
  • ternary operator
  • comments

I will try to be as brief as possible to explain all of this with a code snippet:

# Comments start with # and last for the rest of the linemy_number = 7 # This is a number
my_string = 'hello' # This is a string
# Ternary operator and booleans
cond = true
result = cond ? true : false
# An array. Can hold any kind of objects
arr = ['hello', 1, false]
# Add more elements
arr += ['bye', true]
# You can add a single item without enclosing in array
arr += 'world' # add to array
# Looping
foreach a : arr
message(a) # Function call
endforecach
cpp_compiler = meson.get_compiler('cpp')
mylib = cpp_compiler.find_library('somelib', required : false)
mylib2 = cpp_compiler.find_library('anotherlib', required : false)
# Control flow with logical operation and (or also supported)
if mylib.found() and mylib2.found() # Method from object called
message('Lib found')
endif
# Nested arrays and indexing
some_data = [['somelib', 'dir1'], ['someotherlib', 'dir2']]
foreach sd : some_data
cpp_compiler.find_library(sd[0], dirs : sd[1])
endforeach

I think this is quite easy to understand for any average programmer. I will only mention a few things that could be a bit different or more suprising. Conditionals and loops end with an endif and endforeach, respectively. Arrays are immutable (when using += conceptually a new array is created with all the contents). That means that this code

arr1 = [3]
arr2 = arr1
arr2 += 5

yields arr1 with the value [3] and arr2 with value [3, 5], namely, arr1 is not added the element of arr2.

Strings have some methods for splitting, joining, check if it startswith, underscorifying and others that are spectacularly weird to handle with CMake. Take a look to the string object in the reference if you need more information.

Meson command line

The basic command line in Meson is flexible and not too different from those of CMake and Autotools.

Meson, as CMake, is a build system generator: it generates backend build system files. The default generation uses ninja as a backend, but others can be used. Beware here, since, as far as my knowledge goes, the current support is: ninja best supported, Visual Studio should be working pretty well and XCode is not really production-ready. I use myself ninja all the time as of now and plan to check Visual Studio support, but I cannot make a claim of how well or not it works in practice with a complex project with run_commands, run_targets and custom scripts. The ninja backend is working great, I can say that at least :)

In Meson you have two positional arguments that correspond to the source dir and the build dir. As far as my knowledge goes, if you just list one of the dirs Meson will guess wether that is the builddir or the source dir. So, for example, let us say that you are in the root of your project and do the following:

mkdir build
cd build
meson .. # Here you are mentioning the source dir

The above commands will detect the directory .. as the source directory and will put all the stuff into build directory. Part of the output of this invocation for a project of mine looks like this:

The Meson build system
Version: 0.47.0.dev1
Source dir: /Users/germandiago/Desktop/tpii
Build dir: /Users/germandiago/Desktop/tpii/build
Build type: native build
Project name: tpii
Native C++ compiler: ccache c++ (clang 9.0.0 "Apple LLVM version 9.0.0 (clang-900.0.39.2)")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Configuring tpi-service-config.hpp using configuration
Program git found: YES (/usr/bin/git)
Program cmake found: YES (/usr/local/bin/cmake)
Program make found: YES (/usr/local/bin/make)
Dependency Boost (filesystem, regex, iostreams, system) found: YES 1.66, /usr/local/Cellar/boost/1.66.0/
Dependency Boost () found: YES 1.66, /usr/local/Cellar/boost/1.66.0/
Message: Initializating git submodules and compiling needed libraries. This can take a while...
Using json from cache.
Using json-2.1.1-1-wrap.zip from cache.

Yes, I am using CMake to build another project, that is why you see it there :) After the configuration is done you can just do ninjaand merrily see your project building.

You can also, from the root of your project do:

mkdir build
meson build # Here you are mentioning the build dir
ninja -C build

That is, you can just create a build directory and invoke meson from the root. The -C option in ninja tells ninja to build from build directory, such as GNU Make can do.

Now let us go for the command line itself: your terminal and type meson --helpYou will see something similar to this (output cropped to relevant discussion):

optional arguments:-h, --help            show this help message and exit
--buildtype {plain,debug,debugoptimized,release,minsize}
Build type to use (default: debug).
--strip Strip targets on install.
--unity {on,off,subprojects}
Unity build (default: off).

--werror Treat warnings as errors.
--warnlevel {1,2,3} Compiler warning level to use (default: 1).
--layout {mirror,flat}
Build directory layout (default: mirror).
--default-library {shared,static,both}
Default library type (default: shared).
--backend {ninja,vs,vs2010,vs2015,vs2017,xcode}
Backend to use (default: ninja).
--stdsplit Split stdout and stderr in test logs.
--errorlogs Whether to print the logs from failing tests.
--install-umask INSTALL_UMASK
Default umask to apply on permissions of installed
files (default: 022).
-D option Set the value of an option, can be used several times
to set multiple options.
--cross-file CROSS_FILE
File describing cross compilation environment.
-v, --version show program's version number and exit
--wrap-mode {WrapMode.default,WrapMode.nofallback,WrapMode.nodownload,WrapMode.forcefallback}
Special wrap mode to use
--prefix PREFIX Installation prefix (default: /usr/local).

Let us review some of the most common options:

  • buildtype
  • prefix
  • unity
  • -Doption=value

The first time you invoke Meson, it will configure and cache the results of the invocation.

For example, you could do the following:

meson --buildtype=release --prefix=`pwd`/install -Dmyoption=true --unity=on

This would build a release build to be installed in your install directory of your current invocation directory with your custom option (defined in meson_options.txt) and using unity builds.

That is for the first invocation.

Reconfiguring projects

Once projects are configured, you can see the current configuration with meson configure builddir. That will show something similar to this:

Core properties:
Source dir ...
Build dir ...
Core options:Option Current Value Possible Values Description
------ ------------- --------------- -----------
buildtype debug [plain, debug, debugoptimized, release, minsize] Build type to use.
warning_level 1 [1, 2, 3] Compiler warning level to use.
werror false [true, false] Treat warnings as errors.
strip false [true, false] Strip targets on install.
unity off [on, off, subprojects] Unity build.
default_library shared [shared, static, both] Default library type.
install_umask 18 Default umask to apply on permissions of installed files.
Option Current Value Description
------ ------------- -----------
backend_max_links 0 Maximum number of linker processes to run or 0 for no limit
Base options:
Option Current Value Possible Values Description
------ ------------- --------------- -----------
b_asneeded true [true, false] Use -Wl,--as-needed when linking
b_colorout always [auto, always, never] Use colored output
b_coverage false [true, false] Enable coverage tracking.
b_lto false [true, false] Use link time optimization
b_ndebug false [true, false, if-release] Disable asserts
b_pch true [true, false] Use precompiled headers
b_pgo off [off, generate, use] Use profile guided optimization
b_sanitize none [none, address, thread, undefined, memory, address,undefined] Code sanitizer to use
b_staticpic true [true, false] Build static libraries as position independent
Compiler arguments:
cpp_args []
Linker args:
cpp_link_args []
Compiler options:
Option Current Value Description
------ ------------- -----------
cpp_std none C++ language standard to use
Directories:
Option Current Value Description
------ ------------- -----------
prefix /usr/local Installation prefix.
libdir lib Library directory.
libexecdir libexec Library executable directory.
bindir bin Executable directory.
sbindir sbin System executable directory.
includedir include Header file directory.
datadir share Data file directory.
mandir share/man Manual page directory.
infodir share/info Info page directory.
localedir share/locale Locale data directory.
sysconfdir etc Sysconf data directory.
localstatedir /var/local Localstate data directory.
sharedstatedir /var/local/lib Architecture-independent data directory.
Project options:
Option Current Value Possible Values Description
------ ------------- --------------- -----------
build_boost_from_source false [true, false] Download and build boost from sources and use that dependency.
build_tests false [true, false] Build the test suite
enable_content_creation false [true, false] Enable custom commands to create website .html contents from .org files
enable_tpi false [true, false] Build standalone tpi
enable_tpi_service true [true, false] Build the tpi server
tpi_service_enable_https false [true, false] Wether https is enabled or not. Used for base url
tpi_service_host localhost:8085 Default host or ip address for tpi-service. It can include the port separated by colon
tpi_service_listen_port 8085 Default port for tpi-service. It will be used as default when running tpi-service without port argument
Testing options:
Option Current Value Possible Values Description
------ ------------- --------------- -----------
stdsplit true [true, false] Split stdout and stderr in test logs.
errorlogs true [true, false] Whether to print the logs from failing tests.

Any of these options that you see in the list can be changed with -Doptionname=value. You can see that the possible values are listed in the output. This should make things easy. So now let us remove the unity build from the project and build a debug release with static libraries

meson configure -Ddefault_library=static -Dbuildtype=debug -Dunity=off builddir

Now, your build directory should be already ready to compile with the new configuration.

You can configure any option with the -Doption=value notation, even the built-in ones, since Meson 0.46.0 (such as prefix and default_library).

Meson subcommands and testing

In Meson there are several subcommands, with a style that follows the popular git version control. Two of them are meson introspect(as its name says, for introspecting metadata about targets such as build files, etc.) and meson test. We will see only meson testhere.

In Meson, when you add tests in your meson.buildfile with the testfunction and you can run them normally with the backend invocation, for example ninja test, without the use of meson test. As we will see the meson subcommand is for more advanced uses and not always required.

This is how you add a test in Meson:

exe = executable('myexe', sources : ['source.cpp'])
test('mytest', exe, args : ['--somearg'], env : ['ENVVAR=val'])

As you can see test supports arguments and environment variables. Now running ninja testcan run that test. Tests are run in parallel in Meson.

Tests are often run together with -Db_coverage=true to get reports about your tests coverage.

There is also a benchmarkfunction similar to test that has its corresponding target in ninja to run benchmarks. Benchmarks are essentially the same as tests, but they are not run in parallel.

Now let us see how you can invoke the test tool in some snippets:

# Same as ninja test
meson test
# Invoke the test with name 'testname'
meson test testname
# Invoke all tests 10 times (for example because you have randomized tests)
meson test --repeat=10
# Run 'testname' under valgrind
meson test --wrap=valgrind testname
# Looking for a strange, unusual crash? First crash puts you in gdb
meson test --gdb --repeat=10000 testname

Conclusion

I think that is all for now and you can get an idea of how the command line works, how the syntax looks and how to invoke some tests under different needs. This is all for the Getting Started in Meson series!

In my next blog post I will start a series of posts to talk about how I did a small real world project, but big enough, which is about to be open sourced under the Boost License. This project makes use of a React web frontend, a library, a C++ executable and compiles and exports some contents in .org mode from Emacs to html files that are later shown after manipulating the DOM by the React frontend.

Some of the things that the project will include are:

  • compiling an executable and libraries
  • using meson_options.txt to tweak the options for the project at configuration time
  • generating a project-config.hpp file for use in the project
  • using run_commands to download a dependency and compile in CMake before configuration
  • running tests
  • using wrap subprojects
  • installing target to run a web service

This next blog post series will not be centered around Meson only, but it will certainly show how things were built and will be published as an example that you can take a look at for your projects. Stay tuned!

Thanks for reading!

--

--

German Diago Gomez

In software industry for more than a decade doing backend software.