Nix and Home-manager in custom directory
February 2022
Introduction
A few months ago I was looking for a way to create a setup for tools and software in my home-folder that was easy and fast to deploy in a new location. After some searching, I discovered Nix and Home-Manager.
From their wiki, NixOS is a Linux distribution based on the Nix package manager and build system. It supports reproducible and declarative system-wide configuration management as well as atomic upgrades and rollbacks, although it can additionally support imperative package and user management. In NixOS, all components of the distribution — including the kernel, installed packages and system configuration files — are built by Nix from pure functions called Nix expressions.
Home-Manager complements the Nix package manager by providing a system to manage a users configuration for the home folder.
These tools have a relatively steep learning curve, but after a few months using it I have to say that it is very worth the investment of time. Nix is a very principled way to approach software deployment and its ideas provide a fresh perspective on how reproducible configuration of software can be done that provides multiple versions at the same time.
Installation
Nix is very easy to install and instructions are provided on the homepage.
However this is only true under the assumption that the user has sudo
permissions or an administrator performs certain setup steps. If these conditions
are not met, deploying nix is a lot trickier. In the rest of the post
I will outline how to deploy nix using home-manager in the home folder without
sudo.
The biggest drawback of the route I have chosen is that the Nix binary cache does not work, which can mean long compile times when deploying new software (long can mean 10+ hours). This can be tricky when trying to install software ad-hoc, but for me is an ok tradeoff for deploying home-folder software where the setup changes much more slowly. Additionally, not every program works without error when compiled from scratch so that ocassionally an override patch is necessary.
Nix-portable
Nix-portable is a wrapper that allows nix to be used in a users home folder without sudo permissions while still being able to use the binary cache. Please go the its website to read about some of the requirements and missing features of this approach.
In my case, it does not support sufficient features to deploy a Home-Manager setup so that I decided not to use it.
Why not use some other tool?
Before going into more details of the setup, I briefly wanted to talk about some of the other tools that could be used for similar purposes.
Homebrew
Homebrew, often called the missing package manager for MacOS (and later also linux), is a tool to easily install software in a user's home directory. Its big advantage is that it can be run without sudo and that a very large amount of packages is available for installation.
It is a very good tool, however it does not provide the same level of independence of the host system as Nix does, so incompatibilities from one OS to the other can happen. Furthermore, it also does not a manager for home-folder configurations such as Home-Manager
Spack or Easybuild
Both Spack and Easybuild are tools to install software on HPC systems. As such they have the ability to install multiple versions of the same software side by side and allow the user to activate them on-demand (e.g. by using environment modules).
Both these systems are very sophisticated and are more targeted towards HPC admins than individual users. In general, availablility of packages and latest versions is lower than for Nix and they also don't provide a complete solution for managing the home-folder configurations.
The build process
Now let us move on to the actual installation process.
Requirements
It is necessary to have a linux distribution available with sudo permissions so that nix can be deployed as described in the user manual. In the following it is assumed that a nix installation is available with nix version >= 2.4.
Setting environment variables
In order to install in a custom location, it is possible to change the directories used by Nix with environment variables. These need to be sourced before the build process.
PREFIX=/home/testuser
export NIX_STORE_DIR=${PREFIX}/nix/store
export NIX_DATA_DIR=${PREFIX}/nix/share
export NIX_LOG_DIR=${PREFIX}/nix/var/log/nix
export NIX_STATE_DIR=${PREFIX}/nix/var/nix
export NIX_CONF_DIR=${PREFIX}/nix/etc/nix
NIX_PROFILES="${NIX_STATE_DIR}/profiles/default ${PREFIX}/.nix-profile"
This corresponds to a nix installation where storedir=${PREFIX}/nix/store
,
localstatedir=${PREFIX}/nix/var/
and sysconfdir=${PREFIX}/nix/etc
, where
storedir
, localstatedir
and sysconfdir
are defined in the
nix installation in nixpkgs defined in
nix/default.nix
and the details for the environment variables in
local.mk
The flake
At first for practice we write a flake for only compiling nix itself. In this flake we need to override some attributes in order to set the custom variables as well as add some patches (on linux, the sandbox test is broken for non-default directories, so we disable it). A basic flake could be
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {self, nixpkgs, flake-utils}:
let
overlay-nix = final: prev:
let prefix = "/home/testuser/nix";
in
{
nix_prefix = (prev.nix.override {
storeDir = "${prefix}/store";
stateDir = "${prefix}/var";
confDir = "${prefix}/etc";
}).overrideAttrs (oldAttrs: rec {
patches = (oldAttrs.patches or []) ++ [./nix_patch_2_5.patch];
});
};
in
flake-utils.lib.eachDefaultSystem
(system:
let pkgs = import nixpkgs {overlays = [overlay-nix]; inherit system;}; in
{
packages.nix = pkgs.nix_prefix;
}
);
}
with patch
diff --git a/tests/common.sh.in b/tests/common.sh.in
index 61abab1d7..3947db160 100644
--- a/tests/common.sh.in
+++ b/tests/common.sh.in
@@ -119,10 +119,6 @@ restartDaemon() {
startDaemon
}
-if [[ $(uname) == Linux ]] && [[ -L /proc/self/ns/user ]] && unshare --user true; then
- _canUseSandbox=1
-fi
-
isDaemonNewer () {
[[ -n "${NIX_DAEMON_PACKAGE:-}" ]] || return 0
local requiredVersion="$1"
With this flake, we can just build a version of nix with
nix build .#nix
.
Flake for home-manager
We also want to use this in home-manager of course. A minimal flake
to set this up is below, together with a minimal home.nix
that
just requires nix
itself and ensures the environment variables
are loaded in the profile. As of this writing, the current version
of nix was 2.5.1.
{
description = "Home Manager NixOS configuration";
inputs = {
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{ self, nixpkgs, home-manager, ... }:
{
homeConfigurations = {
testuser = inputs.home-manager.lib.homeManagerConfiguration {
system = "x86_64-linux";
# Home Manager needs a bit of information about you and the
# paths it should manage.
homeDirectory = "/home/testuser";
username = "testuser";
# This value determines the Home Manager release that your
# configuration is compatible with. This helps avoid breakage
# when a new Home Manager release introduces backwards
# incompatible changes.
# You can update Home Manager without changing this value. See
# the Home Manager release notes for a list of state version
# changes in each release.
stateVersion = "21.11";
configuration = { config, pkgs, ... }:
let
overlay-nix = final: prev:
let prefix = "/home/testuser/nix";
in
{
nix_2_3 = (prev.nix_2_3.override {
storeDir = "${prefix}/store";
stateDir = "${prefix}/var";
confDir = "${prefix}/etc";
}).overrideAttrs (oldAttrs: rec {
patches = (oldAttrs.patches or []) ++ [./nix_patch_2_3.patch];
});
nix = (prev.nix.override {
storeDir = "${prefix}/store";
stateDir = "${prefix}/var";
confDir = "${prefix}/etc";
}).overrideAttrs (oldAttrs: rec {
patches = (oldAttrs.patches or []) ++ [./nix_patch_2_5.patch];
});
};
in
{
nixpkgs.overlays = [ overlay-nix];
nixpkgs.config = {
allowUnfree = true;
allowBroken = true;
};
imports = [
./home.nix
];
};
};
};
testuser = self.homeConfigurations.testuser.activationPackage;
defaultPackage.x86_64-linux = self.testuser;
};
}
diff --git a/tests/common.sh.in b/tests/common.sh.in
index 15d7b1ef9..9e2242ac4 100644
--- a/tests/common.sh.in
+++ b/tests/common.sh.in
@@ -86,10 +86,6 @@ killDaemon() {
trap "" EXIT
}
-if [[ $(uname) == Linux ]] && [[ -L /proc/self/ns/user ]] && unshare --user true; then
- _canUseSandbox=1
-fi
-
canUseSandbox() {
if [[ ! $_canUseSandbox ]]; then
echo "Sandboxing not supported, skipping this test..."
For defining the home-folder environment itself, we need a home.nix
file
that we import in the configuration. This one is quite minimal, defining
nix
itself as the only read dependency.
We also set the profile
for the account so that all scripts in profile.d
directory
in the home-folder are being read, where we then set the script to read the environment variables.
{ config, pkgs, ... }:
{
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
home.packages = with pkgs; [
# we want nix itself to be installed by home-manager
nix
];
programs.bash = {
enable = true;
profileExtra = ''
. $HOME/.nix-profile/etc/profile.d/nix.sh
if [ -d $HOME/profile.d ]; then
for i in $HOME/profile.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
'';
};
# load the nix-vars to configure correctly
home.file."profile.d/nix_vars.sh".source = ./nix_vars.sh;
}
As to having nix in home-manager itself, it is not necessary and up to the end user. Having it in there automatically upgrades nix on new deployments, and builds it in one go when building home-manager but it also makes initial deployment trickier, as the first time nix itself has to be uninstalled from the user environment.
Typical steps for installing this new would be:
# set the PATH explicitly to the installed nix
export PATH=$(dirname $(readlink $(which nix))):$PATH
# we remove nix itself that comes pre-installed from the user profile
nix-env -e nix
# build the flake for the testuser
nix build .#testuser
# run the activate script
./result/activate
Discussion
In the setup before, even after compiling nix with a different state and store directory, we still have to load the environment variables for it to work reliably. The reason is that home-manager used incorrect directories e.g. for lock files if they were not set. Setting the environment variables fixed the issue, but I did not track down which particular variable is necessary.
Another observation is that compilation of nix and libraries works very well, up to minor issues like the error in the sandbox tests of nix. I am not sure if this is due to the OS where I compiled it (Ubuntu) or due to the custom nix-store paths.
I had also trouble confirming the new store and state-paths were correctly
picked up by nix. Commands like nix show-config
for example don't list any of
these directories, making it hard to confirm what they are using (but probably,
as I am still very new to this, I don't know how to do this correctly).
I hope this was helpful, but please let me know in the comments below.
Other resources
My starting point was a Github repository that compiles nix for HPC at danielbarter/hpc-nix.