Eén flake om ze allemaal te beheersen

Ondanks hun alomtegenwoordigheid blijft het toevoegen van een flake-input een kleine irritatie die nooit weggaat. Er wordt beweerd dat het een positief kenmerk is dat flakes gefedereerd zijn, maar de realiteit is dat ik de eenvoud van een gecentraliseerde flake wil. Dat was de schoonheid en de kracht van nixpkgs.

Het proces is als volgt: je wilt disko, dus je voegt een URL toe, vervolgens een follows zodat het niet zijn eigen nixpkgs meesleept, en dan doe je dat opnieuw voor de volgende. Dit is in de Nix-community een meme geworden: hoe elke flake zijn eigen flake-utils meesleept.

Ik weiger deze gebruikerservaring te accepteren.

Dus vroeg ik me af: zou één flake elke andere flake kunnen bevatten, zodat je gewoon kunt grijpen naar wat je nodig hebt? 🤯

Omniflake

omniflake brengt duizenden Nix-flakes samen achter één flake-input.

inputs.omniflake.url = "github:fzakaria/omniflake";
inputs.omniflake.inputs.nixpkgs.follows = "nixpkgs";

Zodra je omniflake hebt, kun je het gebruiken als elke andere input.

Een pakket, in een shell of op een systeem:

environment.systemPackages = [
  omniflake.flakes.nh.packages.${system}.default
];

Een overlay:

nixpkgs.overlays = [ omniflake.flakes.rust-overlay.overlays.default ];

Een NixOS-module:

imports = [ omniflake.flakes.disko.nixosModules.disko ];

Of helemaal niets in een flake, direct vanuit de opdrachtregel:

$ nix run 'github:fzakaria/omniflake#flakes.nh.packages.x86_64-linux.default' -- --version

Er is een toegankelijke website die je kunt bezoeken op https://omniflake.com/ om de lijst met beschikbare flakes te bekijken en enkele nuttige documentaties te lezen.

Zodra het is toegevoegd, heb je toegang tot bijna twaalfduizend flakes (op het moment van schrijven), waar je naar behoefte "lazy" toegang tot kunt krijgen. Je betaalt alleen voor wat je gebruikt.

Dit klinkt absurd, maar het werkt. Een flake met duizenden inputs zou onbruikbaar moeten zijn, maar dankzij de "laziness" van de Nix-taal en het flake-lock-mechanisme werkt het perfect.

Hoe is het mogelijk om bijna elke flake in deze ene te bevatten?

Een korte introductie over flakes

Een flake is een directory met een flake.nix die twee dingen declareert: inputs (andere flakes waar het van afhankelijk is) en outputs (een functie van die inputs).

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
  };
}

inputs geeft aan waar je afhankelijk van bent, in globale termen: "nixos-unstable" is een bewegende branch. De flake.lock ernaast geeft precies aan naar welke commit dit is opgelost, zodat de build reproduceerbaar is. Het is vergelijkbaar met package-lock.json van npm, of Cargo.lock.

Een lock-bestand pinnt niet alleen je eigen inputs. Het pinnt de volledige transitieve graaf voor elke flake. Dat betekent dat als twee child-flakes allebei afhankelijk zijn van nixpkgs, ze elk hun eigen kopie van nixpkgs in het lock-bestand hebben, en dit verschillende commits kunnen zijn.

{
  "nodes": {
    "root":    { "inputs": { "agenix": "agenix", "nixpkgs": "nixpkgs" } },
    "agenix":  { "inputs": { "home-manager": "home-manager" },
                 "locked": { "rev": "5182...", "type": "github" } },
    "home-manager": { "inputs": { "nixpkgs": "nixpkgs_2" }, ... },
    "nixpkgs":   { "locked": { "rev": "9fbb...", "type": "github" } },
    "nixpkgs_2": { "locked": { "rev": "50ab...", "type": "github" } }
  }
}

In dit voorbeeld zijn er twee nixpkgs: nixpkgs en nixpkgs_2. Dit is de duplicatie waar iedereen over klaagt, en dit is waarom follows bestaat: follows herschrijft een afhankelijkheid om te wijzen naar een node die je al hebt, in plaats van een andere kopie op te halen. Dit verkleint de graaf, maar het betekent dat er niet meer gebouwd wordt tegen de exacte nixpkgs waartegen de auteur heeft getest.

Hier is een eenvoudige flake die follows gebruikt om nixpkgs te unificeren:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    disko.url = "github:nix-community/disko";
    disko.inputs.nixpkgs.follows = "nixpkgs";
    agenix.url = "github:ryantm/agenix";
    agenix.inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };
}

Relaties in de graaf:

  • Jijdisko (input)
  • Jijagenix (input)
  • diskonixpkgs (follows)
  • agenixhome-manager (input)
  • home-managernixpkgs (follows)

Vaste pijlen zijn inputs; gestreepte pijlen zijn follows-randen die samenvloeien naar één gedeelde nixpkgs.

Inputs zijn "lazy"

De schoonheid van veel waanzin in Nix is dat het een "lazy" taal is. Een input die geen output raakt, wordt nooit opgehaald.

Je kunt dit destructief bewijzen: lock een flake met twee inputs, en corrumpeer vervolgens één entry in flake.lock zodat deze onmogelijk kan worden opgelost.

$ sed -i 's/2810303efc.../0000000000000000000000000000000