Der Nix Package Manager

Info

Datum: 18. 09. 2023 um 22:38:48

Schlagworte: Linux OSX und MacOS

Kategorie: Linux/Unix

erstellt von Stephan Bösebeck

logged in

ADMIN


Der Nix Package Manager

TOC


NIX package manager und NixOS

Ich bin mehr oder minder zufällig über Nix gestolpert als ich durch Zufall mal angesehen habe, was brew.sh auf meinem Mac so alles installiert hatte. Ich war geschockt zu sehen, dass mittlerweile > 500 Pakete installiert wurden. Von den meisten wusste ich nicht mal, wofür die gut waren (logisch, bei der Menge).

Bei genauerer Betrachtung stellte sich raus, dass viele dieser Pakete Abhängigkeiten waren, oder tools, die ich ein mal ausprobiert habe. Und dann vergessen habe, dass sie da sind 😉

Ich meine Dot-Files schon seit geraumer Zeit in einem Git-Repository. Ich habe da auch ein installationsscript inzugefügt, welches auf einem neuen Mac versucht die Umgebung wieder einzurichten. Das klappt so einigermaßen. Aber nicht immer problemlos. Auf meinen Linux Servern funktioniert es gar nicht - wie auch, brew.sh gibts da nicht. D.h. ich musste mir ein WENN OS==Linux-Konstrukt bauen.

Das wurde mir zu blöd, ehrlich gesagt. Ich hab versucht mich mit xxh zu behelfen (ein tool, welches die lokale Umgebung temporär auf einen Zielrechner überträgt, bevor man via SSH ne schell öffnet). Das war aber auch nur bedingt zu gebrauchen.

Nix ist aber weit mehr als nur ein Package Manager. Die große Besonderheit von nix ist die Reproduzierbarkeit von Installationen. Außerdem die Kapselung von Umgebungen. Aber der Reihe nach...

Warnung

Ein Wort der Warnung: nix und nixOS sind zwar schon einige Jahre bzw. Jahrzehnte in Benutzung, aber die Dokumentation ist wirklich ein Problem. Ich war auf Hilfe von einigen Foren angewiesen, insbesondere Reddit war hilfreich. Aber leider ist das nicht so selbsterklärend, wie man es sich wünschen würde.

Also, das was ich mir hier zu zusammengereimt habe, ist sicherlich nicht 100% korrekt, sondern spiegelt mein Lernprozess wieder. Einige Sachen habe ich mir zusammengereimt, einiges wurde mir netterweise in Reddit von jemandem erklärt, ein wenig was kann man auch der Dokumentation entnehmen (aber ehrlich gesagt nicht so viel, wie man denkt). Das ist auch einer der Gründe, warum ich das hier schreibe - vielleicht braucht jemand noch ein wenig Hilfe bei der Nutzung von nix.

Dazu kommt, dass nix-Dateien in einer funktionalen, Domain spezifischen Sprache geschrieben werden müssen. Was es nicht immer einfach macht zu verstehen, was da gerade passiert. Und die Fehlermeldungen sind wirklich alles andere als eingängig / verständlich. Wenn man beispielsweise ein Paket installieren will, das es nicht gibt, ist das hier die Fehlermeldung:

