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
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:
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:
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
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:
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:
/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:
/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:
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)
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.
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öschtnix-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 entfernennix-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:
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:
> 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:
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 🙄 :
Das ist natürlich nicht sinnvoll, deswegen kann man das in seine nix-config packen.
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:
jetzt sollte man nur noch eine grundlegende Home-Configuration in
~/.config/home-manager/home.nix speichern, z.B. diese hier:
{
# 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/stephan" else "/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:
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
~
-> 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:
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:
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:
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...