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.
Deterministic package system.
Packages are immutable and NixOS will basically create symlinks to the packages. This has some great implications.
Easy rollbacks. NixOS will just symlink to the "old" version again.
System usable during upgrade. For example, a Firefox upgrade is basically getting a new immutable Firefox binary and symlinking to the new binary. The old binary remains available until it is garbage collected. So, in practise you use the old binary until you restart the program.
Ease of contribution to package system.
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.
Nix expression language.
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.
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
];
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
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
];
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";
};
};
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.
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
];
}
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.
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:
Fix the binary.
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).