I am very aware this touches on a heated topic, that has divided the community. This post is not meant to start a discussion, but a conversation.
I am just looking for setups (or blogs that discuss) that are not using flakes, which setup multiple systems using other projects (which might use flakes) to maintain/deploy multiple nixos configurations from a git repo.
I initially started out using flakes, as they seemed easier to get started with, but have always been wondering how to achieve the same goals using stable features.
I have not been able to find a lot of material on this topic though, but maybe Iāve been looking in the wrong places.
Are there people willing to share their repos, setups or tools they use? I am also interested in relevant docs or recent blogs.
# system.nix
let
# Pinned Nixpkgs archive
#
# Use `curl -I https://channels.nixos.org/nixos-26.05` to get the
# latest commit of the stable channel and `nix-prefetch-url --unpack`
# to compute its sha256 hash.
nixpkgs = builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/c217913993d6.tar.gz";
sha256 = "026mprs324330pfazlgbw987qmsa8ligglarvqbcxzig2kgw0lqg";
};
in
import "${nixpkgs}/nixos" {
# Build NixOS using an external configuration.nix file,
# or directly set your options here
configuration = ./configuration.nix;
}
If instead of defining a single import ${nixpkgs}/nixos { configuration....}, you define an attrset of { host1 = import.... ; host2 = import... }, then you would use nixos-rebuild -f system.nix -A host1 switch. You can omit the -f system.nix if it is placed in /etc/nixos/.
Pinning in the release notes example is using builtins, but there are other third-party tools like npins, nixtamal, niv, yea, pinch, etc.
IIUC, before system.nix, the more typical way would be to have separate host{1,2,3}-configuration.nix files, which would be passed with -f. See for example infra/build at main Ā· NixOS/infra Ā· GitHub which has individual host/default.nix configuration for build machines, which import from github:NixOS/infra/modules
I should also probably mention that flake inputs might either be handled by let... in assignments or else as function arguments depending on context.
For example, a default.nix providing some override to a package might look like:
# default.nix
let
nixpkgs = builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/c217913993d6.tar.gz";
sha256 = "026mprs324330pfazlgbw987qmsa8ligglarvqbcxzig2kgw0lqg";
};
in
{
pkgs ? import nixpkgs {},
...
}:
pkgs.hello.overrideAttrs (oldAttrs: {
name = "my-hello";
})
building:
You can then nix-build default.nix to use the pinned version, since nix-build of a function with default arguments will āmagicallyā pass all default arguments.
If you want something like flakeās follows, that gets replaced by either manually passing an argument in a .nix file like import ./default.nix { pkgs = import... } or on the command line like nix-build --args pkgs "import...".
ephemeral shell:
Very annoyingly, I havenāt found a pattern for nix-shell that is as nice as
nix run flakref#output-attr
To use the above default.nix in a nix-shell, I do something like
The /abs/path... can be replaced with $(pwd) or a URL to a github tarball as well
I would love it if someone has some better pattern for calling this, or if there was some parity in syntax for nix-shell to call into third-party repos. To be fair, I suppose I could also write a shell helper.
Itās also worth noting that the notes there on special args are stale now. Partly inspired by that blog, I authored this incredibly large PR to plug specialArgs into the default entry point.
EDIT: my timelines must be crossed on this, because the blog mentions my PR. Iām old, thatās my excuse and I am sticking to it.
Even though it now supports specialArgs, I think the nixos/default.nix entrypoint is still worse than the nixos/lib/eval-config.nix one. It doesnāt even support specifying a list of modules! eval-config.nix is what lib.nixosSystem uses, and works like youād expect:
# system.nix
let
sources = import ./other/npins;
# Read impurely from `builtins.currentSystem`. feel free to specify
# system if you'd like
pkgs = import sources.nixpkgs { config.allowUnfree = true; };
nixosSystem = import (sources.nixpkgs + "/nixos/lib/eval-config.nix");
in {
some-hostname = nixosSystem {
# optional, but if you need pkgs outside of this scope,
# then reusing it here prevents it from imported twice.
# good for evaluation perf!
inherit pkgs;
specialArgs = ; # your special args here
modules = ; # your module list here
};
;
why is it worse? If you pass the pkgs already configured then your module options for configuring the pkgs attrset donāt work. That seems like a strict downgrade to me?
edit: what do you mean doesnāt support passing modules?
My primary issue with the function is that it takes a single module rather than a list of modules. You can achieve the same thing with imports, but coming from lib.nixosSystem, I just wanted to recreate the same behavior.
If you pass the pkgs already configured then your module options for configuring the pkgs attrset donāt work. That seems like a strict downgrade to me?
Sure, you canāt use those options (although you can set the same things outside of those options, as you see with my config.allowUnfree setup). But, if you want to do something like:
let
# ...
wrappers = import ./wrappers.nix { inherit pkgs; };
in {
some-host = nixosSystem {
inherit pkgs;
specialArgs = { inherit wrappers; };
modules = []; # your modules here
};
}
Since we want access to pkgs in our set of wrappers, and itās passed in specialArgs, we need to define it outside of the scope of the nixos system. If we didnāt do inherit pkgs, then the module system would reimport it, which is approximately a 0.25s delay.
Ok, I think the complaint about multiple imports is⦠not worth talking about consider configuration.imports = [ ]. Agree to disagree. On the not caring about options: I think for a single machine configuration that makes sense. If you want to configure multiple machines, then you canāt assume they all look the same. You also are assuming that you need to import pkgs outside of the configuration. You donāt. The whole point of the specialArgs patch is to be able to feed something like sources into modules, which can receive the configured pkgs, exactly once.
One correction I have to issue for myself: I said you canāt configure the passed in pkgs, and thatās not strictly true! you can append overlays which is useful to know.
I second the system.nix recommendation, as itās meant to be a (limited) equivalent to flake.nix, and btw:
I doubt it, the heat comes when people ask questions like āwhen will flakes be stableā or heavily criticise/praise flakes, because that becomes opinion based and everyone has their 2c (not excluding myself). I donāt think anyone will personally take offence that you want to not-use flakes.
I know you asked for setups managing multiple machines; youāll have to scroll back in time to before I retired the second machine I was managing under this setup. Switching is a little awkward, in part since nix-darwin leans channel-heavy.
I use this as-is on my personal machine, and as an input to my work dotfiles, which:
are based on flakes,
mix in some company-specific config, secrets, and private packages,
and uniformly configure the MacOS host (just home-manager, no nix-darwin) and a NixOS VM.
If you want something like flakeās follows, that gets replaced by either manually passing an argument in a .nix file like import ./default.nix { pkgs = import... } or on the command line like nix-build --args pkgs "import...".
which has a pattern for a package.nix/default.nix that can be either imported or callPackageded easily. The basic idea is to write in a callPackage pattern, but define the arguments/package dependencies in terms of a pkgs argument that can take a default value.
i.e.
{
pkgs ? import <nixpkgs> {},
# pkgs ? import (import ./npins).nixpkgs {}, # for a repo that pins nixpkgs with npins
stdenv ? pkgs.stdenv,
dep1 ? pkgs.dep1,
dep2 ? pkgs.dep2,
...
}:
stdenv.mkDerivation {
# derivation defined here which has dependencies on pkgs.dep1 and pkgs.dep2
# syntactically should be defined using dep1 and dep2
}
Should actually be depending on dep1 and dep2. An important part of using this mechanism correctly is that you never mention pkgs again after the argument listās default values. Otherwise you can mess some things up, such as splicing for cross-compilation.