Aller au contenu
Administration Linux medium

Importer, factoriser et pinner ses dépendances Nix

12 min de lecture

Un shell.nix qui importe <nixpkgs> utilise la version du channel installé sur votre machine. Rien ne garantit que votre collègue a le même. Résultat : Python 3.12.13 chez vous, Python 3.12.8 chez lui. Ce guide montre comment modulariser vos fichiers Nix et figer les versions pour que tout le monde travaille avec exactement les mêmes paquets.

  • Séparer la configuration dans plusieurs fichiers avec import
  • Passer des arguments entre fichiers avec inherit
  • Fusionner des configurations avec l’opérateur //
  • Figer une version de nixpkgs avec fetchTarball et un hash
  • Trouver le sha256 d’une archive avec nix-prefetch-url
  • Comprendre pourquoi les flakes ont été créés

Dès que votre projet Nix dépasse quelques lignes, vous rencontrez ces situations :

  • votre shell.nix mélange configuration, paquets Python, outils CLI et variables d’environnement dans un seul fichier qui grandit vite,
  • vous voulez réutiliser la même liste de paquets Python dans plusieurs projets sans copier-coller,
  • deux développeurs ont des résultats différents parce que leurs channels Nix ne pointent pas vers la même version de nixpkgs,
  • vous avez besoin d’une version précise d’un outil pour reproduire un bug signalé en CI.

La modularisation et le pinning résolvent chacun de ces problèmes.

import charge et évalue un fichier .nix. Si le fichier contient un attrset, il retourne cet attrset. Si le fichier contient une fonction, il retourne cette fonction (qu’il faut ensuite appeler).

config.nix
{
projectName = "mon-api";
projectVersion = "2.0.0";
debugMode = true;
listenPort = 5000;
}

Ce fichier est un simple attrset — import ./config.nix retourne directement { projectName = "mon-api"; ... }.

Extraire les paquets Python dans python-packages.nix

Section intitulée « Extraire les paquets Python dans python-packages.nix »
python-packages.nix
{ pkgs, config }:
let
basePackages = ps: [
ps.requests
ps.flask
];
devPackages = ps: [
ps.pytest
ps.black
];
in
pkgs.python312.withPackages (ps:
basePackages ps
++ (if config.debugMode then devPackages ps else [])
)

Ce fichier est une fonction qui attend pkgs et config. Il faut l’appeler avec les arguments : import ./python-packages.nix { inherit pkgs config; }.

shell.nix
{ pkgs ? import <nixpkgs> {} }:
let
config = import ./config.nix;
pythonEnv = import ./python-packages.nix { inherit pkgs config; };
debugTools = if config.debugMode then with pkgs; [
htop
ncdu
] else [];
in
pkgs.mkShell {
name = "${config.projectName}-dev";
packages = with pkgs; [
pythonEnv
git
ripgrep
] ++ debugTools;
PROJECT_NAME = config.projectName;
PROJECT_VERSION = config.projectVersion;
LISTEN_PORT = toString config.listenPort;
}
Fenêtre de terminal
nix-shell --run "echo \$PROJECT_NAME \$PROJECT_VERSION \$LISTEN_PORT && which htop"
Résultat
mon-api 2.0.0 5000
/nix/store/zalk6ygj90a1d1jyk6rn4ha22fh19gzd-htop-3.4.1/bin/htop

La configuration est centralisée dans config.nix — un seul endroit à modifier pour changer le nom du projet, la version ou le mode debug.

L’opérateur // fusionne deux attrsets. Les clés du côté droit écrasent celles du côté gauche. C’est idéal pour un système de surcharges :

defaults.nix
{
projectName = "mon-api";
projectVersion = "2.0.0";
debugMode = false;
listenPort = 5000;
}
overrides.nix
{
debugMode = true;
listenPort = 8080;
}
shell.nix (extrait)
let
defaults = import ./defaults.nix;
overrides = import ./overrides.nix;
config = defaults // overrides;
in
# config.debugMode = true (surchargé)
# config.listenPort = 8080 (surchargé)
# config.projectName = "mon-api" (hérité)
Fenêtre de terminal
nix-shell --run "echo \$LISTEN_PORT"
Résultat
8080

Le port est bien 8080 (overrides) au lieu de 5000 (defaults), et projectName reste "mon-api" car il n’est pas surchargé.

Créez un fichier par environnement :

mon-projet/
├── shell.nix
├── defaults.nix
├── overrides-dev.nix # debugMode=true, port 8080
├── overrides-staging.nix # debugMode=false, port 5000
└── python-packages.nix

Sélectionnez l’environnement depuis shell.nix :

config = defaults // (import ./overrides-dev.nix);

Quand une logique revient dans plusieurs projets, extrayez-la dans un fichier :

