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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
|
---
title: Design rationale
...
This is the original design rationale for Meson. The syntax it
describes does not match the released version
==
A software developer's most important tool is the editor. If you talk
to coders about the editors they use, you are usually met with massive
enthusiasm and praise. You will hear how Emacs is the greatest thing
ever or how vi is so elegant or how Eclipse's integration features
make you so much more productive. You can sense the enthusiasm and
affection that the people feel towards these programs.
The second most important tool, even more important than the compiler,
is the build system.
Those are pretty much universally despised.
The most positive statement on build systems you can usually get (and
it might require some coaxing) is something along the lines of *well,
it's a terrible system, but all other options are even worse*. It is
easy to see why this is the case. For starters, commonly used free
build systems have obtuse syntaxes. They use for the most part global
variables that are set in random locations so you can never really be
sure what a given line of code does. They do strange and unpredictable
things at every turn.
Let's illustrate this with a simple example. Suppose we want to run a
program built with GNU Autotools under GDB. The instinctive thing to
do is to just run `gdb programname`. The problem is that this may or
may not work. In some cases the executable file is a binary whereas at
other times it is a wrapper shell script that invokes the real binary
which resides in a hidden subdirectory. GDB invocation fails if the
binary is a script but succeeds if it is not. The user has to remember
the type of each one of his executables (which is an implementation
detail of the build system) just to be able to debug them. Several
other such pain points can be found in [this blog
post](http://voices.canonical.com/jussi.pakkanen/2011/09/13/autotools/).
Given these idiosyncrasies it is no wonder that most people don't want
to have anything to do with build systems. They'll just copy-paste
code that works (somewhat) in one place to another and hope for the
best. They actively go out of their way not to understand the system
because the mere thought of it is repulsive. Doing this also provides
a kind of inverse job security. If you don't know tool X, there's less
chance of finding yourself responsible for its use in your
organisation. Instead you get to work on more enjoyable things.
This leads to a vicious circle. Since people avoid the tools and don't
want to deal with them, very few work on improving them. The result is
apathy and stagnation.
Can we do better?
--
At its core, building C and C++ code is not a terribly difficult
task. In fact, writing a text editor is a lot more complicated and
takes more effort. Yet we have lots of very high quality editors but
only few build systems with questionable quality and usability.
So, in the grand tradition of own-itch-scratching, I decided to run a
scientific experiment. The purpose of this experiment was to explore
what would it take to build a "good" build system. What kind of syntax
would suit this problem? What sort of problems would this application
need to solve? What sort of solutions would be the most appropriate?
To get things started, here is a list of requirements any modern
cross-platform build system needs to provide.
### 1. Must be simple to use
One of the great virtues of Python is the fact that it is very
readable. It is easy to see what a given block of code does. It is
concise, clear and easy to understand. The proposed build system must
be syntactically and semantically clean. Side effects, global state
and interrelations must be kept at a minimum or, if possible,
eliminated entirely.
### 2. Must do the right thing by default
Most builds are done by developers working on the code. Therefore the
defaults must be tailored towards that use case. As an example the
system shall build objects without optimization and with debug
information. It shall make binaries that can be run directly from the
build directory without linker tricks, shell scripts or magic
environment variables.
### 3. Must enforce established best practices
There really is no reason to compile source code without the
equivalent of `-Wall`. So enable it by default. A different kind of
best practice is the total separation of source and build
directories. All build artifacts must be stored in the build
directory. Writing stray files in the source directory is not
permitted under any circumstances.
### 4. Must have native support for platforms that are in common use
A lot of free software projects can be used on non-free platforms such
as Windows or OSX. The system must provide native support for the
tools of choice on those platforms. In practice this means native
support for Visual Studio and XCode. Having said IDEs invoke external
builder binaries does not count as native support.
### 5. Must not add complexity due to obsolete platforms
Work on this build system started during the Christmas holidays of 2012.
This provides a natural hard cutoff line of 2012/12/24. Any
platform, tool or library that was not in active use at that time is
explicitly not supported. These include Unixes such as IRIX, SunOS,
OSF-1, Ubuntu versions older than 12/10, GCC versions older than 4.7
and so on. If these old versions happen to work, great. If they don't,
not a single line of code will be added to the system to work around
their bugs.
### 6. Must be fast
Running the configuration step on a moderate sized project must not
take more than five seconds. Running the compile command on a fully up
to date tree of 1000 source files must not take more than 0.1 seconds.
### 7. Must provide easy to use support for modern sw development features
An example is precompiled headers. Currently no free software build
system provides native support for them. Other examples could include
easy integration of Valgrind and unit tests, test coverage reporting
and so on.
### 8. Must allow override of default values
Sometimes you just have to compile files with only given compiler
flags and no others, or install files in weird places. The system must
allow the user to do this if he really wants to.
Overview of the solution
--
Going over these requirements it becomes quite apparent that the only
viable approach is roughly the same as taken by CMake: having a domain
specific language to declare the build system. Out of this declaration
a configuration is generated for the backend build system. This can be
a Makefile, Visual Studio or XCode project or anything else.
The difference between the proposed DSL and existing ones is that the
new one is declarative. It also tries to work on a higher level of
abstraction than existing systems. As an example, using external
libraries in current build systems means manually extracting and
passing around compiler flags and linker flags. In the proposed system
the user just declares that a given build target uses a given external
dependency. The build system then takes care of passing all flags and
settings to their proper locations. This means that the user can focus
on his own code rather than marshalling command line arguments from
one place to another.
A DSL is more work than the approach taken by SCons, which is to
provide the system as a Python library. However it allows us to make
the syntax more expressive and prevent certain types of bugs by
e.g. making certain objects truly immutable. The end result is again
the same: less work for the user.
The backend for Unix requires a bit more thought. The default choice
would be Make. However it is extremely slow. It is not uncommon on
large code bases for Make to take several minutes just to determine
that nothing needs to be done. Instead of Make we use
[Ninja](https://ninja-build.org/), which is extremely fast. The
backend code is abstracted away from the core, so other backends can
be added with relatively little effort.
Sample code
--
Enough design talk, let's get to the code. Before looking at the
examples we would like to emphasize that this is not in any way the
final code. It is proof of concept code that works in the system as it
currently exists (February 2013), but may change at any time.
Let's start simple. Here is the code to compile a single executable
binary.
```meson
project('compile one', 'c')
executable('program', 'prog.c')
```
This is about as simple as one can get. First you declare the project
name and the languages it uses. Then you specify the binary to build
and its sources. The build system will do all the rest. It will add
proper suffixes (e.g. '.exe' on Windows), set the default compiler
flags and so on.
Usually programs have more than one source file. Listing them all in
the function call can become unwieldy. That is why the system supports
keyword arguments. They look like this.
```meson
project('compile several', 'c')
sourcelist = ['main.c', 'file1.c', 'file2.c', 'file3.c']
executable('program', sources : sourcelist)
```
External dependencies are simple to use.
```meson
project('external lib', 'c')
libdep = find_dep('extlibrary', required : true)
sourcelist = ['main.c', 'file1.c', 'file2.c', 'file3.c']
executable('program', sources : sourcelist, dep : libdep)
```
In other build systems you have to manually add the compile and link
flags from external dependencies to targets. In this system you just
declare that extlibrary is mandatory and that the generated program
uses that. The build system does all the plumbing for you.
Here's a slightly more complicated definition. It should still be
understandable.
```meson
project('build library', 'c')
foolib = shared_library('foobar', sources : 'foobar.c',\
install : true)
exe = executable('testfoobar', 'tester.c', link : foolib)
add_test('test library', exe)
```
First we build a shared library named foobar. It is marked
installable, so running `meson install` installs it to the library
directory (the system knows which one so the user does not have to
care). Then we build a test executable which is linked against the
library. It will not be installed, but instead it is added to the list
of unit tests, which can be run with the command `meson test`.
Above we mentioned precompiled headers as a feature not supported by
other build systems. Here's how you would use them.
```meson
project('pch demo', 'cxx')
executable('myapp', 'myapp.cpp', pch : 'pch/myapp.hh')
```
The main reason other build systems cannot provide pch support this
easily is because they don't enforce certain best practices. Due to
the way include paths work, it is impossible to provide pch support
that always works with both in-source and out-of-source
builds. Mandating separate build and source directories makes this and
many other problems a lot easier.
Get the code
--
The code for this experiment can be found at [the Meson
repository](https://github.com/mesonbuild/meson). It should be noted
that (at the time of writing) it is not a build system. It is only
a proposal for one. It does not work reliably yet. You probably
should not use it as the build system of your project.
All that said I hope that this experiment will eventually turn into a
full blown build system. For that I need your help. Comments and
especially patches are more than welcome.
|