HUIJZER.XYZ

NixOS configuration highlights

2019-12-01

I have recently started paying attention to the time spent on fine-tuning my Linux installation. My conclusion is that it is a lot. Compared to Windows, I save lots of time by using package managers to install software. Still, I'm pretty sure that most readers spend (too) much time with the package managers as well. This can be observed from the fact that most people know the basic apt-get commands by heart.

At the time of writing, I'm happily running and tweaking NixOS for a few months. NixOS allows me to define my operating system state in one configuration file configuration.nix. I have been pedantic and will try to avoid configuring my system outside the configuration. This way, multiple computers are in the same state and I can set-up new computers in no time. The only manual steps have been NixOS installation, Firefox configuration, user password configuration and applying a system specific configuration. (The latter involves creating two symlinks.)

In general, there are the following things which are great.

Packages are immutable and NixOS will basically create symlinks to the packages. This has some great implications.

It is relatively easy to contribute to the Nix Packages collection. This results in a large number of available and well-maintained packages. Compared to the Debian packages it is a major improvement. I'm often surprised by the fact that some obscure tool is actually available in NixOS.

The Nix expression language is declarative. Basically, it will read all the code and collect it in a data structure. Only when this is complete the system state will be changed. This means that the language does not care about order or duplication. In effect, this eases refactoring and avoids checking "preconditions". To explain the latter, consider writing a script which assumes ripgrep to be installed. The language allows for defining such requirements multiple times. So, even when removing the ripgrep requirement at other places, it will still be available for my script.

Below are some of what I consider highlights of the configuration. To see more information about the options, checkout https://search.nixos.org.

  1. Font installation
  2. Shell functions
  3. Home Manager
  4. Git credentials
  5. Bash
  6. R
  7. Containerized services
  8. Fixing "could not be executed"

Font installation

Most Linux distributions lack pretty fonts. You'll notice this when, for example, browsing the web. I found installing fonts to be difficult at times, even in Ubuntu. For NixOS it could not be easier:

# /etc/nixos/configuration.nix

fonts.fonts = with pkgs; [
  hermit
  source-code-pro
];

Shell functions

At one point using abbreviations or aliases is not going to be enough and functions are needed. This should be possible by defining Fish functions directly. I have not yet been successful in doing that declaratively. A workaround is as follows.

# /etc/nixos/configuration.nix

import = [
  ./scripts.nix
];
# /etc/nixos/scripts.nix

let
  fish-shebang = "#!${pkgs.fish}/bin/fish";
  define-script = name: script: pkgs.writeScriptBin name script;
  define-fish-script = name: script: define-script name (fish-shebang + "\n\n" + script);
in {
  environment.systemPackages = [
    (define-fish-script "decrypt-folder" ''
      if test (count $argv) -eq 0
        echo "usage: decrypt-folder <folder>"
        exit
      end

      set name (string replace -r "\.tar\.gz\.gpg" "" $argv[1])

      gpg $name.tar.gz.gpg
      tar -xzf $name.tar.gz
      rm $name.tar.gz
    '')

    (define-fish-script "git-add-commit-push" ''
      git add .
      git commit -m "$argv[1]"
      git push
    '')
  ];
}

For the latter it is convenient to add the abbreviation

abbr gacp 'git-add-commit-push'

to the Fish shell configuration.

Another great thing about this setup is the ease of looking up commands. Often, I find myself in need of some bash code which I have used in one of the scripts. To see the code from the terminal in Fish, use cat (which <script>). For example, cat (which git-add-commit-push) gives

$ cat (which git-add-commit-push)
#!/nix/store/ajwff4bi6mp2n7517ps890rnk4xgzj4r-fish-3.0.2/bin/fish

git add .
git commit -m "$1"
git push

Home Manager

Actually, Home Manager is not the prettiest part of NixOS. It seems to be an extension of the configuration provided by the vanilla operating system. The installation instructions on Github advise to do all kind of mutation operations. To avoid this, we import the folder home-manager somewhere in our configuration.

# /etc/nixos/configuration.nix

imports = [
  ./home-manager
];

This works only if our folder contains a default.nix file. So, lets create that and import home-manager. Here, the version (ref) is fixed to 19.09. (You can decide to not fix the version. However, it might cause your system to suddenly break one day.) The imports below the fetchGit line define specific home-manager configurations.

# /etc/nixos/home-manager/default.nix