lib/mk-env-vars.nix
config: {
PROJECT_NAME = config.projectName;
PROJECT_VERSION = config.projectVersion;
LISTEN_PORT = toString config.listenPort;
DEBUG = if config.debugMode then "1" else "0";
}
shell.nix (extrait)
let
config = import ./config.nix;
mkEnvVars = import ./lib/mk-env-vars.nix;
envVars = mkEnvVars config;
in
pkgs.mkShell ({
packages = [ ... ];
} // envVars)

L’opérateur // fusionne l’attrset mkShell avec les variables d’environnement — elles sont automatiquement exportées.

Par défaut, import <nixpkgs> {} utilise le channel système. Ce channel change à chaque mise à jour. Conséquence :

Machine A (channel à jour)Machine B (channel ancien)
Python 3.12.13Python 3.12.8
git 2.53.0git 2.47.2

Deux mois d’écart dans les channels produisent des différences visibles. Pour un environnement reproductible, il faut figer la source.

Remplacez import <nixpkgs> {} par un import d’archive fixe :

shell.nix
{ pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz";
sha256 = "sha256:1s2gr5rcyqvpr58vxdcb095mdhblij9bfzaximrva2243aal3dgx";
}) {}
}:
pkgs.mkShell {
packages = [ pkgs.python312 pkgs.git ];
}

Maintenant, peu importe le channel installé sur la machine :

Fenêtre de terminal
nix-shell --run "python3 --version && git --version"
Résultat
Python 3.12.8
git version 2.47.2

Les versions sont celles de nixos-24.11 — figées, reproductibles.

Pour encore plus de précision, utilisez un commit spécifique de nixpkgs :

shell.nix (commit précis)
{ pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/4c1018dae018162ec878d42fec712642d214fdfa.tar.gz";
sha256 = "sha256:..."; # hash du commit
}) {}
}:
pkgs.mkShell {
packages = [ pkgs.python312 ];
}

Un commit ne change jamais — c’est la garantie la plus forte de reproductibilité sans flakes.

Deux méthodes :

Mettez un sha256 vide et lancez nix-shell. L’erreur affiche le bon hash :

sha256 = ""; # ← intentionnellement vide
Message d'erreur
error: hash mismatch in file downloaded from '...':
specified: sha256:000000000000000000000000000000
got: sha256:1s2gr5rcyqvpr58vxdcb095mdhblij9bfzaximrva2243aal3dgx

Copiez la valeur got: dans votre fichier.

Fenêtre de terminal
nix-prefetch-url --unpack https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz
Résultat
1s2gr5rcyqvpr58vxdcb095mdhblij9bfzaximrva2243aal3dgx

Préfixez avec sha256: dans le fichier Nix.

Fenêtre de terminal
nix eval --impure --expr '(import <nixpkgs> {}).python312.version'
Résultat
"3.12.13"

Le site Nixhub.io permet de rechercher quelle version de nixpkgs contient une version précise d’un paquet. Si vous avez besoin de Python 3.12.8 exactement, Nixhub vous indique quel commit ou quelle branche utiliser.

Structure recommandée pour un projet multi-fichiers

Section intitulée « Structure recommandée pour un projet multi-fichiers »
mon-projet/
├── shell.nix # point d'entrée, import des modules
├── defaults.nix # configuration par défaut
├── overrides.nix # surcharges locales (gitignored si besoin)
├── python-packages.nix # paquets Python paramétrés
└── lib/
└── mk-env-vars.nix # fonctions utilitaires

Principes :

  • Un fichier par responsabilité (configuration, paquets, utilitaires)
  • defaults.nix versionné, overrides.nix optionnel et local
  • Les fonctions utilitaires dans lib/
  • Le pinning dans shell.nix (ou dans un nixpkgs.nix dédié)

Limites du pinning classique : pourquoi les flakes existent

Section intitulée « Limites du pinning classique : pourquoi les flakes existent »

Le pinning avec fetchTarball fonctionne, mais présente des inconvénients :

LimiteImpact
Hash manuelVous devez chercher et coller le sha256 à chaque mise à jour
Pas de lock partagéChaque développeur doit avoir le bon hash dans le fichier
Mise à jour fastidieuseChanger de version = modifier l’URL + recalculer le hash
Pas de compositionCombiner plusieurs sources (overlay, modules) est complexe

Les flakes résolvent ces problèmes avec :

  • un fichier flake.lock automatique et versionnable,
  • une commande nix flake update pour mettre à jour,
  • une composition native de plusieurs inputs.

Le pinning classique reste parfaitement valide pour les projets simples ou les environnements sans flakes. Pour les projets d’équipe, les flakes sont la solution recommandée.

SymptômeCause probableSolution
hash mismatchsha256 incorrect ou archive modifiéeRecalculer avec nix-prefetch-url --unpack
cannot look up '<nixpkgs>'Mode pur activé sans channelAjouter --impure ou figer avec fetchTarball
import ./file.nix échoueChemin relatif incorrectVérifier que le fichier existe au bon endroit
Argument manquant dans importFichier = fonction non appeléePasser les arguments : import ./f.nix { inherit pkgs; }
Attribut absent après //Clé non définie dans defaultsVérifier les deux attrsets avant fusion
  • import ./file.nix charge un fichier Nix — vous pouvez séparer configuration, paquets et utilitaires.
  • Si le fichier est une fonction, il faut l’appeler avec ses arguments : import ./f.nix { inherit pkgs; }.
  • L’opérateur // fusionne deux attrsets — idéal pour un système defaults + overrides.
  • fetchTarball avec un sha256 fige la version de nixpkgs — plus aucune dérive entre machines.
  • Le hash se récupère avec nix-prefetch-url --unpack ou en laissant Nix le calculer (sha256 vide).
  • Pour les projets d’équipe, les flakes (guide suivant) remplacent avantageusement le pinning manuel.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn