trying out flakes
Tl;DR
If you are tempted to try flake
-based system configuration on your
flake-less NixOS
but are a bit afraid of breaking existing setup
it can be done in a few lines of flake.nix
without breaking backwards
compatibility. I did it this way:
{
description = "Desktop system config";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
outputs = { self, nixpkgs }: {
nixosConfigurations = {
nz = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
# Remove the need for `<nixos>` / `<nixos-config>`
# management by `root` channel.
({config, pkgs, ...}: {
nix.nixPath = [
"nixpkgs=${nixpkgs}"
"nixos-config=/etc/nixos/configuration.nix"
]; }) ]; }; }; };
}
The only thing you need to do on top is to create git
repository out
of /etc/nixos
and you are done:
# cd /etc/nixos
# git init
# git add flake.nix # and maybe a few more files you have there
# nixos-rebuild --impure swith
Done! Chances are you don’t even need --impure
. One more touch is to
adapt automatic updates if you have those:
{ ... }:
{
system.autoUpgrade.enable = true;
# workaround unrecognized --no-build-output
system.autoUpgrade.flake = "/etc/nixos";
system.autoUpgrade.flags = [
# did not move out home directory overlay definition yet
"--impure"
# why not all inputs then?
"--update-input" "nixpkgs"
"--commit-lock-file"
];
}
Now it’s all done!
Long story
When I started using nix
daily I only heard of
nix flakes a bit. I did not dare to
try using them. Either for local development or for system-wide
configuration. From the documentation and notes around it felt
incompatible to current channel-based system configuration approach.
My biggest achievement was to run packages right off github
pull
requests like:
$ nix run github:NixOS/nixpkgs/pull/175618/merge#firefox
That felt like magic: no need to clone a repository or reconfigure
anything in the system. You just build-and-run the expression out of
internet. But I was not sure I wanted more of flakes
:)
The need arose
Things have changed when Sandro tried
nix-olde on flake
-based system
and got cryptic backtraces
back.
I realized I completely forgot about flakes
existence. By now I had
about a year of experience dealing with channel-based system
configuration. I took it as a good opportunity to have a more detailed
look at flakes
.
Normally nix-olde
instantiates system derivation out of <nixpkgs>
expression via:
$ nix-instantiate '<nixpkgs/nixos>' -A system
/nix/store/66db0cgpvcbdfmqaz86wfv264w7k63n8-nixos-system-nz-23.05pre-git.drv
And then parses the .drv
to extract the details about outdated
packages. <nixpkgs>
is usually maintained by root
via
nix-channel
.
But what about flakes
? Now does the equivalent work there if we build
the whole system out of it?
I made the simplest conversion possible by defining my system flake out
of existing /etc/nixos/configuration.nix
:
# $ cat /etc/nixos/flake.nix
{
description = "Desktop system config";
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
outputs = { self, nixpkgs }: {
nixosConfigurations.nz = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
]; }; };
}
Surprisingly that was enough to make nixos-rebuild --impure switch
build my system successfully. Note: I intentionally build it from
nixpkgs
master
branch and not an unstable
channel.
Then I removed <nixpkgs>
channel defined by root
user. I’m not sure
if I’m holding it wrong but it was enough to break nix-olde
. I
updated
nix-olde
to support flake
configuration. The gist of it is to
instantiate the system with a new path:
$ nix eval --impure --raw /etc/nixos#nixosConfigurations.$(hostname).config.system.build.toplevel.drvPath
/nix/store/x93fsz8451b0vxyz07db9879gllaq7a5-nixos-system-nz-23.05.20230205.b030e4a.drv
Then I noticed that nix-update
and even nix develop -f.
relies on
<nixpkgs>
variable to be present. I defined compatible variable to
match system’s nixpkgs
checkout:
# $ cat /etc/nixos/flake.nix
{
description = "Desktop system config";
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
outputs = { self, nixpkgs }: {
nixosConfigurations.nz = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
({config, pkgs, ...}: {
nix.nixPath = [
"nixpkgs=${nixpkgs}"
"nixos-config=/etc/nixos/configuration.nix"
]; }) ]; }; };
}
That way I could still rebuild my system with
nix build -f nixos system
and similar commands. And I did not change
flake.nix
since. I still can use all the existing tools that rely on
<nixpkgs>
path.
To fix auto-upgrade I had to reconfigure it slightly:
# $ cat /etc/nixos/configuration.nix
{ config, pkgs, ... }:
{
system.autoUpgrade.enable = true;
# workaround unrecognized --no-build-output
system.autoUpgrade.flake = "/etc/nixos";
system.autoUpgrade.flags = [
# did not move out home directory overlay definition yet
"--impure"
# why not all inputs then?
"--update-input" "nixpkgs"
"--commit-lock-file"
# Give all the CPUs to one job at a time.
"--max-jobs" "1"
];
}
Here --impure
allows file access outside explicit input
s (I have a
few overlays lying all over the system). And --update-input nixpkgs
pulls nixpkgs
update every time the system tries to update.
The result is not as scary as I initially suspected. This setup did not
break most of my workflows around local nixpkgs
checkouts.
Now I can run other packages (similar to nixpkgs
input) right from
git
state. For example my typical uselex
package is defined via git
snapshot:
# $ cat cat ~/overlays/uselex/default.nix
{ lib
, stdenv
, fetchFromGitHub
, ruby_3_1
, binutils-unwrapped-all-targets
, unstableGitUpdater
}:
rec {
stdenv.mkDerivation pname = "uselex";
version = "unstable-2022-08-29";
src = fetchFromGitHub {
owner = "trofi";
repo = "uselex";
rev = "5cf79a872f3331ce87171e66cf27c430585f65af";
sha256 = "sha256-0aFJaGLcrrEkOH3cFs2uHjkCUw9ndckngfnb0J1FK7c=";
};
# ... more stucff
}
I was able to define it’s live
version by passing an input explicitly:
# $ cat ~/overlays/flake.nix
{
description = "trofi's overlay";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
uselex = {
url = "github:trofi/uselex";
flake = false;
};
};
outputs = { self, nixpkgs, uselex }@inputs: let
np = import nixpkgs { system = "x86_64-linux"; };
in {
packages."x86_64-linux" = rec {
uselex = np.callPackage ./uselex {};
uselex_live = uselex.overrideAttrs (oa: {
version = inputs.uselex.shortRev;
src = inputs.uselex;
}); }; };
}
Here I defined 2 packages:
uselex
: a package as it’s defined inuselex/default.nix
uselex_live
: a package that usesmaster
branch fromgit
and reuses rest ofuselex
definition.
Now I can build both as:
$ cd ~/overlays
$ nix build .#uselex .#uselex_live
$ nix flake info
warning: 'nix flake info' is a deprecated alias for 'nix flake metadata'
warning: Git tree '/home/slyfox/.config/nixpkgs' is dirty
Resolved URL: git+file:///home/slyfox/overlays/nixpkgs
Locked URL: git+file:///home/slyfox/overlays/nixpkgs
Description: trofi's overlay
Path: /nix/store/s2b6pagz9i55jq71jfp3ml9y2dyl0mlr-source
Last modified: 2023-01-26 22:07:27
Inputs:
├───nixpkgs: github:NixOS/nixpkgs/f69c8b761a683940edeed0c23da1a5b8bd50bed3
└───uselex: github:trofi/uselex/5cf79a872f3331ce87171e66cf27c430585f65af
Using --override-input
flag we can redirect default target commit to
locally modified tree or any other commit:
# no overrides:
$ nix build .#uselex_live
$ ls -ld result result-1
result -> /nix/store/76qkbdna1y4adbkk9k2g7znw2v3yyr7s-uselex-5cf79a8
# redirect to the local tree:
$ nix build .#uselex_live --override-input uselex ~/dev/git/uselex
• Updated input 'uselex':
'github:trofi/uselex/5cf79a872f3331ce87171e66cf27c430585f65af' (2022-08-29)
→ 'git+file:///home/slyfox/dev/git/uselex?ref=refs%2fheads%2fmaster&rev=5cf79a872f3331ce87171e66cf27c430585f65af' (2022-08-29)
$ ls -ld result result-1
result -> /nix/store/76qkbdna1y4adbkk9k2g7znw2v3yyr7s-uselex-5cf79a8
# redirect to the arbitrary commit or branch:
$ nix build .#uselex_live --override-input uselex github:trofi/uselex/fe54bc12013a2a28f1638bdd5faa2f81d4d8fd1c
• Updated input 'uselex':
'github:trofi/uselex/5cf79a872f3331ce87171e66cf27c430585f65af' (2022-08-29)
→ 'github:trofi/uselex/fe54bc12013a2a28f1638bdd5faa2f81d4d8fd1c' (2017-07-28)
$ ls -ld result result-1
result -> /nix/store/82maym4hks1nfcprharxwrpvf7ck6hz4-uselex-fe54bc1
Note how input reports new commit it switches to and encodes it into
package name (as we requested it with version = inputs.uselex.shortRev;
).
One can even add a short alias for an arbitrary flake repository:
$ nix registry add ul github:trofi/uselex-flake
$ nix run ul#uselex_live --no-write-lock-file
...
== SYNOPSIS (uselex-0.0.1)
uselex.rb - look for USEless EXports in object files
$ nix registry remove ul
And for completeness here is how flake.lock
looks like:
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1675628371,
"narHash": "sha256-KsAGKX6R5OZ4mvX0v9I8rXoQD62NG8bNq2vDh731fUk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "461ef24010bec9df46f9f982e27441d83a856563",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"uselex": "uselex"
}
},
"uselex": {
"flake": false,
"locked": {
"lastModified": 1661761258,
"narHash": "sha256-0aFJaGLcrrEkOH3cFs2uHjkCUw9ndckngfnb0J1FK7c=",
"owner": "trofi",
"repo": "uselex",
"rev": "5cf79a872f3331ce87171e66cf27c430585f65af",
"type": "github"
},
"original": {
"owner": "trofi",
"repo": "uselex",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
Parting words
You don’t have to start from scratch if you are migrating from a typical
/etc/nixos/configuration.nix
to flake.nix
.
While slightly verbose flakes
are not too scary as a concept. They
allow one to encode (and persist) all external inputs into /nix/store
and optionally write it into the flake.lock
to be able to restore
exact build environment when needed.
flakes
still provide flexibility of switching the revisions back and
forth for individual inputs for test purposes.
Have fun!