Nix Pills, Chapter 6 (Kommentar)

Das sechste Kapitel der Nix Pills, Our First Derivation, führt den zentralen Grundbegriff der Nix-Paketverwaltung ein. Das Konzept wird über die `derivation`-Funktion erläutert. Derivations sind der Rückgabewert dieser Funktion. Welche Derivation zurückgegeben wird, hängt von Inputs ab, die in Form einer Attributmenge übergeben werden.

Durch die Instanziierung der Paketbeschreibung werden Store Derivations (.drv-Dateien) erstellt. Erst durch die Realisierung der Store Derivations wird die entsprechende Derivation gebaut und die Outputs im Store abgelegt.

Das sechste Kapitel der Nix Pills, Our First Derivation, führt den zentralen Begriff der Nix-Paketverwaltung ein. Wir erfahren, was Derivations sind und zu welchem Zweck Store Derivations (.drv-Dateien) genutzt werden. Derivations werden durch die derivation-Funktion und auf der Grundlage einer Attributmenge erzeugt.

Der Input der derivation-Funktion: eine gewöhnliche Attributmenge

Derivations werden von der derivation-Funktion erzeugt. Ihr wird eine Attributmenge als Argument übergeben, die bereits einige Ähnlichkeit mit der Derivation aufweist, die von der Funktion zurückgegeben wird. Damit eine Derivation erstellt werden kann, sind drei Attribute als Input zwingend erforderlich:

Über optionale Attribute erfahren wir in Chapter 6 nahezu nichts. Das offizielle Bedienungshandbuch nennt zwei weitere, die häufig verwendet werden.

Dadurch, dass outputs im Beispiel aus dem Beitrag nicht definiert wird, ergeben sich spezielle Eigenschaften der erzeugten Derivations. Diese zeigen sich (wie wir unten sehen werden) an den Werten der out- und all-Attribute.

Der Output der derivation-Funktion: eine Derivation

Die derivation-Funktion gibt eine Derivation zurück. Dieser Output ist selbst wiederum eine Attributmenge. Es wird eine Methode angeführt, wie wir diese Feststellung verifizieren können:

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> builtins.isAttrs d
true

Natürlich handelt es sich nicht um exakt die gleiche Menge (derivation ist natürlich nicht die Identitätsfunktion). Wenn wir uns die Attributnamen der zurückgegebenen Menge anzeigen lassen, dann finden wir einige darunter, die noch nicht in der Inputmenge waren:

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> builtins.attrNames d
[ "all" "builder" "drvAttrs" "drvPath" "name" "out" "outPath" "outputName" "system" "type" ]

Für eine gegebene Output-Menge erhalten wir die Derivation, die als Input für derivation diente, über das drvAttrs-Attribut:

nix-repl> d.drvAttrs
{ builder = "mybuilder"; name = "myname"; system = "mysystem"; }

Wir erfahren ein paar Dinge über die neuen Attribute:

Wenn das outPath-Attribut definiert ist, dann kann die Menge an die eingebaute toString-Funktion übergeben werden, um Pfad zu erhalten.

nix-repl> d.outPath
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
nix-repl> builtins.toString d
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"

Um ehrlich zu sein verstehe ich den Zweck von toString nicht. Wenn wir den gleichen String-Wert erhalten, könnten wir dann nicht auch einfach <Attributmenge>.outPath nutzen?

Der Nebeneffekt der derivation-Funktion: eine .drv-Datei

Wie im vorausgegangen Abschnitt erläutert, erzeugt die derivation-Funktion eine Derivation. Das ist der Rückgabewert der Funktion. Für eine funktionale Programmiersprache untypisch hat sie darüber hinaus einen Nebeneffekt. Es wird eine Store Derivation in Form einer drv-Datei erzeugt.

Der Zweck dieser Dateien wird in Analogie mit C erläutert:

Ehrlich gesagt verstehe ich den Nutzen von Store Derivations (.drv-Dateien) trotzdem nicht. Enthalten die Dateien besondere Informationen, die nicht aus den Input-Mengen abgeleitet werden könnten? Jedenfalls enthalten drv-Dateien so etwas wie Minimalinformationen.

Das Dateiformat ist lesbar, aber nicht ohne Weiteres strukturiert. Mit nix derivation show lassen sie sich pretty-printen. Ihr Inhalt ergibt sich daraus, wie die Attributmenge beschaffen war, die derivation als Input übergeben wurde.

$ nix derivation show /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
{
  "/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {},
    "platform": "mysystem",
    "builder": "mybuilder",
    "args": [],
    "env": {
      "builder": "mybuilder",
      "name": "myname",
      "out": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname",
      "system": "mysystem"
    }
  }
}