> nix-shell -p gibtsnicht
error:
       … while calling the 'derivationStrict' builtin

         at /builtin/derivation.nix:9:12: (source not available)

       … while evaluating derivation 'shell'
         whose name attribute is located at /nix/store/0i3h2pbjvxf160a0m9bwbh29742k1xmc-nixpkgs/nixpkgs/pkgs/stdenv/generic/make-derivation.nix:300:7

       … while evaluating attribute '__impureHostDeps' of derivation 'shell'

         at /nix/store/0i3h2pbjvxf160a0m9bwbh29742k1xmc-nixpkgs/nixpkgs/pkgs/stdenv/generic/make-derivation.nix:433:7:

          432|       __propagatedSandboxProfile = lib.unique (computedPropagatedSandboxProfile ++ [ propagatedSandboxProfile ]);
          433|       __impureHostDeps = computedImpureHostDeps ++ computedPropagatedImpureHostDeps ++ __propagatedImpureHostDeps ++ __impureHostDeps ++ stdenv.__extraImpureHostDeps ++ [
             |       ^
          434|         "/dev/zero"

       error: undefined variable 'gibtsnicht'

       at «string»:1:107:

            1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell{ buildInputs = [ (gibtsnicht) ]} ""

von "undefined variable" auf "das Paket gibt es nicht" zu kommen, erfordert schon ein wenig Hirnschmalz.

Diese Sprache kann man allerdings auch testen, quasi on the fly:

-> nix repl
Welcome to Nix 2.17.0. Type :? for help.

nix-repl> :l <nixpkgs>
Added 19981 variables.

nix-repl> :b pkgs.hello

This derivation produced the following outputs:
  out -> /nix/store/wpkkavpwx3k1wa14vw6qy86wvl8dri0q-hello-2.12.1

nix-repl> "${pkgs.hello}"
  "/nix/store/wpkkavpwx3k1wa14vw6qy86wvl8dri0q-hello-2.12.1"

in dem Beispiel oben habe ich die variablen importiert, die die einzelnen Softwarepakete repräsentieren (deswegen kam oben auch eine "undefined variable" Meldung - pakete/derivate/flakes sind in diesem Zusammenhang nur Variablen). Dann habe ich das Hello-Paket gebaut :b. Da ist allerdings nichts passiert, die Quellen haben sich ja nicht verändert, man hat also nur den Pfad angezeigt bekommen.

mit diesem REPL kann man auch die installation im aktuellen profil / env durchführen

nix-repl> :i pkgs.hello

das würde jetzt das Gnu-Hello paket im aktuellen profil installieren (ähnlich zu nix-env).

kompliziert wird es, weil nix eben eine funktionale, domeinspezifische Sprache ist. Und die Domain hier ist die beschreibung von Installationen und Softwarepaketen sowie deren Abhähgigkeiten. Das merkt man recht schnell, z.B. beim rechnen, oder besser, so wie man es aus anderen Sprachen gewohnt ist:

nix-repl> 1+5
6

nix-repl> 2*12
24

nix-repl> 12/2
/caluga.de/blog/12/2

nix-repl>

rechnen muss man in nix aber eben nur selten, deswegen ist das kein größeres Problem, erklärt nur recht deutlich, wo man evtl. "durcheinander" kommen könnte.

Außerdem stößt man schnell auf sinnvolle Funktionen, die man aber eigentlich noch gar nicht nutzen soll, weil leider noch Pre-Release. (nix command z.B.). Das führt schnell zu Frustration, weil man immer wieder (gerade am Anfang) irgendwie gegen eine Wand läuft.

Ich hab mir das angesehen, weil es ein Cooles Projekt mit coolen Features ist. Aber es ist sicher nix (pun intended 😉) für jeden.

Der Nix-Package Manager

Zunächst ist der nix-package manager nix weiter als ein Package manager - ich möchte was installieren, und das tut er. Grundsätzlich kann nix auf zwei "Ebenen" eingesetzt werden: Systemweit, d.h. er verwaltet auch die Systemwerkzeuge und tools, oder lokal. Am einfachsten kann man die System Variante im Einsatz in NixOS sehen. Auch dort sind alle builds reproduzierbar, bis hin runter zum Kernel.

Die lokale Variante kann man auf jedem Linux, MacOS oder auch in der "Unix-Shell" von Windows installieren. Das ist ein lokales Package Management ähnlich zu brew.sh.

Das ganze geht nur noch viel weiter, als man jetzt denkt. Denn wo ist der Vorteil, Sotware einfach lokal laufen zu lassen - kein Problem, wird man denken. Aber es geht noch weiter:

Nix installiert nicht nur die Software lokal, sondern auch deren Abhängigkeiten und behält eine Prüfsumme der Binärabhängigkeiten! Das klingt wieder erst mal nur wenig spektakulär, hat aber ziemlich coole Implikationen:

Z.B. das Kommando ls unter OSX hat folgende Abhängigkeiten:

-> otool -L /bin/ls
    /bin/ls:
        /usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0)
        /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)

Wenn sich eine der verlinkten Bibliotheken ändert, z.B. durch ein Systemupdate, dann würde ls evtl. nicht mehr funktionieren. Das ist für Software, die Teil des OS ist natürlich kein Problem. Für nachinstallierte Tools aber evtl. schon. Genau da kommt nix ins spiel. Schauen wir uns die Abhängigkeiten bei dem von mir via nix installierten tool exa an:

-> otool -L $(which exa)
/Users/stephan/.nix-profile/bin/exa:
        /nix/store/sp25w6mky64jq7klf45rgnfbm1vgj8yv-libiconv-50/lib/libiconv.dylib (compatibility version 7.0.0, current version 7.0.0)
        /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 59754.60.13)
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1770.255.0)
        /nix/store/sryf7yi7va83fs966bhf278zwjn1w6sr-zlib-1.2.13/lib/libz.dylib (compatibility version 1.0.0, current version 1.2.13)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1292.60.1)

Besonders die dynamische Abhängigkeit zu einem libiconv ist da von Interesse - diese wurde auch von nix installiert und wird auch davon verwaltet. Der Pfad zu dieser dynamischen Bibliothek enthält die Prüfsumme der binärdatei! Damit können verschiedene libiconv gleichzeitig installiert und verlinkt sein. Kein überschreiben von libs mit ungewollten Seiteneffekten[^zugegeben, kommt in OSX so gut wie nie vor. Unter Linux und vor allem Windows schon eher].

Schauen wir uns das unter linux noch mal an:

> ldd /bin/ls
        linux-vdso.so.1 (0x00007ffd70847000)
        libcap.so.2 => /usr/lib/libcap.so.2 (0x00007f1f87e7f000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f1f87c00000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f1f87ec7000)
-> ldd $(which exa)
        linux-vdso.so.1 (0x00007ffe84ffd000)
        libz.so.1 => /nix/store/p9a2nhhpa2dwyw1sy5gr4482ddqmwpkx-zlib-1.2.13/lib/libz.so.1 (0x00007f41aec4e000)
        libgcc_s.so.1 => /nix/store/4igdc32rmnijcra8y3r1h42987ghzag2-gcc-12.3.0-lib/lib/libgcc_s.so.1 (0x00007f41aec2d000)
        libm.so.6 => /nix/store/ibp4camsx1mlllwzh32yyqcq2r2xsy1a-glibc-2.37-8/lib/libm.so.6 (0x00007f41aeb4d000)
        libc.so.6 => /nix/store/ibp4camsx1mlllwzh32yyqcq2r2xsy1a-glibc-2.37-8/lib/libc.so.6 (0x00007f41ae967000)
        /nix/store/ibp4camsx1mlllwzh32yyqcq2r2xsy1a-glibc-2.37-8/lib/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f41aedd8000)

Hier ist die Trennung noch deutlicher, selbst die zentrale libc ist eine von nix verwaltete Version!

Reproduzierbarkeit

