Wer als Entwickler keine kryptischen Makefiles mehr sehen kann, sollte einen Blick auf das neue Buildsystem Meson werfen. Es verspricht simple Bedienung und doch Scripting-Möglichkeiten. Daneben kann es externe Testwerkzeuge einspannen und unterstützt Linux, Windows und Mac OS X.
Der finnische EntwicklerJussi Pakkanen war von den bestehenden Buildsystemen frustriert. Seiner Auffassung nach nutzen sie stumpfsinnige Syntax in ihren Konfigurationsdateien und legen gerne ein nicht vorhersehbares Verhalten an den Tag [1]. In den Weihnachtsferien 2012 beschloss er deshalb, sein eigenes Buildsystem zu entwickeln. Es sollte möglichst schnell zu Werke gehen, zuverlässig arbeiten, einfach zu benutzen sein, auf allen großen Betriebssystemen laufen und wichtige Testtools wie Valgrind einbinden.
Bereits zwei Monate später veröffentlichte Pakkanen eine erste Version seines Buildsystems, das er als studierter Physiker nach dem Teilchen Meson benannte. Seitdem schreitet die Entwicklung im Eiltempo voran. Die bei Redaktionsschluss aktuelle Version 0.17.0 läuft trotz ihrer niedrigen Nummer bereits äußerst stabil und enthält alle vom Programmautor geplanten Features.
Herzeigbare Features
Wie der Artikel im Folgenden zeigt, baut das System Executables sowie Bibliotheken und unterstützt mehrere Verzeichnisse für unterschiedliche Builds aus derselben Quelle. Die flexible Konfigurationssprache ist leicht zu erlernen, lässt dem Entwickler viele Möglichkeiten und kennt If-Statements.
Meson selbst ist komplett in Python 3 geschrieben und steht unter der Apache License 2.0. Einen kleinen Wermutstropfen gibt es allerdings: Derzeit verdaut Meson lediglich Quellcode in den Programmiersprachen C, C++, Java und Vala. Meson stopft nicht selbst den Quellcode in den passenden Compiler, sondern erzeugt lediglich Konfigurationsdateien für ein anderes bestehendes Buildsystem. Unter Linux generiert Meson Buildfiles für das relativ unbekannte, aber recht flinke Mini-Buildsystem Ninja [2]. Alternativ spuckt Meson auch Projektdateien für Visual Studio 2010 oder Xcode aus. Alle diese externen Hilfsprogramme bezeichnet Meson als Backends.
Wer das Buildsystem unter Linux nutzen möchte, muss daher zunächst über die Paketverwaltung Ninja und Python 3 nachinstallieren. Unter Ubuntu übernimmt das:
sudo apt-get install python3 ninja-build
Danach lädt der Entwickler die aktuelle Meson-Version bei Sourceforge herunter [3] und entpackt das Archiv.
Die Installation übernimmt das Skript »install_meson« , das man als Benutzer Root startet. Standardmäßig kopiert es das Buildtool in Unterverzeichnisse von »/usr/local/« . Über den Parameter »–prefix Pfad« dürfen Anwender aber auch ein anderes Ziel wählen.
Nutzer müssen Meson übrigens nicht zwingend installieren, sondern können es auch direkt aus einem beliebigen Verzeichnis aufrufen. Der kurze Befehl »meson« ist dann im Folgenden aber stets durch »Pfad/meson.py« zu ersetzen.
Bauplan
Als Nächstes muss der Entwickler im Verzeichnis mit seinem selbst geschriebenen Quellcode eine kleine Konfigurationsdatei namens »meson.build« anlegen. Ein Beispiel für eine solche Datei präsentiert Listing 1, das Python-Programmierern vertraut erscheinen dürfte: Meson nutzt innerhalb der Datei »meson.build« eine eigene Programmiersprache, die sich leicht an Python anlehnt. »project()« und »executable()« sind dabei zwei Funktionen, die Meson in etwas inkonsistenter Terminologie auch als Kommandos bezeichnet. Jede Anweisung muss in einer eigenen Zeile stehen.
Listing 1
Mehrere Quellcodedateien
01 project('Hallo Welt', 'c') # Name des Projekts
02 src = ['main.c', 'datei1.c', 'datei2.c']
03 executable('hallo', sources: src)
Ein Projekt starten
Ein neues Projekt definiert »project()« , es erhält dazu zwei Argumente: den Projektnamen und die verwendete Programmiersprache. In Listing 1 heißt das Projekt »Hallo Welt« , dessen Quellcode in der Programmiersprache C vorliegt. Mit diesem Wissen kann Meson automatisch den korrekten Compiler wählen. C++-Programmierer verwenden als zweiten Parameter das Kürzel »cpp« . Strings sind mit einfachen Anführungszeichen einzurahmen. Das »#« leitet einen Kommentar ein, bis zum Zeilenumbruch ignoriert Meson dann alles.
Die zweite Zeile in Listing 1 definiert eine neue Variable mit dem Namen »src« . Diese wiederum nimmt ein Array mit den Namen der zu übersetzenden Quellcode-Dateien auf. Die eckigen Klammern erzeugen hier das Array. Neben Arrays verdauen Variablen auch Ganzzahlen (»zahl = 123« ), Wahrheitswerte (»stimmt = true« ) und Strings (»name = ‘Peter’« ). Den Typ muss man nicht explizit angeben (dynamische Typisierung), die Werte lassen sich allerdings nicht ineinander umwandeln. Abschließend muss der Entwickler in »meson.build« stets ein so genanntes Build-Target definieren. Es legt fest, ob Meson eine Bibliothek, ein Programm oder beides ausspucken soll.
In Listing 1 sorgt die Funktion »executable()« dafür, dass der Compiler ein Programm generiert. Als Argumente erhält sie den Programmnamen und die Liste mit den zu übersetzenden Dateien.
Neben diesen beiden Parametern kennt »executable()« noch einige weitere. Um die Lesbarkeit zu erhöhen, dürfen Entwickler vor das jeweilige Argument den Namen des zugehörigen Parameters setzen. In Listing 1 ist das bei »sources: src« geschehen. Dieses Konzept kennen andere Sprachen auch als Keyword Arguments oder Named Parameters.
Neben »execute()« gibt es noch die beiden weiteren Build-Targets »static_library()« und »shared_library()« , die eine statische beziehungsweise dynamische Bibliothek erzeugen. Der folgende Aufruf baut aus den Dateien »lib1.c« und »lib2.c« eine dynamische Bibliothek mit dem Namen »libhallo« in der Version 1.2.3:
shared_library('hallo', ['lib1.c', 'lib2.c'],version : '1.2.3')
Die Vergabe einer Versionsnummer ist dabei optional. »static_library()« ruft man analog auf, muss dort jedoch die Versionsnummer weglassen. Entwickler dürfen mehrere verschiedene Build-Targets gleichzeitig vorgeben, Meson erstellt sie dann automatisch nacheinander.
Die drei Zeilen aus Listing 1 sind schon alles, was Meson benötigt. Den passenden Compiler einschließlich der notwendigen Parameter wählt das Tool selbst.
Trennkost
Um seinen Quellcode übersetzen zu können, muss der Entwickler zunächst ein neues Unterverzeichnis anlegen. In ihm landen alle vom Compiler erzeugten Ausgaben. Um dieses so genannte Buildverzeichnis kommt niemand herum: Meson besteht auf einem separaten Verzeichnis und lässt sich auch nicht mit Tricks dazu bringen, die Objektdateien im Quellcodeverzeichnis abzulegen.
Diese scheinbare Gängelung hat gleich mehrere Vorteile: Zum einen bleiben die vom Compiler ausgespuckten Dateien vom Quellcode getrennt. Das erhöht gerade bei komplexen Projekten nicht nur die Übersicht, die erzeugten Objektdateien geraten auch nicht in die Fänge einer Versionskontrolle. Zum anderen lassen sich gleichzeitig mehrere unabhängige Builds mit unterschiedlichen Konfigurationen erstellen. So könnte ein Buildverzeichnis die Releaseversion, ein zweites hingegen eine Debugversion aufnehmen.
Wie diese Buildverzeichnisse heißen und wo sie liegen, entscheidet der Entwickler. Meist befindet sich das Standard-Buildverzeichnis auf der obersten Ebene des Quellcodeverzeichnisses und heißt schlicht »build« :
cd hallowelt/src mkdir build
Das neue Buildverzeichnis muss der Entwickler einmalig von Meson einrichten lassen (siehe auch Abbildung 1):
meson ~/hallowelt/src ~/hallowelt/src/build
Der erste Parameter nennt das Verzeichnis mit dem Quellcode, das gleichzeitig auch die Datei »meson.build« beherbergt. Der zweite Parameter verrät das Buildverzeichnis. Dort erstellt Meson jetzt alle notwendigen Konfigurationsdateien respektive Buildfiles für Ninja. Von diesem kann sich der Entwickler nun das eigentliche Programm bauen lassen:
cd build ninja
Ninja spannt automatisch alle vorhandenen Prozessorkerne ein. Möchte der Entwickler später sein Programm neu übersetzen, genügt ein Aufruf von »ninja« im »build« -Verzeichnis. Weitere Kommandos sind nicht erforderlich: Meson hat Ninja so eingestellt, dass es geänderte Quellcodedateien automatisch erkennt und nur diese neu baut – selbst bei einer nachträglichen Änderung der Datei »meson.build« .
Standardmäßig fügt der Compiler Debug-Informationen in die Binärdateien ein und gibt alle Warnungen aus. Sofern der C-Compiler GCC zum Einsatz kommt, aktiviert Meson unter anderem die Parameter »-g« und »-Wall« . Wer ein optimiertes Programm für eine Veröffentlichung benötigt, muss dem Meson-Aufruf den Parameter »–buildtype=release« mitgeben. Wie man weiteren Einfluss auf den Übersetzungsprozess nimmt, verrät der Kasten “Volle Kontrolle”.
Volle Kontrolle
Meson entscheidet selbst, welchen Compiler es mit welchen Parametern aufruft. Ab und an möchten Entwickler jedoch den Übersetzungsprozess steuern, beispielsweise um ein Paket für eine ganz bestimmte Distribution zu schnüren. Auch das ist kein Problem: Die zu verwendenden Compiler gibt der Entwickler einfach über die entsprechenden Umgebungsvariablen vor. Im folgenden Beispiel würde Meson statt der GCC den Konkurrenten Clang einsetzen:
CC=clang meson ~/hallowelt/src ~/hallowelt/src/build
Auch Compilerflags lassen sich vorgeben oder überschreiben, indem der Entwickler sie vor dem Aufruf von »meson« in die entsprechenden Umgebungsvariablen packt. Der Parameter »–buildtype=plain« schaltet zudem die Meson-eigenen Vorgaben ab:
CFLAGS="-o2" meson --buildtype=plain ~/hallowelt/src ~/hallowelt/build
Soll Ninja das übersetzte Programm installieren, lässt sich das Zielverzeichnis in der Umgebungsvariablen »DESTDIR« hinterlegen:
DESTDIR=/opt/hallowelt ninja install
Alternativ kann man auch nur ein anderes Präfix vorgeben: »ninja install –prefix /usr« .
Unter Linux verwendet Meson immer Ninja als Backend. Ein Projekt für Visual Studio 2010 erzeugt der Parameter »–backend=vs2010« , das Pendant für Xcode generiert Meson mit »–backend=xcode« . Ninja kann das übersetzte Programm zudem direkt installieren. Dazu genügt der Aufruf von »ninja install« .
Objekt der Begierde
Die von Meson in »meson.build« verwendete Programmiersprache ist objektorientiert. Entwickler dürfen zwar nicht selbst Klassen implementieren, einige Funktionen geben jedoch ein Objekt zurück. »shared_library()« liefert beispielsweise ein Objekt, welches das entsprechende Build-Target repräsentiert. Dieses lässt sich in einer Variablen auffangen:
lib = static_library('hallo', ['lib1.c','lib2.c'])
Das Objekt darf der Programmierer dann als Argument an andere Funktionen übergeben – etwa an »executable()« , welches das zu erstellende Programm direkt gegen die Bibliothek linkt:
executable('hallo', 'main.c', link_with :lib)
Wer die in »lib2.c« definierten Funktionen verwendet, sollte sie zuvor von einem Testprogramm prüfen lassen. Das braucht er nicht gegen die komplette dynamische Bibliothek, sondern nur gegen die Objektdatei von »lib2.c« zu linken.
Netterweise besitzt das von »shared_library()« zurückgelieferte Objekt selbst eine Methode »extract_objects« , die wiederum eine erstellte Objektdatei zurückliefert. Diese Methode ist wie in vielen objektorientierten Sprachen in Punkt-Notation aufzurufen:
obj = lib.extract_objects('lib2.c')
Das zurückgelieferte Objekt kann man jetzt gegen das Programm linken lassen:
executable('hallo', 'main.c', objects : obj)
Auch viele andere Objekte kennen Methoden, die der Programmierer analog aufruft und verwendet.
Abstieg
Wenn der Quellcode in weiteren Unterverzeichnissen liegt, wechselt die Funktion »subdir« in eines der Unterverzeichnisse – im folgenden Beispiel ins Verzeichnis »gui/« :
subdir('gui')
Anschließend wertet Meson die in diesem Verzeichnis befindliche Konfigurationsdatei »meson.build« aus. Sie darf allerdings nicht die Funktion »project« verwenden, da schon die übergeordnete »meson.build« ein Projekt definiert. Lagern die Headerdateien von C- oder C++-Projekten im Unterverzeichnis »include« , holt der Programmierer dieses wie folgt in den Suchpfad des Compilers:
header = include_directories('include')
executable('hallo', 'main.c',include_dirs : header)
Die meisten größeren Projekte verwenden externe Bibliotheken wie »zlib« oder das GTK-Toolkit. Um eine solche Bibliothek einzubinden, sind gerade mal zwei Zeilen erforderlich:
gtk = dependency('gtk+-3.0')
executable('hallo', 'main.c', deps : gtk)
Die Funktion »dependency()« prüft zunächst, ob die Bibliothek auf dem System installiert ist. Fehlt sie, bricht der Übersetzungsprozess ab. Andernfalls linkt Meson sie zum Programm »hallo« hinzu. Die Automatik arbeitet mit allen Bibliotheken, die eine »pkg-config« mitliefern. Einige besonders bekannte Frameworks ohne diese Datei kennt Meson netterweise selbst. Dazu gehören derzeit Boost, Qt 5 sowie Gtest und Gmock. Im Fall von Boost lassen sich so bequem die gewünschten Module auswählen:
boost = dependency('boost', modules :['thread', 'utility'])
Meson kennt auch If-Abfragen, die aufwändigere Prüfungen ermöglichen. Ein Beispiel zeigt Listing 2: Es prüft in einem C-Projekt, ob die Headerdatei »sys/fstat.h« vorhanden ist. Dazu bekommt die erste Zeile von Meson den Compiler überreicht. Diesen befragt die zweite Zeile, ob die Headerdatei existiert (Abbildung 2).
Listing 2
Bedingte Verzweigung
01 compiler = meson.get_compiler('c')
02 if compiler.has_header('sys/fstat.h')
03 # Header existiert
04 else
05 # Header existiert nicht
06 endif
Tests
Meson gestattet einfache Unit-Tests, deren Definition Listing 3 demonstriert. In ihm definiert die Funktion »test()« einen neuen Testfall mit dem Namen »Ein Test« . Er startet das Programm »hallo« und übergibt ihm dabei die Parameter »–debug« und »–encode« . Zuvor setzt Meson noch die Umgebungsvariable »HALLOLANG« auf den Wert »DE_de« und die Umgebungsvariable »HALLODIR« auf den Wert »/opt/hallowelt« . Sollte das Programm »hallo« eine »0« zurückgeben, gilt der Test als bestanden, bei jedem anderen Wert als gescheitert.
Listing 3
Unit-Test
01 prg = executable('hallo', 'main.c')
02 test('Ein Test', prg, args : ['--debug', '--encode'], env : ['HALLOLANG=DE_de', 'HALLODIR=/opt/hallowelt'])
Um den Test durchzuführen, übersetzt der Entwickler das Programm wie gewohnt mit »ninja« und ruft dann »ninja test« auf (Abbildung 3). Die Ausgaben des Testdurchlaufs landen in der Datei »meson-logs/testlog.txt« . Sollte der Entwickler mehrere Tests definiert haben, führt Meson sie standardmäßig parallel aus. Wer das verhindern möchte oder muss, teilt dies »test« mit:
test('Ein Test', prg, is_parallel : false)
Ergänzend kann Meson noch weitere Debug- und Testwerkzeuge einbinden. Übergibt man »meson« beispielsweise den Parameter »–enable-gcov« , prüft es, ob das Code-Überdeckungstool Gcovr installiert ist. In diesem Fall steht das neue Build-Target »coverage-text« bereit:
ninja coverage-xml
Mit dieser Zeile wendet Meson automatisch Gcovr auf das Programm an, dessen Ausgaben im Verzeichnis »meson-logs/« in der Datei »coverage.xml« landen. Nach dem gleichen Prinzip lassen sich derzeit Valgrind und Cppcheck einbinden [4].
Weitere Optionen
Meson bietet noch viele weitere Möglichkeiten. So produziert das Tool auf Wunsch passende »pkg-config« -Dateien, unterstützt die Lokalisierung mit Gettext und erlaubt den Einsatz von Crosscompilern. Vor dem Kompilieren erzeugt das Tool bei Bedarf Konfigurationsdateien wie die verbreitete »config.h« . Dabei hilft das in Meson eingebaute Templatesystem, das etwa in der Zeile »#define VERSION “@version@”« den Platzhalter »@version@« automatisch durch den passenden, in »meson.build« hinterlegten String »1.2.3« ersetzt [5]. Des Weiteren lassen sich auch komplett neue Build-Targets definieren [6]. Um diese Funktionen zu aktivieren und zu nutzen, sind meist nur wenigen Zeilen in der Datei »meson.build« notwendig.
Eine sehr ausführliche und verständliche Anleitung findet sich auf Sourceforge [7]. Offene Fragen beantworten die FAQ [8] und die offizielle Mailingliste [9]. IDEs unterstützen Meson derzeit noch nicht, das Buildsystem bietet jedoch ein API, mit dem jeder Entwickler Meson in seine Lieblings-IDE einbinden kann [10]. Ein einfaches GUI steckt in den Kinderschuhen (Abbildung 4). Wer seine Builds beschleunigen möchte, findet zwei Möglichkeiten im Kasten “Mehr Tempo”.
Mehr Tempo
Das Parsen der Headerdateien von Systembibliotheken kann viel Zeit schlucken. Abhilfe schafft das Konzept der Precompiled Headers. Dabei liest der Compiler die Headerdateien einmal ein und speichert dann seinen internen Status in einer Datei auf der Festplatte. Bei der nächsten Übersetzung greift der Compiler einfach auf den Zustand in dieser Datei zurück.
Um Precompiled Headers in Meson nutzen zu können, muss der Entwickler zunächst alle »#include« -Anweisungen in einer neuen Headerdatei ablegen, die im Folgenden »hallo_pch.h« heißen soll. Diese Datei wandert in ein neues Unterverzeichnis namens »pch« . Zudem darf keine Quellcodedatei »hallo_pch.h« einbinden und das Verzeichnis »pch« darf nicht im Suchpfad liegen. Sind diese Voraussetzungen erfüllt, ergänzt der Entwickler nur noch »executable()« um den Parameter »c_pch« :
executable('hallo', sources: src, c_pch : 'pch/hallo_pch.h')
Um die Übersetzungszeit weiter zu optimieren, unterstützt Meson so genannte Unity-Builds. Dabei packt der Entwickler zunächst die Inhalte der Quellcodedateien in eine einzige große und führt diese dann dem Compiler zu. Diese Maßnahme soll je nach Projekt die Compilezeit um bis zu 50 Prozent reduzieren.
Ändert der Entwickler jedoch eine der Dateien, muss der Compiler den kompletten Quellcode erneut übersetzen. Aus diesem Grund sind Unity-Builds in Meson standardmäßig deaktiviert. Um sie einzuschalten, übergibt man »meson« den Parameter »–unity« . Um den Rest kümmert sich das Buildsystem, der Entwickler muss den Quellcode folglich nicht selbst zusammentackern.
Fazit
Obwohl Meson noch recht jung ist, arbeitet das Buildsystem erstaunlich stabil und erfüllt bereits alle von Jussi Pakkanen aufgestellten Anforderungen. Insbesondere nimmt es dem Entwickler einige Arbeit ab: Wer sich zuvor mit den Automake-Tools herumschlagen musste, dürfte Meson nicht wieder hergeben wollen.
Die aktuelle Version ist bereits praxistauglich und durchaus auch für mittelgroße Projekte geeignet. Jussi Pakkanen weist jedoch ausdrücklich darauf hin, dass es sich bei seiner Implementierung um ein Proof-of-Concept handelt und sich im Laufe der weiteren Entwicklung noch einiges ändern könnte. Demnächst wird Meson übrigens im Paketangebot von Debian zu finden sein, lässt er außerdem wissen. (mhu)
Infos
- Meson Design Rationale: http://sourceforge.net/p/meson/wiki/Design%20rationale/
- Ninja: http://martine.github.io/ninja/
- Meson: http://sourceforge.net/projects/meson/
- Autodetecting Features: http://sourceforge.net/p/meson/wiki/Feature%20autodetection/
- Configuration: http://sourceforge.net/p/meson/wiki/Configuration/
- Custom Build Targets: http://sourceforge.net/p/meson/wiki/Custom%20build%20targets/
- Meson-Handbuch: http://sourceforge.net/p/meson/wiki/Manual/
- Meson-FAQ: http://sourceforge.net/p/meson/wiki/FAQ/
- Mailingliste: https://lists.sourceforge.net/lists/listinfo/meson-devel
- IDE-Integration: http://sourceforge.net/p/meson/wiki/IDE%20integration/