Zu den Umgebungsvariablen (definiert unter "env") wird gesagt: “(…) (T)he environment variables passed to the builder are just those you see in the .drv plus some other Nix related configuration (number of cores, temp dir, …). The builder will not inherit any variable from your running shell, otherwise builds would suffer from non-determinism.”

Das Beispiel im Beitrag wird so abgewandelt, dass true hinzugefügt wird. Bei dieser Anwendung handelt es sich um eine Komponente der GNU Coreutils. Das Interessante an dieser Modifikation ist, dass die Coreutils dem "inputDrvs"-Attribut bei Instanziierung automatisch hinzugefügt wird:

$ nix derivation show /nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv
{
  "/nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {
      "/nix/store/hixdnzz2wp75x1jy65cysq06yl74vx7q-coreutils-8.29.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/bin/true",
    "args": [],
    "env": {
      "builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/bin/true",
      "name": "myname",
      "out": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname",
      "system": "x86_64-linux"
    }
  }
}

Beim Bauen einer Derivation werden zunächst die .drv-Dateien der Inputs gebaut, falls die daraus resultierenden Outputs noch nicht im Store vorhanden sind. Erst im Anschluss daran wird die Store Derivation gebaut, dessen inputDrvs sie sind.

Derivation: Instanziierung und Realisierung

Es wird betont, dass eine Derivation durch die derivation-Funktion noch nicht gebaut wurde. Wir haben damit eine Situation, in der Output-Pfade spezifziert wurden; bei der die entsprechenden Verzeichnisse und Inhalte tatsächlich aber (noch) gar nicht existieren.

Die zwei Phasen werden durch jeweils eine Nix-Operation eingeleitet: Durch eine Instanziierung (nix-instantiate <Datei mit Aufruf der Derivationsfunktion>) wird der Ausdruck ausgewertet und die Store Derivation (.drv-Datei) erstellt; und durch eine Realisierung (nix-store -r <.drv Datei>) wird die Store Derivation (die .drv-Datei) gebaut.2 Dazu wird sie als Argument übergeben:

nix-store -r /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv

Die zugrundeliegende Designentscheidung wird folgendermaßen motiviert:

Think, if Nix ever built the derivation just because we accessed it in Nix, we would have to wait a long time if it was, say, Firefox. That’s why Nix let us know the path beforehand and kept evaluating the Nix expressions, but it’s still empty because no build was ever made.

Die Unterscheidung der Schritte hat demnach völlig praktische Gründe.

Im REPL werden Derivations mit :b <Derivation> gebaut. Es wird nicht darauf eingegangen, wie die Operation in anderen Kontexten ausgeführt wird. Wahrscheinlich kombiniert nix-build <Derivation> Instanziierung und Realisierung?3

Die open angeführten name- und outputs-Attribute bestimmen Dateinamen. Die bei der Instanziierung erstellten Store Derivations (.drv-Dateien) finden sich unter /nix/store/<hash>-<name>.drv. Bei der Realisierung werden die eigentlich interessanten Outputs erzeugt. Eine Derivation kann dabei in mehreren Outputs (mit ihren eigenen Verzeichnissen) resultieren. Diese finden sich unter /nix/store/<hash>-<name>[-<output>]. Man spricht von Store Derivation-Pfaden (store derivation paths) und Output-Pfaden (output paths).

In der offiziellen Dokumentation findet sich ein Beispiel. Wenn name = "hello"; gesetzt wurde, dann ergibt sich ein Pfad zur entsprechenden Store Derivation mit dem Format /nix/store/<hash>-hello.drv; und der Output-Pfad hat die Form /nix/store/<hash>-hello[-<output>]. Die letzte (optionale) Pfad-Komponente wird für alle Outputs genutzt, die nicht der Default (out) sind.

derivation {
  name = "example";
  outputs = [ "lib" "dev" "doc" "out" ];
  # ...
}

Mit diesem Input erhalten wir den Store Derivation-Pfad /nix/store/<hash>-example.drv und wir erhalten (entsprechend dem outputs-Attribut) Pfade für alle Outputs: /nix/store/<hash>-example-lib, /nix/store/<hash>-example-dev, /nix/store/<hash>-example-doc, /nix/store/<hash>-example. Für out wird das Basisverzeichnis ohne Output-Suffix erstellt.

Offene Fragen

Fußnoten

  1. Um sich den Typ des aktuellen Systems anzuzeigen, kann builtins.currentSystem im REPL genutzt werden. 

  2. Die Unterscheidung wird erneut in Analogie zu C erläutert: “Think of it as of compile time and link time like with C/C++ projects. You first compile all source files to object files. Then link object files in a single executable.” 

  3. Wie ein späteres Kapitel zeigt, war die Vermutung richtig. nix-build führt die Instanziierung und die anschließende Realisierung der erstellten Store Derivation durch.