Un an de Nix sur macOS
Je me souviens du moment exact où j'ai décidé d'essayer Nix. C'était un jeudi soir, vers 23h, et je fixais un terminal cassé après qu'une mise à jour macOS avait silencieusement réinitialisé mes préférences de dock, cassé trois liens symboliques Homebrew et, d'une manière ou d'une autre, changé mon Python par défaut. Encore une fois. J'entretenais mes dotfiles depuis janvier 2021 (plugins vim, thèmes zsh, configs git, tout le rituel) et tous les quelques mois, quelque chose arrêtait simplement... de fonctionner. Aucune raison claire. Juste des vibes.
Alors le 17 janvier 2025, j'ai ajouté un flake.nix à mon repo dotfiles. Je n'avais aucune idée de ce que je faisais.
L'avant
Laisse-moi te décrire ce que j'avais. Un .zshrc qui avait grandi de façon organique pendant quatre ans. Un Brewfile à peu près exact. Un install.sh que j'avais écrit une fois, exécuté une fois, et plus jamais touché parce que j'avais peur de ce qu'il ferait. Des fichiers de config éparpillés dans ~/.config/ sans aucune trace claire de ceux que j'avais modifiés manuellement et de ceux qui venaient du repo.
Le setup marchait. Je pouvais écrire du code, pousser des commits, déployer des services. Mais à chaque fois que je devais configurer une nouvelle machine ou déboguer un chemin cassé, je passais une demi-journée à lire mon propre historique shell pour comprendre ce que j'avais fait six mois plus tôt.
Le pire, c'était la situation des émulateurs de terminal. J'étais passé d'Alacritty à Kitty puis à Ghostty, et chaque migration laissait derrière elle des fichiers de config, des entrées PATH et des caches de polices que j'oubliais jusqu'à ce qu'ils causent un conflit bizarre.
Découvrir Nix
Nix est un de ces outils que tout le monde décrit différemment. « C'est un gestionnaire de paquets. » « C'est un système de build. » « C'est un langage de programmation fonctionnel pour la configuration système. » Tout ça est techniquement vrai et rien de tout ça ne m'a aidé à comprendre pourquoi ça devrait m'intéresser.
Ce qui a finalement cliqué, c'est ça : Nix te permet de décrire à quoi ton système devrait ressembler, puis il le rend conforme. Tu changes la description, il calcule le diff. Tu casses quelque chose, tu fais un rollback. Rien de plus.
Sur macOS, nix-darwin étend ça au niveau système (paquets Homebrew, paramètres macOS, mapping clavier, règles de pare-feu) et Home Manager gère le niveau utilisateur (dotfiles, alias shell, variables d'environnement).
Mon système, c'est maintenant deux fichiers et un Makefile. flake.nix pour le système. home.nix pour mon environnement utilisateur. make rebuild pour tout appliquer. Pas de script d'installation. Pas d'étapes manuelles. Pas de « est-ce que j'ai pensé à lancer cette commande ».
Comment ça s'est vraiment passé
Ça n'a pas été instantané. En regardant mon historique git, je peux tracer exactement comment ça s'est déroulé.
Le premier flake.nix en janvier 2025 était prudent. J'ai juste déplacé quelques paquets : neovim, ripgrep, jq, rustup. J'ai aussi ajouté nil (le serveur de langage Nix) et nixfmt parce qu'éditer des fichiers Nix sans outillage, c'est comme écrire du Rust sans rust-analyzer. Techniquement possible, pratiquement misérable.
Deux mois plus tard j'ai ajouté Home Manager, et c'est là que tout a changé. Avant, je créais des liens symboliques avec un script shell en croisant les doigts. Maintenant je pouvais juste les déclarer :
home.file.".config/ghostty/config".source = ./config/ghostty/config;
home.file.".config/git/config".source = ./config/git/config;
home.file.".config/zed/settings.json".source = ./config/zed/settings.json;Tu modifies le fichier source, tu lances make rebuild, le lien symbolique se met à jour. Pas de ln -sf. Pas besoin de vérifier si la cible existe. Pas de nettoyage de vieux liens de trois émulateurs de terminal.
Puis en mai j'ai fait une erreur. J'ai essayé de gérer Homebrew entièrement via Nix avec un truc appelé nix-homebrew. L'idée était belle : un seul système pour tout gouverner. La réalité était fragile. Homebrew veut vraiment posséder son propre état. Les taps, les tokens de cask, les cycles de mise à jour. Il a des opinions là-dessus, et se battre contre ces opinions causait plus de problèmes qu'il n'en résolvait.
Alors j'ai tout viré. À la place, j'ai laissé nix-darwin gérer Homebrew déclarativement sans essayer de le remplacer :
homebrew = {
enable = true;
onActivation.cleanup = "zap";
onActivation.autoUpdate = true;
onActivation.upgrade = true;
brews = [ "go" "gh" "lazygit" "grpcurl" ... ];
casks = [ "ghostty" "zed" "docker-desktop" ... ];
};Cette ligne onActivation.cleanup = "zap" est la plus importante. Elle signifie : si un paquet n'est pas dans cette liste, supprime-le. Le flake devient la source de vérité. Si tu fais un brew install manuellement, ça sera supprimé au prochain rebuild. Ça te force à tout déclarer. Au début c'était agaçant. Puis c'est devenu libérateur.
Le weekend du PATH
Celui-ci mérite sa propre section parce qu'il m'a coûté deux jours et j'en suis encore un peu agacé.
Nix et Homebrew veulent tous les deux contrôler ce qui est dans ton PATH. Le daemon Nix doit être sourcé avant tout le reste, mais le shellenv de Homebrew modifie aussi le PATH. Si tu te trompes dans l'ordre, tu te retrouves à exécuter le git système au lieu de celui de Homebrew, ou le python de Homebrew au lieu de celui de pyenv. J'ai passé beaucoup de temps à fixer la sortie de echo $PATH avant d'arriver à ça :
programs.zsh.shellInit = ''
if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then
. '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh'
fi
export PATH="/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:$PATH"
'';Les chemins Nix d'abord, puis Homebrew via shellenv, puis les chemins utilisateur. Simple avec le recul. Pas simple à 2h du matin.
Là où c'est devenu fun
Une fois les fondations solides, vers octobre 2025, j'ai commencé à intégrer tout ce que je configurais manuellement avant. Les associations de fichiers (Zed comme éditeur par défaut pour 38 types de fichiers, avec duti). Le remapping clavier (Caps Lock en Escape, parce que la vie est trop courte pour Caps Lock). Et celui qui m'a rendu irrationnellement heureux : les paramètres macOS.
system.defaults = {
dock = {
autohide = true;
autohide-delay = 0.0;
autohide-time-modifier = 0.15;
show-recents = false;
tilesize = 60;
};
NSGlobalDomain = {
AppleInterfaceStyle = "Dark";
InitialKeyRepeat = 14;
KeyRepeat = 1;
ApplePressAndHoldEnabled = false;
NSAutomaticCapitalizationEnabled = false;
NSAutomaticSpellingCorrectionEnabled = false;
};
};La vitesse d'animation de mon dock se réinitialisait à chaque mise à jour macOS. J'oubliais, je le remarquais trois jours après, je googlais la commande defaults write, je la lançais, je l'oubliais à nouveau. Maintenant make rebuild gère ça. Pour toujours.
La sécurité aussi est déclarative. Pare-feu en mode furtif, Touch ID pour sudo, signature GPG sur tous les commits. Tout dans le flake, tout reproductible.
networking.applicationFirewall = {
enable = true;
enableStealthMode = true;
};
security.pam.services.sudo_local.touchIdAuth = true;Le problème du daemon
Je dois être honnête là-dessus parce que personne d'autre ne semble en parler : le daemon Nix sur macOS est capricieux. Après des mises à jour système, des cycles veille/réveil, ou parfois sans aucune raison apparente, il arrête simplement de répondre. nix-store --version se bloque. Tout casse.
J'en ai eu marre de déboguer ça manuellement, alors j'ai écrit un script qui gère à la fois les installations Determinate Systems et les installations standard :
if [ -f "/Library/LaunchDaemons/systems.determinate.nix-store.plist" ]; then
sudo launchctl bootout system/systems.determinate.nix-store
sudo launchctl bootstrap system /Library/LaunchDaemons/systems.determinate.nix-store.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/systems.determinate.nix-daemon.plist
fiLe Makefile lance ça avant chaque rebuild. Ça ajoute environ une seconde. Ça vaut le coup de ne plus jamais penser au daemon.
La frontière Homebrew
Après un an d'essais et d'erreurs, j'ai appris quelque chose que j'aurais aimé qu'on me dise dès le départ : n'essaie pas de remplacer Homebrew. Contrôle-le, c'est tout.
Nix est parfait pour les binaires purs qui fonctionnent de manière identique sur toutes les plateformes. neovim, ripgrep, jq, tree-sitter. Mais dès que tu as besoin de quelque chose avec une compilation spécifique à macOS (comme postgresql@18) ou d'une app graphique (comme Ghostty ou Zed), Homebrew est simplement meilleur. Se battre contre cette frontière fait perdre du temps. Le pattern onActivation.cleanup = "zap" est la bonne réponse. Laisse Homebrew être Homebrew. Assure-toi juste que le flake décide de ce qu'il installe.
J'ai perdu des semaines à apprendre ça à la dure. Toi, tu n'es pas obligé.
Un an plus tard
Mon repo dotfiles a commencé en janvier 2021 avec des plugins vim et un zshrc. Cinq ans plus tard, c'est une spécification système complète et déclarative. J'ai configuré deux Macs à partir de zéro avec make rebuild. Les deux ont pris moins de 20 minutes, la plupart du temps juste à attendre les téléchargements.
Mais honnêtement, le scénario de la nouvelle installation n'arrive presque jamais. Le vrai gain est plus petit et plus fréquent. Une mise à jour macOS casse quelque chose ? Rebuild. Un nouveau projet a besoin d'un outil ? Une ligne, rebuild. Tu regrettes ? Rollback. Chaque changement est un commit. Chaque état est récupérable.
C'est ce que Nix t'offre. Pas de la magie. Juste un système qui se souvient de ce à quoi il est censé ressembler, même quand toi tu ne t'en souviens plus.