Und damit erreicht man Reproduzierbarkeit: die Abhängigkeiten für eine zu installierende Software werden binär auf Identität geprüft. Damit kann ich sicher stellen, dass wir die identischen binaries als Abhängigkeiten haben. D.h. Wenn ich exa installieren will, wird die LibC mit der Prüfsumme ibp4camsx1mlllwzh32yyqcq2r2xsy1a installiert, falls noch nicht vorhanden. Und damit kann ich sicher gehen, dass der build klappt bzw. das Binary funktioniert (denn auch exa hat eine solche Prüfsumme!).

Das ganze kann man beliebig weiter spinnen und die Kapselung der Software auf die Spitze treiben. Die installierten Pakete sind in sich autark, Nix verwaltet die Abhängigkeiten und die Binärversionen.

nix-shell

Das alles kann man dann nutzen, um temporär mal irgendeine Software zu installieren, die nur kurz zur Verfügung steht. Das Tool dafür ist nix-shell: es startet eine neue Shell, in der ein oder mehrere neu installierte Programme verfügbar sind.

-> hello
zsh: command not found: hello

~
‼️ > nix-shell -p hello

~ ❄️ shell
-> hello
Hello, world!

~ ❄️ shell
-> which hello
/nix/store/wpkkavpwx3k1wa14vw6qy86wvl8dri0q-hello-2.12.1/bin/hello

~ ❄️ shell
-> exit

~
-> hello
zsh: command not found: hello

~
‼️ >

Damit kann auch Versionen ändern, hier ein beispiel für eine andere Version einer Software:

~
-> java -version
openjdk version "17.0.7" 2023-04-18
OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7)
OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode)

~
-> nix-shell -p temurin-bin-20

~ ❄️ shell
-> java -version
openjdk version "20.0.1" 2023-04-18
OpenJDK Runtime Environment Temurin-20.0.1+9 (build 20.0.1+9)
OpenJDK 64-Bit Server VM Temurin-20.0.1+9 (build 20.0.1+9, mixed mode)

~ ❄️ shell
-> exit

~
-> java -version
openjdk version "17.0.7" 2023-04-18
OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7)
OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode)

Diese software wird wie gesagt nur temporär installiert und ist in der login Shell nicht verfügbar. Das betrifft auch alle Abhängigkeiten, die für diese Software evtl. benötigt werden. Auch diese werden für Lebensdauer dieser Shell verfügbar gemacht, danach nicht mehr.

Funktionsweise

Grundsätzlich macht nix nichts anderes als clever mit Umgebungsvariablen (PATH, LD_DYLD_PATH, LD_LIBRARY_PATH etc.) und symbolischen Links umzugehen und so jede Sofware gekapselt zu installieren, obwohl sie dynamisch gelinkt ist. Das ist natürlich im Detail dann viel komplexer, insbesondere die Garbage Collection, also das Abräumen von Abhängigkeiten oder ganz allgemein Paketen, die nicht mehr in Benutztung sind. nix bietet dafür natürlich auch Werkzeuge.

  • nix-store gc - führt eine Garbage Collection im Store durch. Alle nicht mehr genutzten Pakete werden gelöscht
  • nix-env --delete-generations old - siehe unten. Aber nix hebt für die aktuelle Umgebung sog. "Generations" auf, d.h. eine Art Versionshistorie. Die alten versionen kann man damit entfernen
  • nix-collect-garbage -d - auch andere Builds, die nicht im Store sind oder sonst irgendwo hönne "Müll" hinterlassen. Die werden auf diese Weise entfernt

dir-env

Diese installation von Paketen lässt sich noch automatisieren und an ein Verzeichnis binden. Wenn ich also in ein Projektverzeichnis wechsle, werden alle nötigen Tools für dieses Projekt installiert und in der Shell verfügbar gemacht. Das passiert automatisch beim Wechsel in ein bestimmtes verzeichnis:

-> cd stable-diffusion-webui
direnv: loading ~/stable-diffusion-webui/.envrc
direnv: using nix
direnv: nix-direnv: using cached dev shell
Python env already there - resetting
Python venv activated...
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +DETERMINISTIC_BUILD +HOST_PATH +IN_NIX_SHELL +LD +LD_DYLD_PATH +MACOSX_DEPLOYMENT_TARGET +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_aarch64_apple_darwin +NIX_BUILD_CORES +NIX_CC +NIX_CC_USE_RESPONSE_FILE +NIX_CC_WRAPPER_TARGET_HOST_aarch64_apple_darwin +NIX_CFLAGS_COMPILE +NIX_DONT_SET_RPATH +NIX_DONT_SET_RPATH_FOR_BUILD +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_IGNORE_LD_THROUGH_GCC +NIX_LDFLAGS +NIX_LD_USE_RESPONSE_FILE +NIX_NO_SELF_RPATH +NIX_STORE +NM +PATH_LOCALE +PYTHONHASHSEED +PYTHONNOUSERSITE +RANLIB +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +VIRTUAL_ENV +VIRTUAL_ENV_PROMPT +__darwinAllowLocalNetworking +__impureHostDeps +__propagatedImpureHostDeps +__propagatedSandboxProfile +__sandboxProfile +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~PYTHONPATH ~XDG_DATA_DIRS

stable-diffusion-webui שׂmaster [?] is 📦 v0.0.0 via 🐍 v3.10.12 (.pythonenv) ❄️ nix-shell-env 2s
->

