Skip to main content

Les flakes

Info

Vous pouvez expérimenter cette partie du cours autant sur NixOS que sur Nix seul.

Les channels, c'est sympa, mais ça pose quand même certains problèmes :

  • Il faut manuellement les enregistrer et chacun peut les nommer comme il veut.
  • Ils ne se gèrent pas de façon déclarative (il faut le faire à la main).
  • Les dépôts dont on peut se servir comme channel ont tous leur organisation à eux.
  • En bref, ce n'est ni déclaratif ni reproductible, c'est donc contraire à la philosophie de Nix.

C'est pour ça que beaucoup de gens se retrouvaient à passer, à la place, par un simple builtins.fetchTarball de Nixpkgs ou d'un autre dépôt. Au moins, c'était fait de façon déclarative et reproductible (grâce au hash du commit présent) et ça ne nécessitait aucune action manuelle derrière.

C'est pour régler ces problèmes que sont apparus, il y a peu, les flakes : un moyen de définir précisément ce qu'utilise et ce qu'expose (inputs et outputs) un dépôt git contenant des expressions Nix.

Ça peut paraître simple dit comme ça, mais les flakes sont très vite monté en popularité malgré leur statut de fonctionnalité expérimentale encore aujourd'hui, car ils règlent tous les problèmes cités plus haut en plus d'apporter énormément de nouveaux avantages.

Activer les fonctionnalités expérimentales

En plus des flakes, Nix a aussi récemment introduit une toute nouvelle CLI.

Basée sur la commande unique nix, elle ajoute beaucoup de nouvelles fonctionnalités, simplifie et accélère beaucoup d'opérations, et surtout permet un bien meilleur support des flakes.

Étant encore expérimentaux, les flakes et la nouvelle CLI ne sont pas activés par défaut. Il faut donc bien retenir que leur implémentation peut changer à tout moment.

Pour pouvoir les utiliser, utilisez la procédure suivante :

Ouvrez le fichier /etc/nix/nix.conf (créez le et son dossier parent s'il n'existe pas) et ajoutez-y :

experimental-features = nix-command flakes
Info

Si vous n'avez pas les droits administrateurs sur votre machine, vous pouvez à la place modifier le fichier ~/.config/nix/nix.conf.

Exemple

Avant tout, voyons à quoi ressemble un flake. Ci-dessous, un exemple reprenant la dérivation hello-world qu'on a vue plus tôt :

flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};

outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.${system}.hello-world = pkgs.stdenv.mkDerivation {
pname = "hello-world";
version = "1.0.0";

src = pkgs.fetchgit {
url = "https://github.com/Litarvan/hello-world";
rev = "6a12c0a8f7f5fde57fe4a1ea7eda0afad30877b1";
hash = "sha256-RkBBXNt5qqDEE0wydMxPbYnpOIXuVMtMyWDmoeNW4NU=";
};

buildPhase = ''
make
'';
installPhase = ''
mkdir -p $out/bin
make install PREFIX=$out
'';
};
};
}

Ici, ce flake expose une seule chose : un paquet hello-world pour Linux x86_64, le tout en se basant sur Nixpkgs unstable.

Essayez de coller l'exemple dans un fichier flake.nix, en changeant x86_64-linux pour votre système si besoin (x86_64-darwin sur macOS classique, aarch64-darwin sur macOS ARM, ...), puis lancez la commande suivante :

nix run .#hello-world

Si tout va bien, après un moment, vous devriez voir le message Hello World ! s'afficher. En fond, la commande a fait plusieurs choses :

  • Elle a téléchargé Nixpkgs unstable depuis GitHub.
  • Elle a évalué puis construit la dérivation hello-world.
  • Elle a lancé notre programmé fraîchement compilé.

Tout ça a pu prendre un peu de temps et c'est normal. Essayez de lancer à nouveau la commande et regardez la vitesse à laquelle elle s'exécute cette fois.

Tout ça est un petit aperçu des flakes, expliquons maintenant comment tout ça fonctionne. Mais avant ça, petit aparté :

legacyPackages ?

Si vous avez bien suivi, vous vous rappelez peut-être que nixpkgs expose une fonction qu'il faut appeler pour pouvoir utiliser.

En effet, j'aurais pu écrire ça à la place :

let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in
{
# ...
}

Ça aurait fonctionné sans trop de problème, la fonction permettant même de préciser le système cible (par défaut il utilise normalement builtins.currentSystem qui récupère celui de votre machine).

