Die Fünf-Minuten-IDE: Deskriptive Entwicklungsumgebungen

Seite 2: Gut verwaltet mit Nixpkgs

Inhaltsverzeichnis

Die Nix-Paketverwaltung bietet einen anderen Ansatz, um Entwicklungsumgebungen deskriptiv bereitzustellen. Nixpkgs lässt sich parallel zur Standardpaketverwaltung des Betriebssystems wie apt auf Debian installieren und in der Shell verwenden. Das Werkzeug installiert Pakete nicht in den Standardordnern, sondern unter /nix/store/ mit eindeutigen und unveränderbaren Pfaden pro Paket und Version. Anschließend stellt es die Pakete per Link in der Shell bereit. So können sich auf einem System zeitgleich mehrere Versionen beispielsweise von Node.js oder des Rust-Paketmanagers Cargo befinden.

Die Installation auf Unix-Systemen ist denkbar einfach und gilt für den aktuell angemeldeten Nutzer. Beim Einrichten verlangt das Tool sudo-Zugriff auf root. In der Dokumentation zur Installation sind darüber hinaus auch Wege beschrieben, Nixpkgs ohne sudo installieren zu können.

$ curl -L https://nixos.org/nix/install | sh

Auf Windows-Systemen lässt sich Nixpkgs im Windows Subsystem für Linux WSL 2 installieren. Es stellt allerdings keinen Daemon bereit, da im Subsystem kein systemd zur Verfügung steht. Somit kann unter WSL 2 keine Mehrbenutzerinstallation erfolgen.

Um projektspezifisch die einzelnen Versionen bereitstellen zu können, bringt Nixpkgs die sogenannte Nix-Shell mit. Die Konfiguration erfolgt ordnerspezifisch über shell.nix oder default.nix. Die Shell kennt zwei Ausprägungen: impure bedeutet, dass die aktuelle PATH-Variable und somit installierte Programme ebenfalls in der Nix-Shell zur Verfügung stehen. Das stellt das Standardvorgehen dar. Um eine Shell zu erzeugen, die umgebungsneutral ist und in der alle Abhängigkeiten konfiguriert werden müssen, dient der Parameter pure.

$ nix-shell --pure

Für die Konfiguration kommt die domänenspezifische Sprache (DSL) Nix Expression Language zum Einsatz.

Sie beschreibt rein funktional einen deterministischen Zustand, um eine reproduzierbare Umgebung zu erstellen. Entwicklerinnen und Entwickler können spezifische Channel-Stände des Paket-Repository – bis hin zu SHA-verifizierten Commits in Git-Repositories – nutzen, um die Umgebung zu beschreiben. Passende Paketversionen lassen sich auf der Nix-Paketversionsseite suchen.

Die folgende shell.nix beschreibt eine Entwicklungsumgebung für Rust und installiert neben Rustup und Cargo den rust-analyzer. Für die Umgebung dient ein Overlay zum Überschreiben der Standardkonfiguration dazu, rust-bin.stable.latest.default als Paket bereitzustellen.

{ nixpkgs ? import <nixpkgs> { } }:

let
  rustOverlay = builtins.fetchTarball 
    "https://github.com/oxalica/rust-overlay/" + 
    "archive/master.tar.gz";
  pinnedPkgs = nixpkgs.fetchFromGitHub {
    owner = "NixOS";
    repo = "nixpkgs";
    rev = "1fe6ed37fd9beb92afe90671c0c2a662a03463dd";
    sha256 = 
     "1daa0y3p17shn9gibr321vx8vija6bfsb5zd7h4pxdbbwjkfq8n2";
  };
  pkgs = import pinnedPkgs {
    overlays = [ (import rustOverlay) ];
  };
in
pkgs.mkShell {
  buildInputs = with pkgs; [
    rust-bin.stable.latest.default
    rust-analyzer
  ];
  shellHook = ''
    export RUST_BACKTRACE=1
  '';
}

Aktuell sind die Konfiguration und die Pflege von Git-Ständen allerdings noch etwas umständlich. Künftig sollen Flakes den Prozess deutlich vereinfachen. Derzeit sind sie jedoch noch ein experimentelles Feature.

Damit Nix Zustände wie Paket-Caches nicht projektübergreifend nutzt und potenzielle Versionskonflikte erzeugt, müssen Entwicklerinnen und Entwickler die shell.nix erweitern, um im Falle von Rust ~/.cargo/ in den jeweiligen Projektordner umzubiegen. Dazu dient die PATH-Variable CARGO_HOME innerhalb des shellHook, den das System ausführt, wenn es die Konfiguration lädt.

shellHook = ''
  export CARGO_HOME=$PWD/.cargo
  export RUST_BACKTRACE=1
'';

Die Konfiguration hängt von der Entwicklungsumgebung ab. Beispielsweise erfordert Node.js Werte für NODE_PATH und NPM_CONFIG_PREFIX. Der shellHook dafür sieht folgendermaßen aus:

shellHook = ''
  mkdir -p .nix-node
  export NODE_PATH=$PWD/.nix-node
  export NPM_CONFIG_PREFIX=$PWD/.nix-node
  export PATH=$NODE_PATH/bin:$PATH
'';

