xesite/blog/nixos-desktop-flow-2020-04-...

532 lines
18 KiB
Markdown

---
title: "My NixOS Desktop Flow"
date: 2020-04-25
series: howto
---
Before I built my current desktop, I had been using a [2013 Mac Pro][macpro2013]
for at least 7 years. This machine has seen me through living in a few cities
(Bellevue, Mountain View and Montreal), but it was starting to show its age. Its
12 core Xeon is really no slouch (scoring about 5 minutes in my "compile the
linux kernel" test), but with Intel security patches it was starting to get
slower and slower as time went on.
[macpro2013]: https://www.apple.com/mac-pro-2013/specs/
So in March (just before the situation started) I ordered the parts for my new
tower and built my current desktop machine. From the start, I wanted it to run
Linux and have 64 GB of ram, mostly so I could write and test programs without
having to worry about ram exhaustion.
When the parts were almost in, I had decided to really start digging into
[NixOS][nixos]. Friends on IRC and Discord had been trying to get me to use it
for years, and I was really impressed with a simple setup that I had in a
virtual machine. So I decided to jump head-first down that rabbit hole, and I'm
honestly really glad I did.
[nixos]: https://nixos.org
NixOS is built on a more functional approach to package management called
[Nix][nix]. Parts of the configuration can be easily broken off into modules
that can be reused across machines in a deployment. If [Ansible][ansible] or
other tools like it let you customize an existing Linux distribution to meet
your needs, NixOS allows you to craft your own Linux distribution around your
needs.
[nix]: https://nixos.org/nix/
[ansible]: https://www.ansible.com/
Unfortunately, the Nix and NixOS documentation is a bit more dense than most
other Linux programs/distributions are, and it's a bit easy to get lost in it.
I'm going to attempt to explain a lot of the guiding principles behind Nix and
NixOS and how they fit into how I use NixOS on my desktop.
## What is a Package?
Earlier, I mentioned that Nix is a _functional_ package manager. This means that
Nix views packages as a combination of inputs to get an output:
![A nix package is the metadata, the source code, the build instructions and
some patches as input to a derivation to create a
package](/static/blog/nix-package.png)
This is how most package managers work (even things like Windows installer
files), but Nix goes a step further by disallowing package builds to access the
internet. This allows Nix packages to be a lot more reproducible; meaning if you
have the same inputs (source code, build script and patches) you should _always_
get the same output byte-for-byte every time you build the same package at the
same version.
### A Simple Package
Let's consider a simple example, my [gruvbox-inspired CSS file][gruvboxcss]'s
[`default.nix`][gcssdefaultnix] file':
[gruvboxcss]: https://github.com/Xe/gruvbox-css
[gcssdefaultnix]: https://github.com/Xe/gruvbox-css/blob/master/default.nix
```nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation {
pname = "gruvbox-css";
version = "latest";
src = ./.;
phases = "installPhase";
installPhase = ''
mkdir -p $out
cp -rf $src/gruvbox.css $out/gruvbox.css
'';
}
```
This creates a package named `gruvbox-css` with the version `latest`. Let's
break this down its `default.nix` line by line:
```nix
{ pkgs ? import <nixpkgs> { } }:
```
This creates a function that either takes in the `pkgs` object or tells Nix to
import the standard package library [nixpkgs][nixpkgs] as `pkgs`. nixpkgs
includes a lot of utilities like a standard packaging environment, special
builders for things like snaps and Docker images as well as one of the largest
package sets out there.
[nixpkgs]: https://nixos.org/nixpkgs/
```nix
pkgs.stdenv.mkDerivation {
# ...
}
```
This runs the [`stdenv.mkDerivation`][mkderiv] function with some arguments in an
object. The "standard environment" comes with tools like GCC, bash, coreutils,
find, sed, grep, awk, tar, make, patch and all of the major compression tools.
This means that our package builds can build C/C++ programs, copy files to the
output, and extract downloaded source files by default. You can add other inputs
to this environment if you need to, but for now it works as-is.
[mkderiv]: https://nixos.org/nixpkgs/manual/#sec-using-stdenv
Let's specify the name and version of this package:
```nix
pname = "gruvbox-css";
version = "latest";
```
`pname` stands for "package name". It is combined with the version to create the
resulting package name. In this case it would be `gruvbox-css-latest`.
Let's tell Nix how to build this package:
```nix
src = ./.;
phases = "installPhase";
installPhase = ''
mkdir -p $out
cp -rf $src/gruvbox.css $out/gruvbox.css
'';
```
The `src` attribute tells Nix where the source code of the package is stored.
Sometimes this can be a URL to a compressed archive on the internet, sometimes
it can be a git repo, but for now it's the current working directory `./.`.
This is a CSS file, it doesn't make sense to have to build these, so we skip the
build phase and tell Nix to directly install the package to its output folder:
```shell
mkdir -p $out
cp -rf $src/gruvbox.css $out/gruvbox.css
```
This two-liner shell script creates the output directory (usually exposed as
`$out`) and then copies `gruvbox.css` into it. When we run this through Nix
with`nix-build`, we get output that looks something like this:
```console
$ nix-build ./default.nix
these derivations will be built:
/nix/store/c99n4ixraigf4jb0jfjxbkzicd79scpj-gruvbox-css.drv
building '/nix/store/c99n4ixraigf4jb0jfjxbkzicd79scpj-gruvbox-css.drv'...
installing
/nix/store/ng5qnhwyrk9zaidjv00arhx787r0412s-gruvbox-css
```
And `/nix/store/ng5qnhwyrk9zaidjv00arhx787r0412s-gruvbox-css` is the output
package. Looking at its contents with `ls`, we see this:
```console
$ ls /nix/store/ng5qnhwyrk9zaidjv00arhx787r0412s-gruvbox-css
gruvbox.css
```
### A More Complicated Package
For a more complicated package, let's look at the [build directions of the
website you are reading right now][sitedefaultnix]:
[sitedefaultnix]: https://github.com/Xe/site/blob/master/site.nix
```nix
{ pkgs ? import (import ./nix/sources.nix).nixpkgs }:
with pkgs;
assert lib.versionAtLeast go.version "1.13";
buildGoPackage rec {
pname = "christinewebsite";
version = "latest";
goPackagePath = "christine.website";
src = ./.;
goDeps = ./nix/deps.nix;
allowGoReference = false;
preBuild = ''
export CGO_ENABLED=0
buildFlagsArray+=(-pkgdir "$TMPDIR")
'';
postInstall = ''
cp -rf $src/blog $bin/blog
cp -rf $src/css $bin/css
cp -rf $src/gallery $bin/gallery
cp -rf $src/signalboost.dhall $bin/signalboost.dhall
cp -rf $src/static $bin/static
cp -rf $src/talks $bin/talks
cp -rf $src/templates $bin/templates
'';
}
```
Breaking it down, we see some similarities to the gruvbox-css package from
above, but there's a few more interesting lines I want to point out:
```nix
{ pkgs ? import (import ./nix/sources.nix).nixpkgs }:
```
My website uses a pinned or fixed version of nixpkgs. This allows my website's
deployment to be stable even if nixpkgs changes something that could cause it to
break.
```nix
with pkgs;
```
[With expressions][nixwith] are one of the more interesting parts of Nix.
Essentially, they let you say "everything in this object should be put into
scope". So if you have an expression that does this:
[nixwith]: https://nixos.org/nix/manual/#idm140737321975440
```nix
let
foo = {
ponies = "awesome";
};
in with foo; "ponies are ${ponies}!"
```
You get the result `"ponies are awesome!"`. I use `with pkgs` here to use things
directly from nixpkgs without having to say `pkgs.` in front of a lot of things.
```nix
assert lib.versionAtLeast go.version "1.13";
```
This line will make the build fail if Nix is using any Go version less than
1.13. I'm pretty sure my website's code could function on older versions of Go,
but the runtime improvements are important to it, so let's fail loudly just in
case.
```nix
buildGoPackage {
# ...
}
```
[`buildGoPackage`](https://nixos.org/nixpkgs/manual/#ssec-go-legacy) builds a Go
package into a Nix package. It takes in the [Go package path][gopkgpath], list
of dependencies and if the resulting package is allowed to depend on the Go
compiler or not.
[gopkgpath]: https://github.com/golang/go/wiki/GOPATH#directory-layout
It will then compile the Go program (and all of its dependencies) into a binary
and put that in the resulting package. This website is more than just the source
code, it's also got assets like CSS files and the image earlier in the post.
Those files are copied in the `postInstall` phase:
```nix
postInstall = ''
cp -rf $src/blog $bin/blog
cp -rf $src/css $bin/css
cp -rf $src/gallery $bin/gallery
cp -rf $src/signalboost.dhall $bin/signalboost.dhall
cp -rf $src/static $bin/static
cp -rf $src/talks $bin/talks
cp -rf $src/templates $bin/templates
'';
```
This results in all of the files that my website needs to run existing in the
right places.
### Other Packages
For more kinds of packages that you can build, see the [Languages and
Frameworks][nixpkgslangsframeworks] chapter of the nixpkgs documentation.
[nixpkgslangsframeworks]: https://nixos.org/nixpkgs/manual/#chap-language-support
If your favorite language isn't shown there, you can make your own build script
and do it more manually. See [here][nixpillscustombuilder] for more information
on how to do that.
[nixpillscustombuilder]: https://nixos.org/nixos/nix-pills/working-derivation.html#idm140737320334640
## `nix-env` And Friends
Building your own packages is nice and all, but what about using packages
defined in nixpkgs? Nix includes a few tools that help you find, install,
upgrade and remove packages as well as `nix-build` to build new ones.
### `nix search`
When looking for a package to install, use `$ nix search name` to see if it's
already packaged. For example, let's look for [graphviz][graphviz], a popular
diagramming software:
[graphviz]: https://graphviz.org/
```console
$ nix search graphviz
* nixos.graphviz (graphviz)
Graph visualization tools
* nixos.graphviz-nox (graphviz)
Graph visualization tools
* nixos.graphviz_2_32 (graphviz)
Graph visualization tools
```
There are several results here! These are different because sometimes you may
want some features of graphviz, but not all of them. For example, a server
installation of graphviz wouldn't need X windows support.
The first line of the output is the attribute. This is the attribute that the
package is imported to inside nixpkgs. This allows multiple packages in
different contexts to exist in nixpkgs at the same time, for example with python
2 and python 3 versions of a library.
The second line is a description of the package from its metadata section.
The `nix` tool allows you to do a lot more than just this, but for now this is
the most important thing.
### `nix-env -i`
`nix-env` is a rather big tool that does a lot of things (similar to pacman in
Arch Linux), so I'm going to break things down into separate sections.
Let's pick an instance graphviz from before and install it using `nix-env`:
```console
$ nix-env -iA nixos.graphviz
installing 'graphviz-2.42.2'
these paths will be fetched (5.00 MiB download, 13.74 MiB unpacked):
/nix/store/980jk7qbcfrlnx8jsmdx92q96wsai8mx-gts-0.7.6
/nix/store/fij1p8f0yjpv35n342ii9pwfahj8rlbb-graphviz-2.42.2
/nix/store/jy35xihlnb3az0vdksyg9rd2f38q2c01-libdevil-1.7.8
/nix/store/s895dnwlprwpfp75pzq70qzfdn8mwfzc-lcms-1.19
copying path '/nix/store/980jk7qbcfrlnx8jsmdx92q96wsai8mx-gts-0.7.6' from 'https://cache.nixos.org'...
copying path '/nix/store/s895dnwlprwpfp75pzq70qzfdn8mwfzc-lcms-1.19' from 'https://cache.nixos.org'...
copying path '/nix/store/jy35xihlnb3az0vdksyg9rd2f38q2c01-libdevil-1.7.8' from 'https://cache.nixos.org'...
copying path '/nix/store/fij1p8f0yjpv35n342ii9pwfahj8rlbb-graphviz-2.42.2' from 'https://cache.nixos.org'...
building '/nix/store/r4fqdwpicqjpa97biis1jlxzb4ywi92b-user-environment.drv'...
created 664 symlinks in user environment
```
And now let's see where the `dot` tool from graphviz is installed to:
```console
$ which dot
/home/cadey/.nix-profile/bin/dot
$ readlink /home/cadey/.nix-profile/bin/dot
/nix/store/fij1p8f0yjpv35n342ii9pwfahj8rlbb-graphviz-2.42.2/bin/dot
```
This lets you install tools into the system-level Nix store without affecting
other user's environments, even if they depend on a different version of
graphviz.
### `nix-env -e`
`nix-env -e` lets you uninstall packages installed with `nix-env -i`. Let's
uninstall graphviz:
```console
$ nix-env -e graphviz
```
Now the `dot` tool will be gone from your shell:
```console
$ which dot
which: no dot in (/run/wrappers/bin:/home/cadey/.nix-profile/bin:/etc/profiles/per-user/cadey/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin)
```
And it's like graphviz was never installed.
Notice that these package management commands are done at the _user_ level
because they are only affecting the currently logged-in user. This allows users
to install their own editors or other tools without having to get admins
involved.
## Adding up to NixOS
NixOS builds on top of Nix and its command line tools to make an entire Linux
distribution that can be perfectly crafted to your needs. NixOS machines are
configured using a [configuration.nix][confignix] file that contains the
following kinds of settings:
[confignix]: https://nixos.org/nixos/manual/index.html#ch-configuration
- packages installed to the system
- user accounts on the system
- allowed SSH public keys for users on the system
- services activated on the system
- configuration for services on the system
- magic unix flags like the number of allowed file descriptors per process
- what drives to mount where
- network configuration
- ACME certificates
[and so much more](https://nixos.org/nixos/options.html#)
At a high level, machines are configured by setting options like this:
```
# basic-lxc-image.nix
{ config, pkgs, ... }:
{
networking.hostName = "example-for-blog";
environment.systemPackages = with pkgs; [ wget vim ];
}
```
This would specify a simple NixOS machine with the hostname `example-for-blog`
and with wget and vim installed. This is nowhere near enough to boot an entire
system, but is good enough for describing the base layout of a basic [LXC][lxc]
image.
[lxc]: https://linuxcontainers.org/lxc/introduction/
For a more complete example of NixOS configurations, see
[here](https://github.com/Xe/nixos-configs/tree/master/hosts) or repositories on
[this handy NixOS wiki page](https://nixos.wiki/wiki/Configuration_Collection).
The main configuration.nix file (usually at `/etc/nixos/configuration.nix`) can also
import other NixOS modules using the `imports` attribute:
```nix
# better-vm.nix
{ config, pkgs, ... }:
{
imports = [
./basic-lxc-image.nix
];
networking.hostName = "better-vm";
services.nginx.enable = true;
}
```
And the `better-vm.nix` file would describe a machine with the hostname
`better-vm` that has wget and vim installed, but is also running nginx with its
default configuration.
Internally, every one of these options will be fed into auto-generated Nix
packages that will describe the system configuration bit by bit.
### `nixos-rebuild`
One of the handy features about Nix is that every package exists in its own part
of the Nix store. This allows you to leave the older versions of a package
laying around so you can roll back to them if you need to. `nixos-rebuild` is
the tool that helps you commit configuration changes to the system as well as
roll them back.
If you want to upgrade your entire system:
```console
$ sudo nixos-rebuild switch --upgrade
```
This tells nixos-rebuild to upgrade the package channels, use those to create a
new base system description, switch the running system to it and start/restart/stop
any services that were added/upgraded/removed during the upgrade. Every time you
rebuild the configuration, you create a new "generation" of configuration that
you can roll back to just as easily:
```console
$ sudo nixos-rebuild switch --rollback
```
### Garbage Collection
As upgrades happen and old generations pile up, this may end up taking up a lot
of unwanted disk (and boot menu) space. To free up this space, you can use
`nix-collect-garbage`:
```console
$ sudo nix-collect-garbage
< cleans up packages not referenced by anything >
$ sudo nix-collect-garbage -d
< deletes old generations and then cleans up packages not referenced by anything >
```
The latter is a fairly powerful command and can wipe out older system states.
Only run this if you are sure you don't want to go back to an older setup.
## How I Use It
Each of these things builds on top of eachother to make the base platform that I
built my desktop environment on. I have the configuration for [my
shell][xefish], [emacs][xemacs], [my window manager][xedwm] and just about [every
program I use on a regular basis][xecommon] defined in their own NixOS modules so I can
pick and choose things for new machines.
[xefish]: https://github.com/Xe/xepkgs/tree/master/modules/fish
[xemacs]: https://github.com/Xe/nixos-configs/tree/master/common/users/cadey/spacemacs
[xedwm]: https://github.com/Xe/xepkgs/tree/master/modules/dwm
[xecommon]: https://github.com/Xe/nixos-configs/tree/master/common
When I want to change part of my config, I edit the files responsible for that
part of the config and then rebuild the system to test it. If things work
properly, I commit those changes and then continue using the system like normal.
This is a little bit more work in the short term, but as a result I get a setup
that is easier to recreate on more machines in the future. It took me a half
hour or so to get the configuration for [zathura][zathura] right, but now I have
[a zathura
module](https://github.com/Xe/nixos-configs/tree/master/common/users/cadey/zathura)
that lets me get exactly the setup I want every time.
[zathura]: https://pwmt.org/projects/zathura/
## TL;DR
Nix and NixOS ruined me. It's hard to go back.