GNU Make is one of the most widely used build systems, and over the years I have gotten better at using it despite the syntax and lack of good examples. Still I consider it to be one of my favourite tools.
Actually when compared to a lot of other build systems, it can be considered quite elegant. With a little bit (or a lot) of effort a repo or a project can build amazing complexity. Or break in an unmentionable number of ways.
This has inspired a number of other developers to create build systems that can meet the needs of the modern programmer, ideally with a more documented syntax. As this is understandly a hard task, these efforts were focused on creating smaller build systems with better code bases.
One such example is the ninja build system, originally conceived to help with the monumental task of building and handling the dependencies of the Google Chrome browser; its primary focus is speed. For those out of the loop, LWN offers a brief and interesting summary here.
From my own experience, ninja is a somewhat faster than Makefile builds, and its plainer syntax is admittedly easier to read. However, this is only half the picture, ninja itself is not meant for humans. Instead the goal is to offload the generating of the scripts to other tools that allow for meatspace interactivity.
Increasingly I have seen inroads within the open source community for the meson build system, most notably Mesa and GStreamer. Since meson is both popular and it uses ninja, I figured worth a shot, eh?
From the man pages:
Meson is a build system designed to optimize programmer productivity. It aims to do this by providing simple, out-of-the-box support for modern software development tools and practices, such as unit tests, coverage reports, Valgrind, CCache and the like.
Sounds right fine and dandy, but what can it do? Well I find best to jump into these things with no expectations and test them out on a small project. Think my tiny webkit2 browser qualifies.
The Makefile I had been using for this project was by no means crufty, but adding new functionality to a Makefile is always pain and testing.
For meson, everything begins with a meson.build file in the main repo directory. After creating one, you'll need to specify the project name and lang and any default options. Since this is a pure C99 code base, all that's needed is:
project('sighte','c', default_options : ['c_std=c99'])
So for all the C files used for this project, they will be forced to the C99 standard. Actually with meson you can set the default options of other languages and compilers as well, though this is a bit trickier.
Moving on I needed some way of setting the version number when the build is generated. Originally that was done in make via:
# Version
VERSION = `date +%y.%m`
# If unable to grab the version, default to N/A
ifndef VERSION
VERSION = "n/a"
endif
Not the fanciest, but it works. In meson though, it can simply be treated as a variable.
version_raw = run_command('date','+%y.%m')
if version_raw.returncode() != 0
version_raw = 'n/a'
endif
version = '-D VERSION="' + version_raw.stdout().strip() + '"'
Command execution, return code validation, and I can trim whitespace? When compared to Makefiles, this feels rather elegant. Now I figure that, in theory, there is a Makefile way of doing this. Doubt it would be nearly as readable though.
Managing include paths in Makefiles has always been a bit of a pain, and while technically a programmer could use a series of pkg-config variables, keeping track of any large project is still a bit of a chore. Chances are you've seen something like this:
# Webkit and GTK Include paths
GTKINC = -pthread \
-I${USR_INC}/webkitgtk-4.0 \
-I${USR_INC}/gtk-3.0 \
-I${USR_INC}/gio-unix-2.0 \
-I${USR_INC}/cairo \
-I${USR_INC}/pango-1.0 \
-I${USR_INC}/atk-1.0 \
-I${USR_INC}/gdk-pixbuf-2.0 \
-I${USR_INC}/libsoup-2.4 \
${GLIB_INC}
# Webkit and GTK library flags
GTKLIB = -lwebkit2gtk-4.0 \
-lgtk-3 \
-lgdk-3 \
-ljavascriptcoregtk-4.0 \
-lgio-2.0 \
-lglib-2.0 \
-lgobject-2.0 \
-lsoup-2.4
# Other includes
INCS = -I. -I/usr/include ${GTKINC}
# Other libraries
LIBS = -L/usr/lib -lX11 ${GTKLIB}
Ah yes, Makefile newlines show their ugly presence. Suppose I could concat them all into a single line and make them illegible. Considering there are far worse Makefiles I really can't complain. What is the meson way of doing it, you ask?
atkdep = dependency('atk')
cairodep = dependency('cairo')
gdkdep = dependency('gdk-3.0')
gdkpixbufdep = dependency('gdk-pixbuf-2.0')
gtkdep = dependency('gtk+-3.0')
giounixdep = dependency('gio-unix-2.0')
glibdep = dependency('glib-2.0')
pangodep = dependency('pango')
libsoupdep = dependency('libsoup-2.4')
threaddep = dependency('threads')
webkit2 = dependency('webkit2gtk-4.0')
x11dep = dependency('x11')
all_deps = [atkdep,
cairodep,
gdkdep,
gtkdep,
giounixdep,
glibdep,
pangodep,
libsoupdep,
threaddep,
webkit2,
x11dep]
Unbelievable. This is both readable and free of hidden tab characters and not-so-hidden Makefile-style newlines. Also keep in mind that you may still need to test it out with pkg-config beforehand to get the package name correct. However, I think you will agree that the meson code is likely more maintainable and extensible.
Moving on to build targets, for make the code looked something like this:
release: options
@echo Building $@ version...
@${CC} -s ${SRC}
${CFLAGS} -D DEBUG_MODE=${DEBUG_MODE_OFF} \
-D
VERBOSE_MODE=${VERBOSE_MODE_OFF} -o sighte ${LIBS}
staging: options
@echo Building $@ version...
@${CC} -g ${SRC}
${CFLAGS} -D DEBUG_MODE=${DEBUG_MODE_ON} \
-D
VERBOSE_MODE=${VERBOSE_MODE_OFF} -o sighte ${LIBS}
debug: options
@echo Building $@ version...
@${CC} -g ${SRC} ${CFLAGS}
-D DEBUG_MODE=${DEBUG_MODE_ON} \
-D
VERBOSE_MODE=${VERBOSE_MODE_ON} -o sighte ${LIBS}
Not that bad, I believe there are a number of good ways to optimize it in make, so this could be improved. In meson each of the different build targets have simple comma-separated argument arrays, which can help improve the readability of the buildfile.
files_list = ['sighte.c']
executable('release', files_list,
c_args : ['-O2', '-g0', flags, debug_off, verbose_off],
dependencies : all_deps)
executable('staging', files_list,
c_args : ['-g', flags, debug_on, verbose_off],
dependencies : all_deps)
executable('debug', files_list,
c_args : ['-g', flags, debug_on, verbose_on],
dependencies : all_deps)
A tad different than what I am used to, but the plain view of the compiler arguments and dependencies feels vast and open versus the old Makefile lines. With this code in place my meson.build file is more-or-less complete. To build the ninja scripts, simply call meson with the name of the project whilst in the directory containing the meson.build file.
meson sighte
For the curious, compare the original Makefile and the new meson.build file. All-in-all I am satisfied with both make and meson, as these tool-chains accomplish their goal of automating the build process for the developer. Yet it has intrigued me enough that alternatives exist that could build on (pun intended) the ideas that GNU Make started all of those years ago.
At some future blog post I may attempt to further explore ninja and the other features of meson to a greater degree, since I figure there are still many other avenues to check out.