Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Importer, factoriser et pinner ses dépendances Nix

Par mis à jour le

Améliorer cette page

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.14 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. Les sorties ont été relevées sur Nix 2.35.2.

  • 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, qui relèvent soit de la modularisation, soit du pinning :

  • 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).

Le premier fichier extrait ne contient que des valeurs : nom, version, mode debug et port. C'est le seul endroit à modifier pour adapter le projet.

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"; ... }, sans argument à passer ni fonction à appeler.

Extraire les paquets Python dans python-packages.nix

Section intitulée « Extraire les paquets Python dans python-packages.nix »

Le deuxième fichier est plus riche : il construit l'environnement Python en ajoutant les outils de développement seulement quand debugMode est actif. Il a donc besoin de pkgs et de la configuration.

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; }.

Le point d'entrée importe les deux fichiers et compose l'environnement final. Notez l'appel de python-packages.nix avec inherit pkgs config : c'est ce qui transmet les deux arguments attendus.

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/w31kqb23ydg3fli2b8dsy0gl0ily15bm-htop-3.5.3/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 :

Ce fichier porte la configuration versionnée du projet, celle que toute l'équipe partage. Il doit rester exhaustif : chaque clé susceptible d'être surchargée doit y figurer avec une valeur sûre, ici debugMode = false. Une clé absente de defaults.nix mais consommée ailleurs provoquera une erreur d'attribut manquant dès qu'un développeur travaillera sans overrides.nix.

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

Ce second fichier ne contient que les écarts par rapport aux valeurs par défaut, jamais la configuration complète. Deux clés suffisent ici, les autres sont héritées telles quelles. La fusion est superficielle : // remplace la valeur d'une clé en bloc, il ne fusionne pas récursivement deux attrsets imbriqués. Si debugMode était un attrset plutôt qu'un booléen, redéfinir une seule de ses sous-clés effacerait toutes les autres.

overrides.nix
{
debugMode = true;
listenPort = 8080;
}

La fusion tient en une ligne : defaults // overrides. Les commentaires indiquent pour chaque clé si elle vient des surcharges ou des valeurs par défaut.

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é : la fusion a appliqué la priorité du côté droit.

Créez un fichier par environnement : chacun ne porte que ses écarts, et defaults.nix reste la base commune.

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 dédié, sous forme de fonction :

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 de 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, mesurée entre le nixpkgs-unstable du 29 septembre 2026 et la branche nixos-24.11 :

Machine A (channel à jour)Machine B (channel ancien)
Python 3.12.14Python 3.12.8
git 2.55.0git 2.47.2

Quelques 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, accompagnée de son hash : Nix télécharge l'archive, vérifie le hash, puis évalue cette version de nixpkgs.

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 obtenues sont celles de l'archive téléchargée, et non plus celles du channel local.

Pour encore plus de précision, utilisez un commit spécifique de nixpkgs, avec le hash de son archive calculé par nix-prefetch-url --unpack :

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

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

Deux méthodes donnent la même empreinte, sous deux encodages différents que fetchTarball accepte l'un comme l'autre.

Mettez un sha256 vide et lancez nix-shell. L'erreur affiche le bon hash, au format SRI (sha256- suivi de base64) :

sha256 = ""; # ← intentionnellement vide
Message d'erreur
warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
...
error: NAR hash mismatch in input 'https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz?narHash=sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D', expected 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' but got 'sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g='

Copiez la valeur qui suit but got dans votre fichier : sha256 = "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g="; fonctionne tel quel.

La commande télécharge et dépaquette l'archive, l'ajoute au store et affiche son hash :

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

Préfixez avec sha256: dans le fichier Nix.

nix eval interroge l'arbre de paquets sans rien construire ni télécharger : la réponse arrive en quelques secondes, ce qui en fait le contrôle à faire avant de figer un hash. Le drapeau --impure est indispensable ici, car <nixpkgs> est résolu via la variable NIX_PATH de votre machine, une entrée extérieure que le mode pur refuse par principe. La valeur retournée est donc celle de votre channel, pas celle de l'archive que vous vous apprêtez à figer : pour interroger une version précise, remplacez <nixpkgs> par le même fetchTarball que dans votre shell.nix.

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

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 »

Les fichiers vus dans ce guide se rangent selon leur responsabilité : la configuration, les paquets, les utilitaires, et le point d'entrée qui les assemble.

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 qui se paient à chaque mise à jour et dès que plusieurs sources doivent être combinées :

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.

Deux familles d'erreurs se distinguent dans ce tableau, et elles ne se diagnostiquent pas de la même façon. Les trois premières lignes concernent la source des paquets : l'évaluation n'a même pas commencé. Les trois suivantes concernent le langage Nix lui-même, et le message d'erreur cite alors le fichier et la ligne fautive. Réflexe utile dans les deux cas : relancer avec nix-shell --show-trace, qui déplie la pile d'évaluation et désigne le fichier importé réellement responsable, souvent différent de celui que vous éditiez.

SymptômeCause probableSolution
hash mismatchsha256 incorrect ou archive modifiéeRecalculer avec nix-prefetch-url --unpack
cannot look up '<nixpkgs>' in pure evaluation modenix eval ou flake en mode purAjouter --impure ou figer avec fetchTarball
file 'nixpkgs' was not found in the Nix search pathAucun channel configurénix-channel --update, 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 sur l'archive d'un commit, avec son 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).
  • Une URL de branche (nixos-24.11) n'est pas un pin durable : la branche avance et le hash finit par ne plus correspondre. Seul un commit est immuable.
  • Pour les projets d'équipe, les flakes remplacent avantageusement le pinning manuel grâce au flake.lock.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn