npins instead of flakes?
While I have no quarrels with flakes or people who like them, there are things that annoyed me enough to want to try out living without them. I’ll leave some grievances about flakes at the end since I’d rather talk about my migration.
Getting started with npins
To get started with npins (coming from flakes):
# Use `nix-shell -p npins` if you don't have it in your system/user packages
npins init --bare
npins import-flake This will initialize a directory at npins and populate it with (updated) entries from your flake lockfile. You will access these by importing the helper with import ./npins. These are quite similar to what inputs do with flakes with one big caveat: they only contain information about the source (similar to what would be stored in sourceInfo for an input).
This is where npins’ job is done; the rest is up to you.
System configuration
This commit is where I switched. I will include some relevant parts here but you can reference it if you need full context.
Outputs
Starting with my nixos configuration, the first thing you will notice is that it mimics the outputs structure of flakes. Indeed, I find no issue with making configurations composable and easy to follow/use for others:
{
overrides ? {
multiseat-nix.outPath = "/home/tipson/Dev/multiseat-nix";
},
sources ? (import ./npins // overrides),
...
}:
{
inherit sources;
outPath = ./.;
nixosConfigurations = import ./hosts sources;
nixosModules = import ./modules;
} # usage is equivalent to builtins.getFlake but with control over inputs
> import (builtins.fetchGit https://github.com/mrtipson/systems.nix) {}
{
nixosConfigurations = { ... };
nixosModules = { ... };
outPath = /nix/store/qgy93654j6yxak1a0arz7x93h3jsm403-source;
sources = { ... };
} Entrypoint
To actually setup the entrypoint to be the equivalent as with flakes, we import the same file that is exposed by lib.nixosSystem:
sources:
with builtins;
let
nixosSystem = import "${sources.nixpkgs}/nixos/lib/eval-config.nix";
isSystem = path: pathExists (./${path}/system.nix);
hosts = filter isSystem (attrNames (readDir ./.));
importHost =
host:
{
${host} = nixosSystem (import ./${host}/system.nix sources);
};
in
builtins.foldl' (acc: x: acc // x) { } (map importHost hosts) specialArgs
The only thing left is to replace usage of the inputs specialArg. This can be done without even importing the source first:
-{ self, nixpkgs, ... }@inputs:
-rec {
+sources: {
- specialArgs = { inherit inputs system; };
+ specialArgs = { inherit sources; };
system = "x86_64-linux";
modules = [
./configuration.nix
- inputs.sops-nix.nixosModules.sops
+ "${sources.sops-nix}/modules/sops"
];
} But sometimes you will still need to import stuff:
{
config,
pkgs,
inputs,
sources,
...
}:
let
+ multiseat-nix = import sources.multiseat-nix { overrides = sources; };
+ inherit (inputs.multiseat-nix.packages.${pkgs.system})
- inherit (multiseat-nix.packages)
cage
drm-lease-manager
;
in
... Switching
Building and switching your configuration now becomes sudo nixos-rebuild build -I nixos-config=. --attr=nixosConfigurations.<hostname>.
Home manager configuration
This commit is where I switched. I will include some relevant parts here but you can reference it if you need full context.
Entrypoint
Since rebuilding is done by the home-manager cli, and it in turn calls some internal home-manager nix functions, we need to patch the functionality to allow ourselves the more broad entrypoint.
{
pkgs ? import <nixpkgs> { },
confPath,
confAttr ? null,
check ? true,
}:
let
-
- env = import ../modules {
- configuration =
- if confAttr == "" || confAttr == null then confPath else (import confPath).${confAttr};
- pkgs = pkgs;
- check = check;
- };
-
+ inherit (pkgs) lib;
+ resolved = (conf: if builtins.isFunction conf then conf { } else conf) (import confPath);
in
-{
- inherit (env)
- activationPackage
- config
- pkgs
- options
- ;
-}
+if confAttr == "" || confAttr == null then
+ resolved
+else
+ lib.getAttrFromPath (lib.splitString "." confAttr) resolved Arguments are left unchanged even though check is now ignored as it can be controlled with home-manager.lib.homeConfiguration. To rebuild, you now have to use
home-manager switch -f ~/my/hm/config -A myattr -I home-manager=~/patched/hm.
Simple setup
Next, I’ll show you how I setup HM so I don’t have to clone and patch it myself, as well as not needing to write -I home-manager=... in subsequent runs.
{
overrides ? { },
sources ? import ./npins // overrides,
...
}:
let
pkgs = import sources.nixpkgs { };
home-manager = import (pkgs.applyPatches {
name = "home-manager-patched";
src = sources.home-manager;
patches = [ ./flake-like-entrypoint.patch ];
}) { inherit pkgs; };
in
{
homeConfigurations.example = home-manager.lib.homeManagerConfiguration {
pkgs = import sources.nixpkgs { overlays = [ ]; };
extraSpecialArgs = { inherit sources; };
modules = [ ./home.nix ];
};
patchedHM = home-manager.path;
} A patched home-manager is created, and used to create the configuration. If you use programs.home-manager.enable = true; this will also make it so that home manager will from now on use this (patched) version of itself. Last thing that is done is that the store path to the patched HM is exposed, so for the initial rebuild all you need is home-manager switch -f ~/Dev/home.nix/ -A homeConfigurations.masina -I home-manager=$(nix eval --file default.nix --raw patchedHM)
nix-store --realise $(nix-instantiate --eval --raw -A patchedHM)may also be used instead of nix eval
Drawbacks
- The impure by default nature of non-flake configurations will for sure bite you in the ass sooner or later.
- Widespread adoption of flakes and neglection of
default.nixfiles in projects makes it so one cannot simply import a source and expect a similar interface to a project’s outputs to what flakes have. - Home manager setup requires patching.
Flake grievances
<input>.follows.nixpkgs = "nixpkgs"
This pattern makes sure inputs are using the same nixpkgs version, which prevents duplication and can help with compatibility issues. At the same time, it may introduce issues of its own, so it can’t really be the default behaviour. The end result is that every input gets bloated, instead of just writing
<input>.url = <url>
- Ignoring system registry when updating inputs
I get that the registry is a source of impurity, but updating the lockfile is inherently an impure operation because it depends on when it is initiated. This just makes it impossible for people to lock to the same nixpkgs across different flakes on the system (e.g. devshells, standalone HM, etc.)
- Consuming shell.nix, package.nix, default.nix
People can just use flakes for everything, replacing existing nix expression naming norms, while adding flakes to the many overloaded terms of the nix ecosystem
- Making shit more annoying
Im as masochistic as they get, but making me stage a file to git every time im testing something, toggling on flags like
--override-inputand--impure, or just doing stuff in the repl gets annoying quickly.
- Throwing around the system string like a hot potato
Oh, I love me some
pkgs.stdenv.hostPlatform.system, give me morex86_64-linux, please.