Mais un flake complexe peut se baser sur beaucoup d'autres en inputs, et ceux que j'utilise instancieront sûrement nixpkgs aussi. De ce fait, je risque de me retrouver avec pleins d'instance de nixpkgs différentes. En plus d'être lourd et peu performant (Nixpkgs prend 100Mo de RAM pour être évalué !), il est possible que des modifications sur Nixpkgs ne s'appliquent pas à tout le monde.

De ce fait, le flake de Nixpkgs expose un champ legacyPackages correspondant à une instance de ce dernier pour chaque système, que chacun peut utiliser. Grâce à ça, tout le monde utilise une instance commune.

Dans les flakes, préférez donc l'utilisation de legacyPackages plutôt que import nixpkgs { ... }, sauf si vous avez besoin d'une instance particulière (par exemple avec des overlay).

Pourquoi le nom legacyPackages plutôt que packages ?

Le nom legacyPackages est un peu trompeur, il ne s'agit pas d'un champ déprécié. C'est simplement que le champ packages est un champ particulier utilisé par beaucoup de commandes, qui auraient envie de l'évaluer en entier.

En l'appelant legacyPackages, on évite que tous les paquets de Nixpkgs ne soient évalués lorsque l'on appelle par exemple nix flake metadata sur un flake qui en dépend.

Les inputs

Les inputs sont les dépendances du flake. Ce sont tout simplement d'autres flakes que l'on souhaite utiliser (même s'il est aussi possible d'utiliser des dépôts classiques).

Ici, on importe un seul input qu'on appelle nixpkgs et qu'on récupère depuis le dépôt NixOS/nixpkgs sur la branche nixpkgs-unstable.

Plus précisément, inputs est un set dont chaque attribut a le nom qu'on veut lui donner, et dont la valeur est un set pouvant contenir les attributs suivants :

  • inputs : un sous-set d'inputs suivant le même format, servent à surcharger les inputs utilisés par l'un de ceux qu'on importe
  • flake : true par défaut, mais vous pouvez le mettre à false pour indiquer que ce n'est pas un flake mais un dépôt classique

En plus de ça, vous pouvez ajouter :

  • Soit, si vous voulez manuellement préciser chaque partie de l'URL :
    • type (obligatoire dans ce cas) : le type de l'input (par exemple github, gitlab, indirect, file, etc.)
    • owner : L'owner du dépôt (par exemple NixOS)
    • repo : Le nom du dépôt (par exemple nixpkgs)
    • rev : Un hash de commit
    • ref : Une référence git (comme une branche)
  • Soit, si vous voulez tout mettre dans un URL :
    • url : l'URL du flake dont la syntaxe est [<type>:]<path>(\?<params>)?, par exemple github:NixOS/nixpkgs/nixpkgs-unstable, path:sub-folder/other-flake, ou tarball:https://example.com/archive.tar.gz
  • Soit, si vous voulez récupérer l'input d'un autre input :
    • follows : Permet de récupérer l'input d'un autre input, par exemple si vous voulez récupérer l'input nixpkgs d'un flake my-flake que vous, vous importez, vous pouvez définir follows à my-flake/nixpkgs.
  • Soit, si vous voulez utiliser une référence au registre global des flakes (détaillé plus tard) :
    • type : indirect
    • id : L'identifiant du flake dans le register (comme nixpkgs)

Des exemples seront peut-être plus parlant :

{
inputs = {
# Me/hello (sur un certains commit) depuis gitlab.com
my-flake = {
type = "gitlab";
owner = "Me";
repo = "hello";
rev = "185abe23f377c993837c143994d7757d0d076f93";
};

# Un projet non-flake sur le Gitlab de l'école, branche dev
my-project.url = "git+ssh://gitlab.cri.epita.fr/xavier.login/my-project.git?ref=dev";
my-project.flake = false;

# On ré-utilise le même nixpkgs que my-flake
nixpkgs.follows = "my-flake/nixpkgs";

# On change l'input 'my-project' de my-flake pour suivre le nôtre à la place
my-flake.inputs.my-project.follows = "my-project";
};
}

Les outputs

Les outputs sont la liste des choses que notre flake expose. Concrètement, le champ outputs est une fonction qui prend en paramètre un set contenant tous nos inputs (ainsi que self référençant notre propre flake) et qui doit retourner un set contenant nos outputs rangés dans des catégories en fonction de leur type.

Beaucoup de types d'outputs doivent être définis pour chaque system cible. C'est le cas des paquets (mais pas que !) qui doivent être rangés dans des sous-sets pour chacun desdits system. Un system dans Nixpkgs est un doublet de la forme <cpu>-<os>, par exemple x86_64-linux ou aarch64-darwin.

Les outputs d'un flake peuvent être référencés en ajoutant # à son chemin, suivi du nom de l'output (sans besoin de préciser sa catégorie ou son système). Par exemple, il est possible de référencer notre paquet dans l'exemple précédent via le chemin .#hello-world (dans le cas où le flake serait dans le dossier courant).

Passons en revue les différents types d'outputs :

Les checks

L'ensemble checks permet de définir des tests (oui oui !) à lancer sur le flake, grâce à un framework de tests basé sur du python et fonctionnant dans des machines virtuelles.

Attention

Étant basé sur KVM, le framework de test ne fonctionne que sur Linux.

Exemple

{
name = "Nom du test";

nodes.machine1 = { config, pkgs, ... }: {
# ...
};

testScript = ''
print('Hello World')
'';
}

Ici :

  • nodes est un set où chaque attribut est une machine virtuelle qui sera utilisée pour le test. Les valeurs de ses attributs sont tout simplement la configuration NixOS qui sera utilisée.
  • testScript est un script python qui sera exécuté dans chaque machine virtuelle, correspondant au test lui-même.

Les sets de tests sont des sortes de modules, il est donc aussi possible de définir un champ imports permettant d'en importer d'autres.

Pour créer un test à proprement parler à partir d'une définition, il est possible d'importer le framework de test de NixOS :

let
# Hors d'un flake, il est possible de l'importer via :
# nixos-lib = import "${nixpkgs}/nixos/lib" {};
nixos-lib = nixpkgs.lib.nixos;
in
nixos-lib.runTest {
imports = [ ./my-test.nix ];

# Il faut définir l'instance de Nixpkgs utilisée par le système de test (pour
# l'hôte du coup et non les VMs)
hostPkgs = nixpkgs.legacyPackages.x86_64-linux;
}

Cette expression construira une dérivation de test, qui peut être incluse dans les nos outputs checks :

{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
nixos-lib = nixpkgs.lib.nixos;
in
{
checks.${system}.my-test = nixos-lib.runTest {
imports = [ ./my-test.nix ];
hostPkgs = nixpkgs.legacyPackages.${system};
};
};
}

Il est aussi possible que vous rencontriez des tests construits avec l'ancien système, déprécié depuis peu (car il n'était initialement pas destiné à être utilisé hors de NixOS) :

{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
in
{
checks.${system}.my-test = (import "${nixpkgs}/nixos/lib/testing-python.nix" {
inherit system;
pkgs = nixpkgs.legacyPackages.${system};
}).makeTest (import ./my-test.nix);
};
}

Pour plus d'information, je vous invite à lire la documentation de NixOS sur les tests.

Utilisation

Les tests peuvent être lancés avec la commande suivante :

nix flake check [flake]

Le paramètre flake est optionnel, par défaut, il ira chercher dans le dossier courant.

La commande va lancer tous les tests définis dans la dérivation, mais aussi vérifier que le flake est bien défini (au niveau de ses inputs et outputs).

Pour plus d'information, je vous invite à lire la documentation de la commande nix flake check.

Les packages

L'ensemble packages permet de définir des paquets, tout simplement.

Par exemple, pour exporter le paquet sl de Nixpkgs :

{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.${system}.sl = pkgs.sl;
};
}

Utilisation

Les paquets définis peuvent ensuite être utilisés via plusieurs moyens :

  • nix run .#sl : Tente de lancer le binaire principal d'un paquet en cherchant dans le champ meta.mainProgram de sa dérivation ou alors $pname (car le chemin de ce champ est relatif au dossier bin de la dérivation).
  • nix build .#sl : Construit le paquet et créé un lien result dans le dossier courant vers la sortie de la dérivation
  • nix shell .#sl : Ouvre un shell dans lequel le paquet est disponible
  • nix develop .#sl : Ouvre un shell dans lequel les dépendances du paquet sont disponibles, et où il est possible d'appeler ses étapes de build (buildPhase, installPhase, etc.)
  • nix bundle .#sl : Permet d'exporter de Nix une application et ses dépendances dans un binaire portable (oui oui). Ne fonctionne que sur Linux (à l'heure où ce cours est écrit).

Par défaut

Il est possible de préciser le paquet par défaut du flake en définissant un paquet appelé default dans la liste des paquets, ou en définissant un output appelé defaultPackage.

Si un paquet par défaut est précisé, il sera possible de le référencer simplement par le flake, par exemple, il sera possible de le lancer en faisant, dans le dossier de le flake :

nix run

Les applications

Les applications sont des paquets qui sont destinés à être lancés, et qui ont donc normal un binaire principal. Ils sont définis dans l'output apps.

Une app peut être définie soit par un paquet, soit par un set de la forme :

{
type = "app";
program = "${pkgs.sl}/bin/sl";
}

L'attribut program doit être un string correspondant au chemin du binaire.

Exemple

{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
apps.${system}.sl = {
type = "app";
program = "${pkgs.sl}/bin/sl";
};
};
}

Utilisation

Les applications ont la même utilisation que les paquets, vus plus haut.

Par défaut

Tout comme les paquets, il est possible de définir une application par défaut via une app appelée default ou l'output defaultApp.

Si à la fois un package et une app par défaut sont définis, c'est l'app par défaut qui sera lancée par les commandes où l'on ne précise pas la cible (comme nix run).

Les bundlers

Je mentionne rapidement ce type d'output (même s'il est très peu probable que vous en ayez besoin) car à l'heure actuelle, il existe quasi aucune véritable liste exhaustive des outputs possibles, je préfère donc ne rien oublier de tout ce que j'ai pu trouver.

Les bundlers sont des fonctions servant à la commande nix bundle permettant de rassembler un paquet et ses dépendances dans un binaire portable.

Par défaut, Nix utilise le bundler toArx basé sur le logiciel Arx et défini sur le dépôt (et flake) de nix-bundlers, mais il en existe d'autres défini par ce dernier, par exemple :

  • toDockerImage : Permet de créer une image Docker à partir d'un paquet et de ses dépendances.
  • toDEB : Permet de créer un paquet Debian à partir d'un paquet et de ses dépendances.
  • Et plus !

Les outputs du champ bundlers permet d'exporter des types de bundlers.

Utilisation

La commande nix bundle accepte un paramètre --bundler permettant de préciser le bundler à utiliser via un chemin flake, par exemple :

nix bundle --bundler github:NixOS/bundlers#toDockerImage .#sl

Si la cible n'est pas précisée, l'application ou le package par défaut sera utilisé.

Par défaut

Tout comme pour les paquets et les applications, il est possible de définir un bundler par défaut via un bundler appelé default ou l'output defaultBundler.

Les legacy packages

Comme expliqué plus haut, ce champ est utilisé par nixpkgs comme instance commune de ce dernier. Il ne sert à rien d'autre en particulier, et il n'y a pas vraiment d'intérêt à le définir dans un flake.

Malgré tout, les commandes utilisant des paquets (comme nix run, nix build, etc.) iront chercher les paquets à l'intérieur en second lieu.

Les overlays

Il est possible de définir des overlays Nixpkgs dans le champ overlays. Ces derniers n'ont pas besoin d'être définis pour chaque système cible.

Exemple

{
outputs = { self, nixpkgs, ... }: {
overlays.example = self: super: {
sl = super.sl.override { ncurses = super.ncurses5; };
};
};
}

Utilisation

Ils ne seront utilisés par aucune commande en particulier, mais pourront être importés par d'autres flakes utilisant celui-ci en input.

Par défaut

Un overlay par défaut peut être défini via l'output overlay.

Les modules NixOS

Il est possible de définir des modules de configuration dans le champ nixosModules. Ces derniers n'ont pas besoin d'être définis pour chaque système cible.

Exemple

{
outputs = { self, nixpkgs, ... }: {
nixosModules.example = { lib, config, pkgs, ... }: {
options.programs.sl.enable = lib.mkEnableOption "sl";

config = lib.mkIf config.programs.sl.enable {
environment.systemPackages = [ pkgs.sl ];
};
};
};
}

Utilisation

Ils ne seront utilisés par aucune commande en particulier, mais pourront être importés par d'autres flakes utilisant celui-ci en input.

Par défaut

Un module par défaut peut être défini via l'output nixosModule.

Les configurations NixOS

Il est possible de définir des configurations NixOS dans le champ nixosConfigurations. Ces derniers n'ont pas besoin d'être définis pour chaque système cible directement.

Pour définir une configuration, la fonction nixpkgs.lib.nixosSystem peut être utilisée. Elle accepte, entre autre, les attributs suivants :

  • system : Le système cible de la configuration
  • modules : La liste des modules NixOS à charger
  • pkgs : L'instance de Nixpkgs à utiliser
  • specialArgs : Des attributs en plus qui seront donnés aux modules évalués
  • et plus...

