La semaine dernière, j’ai débuté un nouveau job 🎉 Et qui dit nouveau job dit nouvelle machine de travail.

Si j’ai passé plus des dix dernières années de ma carrière sur ArchLinux, je n’ai cette fois-ci pas eu le luxe de rester sur mon OS de prédilection. J’ai eu le choix entre macOS et Windows. Autant dire que je n’ai pas eu à réfléchir très longtemps.

J’ai parlé il y a peu de Nix, un gestionnaire de paquets pas comme les autres. Et aussi de NixOS, une distribution Linux équipé de Nix pour seul moyen de gestion.

Hé bien Nix fonctionne sur macOS. Et Nix-Darwin porte une bonne partie des fonctionnalités de NixOS sur la plateforme d’Apple.

Cet article est un condensé de mon expérience avec tous ces outils pour tenter de me rapprocher le plus possible d’une expérience agréable sur ce nouvel ordinateur.

Le problème

Je ne suis par réellement néophyte sur macOS. Notamment, je me suis déjà frotté à l’absence de gestionnaire de paquets natif sur cette plateforme.

Et d’ailleurs, même les OS avec un gestionnaire de paquets digne de ce nom ont un défaut : il faut tout réinstaller et reconfigurer à chaque installation. Alors, oui, il y a bien des solutions pour aller plus vite. Je pense notamment aux dotfiles et l’usage de stow. Couplé à git, il devient possible d’automatiser presque toute la configuration.

Mais quid de l’installation ? Oui, il est possible de lister les paquets installés sur une machine et de s’en servir sur une autre. Personnellement, je n’affectionne pas cette approche, car chaque machine a son usage et ne requiert pas les mêmes programmes installés. Je préfère n’avoir que ce qui est nécessaire là où c’est nécessaire. Ni plus, ni moins.

La solution

Only a Sith deals in absolutes

Obi-Wan

Oui, LA solution. Nan, il doit y en avoir d’autres . Mais c’est celle que j’ai choisie.

Je veux avoir mes outils en ligne de commande habituels à portée de main. Et je les veux configurés.

Je veux aussi un certain nombre de logiciels avec interface graphique. Oui, je sais, stupéfaction etc, mais je ne suis pas encore prêt à passer sur un navigateur web 100% CLI à tout hasard.

Et par-dessus le marché, je veux tout ça vite. Hé bien. Voilà, vite et bien 👌

Et c’est ce que permet le combo suivant :

  • Nix : le gestionnaire de paquet qui secoue les traditions des systèmes posix en remettant en cause la façon de ranger et d’accéder aux binaires, bibliothèques et réglages,
  • NixDarwin : la surcouche qui sait éplucher la pomme dans le sens des plumes pour des réglages à l’échelle du système d’exploitation macOS,
  • HomeManager : la surcouche permettant d’automatiser installations et réglages à l’échelle d’une session utilisateur,
  • HomeBrew : le gestionnaire de paquets capable d’apporter les outils avec interfaces graphiques.

J’ai fait l’erreur de foncer tête baissée sur ces outils dans le désordre. J’ai bien cru avoir endommagé la machine suite à des manipulations assurément hasardeuses.

Move fast, break things comme disait l’autre. Mais attention, qui casse paie comme disait l’autre autre.

Les manipulations décrites par la suite sont à effectuer à vos risques et périls. Surtout s’il s’agit d’une machine de travail 😨

Nix

Le procédé d’installation de nix est décrit directement sur le site dédié. Je ne préfère pas copier ces commandes ici, car je pense qu’il faut toujours aller les prendre à la source de vérité au moment de l’installation. Il y a plus de détails sur ce que l’installateur effectue dans la documentation. Et il existe aussi cette page concernant la désinstallation si besoin.

À la fin, il doit être possible d’obtenir les informations sur l’état du système:

% nix-shell -p nix-info --run "nix-info -m"

 - system: `"aarch64-darwin"`
 - host os: `Darwin 22.5.0, macOS 13.4.1`
 - multi-user?: `yes`
 - sandbox: `no`
 - version: `nix-env (Nix) 2.15.1`
 - channels(root): `"nixpkgs"`
 - nixpkgs: `/nix/var/nix/profiles/per-user/root/channels/nixpkgs`