imports = [
  "${builtins.fetchGit {
    url = "https://github.com/rycee/home-manager";
    ref = "f856c78a4a220f44b64ce5045f228cbb9d4d9f31";
  }}/nixos"

  ./git.nix
];

Git credentials

Storing Git credentials took me way too long to figure out. So, here it is. To use the Git credential helper libsecret (gnome-keyring) write

# /etc/nixos/home-manager/git.nix

environment.systemPackages = [
  pkgs.gitAndTools.gitFull # gitFull contains libsecret.
]

home-manager.users.rik.programs.git = {
  enable = true;

  # Some omitted settings.

  extraConfig = {
    credential.helper = "libsecret";
  };
};

Bash

The default shebang for Bash is

#!/bin/bash

this won't work in NixOS since it is a "form of global system state". Instead use

#!/usr/bin/env bash

which will also work in GitHub CI jobs.

R

The R programming language is a language with built-in support for package installations. R is immutable in NixOS. To install packages two wrappers are provided. One for R and one for RStudio. Next, an example is given to configure R and RStudio with various packages from CRAN, and one package built from GitHub source. Specifying the package from GitHub in the NixOS configuration avoids having to run devtools::install_github("<repository>") on each computer.

# /etc/nixos/r.nix
{ pkgs, ... }:
with pkgs;
let
  papajaBuildInputs = with rPackages; [
    afex
    base64enc
    beeswarm
    bookdown
    broom
    knitr
    rlang
    rmarkdown
    rmdfiltr
    yaml
  ];
  papaja = with rPackages; buildRPackage {
  name = "papaja";
  src = pkgs.fetchFromGitHub {
    owner = "crsh";
    repo = "papaja";
    rev = "b0a224a5e67e1afff084c46c2854ac6f82b12179";
    sha256 = "14pxnlgg7pzazpyx0hbv9mlvqdylylpb7p4yhh4w2wlcw6sn3rwj";
    };
    propagatedBuildInputs = papajaBuildInputs;
    nativeBuildInputs = papajaBuildInputs;
  };
  my-r-packages = with rPackages; [
    bookdown
    dplyr
    papaja
  ];
  R-with-my-packages = pkgs.rWrapper.override{
    packages = my-r-packages;
  };
  RStudio-with-my-packages = rstudioWrapper.override{
    packages = my-r-packages;
  };
in {
  environment.systemPackages = with pkgs; [
    R-with-my-packages
    RStudio-with-my-packages
  ];
}

Containerized services

Creating background services couldn't be more easy. Here, I use the Podman backend. If you prefer the normal Docker backend, then remove the line containing backend = "podman".

virtualisation.oci-containers = {
  backend = "podman";
  containers = {
    "redis" = {
      image = "redis:6-alpine";
      cmd = [ "redis-server" "--port" "6379" "--user" "username" ];
      ports = [ "6379:6379" ];
    };
  };
};

To inspect the state of the service, use systemctl status podman<TAB>, where pressing <TAB> should allow you to see and autocomplete the running podman services. For example, systemctl status podman-redis.service. To see how to build your own container to be used as a service, see Running Isso on NixOS in a Docker container.

Fixing "could not be executed"

Of course, there are also some downsides. One is that many binaries assume an interpreter to be available at /lib64/ld-linux-x86-64.so.2. Specifically, the binaries will show the following in patchelf:

$ patchelf --print-interpreter somebinary
/lib64/ld-linux-x86-64.so.2

whereas it should be something along the lines of

$ patchelf --print-interpreter somebinary
/nix/store/lyl6nysc3i3aqhj6shizjgj0ibnf1pvg-glibc-2.34-210/lib/ld-linux-x86-64.so.2

You can fix this in two ways:

  1. Fix the binary.

  2. Fix your system.

I've been doing the first thing for about a year. It works but it is very tricky especially when other software calls the binaries for you. To solve it globally, you can add

system.activationScripts.ldso = lib.stringAfter [ "usrbinenv" ] ''
    mkdir -m 0755 -p /lib64
    ln -sfn ${pkgs.glibc.out}/lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2.tmp
    mv -f /lib64/ld-linux-x86-64.so.2.tmp /lib64/ld-linux-x86-64.so.2 # atomically replace
  '';

to your config (source: https://github.com/gytis-ivaskevicius/nixfiles).

Thanks to Norbert Melzer and Gytis Ivaskevicius (https://discourse.nixos.org/t/making-lib64-ld-linux-x86-64-so-2-available/19679/2).