Tous sont facultatifs exceptés modules. Si vous ne définissez pas system ou pkgs, vous pouvez le faire directement dans la configuration via les options nixpkgs.system et nixpkgs.pkgs. Sinon, les valeurs du système hôte seront utilisées.

Exemple

{
outputs = { self, nixpkgs, ... }: {
nixosConfigurations.example = nixpkgs.lib.nixosSystem {
modules = [
# Si vous avez défini le module de l'exemple précédent, vous pouvez
# l'inclure ici via self.nixosModules.example
];
};
};
}
Pour aller plus loin

La fonction nixpkgs.lib.nixosSystem est en vérité un wrapper (spécifiquement pour les flakes) vers la fonction définie dans nixpkgs/nixos/lib/eval-config.nix, que vous pouvez utiliser dans un contexte plus classique.

Utilisation

La commande nixos-rebuild utilisera le fichier /etc/nixos/flake.nix s'il est présent (en priorité sur /etc/nixos/configuration.nix). Sinon, vous pouvez lui préciser le chemin d'un flake via le paramètre --flake, par exemple :

sudo nixos-rebuild switch --flake .

Par défaut, la commande cherchera une configuration ayant le nom de l'hostname de la machine, mais il est aussi possible de lui préciser le nom de la configuration à utiliser :

sudo nixos-rebuild switch --flake .#example

Grâce à ça, vous pouvez même build une configuration venant directement de votre GitHub :

sudo nixos-rebuild switch --flake github:me/my-flake#my-config

Les dev shells

Il est possible de définir des shells de développement (l'équivalent des nix-shell) dans le champ devShells.

Les shells de développement sont, comme pour des nix-shell, des dérivations dont les dépendances seront disponibles dans le shell qui sera ouvert, en plus des fonctions du builder (buildPhase, installPhase, etc.).

Exemple

{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.example = pkgs.mkShell {
buildInputs = with pkgs; [ gcc gnumake ];
};
};
}

Utilisation

Les dev shells s'ouvrent à l'aide de la commande nix develop (et non nix shell, voir le bonus plus bas), par exemple :

nix develop .#example

Par défaut

Il est possible de préciser le shell par défaut du flake en définissant un paquet appelé default dans la liste des dev shells, ou en définissant un output appelé devShell.

Bonus : faire persister un dev shell

Tout comme avec nix-shell, il est possible de faire persister dans le store les dépendances d'un dev shell pour éviter que le garbage collector ne les supprime.

Pour cela, plus besoin de commande complexe, il suffit de rajouter le paramètre --profile fichier pour enregistrer le cache de l'évaluation de l'environnement dans un fichier, qui sera réutilisable ensuite.

Après le passage du garbage collector, il faudra tout de même que nix récupère et évalue à nouveau les inputs s'ils ont été supprimés.

Le formateur

Il est possible de définir un formateur Nix dans le champ formatter.

Un formateur est tout simplement un paquet executable (avec un binaire à l'emplacement bin/${pname} ou un champ meta.mainProgram) acceptant en paramètre des fichiers Nix à formater.

Exemple

{
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
formatter.${system} = pkgs.nixpkgs-fmt;
};
}

Utilisation

Le formateur s'utilise à l'aide de la commande nix fmt, par exemple :

nix fmt ./**/*.nix

La commande passera simplement les paramètres à votre formateur.

Les templates

Il est possible de définir des templates de flakes dans le champ templates. Ces derniers n'ont pas besoin d'être définis pour chaque système cible.

Un template est un set avec deux attributs :

  • path : Le chemin vers le template (le mieux, c'est que ça soit une valeur de type path !)
  • description : Une description du template

Exemple

{
outputs = { self, ... }: {
templates.example = {
description = "Un template d'exemple";
path = ./example-template;
};
};
}

Utilisation

Les templates s'utilisent à l'aide de la commande nix flake init et le paramètre --template (ou -t), par exemple :

nix flake init -t github:NixOS/templates#c-hello

Par défaut

Il est possible de préciser le template par défaut du flake en définissant un template appelé default dans la liste des templates, ou en définissant un output appelé defaultTemplate.

Les jobs Hydra

Les jobs hydra sont des dérivations qui seront évaluées par Hydra, la CI de Nix.

Étant très spécifique, nous ne détaillerons pas ici comment les définir, mais sachez que ça existe et que contrairement aux autres outputs, il faut définir des sous attributs pour chaque job et définir les systèmes cibles à l'intérieur des jobs (donc par exemple hydraJobs.example.x86_64-linux et non hydraJobs.x86_64-linux.example).

Les outputs non-standards

Nous avons vu ici la liste des outputs standards, c'est-à-dire ceux qui sont utilisés par des commandes de Nix (ou autre logiciel officiel). Mais en vérité, il n'y a aucune limitation quant aux outputs que vous pouvez définir, ce seront simplement des attributs accessibles par les flakes utilisant la vôtre en input.

Comme vu au début, Nixpkgs défini par exemple un output non-standard legacyPackages, mais aussi lib qui contient la librairie Nixpkgs (et celle de NixOS dans lib.nixos, avec des petits bonus).

La plupart des commandes (nix run, nix build, etc.) iront chercher directement le chemin que vous leur donnez à la racine des outputs si jamais elles ne les trouvent pas dans les outputs standard. Il est donc théoriquement possible de définir un paquet comme un output, sans le ranger dans packages ou legacyPackages ni dans un system, et de l'utiliser avec nix run ou nix build (mais ne faites pas ça en vrai).

Pour plus d'information sur les outputs de de Nixpkgs, vous pouvez directement jeter un œil à son fichier flake.nix.

Afficher la liste des inputs et des outputs dans la console

Il est possible de lister les inputs d'un flake via la commande suivante :

nix flake metadata
Remarque

Cette commande est le nouveau nom de nix flake info, toujours disponible, mais dépréciée.

Et pour lister ses outputs, la commande suivante peut être utilisée :

nix flake show

Pour plus d'information, je vous invite à lire la documentation de la commande nix flake metadata.

Verrouillage des entrées

Si vous avez tenté l'une des commandes précédentes sur un flake local, vous avez peut-être remarqué qu'un fichier flake.lock est apparu.

Ce fichier est un peu l'équivalent des lockfiles des autres gestionnaires de paquet (comme le package-lock.json ou Cargo.lock). Si vous l'ouvrez, vous y verrez la liste de vos inputs avec pour chacun, des informations comme le commit Git utilisé ou leur hash.

Il est important de push ce fichier, étant le moyen de garantir la reproductibilité de votre flake.

Nix mettra automatiquement à jour ce fichier si vous modifiez la définition de vos inputs, mais si vous voulez mettre à jour un input venant d'un URL ou d'un dépôt dont vous n'avez pas précisé le commit, vous pouvez utiliser la commande nix flake update :

nix flake update [input1] [input2...]

Si vous ne précisez aucun input, ils seront tous mis à jour à la fois.

La commande nix repl avec des flakes

Il est possible (et très utile) d'utiliser la commande nix repl avec des flakes. Pour cela, il suffit d'utiliser la commande REPL :lf avec un chemin de flake :

:lf github:NixOS/nixpkgs

Les outputs du flake seront alors disponibles dans des variables globales.

Pour aller plus loin

Il est aussi possible d'activer la feature expérimentale repl-flake (via le fichier de configuration, l'option, ou directement via le paramètre --extra-experimental-features), ce qui permettra de passer un flake en paramètre à la commande.

nix --extra-experimental-features repl-flake repl github:NixOS/nixpkgs

Cela ouvrira un REPL Nix avec les outputs du flake en question disponibles dans des variables globales.

Bonus : La commande nix shell

Comme vu précédemment, nix develop permet d'ouvrir un shell avec les dépendances d'une dérivation, remplaçant donc une partie de l'usage de la commande nix-shell. Mais pour remplacer l'usage de nix-shell -p paquets..., il existe la commande nix shell pouvant être utilisée de la manière suivante :

nix shell nixpkgs#hello nixpkgs#firefox etc.

Pour plus d'information, je vous invite à lire la documentation de la commande nix shell.

Le debugger

Depuis peu, Nix dispose d'un système de débogage. Les commandes de la nouvelle CLI telles que nix develop ou nix shell acceptent un paramètre --debugger, permettant d'ouvrir un REPL lors de l'échec de l'évaluation d'une expression, ou lorsqu'un appel à builtins.break est évalué.