In diesem Fall, wenn ich in das Verzeichnis von Stable Diffusion wechsle, wird automatisch ein Python Umgebung in der richtigen Version zur Verfügung gestellt. Auch alle Abhängigkeiten werden installiert[^da Python normalerweise seine dependencies selbst verwaltet via pip ist man da auf ein wenig trickserei angewiesen]. Verlasse ich das Verzeichnis, wird der ursprüngliche Zustand wieder hergestellt.

nix-shell vs. nix-env

im Jargon von nix befindet man sich also User in seiner "Umgebumg" (environment oder kurz env). Und diese kann man natürlich auch "manipulieren". Mit nix-env --install tree wird das Pakage tree in meiner aktuellen Umgebung installiert und verfügbar gemacht. Das ist dann auch über die Lebensdauer der aktuellen Shell hinaus verfügbar.

Damit entspricht das in etwa einem brew install tree auf dem Mac oder apt-get install tree unter linux. Das ist ja ganz nett (wegen der Reproduzierbarkeit und so), aber die Installation eines neuen Systems wird damit auch nicht zwingend vereinfacht.

nix-env bietet aber natürlich alle Funktionen, die man erwarten würde: Installieren, Deinstallieren, auflisten installierter Pakete, suchen nach installierbaren Paketen etc.

ich würde die Pakete, welche ich über nix-env quasi daueraft installiert habe dann eher in die home-manager-Konfiguration packen, damit ich eine zentrale beschreibung behalte.

Nix home-manager

Jetzt kommen wir zum eigentlichen Tool, mit dem bei mir alles losgegangen ist. Ich wollte eine Möglichkeit zu beschreiben, welche Tools ich gerne auf meinem System habe. Und da sich das andauernd ändert, wäre es schön, das über z.B. git auf verschiedene Rechner zu synchronisieren. der Nix home-manager bietet genau das.

Eigentlich ist es nur ein File ~/.config/home-manager/home.nix in dem man reinschreibt, was der home-manager denn so tun soll. Dabei kann alles mögliche darüber verwaltet werden:

  • Installieren von Software für die lokale nix-env
  • Einstellungen in zshrc / bashrc
  • Umgebungsvariablen setzen
  • starship prompt settings
  • zsh plugins
  • bestimmte Dateien via nix anbieten (z.B. andere Config Dateien oder Skripte)
  • Konfiguration insbesondere unter Linux für KDE / Gnome, zugehörige tools etc.
  • und 100e features mehr

Das spannende daran ist wieder, dass nix als Basis für den home-manager und alles, was der Home-manager installiert genutzt wird. Und das bringt noch ein Feature mit sich, welches auch manchmal sicher von Nutzen sein kann:

Sobald man mit home-manager switch die aktuelle Konfiguration "installiert", wird das bestehende Environment als "Generation" gespeichert. Und zu diesen "Generations" kann ich beliebig wechseln, d.h. falls meine Installation gerade probleme bereitet, kann ich zu einer vorherigen Generation wechseln, von der ich weiß, dass noch alles funktioniert hat. Tolles feature, insbesondere unter Linu.

Reproduzierbarkeit vs. Wiederholbarkeit

Wie oben schon beschrieben, wird mit nix versucht, die Ergebnisse eines Softwarebuilds reproduzierbar zu machen. Das bedeutet im Detail, dass egal wann ich den build laufen lasse, egal was sich bei den Dependencies sonst noch so getan hat, ich immer das selbe Ergebnis bekomme!

flake vs. nix

Grundsätzlich funktionert eine nix-installation erst mal so wie es von einem package manager erwartet wird. D.h. diese Abhängigkeiten werden quasi "intern" verwaltet.

In so einem Nix-File kann ich dann Installationen von Paketen festlegen, Configurationen etc. Würde aber bei jedem ausführen, auch immer die aktuellste Version der von mir gewünschten Pakete wählen.

Mit Hilfe von sog. flakes werden die Versionen im jetzt Zustand eingefroren.

Ein Flake ist quasi ein Nix + aktuelle VErsionsnummern. Im detail passiert nicht viel mehr, als dass zu dem Flakefile ein Lockfile erzeugt wird, in der die verwendeten Prüfsummen aufgelistet sind. Und falls man das Flake noch mal ausführen möchte, nimmt nix eben dieses Lockfile zu Hilfe um die richtigen Versionen zu nutzen.

Das kann man auch im Homemanager oder in dir-env nutzen. Das erzeugt auch eine Reproduzierbarkeit in der Entwicklung - meine Entwicklungsumgebung ist immer gleich, auch nach dem Wechsel eines Rechners, daheim vs. im Büro etc.

In diesem Zusammenhang kommt dann auch gleich die Frage, wie man denn dann ein Update der Software hinbekommt...

nix flake update akutualisiert das Flake im aktuellen verzeichnis. Das kann

man auch für den Home-Manager nutzen:

> cd .config/home-manager
> nix flake update
warning: updating lock file '~/.config/home-manager/flake.lock':
• Updated input 'home-manager':
    'github:nix-community/home-manager/75cfe974e2ca05a61b66768674032b4c079e55d4' (2023-08-15)
  → 'github:nix-community/home-manager/f5c15668f9842dd4d5430787d6aa8a28a07f7c10' (2023-08-30)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/8353344d3236d3fda429bb471c1ee008857d3b7c' (2023-08-15)
  → 'github:nixos/nixpkgs/e7f38be3775bab9659575f192ece011c033655f0' (2023-08-30)

danach noch ein home-manager switch um alle updates zu installieren.

Meine Nix-Journey

Das ganze ist schon ein paar Monate her, dass ich auf Nix gestoßen bin. In meiner IT-Bubble wurde mir Nix immer mal wieder vorgeschlagen und ich fand das spannend, aber hab mir nicht die Zeit dafür genommen.

Irgendwann stolperte ich über eine Bemerkung, dass man mit nix auch brew.sh ersetzen kann, bzw. es wurde eben als Alternative zu brew und MacPorts angepriesen.

Und als ich die 500 installierten Pakete auf meinem System bemerkt habe, wollte ich das ausprobieren.

Der Start war aber etwas gruselig. Der Installer von nix will irgendwas in /nix installieren, ein neues Filesystem quasi. Ich war mir da nicht soo sicher, empfinde das als ziemlich gefährlich. Es wird sogar in '/etc/fstab' ein eintrag für /nix gemacht...

An der stelle blieb ich stehen, hab mir gedacht ich probiere das erst mal anders...

NixOS in einer VM

Nix kann man auch systemweit einsetzen, und das passiert in NixOS. Die Installation habe ich bei mir zum Testen in einer VM getan. Ich wollte sehen, ob sich der "Aufwand" lohnt.

In der VM habe ich also NixOS installiert, und mich mit den konzpten (die ich oben beschrieben habe), etwas vertraut gemacht. Das würde ich eigentlich jedem empfehlen, der mit Nix rumspielen will - mach das zuerst mal in einer VM...

NixOS habe ich auch als Entwicklungsumgebung eingerichtet, also grafisches Frontend KDE und Plasma, WezTerm etc.

In dem Sinn unterscheidet sich NixOS nicht wirklich von Ubunto, Debian oder Fedora. Eigentlich ist es noch am ehesten mit ArchLinux zu vergleichen, denn beide Distributionen bieten "rolling updates", was es wirklich einfacher macht, sein System aktuell zu halten.

Bei nix hat man aber einfach ein paar mehr möglichkeiten, als mit einem "klassischen" Ansatz: ich kann eine spezifische Version von irgendwas installieren, und das beißt sich nicht mit bestehender Software. Ich konnte beispielsweise in der VM einen Apache in einer Uraltversion installieren, die so eigentlich gar nicht mehr laufen sollte (libSSL musste auch in uralt verfübar sein).

da konnte ich ein wenig rumspielen, ohne mein System in Gefahr zu bringen. Als ich dann das ganze auch noch in einer anderen Linux-Distribution ausprobiert hatte, war ich mir sicher, dass das am Mac auch geht.

nix package manager auf OSX

die Installation des Nix-Package Managers läuft relativ einfach:

sh <(curl -L https://nixos.org/nix/install)

Dann wird man einige Fragen beantworten müssen (meistens mit YES) und danach ist eigentlich alles fertig. die commandos nix, nix-build, nix-channel, nix-env und nix-store sollten jetzt zur verfügung stehen (evtl. neu neue Shell öffnen!).

damit hätte man brew eigentlich schon ersetzt. Wenn ich etwas installieren will, rufe ich nix-env --install PACKAGE auf, wenn ich was löschen will analog eben nix-env --uninstall PACKAGE. mit --upgrade kann ich alles aktualisieren oder, falls angegeben, nur ein bestimmtes Paket.

mit nix-env --query bekomme ich alle packages, die in meine Environment installiert wurden.. und so weiter.

Will man allerdings nach paketen suchen, ist das aktuell etwas dämlich gelöst. Man muss dafür beim Aufruf eine option für "experimental features" einschalten 🙄 :

nix --extra-experimental-features "nix-command flakes" search nixpkg

Das ist natürlich nicht sinnvoll, deswegen kann man das in seine nix-config packen.

> cat .config/nix/nix.conf
experimental-features = nix-command flakes

Noch zur Erklärung: nix search nixpkgs durchsucht die Standard Nix-Packages nixpkgs. Es gibt (theoretisch) auch andere Sammlungen die man durchsuchen könnte.

Beim ersten aufruf ist das echt langsam und brauch eine Weile, bis alle Paketbeschreibugen runter geladen wurden. Evtl. ist man schneller, wenn man einfach auf nixos.org direkt zu suchen.

home-manager

Nach dem ganzen Vorgeplänkel kommen wir nun zum eigentlichen Star dieses Posts. Der Nix home-manager ist eine Software, die versucht die Installation eines Benutzerverzeichnisses nebst allen benötigten Softwarepaketen und Konfigurationen zu vereinheitlichen und zu vereinfachen.

eigentlich benötigt man für den Home-Manager in der einfachsten "Ausprägung" nur den nix Paketmanager (s.o.) und dann ruft man nur noch diese Zeile auf:

nix-shell '<home-manager>' -A install

jetzt sollte man nur noch eine grundlegende Home-Configuration in ~/.config/home-manager/home.nix speichern, z.B. diese hier:

{ config, pkgs,lib, ... }:
{
  # Home Manager needs a bit of information about you and the paths it should
  # manage.
  home.username = "stephan";
  home.homeDirectory="/Users/stephan";
  # home.homeDirectory = if isMac then "/Users/stephanelse "/home/stephan";
  home.stateVersion = "23.05"; # Please read the comment before changing.
  home.packages = [
    # # Adds the 'hello' command to your environment. It prints a friendly
    # # "Hello, world!" when run.
    # pkgs.hello
    pkgs.nodejs
    pkgs.libiconv
    pkgs.git
    pkgs.llvm
    pkgs.jq
    pkgs.python3Full
    pkgs.mosh
    pkgs.pinentry_mac
    pkgs.viu
    pkgs.wget
    pkgs.zoxide
    pkgs.tig
    pkgs.stdenv
    pkgs.coreutils
    pkgs.findutils
    pkgs.exa
    pkgs.htop
    pkgs.btop
    pkgs.zsh-syntax-highlighting
    pkgs.zsh-autosuggestions
    pkgs.ripgrep
    pkgs.tldr
    pkgs.fzf
    pkgs.vifm
    pkgs.neovim
    pkgs.tldr
    pkgs.curl
    pkgs.wget
    pkgs.sqlite
    pkgs.stylua
    pkgs.nerdfonts
    pkgs.oh-my-zsh
    pkgs.starship
    pkgs.kitty
    pkgs.gnupg
    pkgs.thefuck
    pkgs.jetbrains-mono
    # # It is sometimes useful to fine-tune packages, for example, by applying
    # # overrides. You can do that directly here, just don't forget the
    # # parentheses. Maybe you want to install Nerd Fonts with a limited number of
    # # fonts?
    # (pkgs.nerdfonts.override { fonts = [ "FantasqueSansMono]})

    # # You can also create simple shell scripts directly inside your
    # # configuration. For example, this adds a command 'my-hello' to your
    # # environment:
    # (pkgs.writeShellScriptBin "my-hello" ''
    #   echo "Hello, ${config.home.username}!"
    # '')
  ] ;

  # Home Manager is pretty good at managing dotfiles. The primary way to manage
  # plain files is through 'home.file'.
  home.file = {
      ".config/wezterm/wezterm.lua".source=./wezterm.lua;

    # # You can also set the file content immediately.
    # ".gradle/gradle.properties".text = ''
    #   org.gradle.console=verbose
    #   org.gradle.daemon.idletimeout=3600000
    # '';
      ".ideavimrc".source=./ideavimrc;
      ".config/bin".source=./bindir;
  };


  # You can also manage environment variables but you will have to manually
  # source
  #
  #  ~/.nix-profile/etc/profile.d/hm-session-vars.sh
  #
  # or
  #
  #  /etc/profiles/per-user/stephan/etc/profile.d/hm-session-vars.sh
  #
  # if you don't want to manage your shell through Home Manager.
  home.sessionVariables = {
    EDITOR = "nvim";
    PATH="$PATH:$HOME/.config/bin";
    LIBRARY_PATH = ''${lib.makeLibraryPath [pkgs.libiconv]}''${LIBRARY_PATH:+:$LIBRARY_PATH}'';
  };

  # Let Home Manager install and manage itself.
  programs.home-manager.enable = true;
  programs.java.enable=true;

  # configuration of my starship prompt
  programs.starship = {
      enable=true;
      enableBashIntegration=true;
      enableZshIntegration=true;
      settings={
          add_newline = true;
          scan_timeout=10;
          character = {
              success_symbol ="-> ";
              error_symbol="‼️ >";
          };
          battery = {
              disabled = true;
          };
          username={
              style_user="bright-white bold";
              style_root="bright-red bold";
          };
          hostname={
              style="bright-green bold";
              ssh_only=true;
          };
          nix_shell={
              symbol="❄️ ";
              format = "[$symbol$name]($style) ";
              style="bright-purple bold";
          };
          git_branch={
              only_attached=true;
              format="[$symbol$branch]($style) ";
              symbol="שׂ";
              style="bright-yellow bold";
          };
          git_commit = {
              only_detached=true;
              format="[ﰖ$hash]($style) ";
              style = "bright-yellow bold";
          };
          git_state={
              style="bright-purple bold";
          };
          git_status = {
              style = "bright-green bold";
          };
          directory = {
               read_only = " ";
               truncation_length = 0;
          };
          cmd_duration={
              format="[$duration]($style)";
              style="bright-blue";
          };
          jobs={
              style="bright-green";
          };

          # format = "$all$directory$character";
          # format = "$user@$host:(bold blue)$directory(bold blue)";
      };
  };
  programs.zsh= {
      enable = true;
      enableCompletion = true;
      shellAliases = {
          vi = "nvim";
          ls = "exa --icons --git";
          ll = "exa --icons --git -l";
          nixUpdateSys = "sudo nix-channel update; sudo nixos-rebuild switch";
          nixUpdate = "nix-channel --update;home-manager switch";
          nixSearch = "nix --extra-experimental-features \"nix-command flakes\" search nixpkgs";
          nixgc = "nix-store --gc; nix-env --delete-generations old; nix-collect-garbage -d";
      };
      autocd=true;
      oh-my-zsh= {
          enable=true;
          custom="$HOME/.config/omz-custom";
          plugins=[
              "gitfast"
              "thefuck"
              "rust"
              "themes"
              "emoji"
              "macos"
              "common-aliases"
              "jsontools"
              "mosh"
              "pass"
              "fzf"
          ];
          theme = "robbyrussell";
      };
      plugins=[
        {
            name = "autosuggestions";
            src = "${pkgs.zsh-autosuggestions}/share/zsh/site-functions";
        }
        {
            name = "fast-syntax-highlighting";
            src = "${pkgs.zsh-fast-syntax-highlighting}/share/zsh/site-functions";
        }
        {
            name = "zsh-nix-shell";
            file = "nix-shell.plugin.zsh";
            src = pkgs.fetchFromGitHub {
              owner = "chisui";
              repo = "zsh-nix-shell";
              rev = "v0.5.0";
              sha256 = "0za4aiwwrlawnia4f29msk822rj9bgcygw6a8a6iikiwzjjz0g91";
            };
        }
      ];
      initExtra= ''
          eval "$(zoxide init zsh)"
      '';
  };
  programs.git = {
      enable=true;
      userName = "Stephan Bösebeck";
      userEmail = "sb@caluga.de";

  };

  programs.fzf = {
      enable = true;
      enableZshIntegration = true;
  };

  programs.gpg.enable=false;
  home.file.".gnupg/gpg-agent.conf".text = ''
    pinentry-program ${pkgs.pinentry_mac}/Applications/pinentry-mac.app/Contents/MacOS/pinentry-mac
    personal-digest-preferences SHA256
    cert-digest-algo SHA256
    default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed
  '';
#   services.gpg-agent = {
#    enable = true;
#    # pinentryFlavor = "mac";
#      # pinentryFlavor = null;
#    pinentryFlavor="aarch64-linux";
#       extraConfig = ''
#         pinentry-program ${pkgs.pinentry-mac}/bin/pinentry-rofi
#         auto-expand-secmem
#       '';
};
  programs.ssh= {
    enable=true;
    compression = true;
    forwardAgent=true;

    matchBlocks= {
        "frodo.*"={
            user="stephan";
            identityFile="~/.ssh/id";
        };
    };
  };

  # home.file.".config/wezterm/wezterm.lua".source=./wezterm.lua;
}

dir-env

dir-env ist ein wirklich praktisches Tool das mit hilfe von nix automatisch, beim Wechsel in ein Verzeichnis die dafür nötigen Tools, Softwarepakte etc. installiert.

dir-env muss einfach installiert werden. Dazu einfach dir-env in der home-manager Konfiguration aktivieren:

  programs.direnv = {
    enable = true;
    enableZshIntegration = true;
    nix-direnv.enable = true;
  };

Damit funktioniert dir-env theoretisch.

Wenn ihr so ein Verzeichnis einrichten wollt, dann benötigti ihr dazu zwei dinge:

In dem Verzeichnis benötigt es ein File names .envrc. Das besteht, soweit ich das kenne, nur aus einer Zeile: use nix oder use flake.

bei use nix, wird ein file namens shell.nix ausgeführt, sobald ihr in das Verzeichnis wechselt. Wenn ihr use flake eingestellt hab, entsprechend ein flake.nix. Den Unterschied zwischen flake und nix habe ich weiter oben mal beschrieben, aber noch mal in diesem Fall: bei use nix wird quasi immer die aktuelle Version der referenzierten Software installiert. Bei use flake immer die zuletzt ausgewählte.

ihr bekommt dann eine Fehlermeldung, sowas wie

-> mkdir tmp

~
-> echo "use nix" > tmp/.envrc

~
-> cd tmp
direnv: error /Users/stephan/tmp/.envrc is blocked. Run `direnv allow` to
approve its content

wie in der Fehlermeldung schon erwähnt wird, müsst ihr nur direnv allow machen um das nix auszuführen.

Das praktische ist, dass dir-env das flake oder nix file die ganze zeit überwacht, also nicht nur, wenn ihr ins verzeichnis wechselt. Wenn ich also eine änderung an dem file mache, wird es automatisch geladen auch ohne, dass ich aus dem verzeichnis raus und wieder zurück muss. Praktisch...

ACHTUNG: Das ist keine nix-shell! Bei exit oder CTRL-D ist man komplett draußen (passiert mir andauernd 😉 )

dir-env für Software Entwickler

dir-env ist glaub ich das wichtigste tool geworden in meinem Setup. Da ich aktuell in der Arbeit und auch privat zwischen insgesamt ca. 20 Projekten hin und her springe, und alle verschiedene Voraussetzungen haben, ist das ein Segen!

  • ich habe ein Projekt, in dem benötige ich zwingend JDK1.8 (ja, gruselig, ich weiß).
  • mehrere Projekte nutzen JDK11
  • wieder andere JDK17
  • und meine BlogSoftware JDK20
  • einige Projekte in Rust
  • ein paar andere in Python, und das in verschiedenen Versionen
  • und einige einen Mix aus Python und Java

klar, kann man so was auch mit SDKMAN oder ähnlichem lösen. Doch die dir-env-Methode empfinde ich als sauberer.

Und ja, wenn man eine IDE nutzt, ist das Problem auch geringer. Aber ich arbeite eben sehr viel über die Kommandozeile, weil ich da schneller von A nach B komme.

Mein erstes Derivat

Was war ein Derivat noch gleich? Das ist im Endeffekt die Beschreibung, was es zu installieren gilt. Die Beschreibung, wie eine Software gebaut und installiert wird. Das sind die Beschreibungen, die auch in nixpkgs drin sind. Und so ein Derivat zu bauen ist im endeffekt eine Installationsanleitung für nix zu machen. Das ist zu vergleichen mit dem bauen einer apt-Paketes (deb) oder rpm oder ähnliches. Nur, und das ist das gute dran, viel einfacher:

let
   pkgs=import <nixpkgs> {};
in
   pkgs.stdenv.mkDerivation{
       name="NAME OF THE PACKAGE";
       buildInputs=[ LIST OF DEPENDENCIES HERE ];
        src = ./.;
       dontStrip=true;
       buildPhase = ''
           echo "Shell commands, how to build go here"
           make compile
       '';
       installPhase=''
          echo "Installing software in $out"
        '';
       system = builtins.currentSystem;
   }

Das ist in aller Kürze, wie so ein Derivat aussehen könnte. Wichtig ist hier, dass die Variable $out das einzige ist, was während install und build phase beschrieben werden darf. Netzwerktraffic ist auch nicht erlaubt während des bauen.

Und das bringt uns gleich zum Java Problem

Java Probleme

