Currently, to declaratively manage just user-level packages (and nothing else about the machine), the options are:
- home-manager. It’s quite large and wants to do a lot of things; it winds up eating some seconds of bonus Nix evaluation for most tasks.
-
nix-env
[1] can do exactly the desired thing, but then locking the nixpkgs version is kind of annoying, since one would needflake-compat
. -
home-mangler
. A fair amount of Rust code to wranglenix profile
into installing declared sets of packages. -
nix-profile-declarative
. This builds a nix-env format manifest by reusing the nix internals; this is really cool and I only found it while writing this post. It could likely be adapted to use flakes. The manifest is not strictly required though; NixOS doesn’t have one.
None of these were not quite what I was envisioning: simply build the profile with Nix flakes and then switch to it. I sought out to build exactly that.
I realized that NixOS/home-manager is using exactly the machinery to necessary to achieve this in nixos-rebuild
. The machinery in question is nix-env --set
, which is used for generations in NixOS, and allows rollbacks like standard profiles.
Another realization is the cute trick of using more levels under packages.*
, as done with nix2container’s nix run .#dockerImage.copyTo <...>
and similar.
Combining these ideas together, we can make declarative flake profiles in 14 lines of Nix code by leveraging buildEnv
and adding switch/rollback shell scripts as attributes on that derivation:
# Creates a profile. Arguments are forwarded to pkgs.buildEnv.
#
# The following attributes are available:
# - .switch: switches to the profile
# - .rollback: rolls back to the previous version of the profile
args @ { pkgs, name ? "flake-profile", ... }:
let
args' = builtins.removeAttrs args [ "pkgs" ];
env = pkgs.buildEnv (args' // {
inherit name;
});
in
env // {
switch = pkgs.writeShellScriptBin "switch" ''
nix-env --set ${env} "$@"
'';
rollback = pkgs.writeShellScriptBin "rollback" ''
nix-env --rollback "$@"
'';
}
To make a declarative profile, make a flake:
{
description = "Basic usage of flakey-profile";
inputs = {
flakey-profile.url = "github:lf-/flakey-profile";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, flakey-profile }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
in
{
# Any extra arguments to mkProfile are forwarded directly to pkgs.buildEnv.
#
# Usage:
# Switch to this flake:
# nix run .#profile.switch
# Revert a profile change:
# nix run .#profile.rollback
# Build, without switching:
# nix build .#profile
packages.profile = flakey-profile.lib.mkProfile {
inherit pkgs;
paths = with pkgs; [
hello # put more packages here
];
};
});
}
and then you can switch to the profile with: nix run .#profile.switch
, and rollback with nix run .#profile.rollback
.
The code is here as CC0: GitHub - lf-/flakey-profile: Declarative profiles with nix flakes
Other related posts:
- Transitioning from imperative to declarative package management with nix alone
- Flakes as a unified format for profiles
[1]: nix-env --log-format bar-with-logs --install --remove-all --file ./env.nix