i like your thoughts on default.nix and providing schema - i had such thoughts myself for a while now
thanks for sharing your article
As to why not use
import "${nixpkgs}/nixos", following the channels-utilizing convention ofimport <nixpkgs/nixos>? Well, that was what I thought would look nicer as well, but that interface doesnāt allow for settingspecialArgs, for some reason; the only arguments it accepts areconfigurationandmodules. I might submit a pull request, but I can imagine thereās also just some reason for this choice that I donāt know.
By the way, thereās an open PR for allowing specialArgs the other way, courtesy of @bme:
And Iād personally use _module.args, where possible (which is settable via modules already).
Love this. Thanks for the write up. Iām playing with the thought of switching from flakes to npins as well for a couple of weeks now. And I also had the idea to using a similar Schema to flakes.
I donāt want to turn this thread into a debate about adding specialArgs to <nixpkgs/nixos> but if you have feelings about it Iām definitely interested in pushing my PR along, so comments in support welcome, or other alternative solutions which allow for npins sources to be used as modules that is as āergonomicā as pushing them through special args very welcome.
nixos-rebuild -f . -A blah is what we can move to today, and is significantly better than the āchannelā status-quo for non-flakes usage. Solving this ux wart (nice way to gather inputs flakes style without running into infrec problems) is an important stepping stone before trying to promote it in manuals / getting started materials.
I thought _module.args was a bad code smell, I guess. But honestly last time I wrote code using it was a while ago, so I might be misremembering things.
I really like the direction this is going and would love to have better conventions for default.nix. Another thing Flakes provide is pure evaluation. If I understand this correctly it just forbids certain things like network or environment variable access, so things like builtins.fetchurl without a hash do not work. Wouldnāt an alternative be to lint the code for āimpureā behavior?
Itās the opposite: specialArgs is the smell, if the arg isnāt needed in imports etc.
Enable the pure-eval option in nix.
Iām using nix-darwin and I wrote my own script for rebuilding the system to support pure evaluation (which I ended up not using as I donāt wanna copy my configuration to store on each rebuild).
It might need some adjustment for nixos but still might be of some help in understanding pure evaluation without flakes:
I also prefer taking system and other overridables as arguments as it allows for super easy cross compilation by just passing an instance of nixpkgs with crossSystem set (as opposed to flake-like schema where I donāt know how youāre supposed to do cross compilation properly):
This resonates extremely well with me, thank you for sharing your excellent write-up on this topic!
Iāve just been through a similar journey: as a long-term classic Nix user, Iāve recently worked with flakes extensively in a project, and what good things it brings to the table are (1) standardisation of the entry-point, and (2) pinning. As youāve mentioned, there are substantial downsides being brought in as well (for me personally), and so I am wanting to stick to classic Nix.
Iāve actually started working on a flake-like configuration system in classic Nix that mimics (and extends, albeit in opinionated ways for my own personal use) the excellent flake-parts library.
I am pretty sure you could just use flake-parts for your entry point in a classic Nix project, as most (all?) code is actually flakes-agnostic.
What about evaluation caching? Does classic Nix support evaluation caching?
Makes sense⦠that said, I played with using _module.args when I was coming up with this structure, and I ended up using it in specialArgs primarily because I really wanted to be able to do Flake style module importing (imports = [ self.nixosModules.my-cool-module ];). Propagating the value using _modules.args = { inherit sources self; } would be nice, but then it wouldnāt be able to be used in imports.
I should probably add a point about this in the post.
Itād be nice if just using nixos-rebuild --option pure-eval true -f . -A nixosConfigurations.<host> did the trick, but then thereās the problem of needing to copy the project tree into the Nix store before attempting to evaluate anything from it; Flakes does this automatically. (turns out thereās an issue somewhat tangentially referring to this too: Practical pure eval for paths in non-Flake CLIs Ā· Issue #9329 Ā· NixOS/nix Ā· GitHub)
The eval-cache is just a Flakes thing as far as I know. To be honest, Iām not sure why, technically speaking, evaluation caching has to be limited to the Flakes model though. Probably something to do with the pure evaluation requirement?
No, but flakes also really only support it in very limited cases and I think you donāt hit those cases most of the time
I really like the writeup.
I feel like a missing tool is something like nix flake show, that could walk a default.nix and report any nonstandard attributes (kinda like a linter). Itād take a stance on what it thinks is the ārightā way to lay things out, but people could iterate on tools like this to see what works well.
Unless thereās a technical reason that this needs to be part of the nix evaluation engine? (meaning less opportunity for rapid iteration, and less room for competing tools)
(Sorry @WeetHet, I accidentally replied to your message, this response is to the OP itself)
I really love this idea! I know your approach here is to skip flakes entirely, but Iām wondering if something like this would maybe be a possible path forward for flakes themselves?
Looking at the way you layed out your default.nix, it seems very possible to specify a āflakes 2.0ā schema and implement functionality in nix that enables (nearly) all the features flakes currently have with a more function-like syntax.
A āflake 2.0ā could look something like this:
{
self ? (import ./. {}),
sources ? (import ./flake.lock),
system,
nixpkgs ? sources.nixpkgs { localSystem = system; },
nix-darwin ? sources.nix-darwin { nixpkgs = nixpkgs; },
home-manager ? sources.home-manager { nixpkgs = nixpkgs; },
...
}@inputs:
flake {
flakeSchemaVersion = 2;
nixosConfigurations.ilo = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {
inherit nixpkgs self sources;
};
modules = [ ./nixos/ilo/configuration.nix ];
}
nixosModules.my-cool-module = import ./modules/nixos/my-cool-module;
packages = let
callPackage = nixpkgs.lib.callPackage;
in
{
package1 = callPackage ./packages/package1 {};
package2 = callPackage ./packages/package2 {};
};
nixConfig.bash-prompt = "test> ";
}
flake would be a new builtin (like derivation) that would read flakeSchemaVersion and, based on the version number, enforce that the attrset matches the schema. If it doesnāt, evaluation is aborted with an error. If it does, flake would also add _type = "flake"; to the resulting attrset to allow special-casing flakes during evaluation, for example to allow the "${self}/folder/file"-pattern you outlined and to print them like Ā«flake /nix/store/15p0ac1xa3mrxngm4vvqppiy6qd8a86l-sourceĀ». It could also add inherit inputs; or other things, whatever is required to make this ergonomic both for the user and the evaluator.
The only basic requirement for a flake.nix file would be that it MUST contain a function that, if called with just the system argument in pure evaluation mode, produces a flake. All other requirements would depend on the flakeSchemaVersion, though itās probably best to stay close to what we currently have. The evaluation itself would run just like for any nix file including lazy evaluation, so inputs arenāt downloaded when that isnāt needed.
import ./flake.lock would probably have to be handled specially because as I understand, itās somewhat undesirable to make the lock file a nix expression itself, but this would be a completely separate and optional mechanism. If you wanted to use npins or niv, you absolutely could, nix would just provide a default implementation of version locking for a good out-of-the-box UX.
One thing thatās absent here is the per-system-attributes, so you donāt have packages.x86_64-linux.package1, just packages.package1. This ābreaksā easy enumeration of platforms per package the way flakes do it right now, but I donāt think thatās an issue: The flakes schema could just specify that attributes of packages need to contain a meta.platforms attribute that is a list of strings, like all packages in nixpkgs already do. This makes cross-compilation in flakes much easier to achieve and allows you to try to compile a package even on other platforms without modifying the flakeās source code. Of course, this only works because there is now a system input, which has been thoroughly discussed in Make handling system parameter more ergonomic Ā· Issue #3843 Ā· NixOS/nix Ā· GitHub already. Another way to view this is that a flake is not the function, but the attrset produced by the function. In this model, the flake itself is pure.
Nothing needs to change about flake-urls and the concept of installables. They are a useful feature and the canonical address of a flake.
Flakes being plain old functions would also make them much easier to explain and understand because the entire mechanism is out in the open, they could be evaluated with nix eval, played around with on the repl, become easier to compose, and using --arg and --argstr could be trivially implemented to work for flakes as well, solving Allow plain Nix expressions as flake inputs Ā· Issue #5663 Ā· NixOS/nix Ā· GitHub, though I do understand that not everyone thinks this is a good idea.
Meanwhile, things like nix flake show, nix flake check, nix flake update and nix flake lock could continue to work as they already are, the change from flakes being an attrset to being a function would allow both kinds of flakes to co-exist for a while despite the attrset schema not being versioned, because the evaluator can detect the type of the expression easily, and the addition of a schema version addresses one of the biggest pain points of backwards- and forwards-compatibility. Separating the locking mechanism from the flake schema version and being able to iterate on them separately might also make it possible to address some of the abstraction issues that flakes currently have.
PR for specialArgs is now merged, so I guess in 25.11 this will get a bit neater.