Nix Darwin

Comme dit un peu plus haut, Nix Darwin est une surcouche. Le projet repose sur Nix est apporte à macOS l’approche déclarative de Nix pour l’installation et la configuration des logiciels.

Il y a deux façons de l’utiliser : la bonne, et la mauvaise via un installateur ou via Flakes. J’ai choisi la bonne de passer par Flakes.

Les Flakes sont une façon de rendre les builds et switchs de Nix et NixDarwin plus déclaratif et plus répétables. Un flake n’est rien d’autre qu’un dossier contenant un fichier flake.nix décrivant les entrées passées à Nix (et Nix Darwin ici) et les sorties de ce(s) dernier(s) qui doivent dépendre uniquement des entrées en questions. Pas de magie, de sources d’informations cachées ni de builds/switchs non répétables.

Voici celui que j’utilise au moment d’écrire ces lignes :

{
    inputs = {
        nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

        home-manager = {
            url = "github:nix-community/home-manager";
            inputs.nixpkgs.follows = "nixpkgs";
        };

        darwin = {
            url = "github:lnl7/nix-darwin/master";
            inputs.nixpkgs.follows = "nixpkgs";
        };
    };

    outputs = { self, nixpkgs, home-manager, darwin }: {
        darwinConfigurations."__HOSTNAME__" = darwin.lib.darwinSystem {
            system = "aarch64-darwin";
            modules = [
                home-manager.darwinModules.home-manager
                ./work
            ];
        };
    };
}

Je pense que c’est le bout de code le plus simple de l’article :

  • Les entrées inputs sont au nombre de trois :
    1. nixpkgs contenant la liste des paquets que Nix pourra installer pour tous les utilisateurs de la machine,
    2. home-manager reposant sur nixpkgs pour l’installation et la configuration à la discrétion de chaque utilisateur,
    3. darwin, lui aussi reposant sur nixpkgs et apportant donc les petits bonus par macOS.
  • Les sorties outputs au nombre de une : la configuration Nix Darwin pour la machine __HOSTNAME__ à remplacer par le nom de votre machine bien entendu.

Les autres sorties pourraient par exemple être les configurations pour d’autres machines, d’autres systèmes d’exploitation ou simple des réglages de home-manager pour différents utilisateurs.

La suite, la partie plus complexe à mes yeux, c’est ce qui est appelé dans les modules. Honnêtement, je n’ai pour le moment aucune idée de ce qui se trouve dans celui fourni par home-manager. Je vais me concentrer sur celui que j’ai rédigé et appelé habillement work pour ma machine de travail. Ce qu’il faut noter ici c’est qu’il est possible de séparer les configurations dans des fichiers/dossiers en blocs minimaux à réutiliser au détail pour les besoins des différentes machines.

Work

Par convention, Nix voyant le module work, va aller chercher le dossier éponyme et charger le fichier default.nix. Dont voici le contenu, là encore au moment d’écrire ces lignes :

1{ pkgs, ... }:
2{
3 services.nix-daemon.enable = true;
4
5 system.keyboard = {
6 enableKeyMapping = true;
7 remapCapsLockToEscape = true;
8 };
9
10 users.users."pcoves".home = "/Users/pcoves";
11
12 environment.systemPackages = with pkgs; [
13 ansible
14 awscli2
15 colima
16 docker
17 neovim
18 packer
19 stow
20 terraform
21 tree
22 ];
23
24 programs = {
25 zsh.enable = true;
26 };
27
28 fonts.fontDir.enable = true;
29 fonts.fonts = with pkgs; [
30 (nerdfonts.override { fonts = [ "FiraCode" ]; })
31 ];
32
33 homebrew = {
34 enable = true;
35 onActivation.upgrade = true;
36 casks = [
37 "alacritty"
38 "firefox"
39 ];
40 };
41
42 home-manager.useGlobalPkgs = true;
43 home-manager.useUserPackages = true;
44 home-manager.users.pcoves = { pkgs, ... }: {
45 home = {
46 stateVersion = "23.05";
47 file = {
48 ".gnupg/gpg-agent.conf".text = ''
49 default-cache-ttl 600
50 max-cache-ttl 7200
51 enable-ssh-support
52 '';
53 };
54 };
55
56 programs = {
57 gpg = {
58 enable = true;
59 settings = {
60 use-agent = true;
61 };
62 };
63
64 password-store = {
65 enable = true;
66 package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]);
67 };
68
69 tmux = {
70 enable = true;
71
72 clock24 = true;
73 disableConfirmationPrompt = true;
74 keyMode = "vi";
75 shortcut = "s";
76 mouse = false;
77
78 extraConfig = ''
79 #
80 # Update the split keys from " and % to - and |.
81 #
82 unbind '"'
83 bind - split-window -v -c "#{pane_current_path}"
84 unbind %
85 bind | split-window -h -c "#{pane_current_path}"
86
87 #
88 # Update the copy/paste buffes from [ and ] to ( and )
89 #
90 unbind [
91 unbind ]
92 bind ( copy-mode
93 bind ) paste-buffer
94
95 bind-key -T copy-mode-vi 'v' send -X begin-selection
96 bind-key -T copy-mode-vi 'C-v' send -X rectangle-toggle
97 bind-key -T copy-mode-vi 'y' send -X copy-selection
98
99 bind C-l send-keys 'C-l' # Enable the clear screen shortcut.
100
101 set-option -g renumber-windows on # Keep windows's number up to date.
102 set-option -g allow-rename off # Do not update window's name accoring to current task.
103
104 set -g @resurrect-save 'o'
105 set -g @resurrect-restore 'O'
106 set -g @resurrect-capture-pane-contents 'on'
107 set -g @resurrect-strategy-vim 'session'
108 set -g @continuum-restore 'on'
109 '';
110
111 plugins = with pkgs; [
112 tmuxPlugins.resurrect
113 tmuxPlugins.continuum
114 tmuxPlugins.vim-tmux-navigator
115 ];
116 };
117 };
118 };
119}

Oui, ça fait beaucoup d’un coup. Mais le découpage est assez simple dans le cas présent :

  • Tout d’abord, un réglage de Nix lui-même : oui, je veux utiliser le daemon de Nix. Autrement il faudrait le lancer manuellement. Peut-être que les gourous de Nix saurait mieux expliquer, mais je ne vois pas encore de cas d’usage à désactiver ce réglage.
  • Ensuite quelques réglages concernant le comportement du clavier. Étant donné que CapsLock ne devrait pas exister, autant s’en servir pour quelque chose d’utile.
  • Je renseigne le chemin vers mon utilisateur. J’ai cru comprendre que c’était automatique auparavant. Plus maintenant et une erreur sensiblement cryptique apparaît sans cette ligne.
  • Je décris l’ensemble des applications que je veux utiliser sur la machine. Tous les utilisateurs pourront lancer ces commandes. Alors, oui, c’est une machine mono-utilisateur. Mais ce n’est pas toujours le cas, notamment dans le cas de serveurs. Il existe des pages pour rechercher les paquets et options disponibles.
  • Un petit bonus purement esthétique, j’aime beaucoup cette police d’écriture FiraCode alors pourquoi se priver ?
  • Je reviendrai un peu plus tard sur le bloc Homebrew.
  • Toute la fin concerne HomeManager, la surcouche concernant les utilisateurs et je vais la détailler juste après.

Simple non ?

Home-Manager

Contrairement aux réglages précédents qui concernaient la machine toute entière (__HOSTNAME__), home-manager s’occupe des utilisateurs. C’est là que se range notamment la configuration des outils utilisés.

Pour le moment, ce fichier est assez sommaire, j’ai le strict minimum pour pouvoir travailler. Je suppose que cette partie va très vite être séparée en blocs réutilisables et indépendants.

Pour le moment, on y retrouve un peu de configuration pour gnupg et un gros bloc pour tmux. Il faut que je farfouine un peu plus pour savoir si je peux obtenir le meilleur des deux mondes : la configuration de l’OS via Nix et la configuration des outils via mes dotfiles, car je tiens tout de même à pouvoir utiliser mes configurations à la volée, sans Nix si besoin.

Dans l’état actuel des choses, gpg-agent n’est pas accessible via Nix sans Nix Darwin.

Il existe bon nombre de services spécifiques à macOS qui sont rendus disponibles seulement ainsi. Par chance, les messages d’erreur si l’on essaie d’utiliser les versions unix sont assez explicites et la solution est rapide à trouver.

Pour savoir ce qu’il est possible d’installer et comment le configurer, la meilleure ressource à ma connaissance est cette page. Je trouve que l’outil manque de flexibilité sur certain point, mais je suis à peu près sûr que c’est parce que je n’ai pas encore acquis suffisamment d’expertise. J’ai hâte que le moi-du-futur® se moque du moi-du-présent à ce propos.

Homebrew

Homebrew se présente comme The Missing Package Manager for macOS (or Linux). Honnêtement, je n’en ai jamais eu besoin sur Linux. Mais c’est vrai qu’il m’a été indispensable en bien des occasions avant que je ne découvre Nix.

Il s’agit d’un gestionnaire de paquets tout à fait classique à l’usage. Une commande, brew, des sous commandes pour installer/désinstaller des paquets passés en arguments. L’outil classique, la méthode impérative classique.

Pourquoi rajouter Homebrew alors que Nix, Nix Darwin et Home Manager font déjà un si bon travail ? Simplement parce que seul Homebrew permet d’installer des applications avec interfaces graphiques. Les trois autres ne le permettent pas sur macOS pour des questions de droits. Qu’à cela ne tienne, ce petit bloc de configuration installe mon terminal favori et mon navigateur habituel.

Usage

C’est bien beau cette approche déclarative, mais on fait quoi de tout ça ?

Un lecteur attentif

Hé bien on le donne à manger à Nix, on se rend compte que ça échoue, on lance la paire de commande que Nix nous indique, on retente, on constate que ça marche nickel, on est contents. Dans cet ordre bien précis.

1% nix build .#darwinConfigurations.__HOSTNAME__.system --extra-experimental-features "nix-command flakes"
2% ./result/sw/bin/darwin-rebuild switch --flake .
3% printf 'run\tprivate/var/run\n' | sudo tee -a /etc/synthetic.conf
4% /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -t
5% ./result/sw/bin/darwin-rebuild switch --flake .
  1. La première ligne va calculer la dérivation décrite dans la configuration. C’est-à-dire qu’elle va, étant donné les entrées et l’état du système, calculer l’ensemble des modifications à effectuer pour arriver dans l’état désiré.
  2. La seconde ligne va appliquer les modifications. Enfin, essayer.
  3. Les lignes trois et quatre sont indiquées par Nix. C’est pour faire face à une limitation de macOS concernant l’écriture de fichiers/dossiers vers la racine du système.
  4. La dernière ligne applique les modifications demandées. Les logiciels sont installés et configurés, prêts à l’emploi !

Une fois fait, Nix Darwin est installé et les commandes seront plus simples pour calculer les dérivations et les appliquer :

% darwin-rebuild build --flake .
% darwin-rebuild switch --flake .

Conclusion

L’expérience n’est pas des plus aisées. J’ai vraiment cru avoir mis la machine en l’air deux jours après l’avoir reçue. Au final, plus de peur que de mal avec une quasi-nuit blanche à réparer mes bêtises.

Mais désormais, je peux installer et configurer tout un OS en quelques minutes. Je compte bien intégrer ces codes à un dépôt déjà existant contenant les configurations de mes ordinateurs sur Linux et n’avoir plus qu’une seule source de vérité pour toutes mes machines.

Avec le peu de recul dont je dispose sur ces outils, je conseillerais de ne surtout pas les utiliser. À moins de faire un métier similaire au mien, nécessitant l’usage de N logiciels dans M versions différentes et de devoir passer d’une configuration à l’autre très souvent, je suis convaincu que c’est sortir une le grand jeu pour pas grand-chose. Ceci étant dit, j’en ai fait l’expérience auparavant lors de nombreuses installations de serveurs “identiques” sur une courte période de temps, c’est extrêmement efficace.