De auteur uit zijn frustratie over de huidige gebruikerservaring van Nix-flakes, waarbij het toevoegen van meerdere inputs en het handmatig configureren van follows (om duplicatie van nixpkgs te voorkomen) als omslachtig wordt ervaren.
Om dit op te lossen, introduceert de auteur Omniflake. Dit is één centrale flake die duizenden andere flakes bevat, waardoor gebruikers via één enkele input toegang krijgen tot een enorme hoeveelheid pakketten, overlays en NixOS-modules.
Technisch gezien is dit mogelijk dankzij twee kernaspecten van Nix:
- Lazy evaluation: Inputs worden alleen opgehaald als de bijbehorende output daadwerkelijk wordt aangeroepen. Hierdoor blijft het systeem bruikbaar ondanks de enorme hoeveelheid inputs.
- Het flake-lock-mechanisme: Dit zorgt voor reproduceerbaarheid door alle transitieve afhankelijkheden vast te pinnen in een lock-bestand.
Het artikel legt kort uit hoe de graaf van inputs en follows werkt en waarom de 'luiheid' van de Nix-taal cruciaal is voor het succes van Omniflake.
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:
- Jij → disko (input)
- Jij → agenix (input)
- disko → nixpkgs (follows)
- agenix → home-manager (input)
- home-manager → nixpkgs (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
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:
- Jij → disko (input)
- Jij → agenix (input)
- disko → nixpkgs (follows)
- agenix → home-manager (input)
- home-manager → nixpkgs (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