Les flakes
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 :
- Nix
- NixOS
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
Si vous n'avez pas les droits administrateurs sur votre machine, vous pouvez à
la place modifier le fichier ~/.config/nix/nix.conf
.
Dans votre configuration NixOS, rajoutez l'option suivante
nix.settings.experimental-features = [ "nix-command" "flakes" ];
Puis appliquez-la à l'aide d'un sudo nixos-rebuild switch
.
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 :
{
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 quepackages
?
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 importeflake
: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 exemplegithub
,gitlab
,indirect
,file
, etc.)owner
: L'owner du dépôt (par exempleNixOS
)repo
: Le nom du dépôt (par exemplenixpkgs
)rev
: Un hash de commitref
: 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 exemplegithub:NixOS/nixpkgs/nixpkgs-unstable
,path:sub-folder/other-flake
, outarball: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'inputnixpkgs
d'un flakemy-flake
que vous, vous importez, vous pouvez définirfollows
à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 (commenixpkgs
)
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.
É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 champmeta.mainProgram
de sa dérivation ou alors$pname
(car le chemin de ce champ est relatif au dossierbin
de la dérivation).nix build .#sl
: Construit le paquet et créé un lienresult
dans le dossier courant vers la sortie de la dérivationnix shell .#sl
: Ouvre un shell dans lequel le paquet est disponiblenix 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 configurationmodules
: La liste des modules NixOS à chargerpkgs
: L'instance de Nixpkgs à utiliserspecialArgs
: 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
];
};
};
}
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
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.
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
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.
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
.
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 exemplelegacyPackages.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
.
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
.
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.