Um die eigene Shell weiter nutzen zu können, bietet direnv die Möglichkeit, Umgebungen automatisch beim Navigieren in einen Ordner mit einer Datei shell.nix oder default.nix anzupassen. Die Umgebung bleibt bestehen, solange man den Ordner nicht verlässt. Die Umgebung läuft auch in Unterordnern und ermöglicht, in shell.nix spezifizierte Programme zu verwenden. Eine aktuelle Restriktion von direnv ist, dass sich definierte Aliasse nicht anwenden lassen.

Nach der Installation von direnv über die Paketverwaltung verbindet man es mit der aktuellen Shell und startet diese anschließend neu. Mit zsh funktioniert die Einbindung folgendermaßen:

$ nix-env -iA `direnv`
$ echo "eval \"\$(`direnv` hook zsh)\"" >> ~/.zshrc

Um direnv zu zeigen, welche Umgebung es laden soll, muss im Projektordner noch folgende Datei angelegt werden. Das kann einfach per echo erfolgen:

$ echo "use nix" > .envrc

Da direnv unabhängig von Nix-Shell ist und man auch mit direnv Variablen setzen beziehungsweise Programme beim Navigieren in den Ordner ausführen kann, ist in .envrc definiert, dass die in der shell.nix definierte Umgebung geladen werden soll.

Der Befehl direnv allow zeigt direnv beim ersten Laden der Umgebung, dass der Inhalt von .envrc und shell.nix geladen werden soll. Bei weiteren Aufrufen lädt das System die Pakete und Umgebungsvariablen in der aktuellen Shell automatisch.

Für Entwicklungsumgebungen bietet VS Code eine einfache und leicht erweiterbare Methode über Devcontainer. Gerade bei komplexeren Umgebungen ermöglicht die Konfiguration mit Docker Compose, ganze Landschaften auf Knopfdruck aus dem Boden zu heben. Vorkonfigurierte Container und Definitionen aus Docker Hub erleichtern die Einrichtung deutlich.

Allerdings ist man auf VS Code als IDE und Docker festgelegt und kann die Devcontainer im weiteren Verlauf aufgrund der Abhängigkeiten der IDE nicht in CI/CD-Pipelines wiederverwenden. Da sich die definierten Versionen nicht exakt festlegen lassen, bleibt ein gewisser Spielraum.

Unter macOS und Windows sind die Standardkonfigurationen bei I/O behäbig. Bei macOS hilft es, nachzukonfigurieren, wobei die Performance jedoch unter der nativen bleibt. Unter Windows lässt sich nur nahezu native Performance erreichen, wenn man den Code in WSL 2 verwaltet und die WSL-2-Instanz in Docker Desktop unter WSL-Integration aktiviert ist. Gerade bei I/O-lastigen Umgebungen wie Node.js ist der Unterschied mehr als deutlich spürbar.

VS Code Devcontainer sind mit kleinen Einschränkungen die erste Wahl, um im ersten Schritt in kurzer Zeit deskriptive Umgebungen zu schaffen.

Mit shell.nix hingegen erfolgt die Installation der Pakete zwar auf dem lokalen System, aber die Konfiguration lässt sich später in CI/CD-Abläufen weiterverwenden. Auf die Weise ist auch in Betrieb sichergestellt, dass von der Entwicklung bis in die Produktion identische Build- und Laufzeitumgebungen zum Einsatz kommen. Allerdings sollte man den konfigurativen Aufwand nicht unterschätzen und abwägen, ob der Vorteil gerade bei C#, Java und Skriptsprachen die Mühe wert ist. Einen Lichtblick, um den Aufwand für Konfiguration und Wartung der Umgebungen zu reduzieren, stellen Flakes dar, die mit Nix 3.0 zum Standard gehören.

Im Gegensatz zu Devcontainern ist eine exakte Versionsverwaltung und native Performance bezüglich I/O auch unter macOS möglich. Dafür lassen sich Nixpkgs ausschließlich auf Linux/Unix-Systemen ausführen. Unter Windows ist daher WSL 2 der einzig gangbare Weg. Abhängigkeiten lassen sich wie auch bei Devcontainern mit docker-compose abbilden. Die Konfiguration ist hier allerdings etwas aufwendiger, da die Initialisierung zusätzlich implementiert werden muss.

Durch die genaue Beschreibung besticht die Nix-Shell gerade in komplexen Build-Szenarien und sorgt bei Entwicklungen ohne VS Code oder Docker ebenfalls für deskriptive Beschreibungen der Laufzeitumgebungen.

Beide Methoden liefern Wege, Umgebungen zu beschreiben und einfach zu verwenden. Dabei schließen sich die Ansätze nicht gegenseitig aus. Entwicklerinnen und Entwickler können sie kombinieren und Container erstellen, deren Definitionen sie im späteren CI/CD-Prozess weiter verwenden, die aber eine komplette Entkopplung vom eigenen System bieten und durchaus komplexe Entwicklungsumgebungen abbilden können.

Alexander Serowy
arbeitet seit über zehn Jahren als Entwickler und Architekt im Bereich der Softwareentwicklung und hat bei adesso SE ein Zuhause gefunden. Für ihn ist Softwareentwicklung ein sowohl handwerklicher als auch kreativer Beruf, in dem man mit Leidenschaft immer weiter streben kann.

(rme)