Nix Pills, Chapter 8 (Kommentar)

Im achten Kapitel der Nix Pills, Generic Builders, wird ein Build-Skript entwickelt, mit dem GNU Autotools-Projekte allgemein gebaut werden können.

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:

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

Fußnoten

  1. In der Unix-Welt werden Verzeichnisse als Dateien konzeptualisiert. Das Stichwort ist: “Everything is a file”