Wobei das kein echtes Problem mit Java ist, eher mit Maven. Maven möchte die Abhängigkeiten der Software in das lokale Maven repository schreiben. Das geht aber nicht, da nix das Schreiben in andere Verzeichnisse unterbindet. Außerdem hat der Build prozess keinen Zugriff auf das internet...

Lösung bieten sogn. "fixed derivate", also derivate, die vorher schon die Prüfsumme festlegen. Solch ein Derivate muss man für die Abhängigkeiten in Java erstellen und damit kann man dann die Prüfsumme festlegen.

Deployment eines Java Projekts

das herauszufinden hat mich doch ein wenig Zeit gekostet, NIX wird wohl nicht oft von Java-Entwicklern benutzt, bzw. nicht um damit Software zu deployn. Aber das oben genannte in ein File zu packen hat mir mit einem "neuen" Weg für das Deployment verschafft.

für das Deployment von JBLOG2, hier das derivat:

let
   version="2.0.1-SNAPSHOT";
   pkgs=import <nixpkgs> {};
   deps=pkgs.stdenv.mkDerivation {
        name="jblogServer-${version}-deps";
        buildInputs = [ pkgs.temurin-bin-20 pkgs.maven ];
        src=./.;
        buildPhase=''
            echo "building dependency repo"
            while mvn package -Dmaven.repo.local=$out/.m2 -Dmaven.wagon.rto=5400; [ $? = 1 ]; do
                echo "Timeout, restarting"
            done
            find $out/.m2 -type f -regex '.+\(\.lastUpdated\|resolver-status\.properties\|_remote\.repositories\)' -delete;
        '';

        installPhase = ''find $out/.m2 -type f -regex '.+\(\.lastUpdated\|resolver-status\.properties\|_remote\.repositories\)' -delete'';
        outputHashAlgo = "sha256";
        outputHashMode = "recursive";
        outputHash = "L/2Y5Kq8HZQSHFhG56UgmzlCSSQYLUak9GW08ZrEixc=";
   };
in
   pkgs.stdenv.mkDerivation{
       name="jblogServer";
       buildInputs=[ pkgs.temurin-bin-20 pkgs.maven pkgs.makeWrapper ];
       src = ./.;
       dontStrip=true;
       buildPhase = ''
           ${pkgs.temurin-bin-20}/bin/java -version
           ${pkgs.maven}/bin/mvn package -Dmaven.repo.local=$(cp -r ${deps}/.m2 ./ && chmod +x -R .m2 && pwd)/.m2
           ${pkgs.temurin-bin-20}/bin/jar i target/jblog2-2.0.1-SNAPSHOT.jar
       '';
       installPhase=''
           echo "${pkgs.makeWrapper}"
           mkdir -p $out/bin
           cp target/jblog*SNAPSHOT.jar $out/
           makeWrapper ${pkgs.temurin-bin-20}/bin/java $out/bin/jblogServer --add-flags "-Dspring.profiles.active=\$JBLOG_ENV -jar $out/jblog2-2.0.1-SNAPSHOT.jar"
        '';
       system = builtins.currentSystem;
   }

Erklärung: in dem let teil, wird ein fixed derivate names deps erzeugt. Dieses wird "gebaut" indem es alle Abhängigkeiten für das Projekt in das $out-Verzeichnis packt. Dort werden alle dateien entfernt, die irgendwelche Timestamps beinhaltetn (sonst würde sich die Prüfsumme bei jedem Run ändern!).

Und das muss dann den OutputHash ergeben. Gibt es eine Diskrepanz zwischen definiertem Hash und dem berechneten, schlägt der Build fehl. Normalerweise bedeutet dass, dass man eine Dependency geändert hat (hinzugefüht, entfernt, Versionnummer geändert), oder man hat eine "schwammige" Dependency in Maven definiert (sowas wie LATEST) und es gibt ein Update.

Aus diesem Grund sollten alle Abhängigkeiten im pom.xml genau festgelegt sein. Sonst klemmt es manchmal, und man weiß nicht wieso.

Dieses Dependency Derivat wird in der Build-Phase benutz (Dort steht ${deps}/.m2). Dort wird das repository umkopiert in das lokale $out-Verzeichnis und als mvn repo angegeben.

Bei Install wird ein wrapperscript erzeugt, welches das JAR-File starten kann und das Jar-File entsprechend kopiert.

voila.

und mit nix-build jblog.nix Kann ich die Version dann bauen. sie ist dann im Verzeichnes "result" verfügbar.

Oder, und das ist das beste, man startet das PRojekt gleich nach dem bauen:

nix run -f jblog.nix

Damit wird der code compiliert, mit den Dependencies (sowohl JDK als auch etwaig benötigte sonstige tools) und das Script in $out/bin wird gestartet.

Fazit

Nix ist schon eine Ewigkeit verfügbar und dennoch viel zu wenig bekannt. Damit lassen sich einige Probleme der Sofwareentwicklung auf einfach Art und Weise lösen. Man muss allerdings ein wenig Bastelleidenschaft mitbringen und den Willen, auch eine neue Programmiersprache zu lernen.

Dummerweise ist die Dokumentation nicht die allerbeste und man liest oft widersprüchliches. Aber wenn man es mal am laufen hat, hat man wirklich so eine Art "Docker ultralight" gebastelt. (und einfacher als Docker und resourcenschonender ist es allemal).

Ich kann jedem der Linux, Unix oder MacOS benutzt und sich des öfteren mal in der Kommendozeile bewegt nur empfehlen, sich nix mal anzusehen. Es ist allemal gleichmächtig zu brew.sh, bietet aber mehr Möglichkeiten und sogar mehr Packages...