Dans ce REPL, il sera possible d'évaluer des expressions dans le contexte de l'évaluation ratée (donc avec toutes les variables du scope de l'expression de disponibles).

Mise en cache des évaluations

Un des énormes avantages des flakes est le fait qu'automatiquement, Nix mette en cache les évaluations. Cela permet de gagner beaucoup de temps lors de l'exécution de commandes comme nix develop, les rendant bien plus rapides après la première execution que les commandes comme nix-shell.

Le registre des flakes

Si l'on peut référencer le flake nixpkgs directement, sans avoir à préciser github:NixOS/nixpkgs, ce n'est pas grâce à un quelconque channel, mais grâce au registre des flakes.

Il existe un registre centralisé des flakes disponible sur GitHub. C'est un simple JSON listant des alias de flakes ainsi que le chemin complet qui y correspond.

Nix télécharge une copie du registre sur votre ordinateur et s'en sert comme registre global, que vous pouvez voir à l'aide de la commande suivante :

nix registry list
Info

Nix re-télécharge automatiquement le registre lorsqu'il ne le considère plus comme à jour, c'est-à-dire lorsque la version téléchargée est plus vieille que la valeur tarball-ttl de la configuration Nix (la valeur est en secondes et définie par défaut à 3600, donc 1 heure).

En plus de cela, il est aussi possible de définir un registre système dans /etc/nix/registry.json (le fichier n'existe pas forcément), ainsi qu'un registre utilisateur dans ~/.config/nix/registry.json (de même). Quand Nix cherche une entrée dans le registre, il regarde d'abord dans le registre utilisateur, puis le registre système, et enfin le registre global.

Il est possible de manipuler le registre utilisateur via les commandes suivantes :

  • nix registry add <alias> <flake> : ajoute une entrée dans le registre utilisateur.
  • nix registry remove <alias> : supprime une entrée du registre utilisateur.
  • nix registry pin <alias> : verrouille la version actuelle d'un flake (concrètement, cela va enregistrer sa dernière version dans le registre utilisateur).

Par défaut, les flakes sont re-téléchargés si la dernière version téléchargée est plus vieille que la valeur tarball-ttl de la configuration Nix. C'est pour éviter cela qu'il est possible de les pin.

Attention

Je vous conseille très fortement de pin votre flake nixpkgs pour éviter de vous baser sur une version différente à chaque commande exécutée hors d'un flake comme un nix shell.

En revanche, si vous le faites, pensez à la mettre à jour de temps en temps ! Pour cela, supprimez l'entrée du registre utilisateur puis pinez-là à nouveau.

Au tout début, nous avons mentionné les inputs de type indirect. Ces inputs, dont il est possible de ne préciser que le nom, sont des références au registre des flakes.

Il est même possible d'omettre de déclarer des inputs que l'on voudrait importer depuis le registre. Nix détectera qu'on les utilise dans la destructuration du set de la fonction outputs, par exemple :

{
outputs = { self, nixpkgs, ... }: {
# Ici, nixpkgs a été automatiquement récupéré depuis le registre, comme si
# on l'avait enregistré dans les inputs avec :
#
# inputs.nixpkgs = { type = "indirect"; id = "nixpkgs"; }
};
}

Pour plus d'information, je vous invite à lire la documentation de la commande nix registry.

La commande nix profile, succédant à nix-env

Tout comme le reste des commandes, nix-env a aussi été remplacé par une nouvelle commande : nix profile.

Attention

Je vous déconseille d'utiliser nix profile, ou faites-le à vos risques et périls.

Si vous installez un paquet avec nix profile, votre profil nix sera converti au nouveau format (incompatible avec nix-env) et vous ne pourrez plus revenir en arrière sans le remettre à zero. Ce n'est pas forcément grave à première vue, mais nix profile est encore assez nouveau et peut être assez dur à utiliser, en plus de ne pas forcément être très stable.

Si vous avez converti votre profil et que vous voulez revenir en arrière, vous devez entièrement supprimer votre profil à l'aide de la commande suivante :

sudo rm -rf /nix/var/nix/profiles/per-user/$USER/profile

Vous devrez ensuite réinstaller les paquets de votre environnement utilisateur (ce qui inclut possiblement home-manager si vous l'aviez installé de la manière classique !). Si vous êtes sur Nix et non NixOS, Nix lui même aura disparu de votre environnement, mais vous pourrez l'appeler à nouveau via son chemin absolu dans le store, en premier lieu pour le réinstaller :

/nix/store/...-nix-2.X.X/bin/nix-env -iA nixpkgs.nix

La commande dispose de plusieurs sous-commandes permettant de gérer votre environnement utilisateur (ou plutôt, votre profil) :

  • nix profile install <paquet (au format flake)> : ajoute un paquet à votre profil.
  • nix profile list : liste les paquets installés dans votre profil.
  • nix profile uninstall <numéro> : supprime un paquet de votre profil. Cette commande ne prend pas le nom du paquet, mais son numéro dans la liste affichée par la commande précédente (oui, c'est pas ouf, c'est en travail) ou alors le chemin complet de son attribut (par exemple legacyPackages.x86_64-linux.hello).
  • nix profile update : met à jour un paquet de votre profil. Cette commande s'utilise de la même façon que la précédente. Il est possible de préciser .* pour tout mettre à jour d'un coup.
  • nix profile history : liste les générations de votre profil, en précisant leur différence avec la génération précédente (ça par contre c'est ouf).
  • nix profile rollback : permet de revenir à la génération précédente de votre profil, ou à une génération spécifiée par --to.
  • Et plus !

Pour plus d'information, je vous invite à lire la documentation de la commande nix profile.

Info

Sur les PCs des salles machines, ce sont les nouveaux profils qui sont utilisés. Il faut donc utiliser nix profile et non nix-env.

Conseil

Depuis la dernière version de Nix (la 2.13 au moment de l'écriture de ce cours, il y a à peine 2 semaines) il est possible d'utiliser les flakes avec l'ancienne CLI en utilisant la syntaxe flake:chemin là où un fichier est attendu, par exemple :

nix-env -f flake:nixpkgs -iA hello

Ou encore :

nix-shell flake:nixpkgs -A hello

Flake utils

Si vous manipulez des flakes, il est très probable que vous ayez à interagir avec la librairie flake-utils.

Flake utils est un flake tierce devenu presque incontournable, contenant des tonnes de fonctions utiles à la création de flakes. Un exemple est la fonction très pratique eachSystem qui permet d'appliquer une fonction à une liste de systèmes, ou encore eachDefaultSystem le faisant sur chaque système supporté par Nixpkgs et dont il existe un cache :

{
inputs.flake-utils.url = "github:numtide/flake-utils";

outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
defaultPackage = pkgs.hello;

devShell = pkgs.mkShell {
buildInputs = with pkgs; [ gcc gnumake ];
};
}
);
}

L'exemple ci-dessus peut être utilisé autant sur Linux que macOS, ARM ou x86, sans modification et sans avoir recours à builtins.currentSystem.

Flake utils est disponible dans le registre global, il n'est donc pas forcément nécessaire de le déclarer dans les inputs, mais cela peut être plus propre de le faire (tout comme pour nixpkgs).

Flake compat

Il est aussi possible que vous croisiez la librairie flake-compat, exposant de quoi créer des fichiers default.nix et shell.nix facilement sur votre flake, le rendant compatible avec le Nix "sans flake".

Cette librairie n'est plus nécessaire depuis l'existence de builtins.getFlake, permettant de récupérer les outputs d'un flake depuis un contexte classique, déjà disponible depuis plusieurs années.

Si malgré tout vous rencontrez cette librairie (utilisée par exemple sur NixPIE), vous verrez sur le projet en question un fichier default.nix qui exposera le paquet par défaut, et shell.nix qui exposera le dev shell par défaut ou s'il n'est pas défini, le paquet par défaut.

L'option --impure

La plupart des nouvelles commandes Nix fonctionnent par défaut en mode 'pur'. Cela signifie qu'elles n'accepteront pas d'entrées pouvant être variables, comme des fichiers locaux qui ne font pas partie de la source du flake, ou des commandes ou variables venant de l'environnement extérieur.

Par exemple, si vous essayez d'utiliser un paquet 'unfree' avec les nouvelles commandes Nix (comme Discord), ni vos fichiers de configuration (via allowUnfree) ni votre environnement (via NIXPKGS_ALLOW_UNFREE) n'auront d'effet si vous n'ajoutez pas le paramètre --impure.

L'option --impure, disponible sur la plupart des commandes nix, permet de le faire fonctionner en acceptant les entrées non-prédictibles de l'environnement pouvant casser la reproductibilité (et donc la pureté) de l'évaluation. Il est très probable que vous en ayez besoin si vous êtes amené à utiliser la nouvelle CLI.

Conclusion

Les flakes (ainsi que la nouvelle CLI) sont le renouveau de Nix. Même s'il est possible de s'en passer, il est aussi possible de complètement migrer vers eux pour profiter de commandes unifiées, plus modernes, plus rapides, plus puissantes, et plus simples.

En revanche, ce sont encore des fonctionnalités expérimentales, qui sont donc peu documentées, peu stables, et dont l'implémentation peut régulièrement changer.

Malgré tout, l'état des flakes a énormément progressé ces dernières années, et il est largement possible de les utiliser quotidiennement sans trop de problèmes.