Nix Pills, Chapter 8 (Kommentar)
In vorausgegangenen Kapiteln wurden Builder speziell für einzelne Pakete gebaut. Mit diesen Skripten konnten nur die entsprechenden Pakete gebaut werden. Im achten Kapitel der Nix Pills, Generic Builders, werden Pakete mit den GNU Autotools erstellt. Es wird ein Build-Skript entwickelt, das von Besonderheiten einzelner Pakete abstrahiert. Mit dem einen Builder können deshalb verschiedene Pakete gebaut werden.
GNU Autotools ist das meistgenutzte Build-System. Der Zweck des Werkzeugs liegt darin, Unterschiede zwischen verschiedenen Plattforen (verwendeter C-Compiler, Namen von Header-Dateien, Bestehen von Bibliotheksfunktionen etc.) in den Griff zu bekommen. Damit werden Builds für die meisten Programme zu einem zweischrittigen Vorgang: Konfiguration (GNU Autoconf) und Make (GNU Automake).
Im Beitrag wird Darwin (MacOS) angesprochen. Eine Besonderheit der Plattform liegt darin, dass in der Regel clang
statt gcc
als C-Compiler verwendet wird. Hier haben wir es demnach mit einem der angedeuteten Unterschiede zwischen Plattformen zu tun. Im Beitrag wird (noch) nicht darauf eingegangen, wie ein Builder geschrieben werden kann, der auch von Merkmalen der Plattform abstrahiert. Auf diesen Gesichtspunkt wird im Folgenden deshalb nicht weiter eingegangen.
Ein Builder speziell für ein Paket
Im vorausgegangenen Kapitel wurde ein sehr einfaches C-Programm verpackt. Die dazugehörige .c
-Datei wurde im Build-Skript mit eine einfachen Aufruf von gcc
kompiliert. Für ein erstes Beispiel war das in Ordnung. In der Realität nutzen sehr viele Pakete die GNU Autotools.
Hier wird exemplarisch ein sehr einfaches Programm mit diesem Hilfsmittel verpackt, GNU Hello. Im ersten Schritt wird ein Builder-Skript wieder speziell für dieses Vorführprogramm geschrieben (hello_builder.sh
):
export PATH="$gnutar/bin:$gcc/bin:$gnumake/bin:$coreutils/bin:$gawk/bin:$gzip/bin:$gnugrep/bin:$gnused/bin:$bintools/bin"
tar -xzf $src
cd hello-2.12.1
./configure --prefix=$out
make
make install
Das Skript ist in einer Hinsicht untypisch. --prefix=$out
ist notwendig, um den Output im Nix-Store zu erstellen. Wenn ich die offizielle Dokumentation richtig verstehe, dann wird standardmäßig /usr/local
verwendet. Mit dem Flag wird dieser Wert überschrieben und die Outputs landen im richtigen Unterverzeichnis des Store.
Das Skript nutzt eine Reihe von Umgebungsvariablen (gnutar
, gcc
, gnumake
etc.). Wie zuvor muss eine Derivation erzeugt werden, dessen Input-Menge Attribute umfasst, aus denen entsprechende Variablen für die Build-Umgebung erzeugt werden. Hier eine erste Version der hello.nix
:
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "hello";
builder = "${pkgs.bash}/bin/bash";
args = [ ./hello_builder.sh ];
inherit (pkgs) gnutar gzip gnumake coreutils gawk gnused gnugrep;
gcc = pkgs.clang;
bintools = pkgs.clang.bintools.bintools_bin;
src = ./hello-2.12.1.tar.gz;
system = builtins.currentSystem;
}
Beide Paket-Komponenten - das Build-Skript und die Datei mit der Derivation - lassen sich verbessern. Für gewöhnlich sind Derivations Teile einer ganzen Sammlung von Paketen. Wenn sich Pakete ähneln (wie im Falle von Autotools-Projekten), dann können sie sich Komponenten in einem modularen Aufbau teilen, um dadurch Wiederholungen zu vermeiden.
Im Folgenden wird ein Build-Skript entwickelt, das von vielen Paket-spezifischen Merkmalen abstrahiert. Dadurch kann es von allen Projekten verwendet werden, deren Inhalte mit den Autotools gebaut werden.
Wenn man sich den obigen Input für den obigen derivation
-Aufruf (in hello.nix
) anschaut, dann finden sich viele Attribute, die mutmaßlich auch bei Aufrufen der Funktion für andere Derivations übergeben würden. GNU Hello ist so simpel, dass bis auf Name und Quelle wohl keine Attribute einzigartig wären.
Im Folgenden wird eine Menge definiert, die bereits sehr viele Attribute enthält, die für die Erzeugung einer Autotools-Derivation erforderlich sind. Die Menge kann mit einer Paket-spezifischen Attributmenge vereinigt (merge) werden, um zu einem vollständigen Input für die derivation
-Funktion zu gelangen.
Ein generischer Builder für Autotools-Projekte
Unser erstes Ziel ist es demnach, einen Builder für Autotools-Projekte allgemein zu schreiben. Dieses Skript kann den Paketen für diese Projekte beigefügt werden. Hier die für diesen Beitrag finale Version der builder.sh
:
set -e
unset PATH
for p in $buildInputs; do
export PATH=$p/bin${PATH:+:}$PATH
done
tar -xf $src
for d in *; do
if [ -d "$d" ]; then
cd "$d"
break
fi
done
./configure --prefix=$out
make
make install
Es versteht sich, dass einige Grundkenntnisse der Shell-Programmierung und der Autotools vorausgesetzt sind, um den Inhalt dieser Datei zu verstehen. Einige weniger allgemein bekannte Aspekte werden in den Nix Pills erklärt:
set -e
legt fest, dass jeder Fehler zum Abbruch des Build-Vorgangs führt.- Im vorausgegangenen Kapitel wurde darauf hingewiesen, dass die
PATH
-Variable zur Build-Zeit auf/path-not-set
gesetzt wird. Natürlich ist das kein richtiger Pfad. Mitunset PATH
wird der Wert fallengelassen. Dadurch können wir im Folgenden einfach Komponenten an den Pfad anhängen (zu Beginn ist es nun der leere String). - Mit der ersten
for
-Schleife wird dasbin
-Unterverzeichnis allerbuildInputs
dem PATH angehängt. Damit sind die Namen der ausführbaren Binärdateien (gcc
etc.) zur Build-Zeit verfügbar. - Der Quellcode wird Paketen für gewöhnlich in Form eines Archivs hinzugefügt. Mit
tar -xf $src
wird dieses Archiv (in das Temp-Verzeichnis der Build-Zeit) entspackt. - Wenn ich die Annahmen des Skripts richtig verstehe, dann enthält das temporäre Verzeichnis nur ein Unterverzeichnis und dieses Unterverzeichnis enthält (nach dem vorausgegangenen Schritt) die Quelldateien. Die zweite
for
-Schleife iteriert über alle Dateien; wenn die “Datei” ein Verzeichnis ist (if [ -d "$d" ];
),1 dann enthält es den Quellcode und wir wechseln dorthin (cd "$d"
). - Mit den letzten beiden Zeilen werden die beiden Schriite ausgeführt, von denen einleitend die Rede war: Konfiguration und Build.
Damit lässt sich GNU Hello, aber eben auch viele andere Projekte bauen. Abschließend wird gesagt, dass das Skript noch immer viele Annahmen macht. Es ist aber zweifellos weitaus generischer als das oben angeführte erste Builder-Skript.
Modularer Input für die derivation
-Funktion
Derivations werden erstellt, indem der derivation
-Funktion eine Attributmenge übergeben wird. Sehr viele der darin enthaltenen Attribute werden für Pakete der allermeisten Autotools-Projekte übergeben.
Sie alle verwenden das eben geschriebene Skript als Builder; builder
und args
erhalten deshalb in allen Fällen dieselben Werte. Darüber hinaus gibt es eine Reihe von Werkzeugen, die für den Build dieser Gruppe von Programmen benötigt werden (baseInputs
). Es liegt deshalb nahe, diese Attribute bereits in einer Menge zusammenzufassen (defaultAttrs
).
Damit haben wir bereits einen wichtigen Teil einer Komponente, die von hello.nix
und ähnlichen Paketdateien importiert werden kann (sie wird autotools.nix
heißen):
[...]
let
defaultAttrs = {
builder = "${pkgs.bash}/bin/bash";
args = [ ./builder.sh ];
baseInputs = with pkgs; [ gnutar gzip gnumake gcc coreutils gawk gnused gnugrep binutils.bintools ];
buildInputs = [];
system = builtins.currentSystem;
};
in
[...]
Die Menge definiert Attribute für alle im obigen Builder-Skript verwendeten Umgebungsvariablen. Das Skript enthielt darüber hinaus die for
-Schleife, durch die buildInputs
als PATH-Komponenten hinzugefügt wurden. So sind die Symbole der entsprechenden Anwendungen (wie die der baseInputs
) zur Build-Zeit verfügbar. Die Liste bleibt in der Default-Menge leer. In der Paket-spezifischen Menge kann das Attribut genutzt werden, um weitere Dependencies zu bestimmen.
Der Funktion, die eine Derivation für ein bestimmtes Projekt erstellt, kann dann die Vereinigung der Default-Menge und der Projekt-spezifischen Menge als Argument übergeben werden.
let
[...]
in
derivation (defaultAttrs // attrs)
Die Rede von einer Default-Menge deutet das Verhalten des Merge-Operators (//
) an. Attribute, die sich in der einen oder der anderen Menge finden, werden der resultierenden Menge beide hinzugefügt. Wenn sich ein Attribut in beiden Mengen findet, dann wird der Wert der rechten Menge bevorzugt. Das heißt der Wert der Default-Menge wird überschrieben.
Es stellt sich die Frage, warum der Default-Attribbutmenge eine Definition für buildInputs
mit der leeren Liste als Wert hinzugefügt wird. Der Beitrag geht auf diese Frage nicht ein. Vielleicht will man damit den Fall berücksichtigen, in der später eventuell der Default-Menge schon Build-Inputs hinzugefügt werden? Ich glaube beim Merge der Attributmengen werden auch Listen gemergt, wenn sie denselben Schlüssel haben. Vielleicht gibt es Fälle, in denen für das Attribut ein einzelner Wert und keine Liste als Wert festgelegt wird? Dann würde gewährleistet, dass trotzdem ein Attribut mit einer Liste als Wert resultiert.
Funktionale Trennung zwischen Basis-Attributen und Paket-spezifischen Attributen
Unser Ziel ist eine hello.nix
, die von der eben definierten Default-Menge Gebrauch macht. Diese Menge soll mit einer Hello-spezifischen Attributmenge vereinigt und auf dieser Grundlage eine Derivation für das Hello-Projekt erstellt werden.
Zu diesem Zweck muss das im vorausgegangenen Abschnitt entwickelte Modul um einen Aspekt erweitert werden. In defaultAttrs
werden Symbole für eine Reihe von Derivations verwendet. Anders als im obigen hello.nix
wird die pkgs
-Variable aber an keiner Stelle mit Leben gefüllt werden. Und natürlich muss auch die Paket-spezifische Attributmenge (attrs
), mit der in der letzten Zeile gemergt wird, ihren Hello-spezifischen Wert erhalten.
Es liegt nahe, diese beiden Informationen als Argumente bereitzustellen. Folgerichtig wird das in autotools.nix
gespeicherte Modul zu einem Lambdaausdruck umgewandelt.
pkgs: attrs:
let defaultAttrs = {
builder = "${pkgs.bash}/bin/bash";
args = [ ./builder.sh ];
baseInputs = with pkgs; [ gnutar gzip gnumake gcc coreutils gawk gnused gnugrep binutils.bintools ];
buildInputs = [];
system = builtins.currentSystem;
};
in
derivation (defaultAttrs // attrs)
Im Kapitel über die import
-Funktion wurde ein strukturell völlig analoger Fall besprochen. Wir definieren hier eine zu importierende Datei und diese Datei enthält einen Funktionsausdruck. In der importierenden Datei können Argumente an die importierte Funktion übergeben werden.
In gewisser Weise verlangt die Funktion zwei Argumente (es ist ein Fall von Currying). In der finalen Form der hello.nix
wird zunächst ein Argument bereitgestellt. Die dadurch teilweise gesättigte Funktion (partielle Applikation) wird in einer Variable gespeichert:
let
pkgs = import <nixpkgs> {};
mkDerivation = import ./autotools.nix pkgs;
in
[...]
Damit sind alle Komponenten zusammen, die von außen kommen und den allermeisten Autotools-Projekten gemeinsam sind. Die Paket-spezifischen Attribute werden an die mkDerivation
-Funktion übergeben. Hier die finale Version der hello.nix
:
let
pkgs = import <nixpkgs> {};
mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
name = "hello";
src = ./hello-2.12.1.tar.gz;
}
Die Attributmenge, die der Funktion zuletzt übergeben wird, wird vom attrs
-Parameter der autotools.nix
-Funktion aufgenommen.
GNU Hello ist ein sehr einfaches Programm, weshalb nur sehr wenige weitere Attribute notwendig sind. Paket-spezifisch sind nur noch der Name und die Quelldateien. Bei komplexeren Programmen könnten weitere Attribute definiert und zusätzliche Build-Dependencies verlangt werden.
Offene Fragen
- Warum wird der
defaultAttrs
inautotools.nix
eine Definition fürbuildInputs
hinzugefügt?
Fußnoten
-
In der Unix-Welt werden Verzeichnisse als Dateien konzeptualisiert. Das Stichwort ist: “Everything is a file” ↩