Nix and Home Manager at Scale
Goal
We are going to create further hierarchy on both home-manager configurations and shell configurations so that we can separate common configuration out of user specific configurations. This allows us to maintain set of common setup, without compromising user customization.
We are going to start making these changes from https://github.com/rexk/dotfiles/tree/nix-flake-install-stage-04
For those who are impatient, you can find the final result at https://github.com/rexk/dotfiles/tree/nix-flake-install-stage-05
home manager configurations
In order to provide user specific configurations, we would like to make following changes.
- Update home.nix to import user specific configurations, if exists
- Provide common template for invidual users to get started with user specific configurations
- Provide set of common packages.
- Provide means to override those common packages via user specific configurations.
+# This is common homemanager configuration file
+# with common setup.
+#
+# DO NOT MODIFY this file unless, unless
+# the change can meant to be supplied to
+# others.
+#
+# For user specific customization, use home.user.nix
+{ config, pkgs, lib, ... }:
let
+ if-exists = f: builtins.pathExists f;
+ existing-imports = imports: builtins.filter if-exists imports;
user = import ./user.nix;
in
{
+ imports = existing-imports [
+ ./home.user.nix
+ ];
+
home.username = user.name;
home.homeDirectory = user.homeDir;
home.stateVersion = "22.11"; import ./user.nix;
in
{
Above changes, allow us to import home.user.nix
, if exists, otherwise, we are still going
to make use home.nix.
Also note that we are not making any update on home.packages
, yet. This is because
It is a lot easier to manage list of packages only through home.user.nix
, instead of
doing it both in home.nix
and home.user.nix
.
But we don’t want to just hard code set of common packages in home.user.nix
file.
For that, we are going to create provided-pkgs.nix
file
# This file should be maintained by a team X
# DO NOT MODIFY directly
# for local configurations use home.user.nix
{ pkgs, lib, ... }:
let
removeAll = excludes: all: lib.lists.fold (acc: item: lib.lists.remove item acc) all excludes;
provided-pkgs = with pkgs; [
# Provide org-wide packages here
bashInteractive
jq
curl
git
nodejs-18_x
];
with-user-pkgs = inst:
let
excludes = inst.excludes;
includes = inst.includes;
without-excludes = removeAll excludes provided-pkgs;
in
lib.lists.unique (provided-pkgs ++ includes);
in
{
inherit provided-pkgs;
inherit with-user-pkgs;
}
Then, time to add home.user.nix
{ pkgs, lib, ... }:
let
provided-pkgs-args = { inherit pkgs; inherit lib; };
provided-pkgs = import ./provided-pkgs.nix provided-pkgs-args;
in
{
home.packages = with pkgs;
provided-pkgs.with-user-pkgs {
excludes = [ ];
includes = [
gh
exa
lazygit
bat
zellij
unstable.neovim
python311
python311Packages.pip
];
};
programs.starship = {
enable = true;
};
}
The provided-pkgs.nix
provides very neat function for users to manage home-manager
packages. For example, let’s say, I would like to use nodejs 14.x over 18.x. A user
can now update home.user.nix file into
home.packages = with pkgs;
provided-pkgs.with-user-pkgs {
excludes = [
# opt out the packages from provided-pkgs
nodejs-18_x
];
includes = [
nodejs-14_x
];
}
Users can now customize further without needing to intricacy of home-manager’s detail.
While we would like to commit provided-pkgs.nix
, we do not want to check in home.user.nix
into our dotfiles, as it would be different per each user. So instead of checking
in home.user.nix
, we are going to create a file called home.user.nix.tpl
, so that
users can easily get started by copying content of home.user.nix.tpl
as home.user.nix.
shell configurations
Similar to changes of home-manager configurations, we are going to separate create hierarchy of shell configurations.
In the previous post, we’ve created hierarchy for each shell that we are going to support in following manner.
.
├── .bashrc # controlled by nix
├── .zshrc # controlled by nix
├── .config
│ ├── bash
│ │ └── .bashrc # custom bash config
│ ├── zsh
│ │ └── .zshrc # custom zsh config
│ ├── fish
│ │ ├── config.fish # controlled by nix
| │ └── config.base.fish # custom fish config
We would like to change them into followings:
.
├── .bashrc # controlled by nix
├── .zshrc # controlled by nix
├── .config
│ ├── bash
│ │ ├── .bashrc # common bash config
│ │ ├── user.bash # user bash config
│ │ └── secret.bash # place for API tokens, secrets... etc
│ ├── zsh
│ │ ├── .zshrc # common zsh config
│ │ ├── user.zsh # user zsh config
│ │ └── secret.zsh # place for API tokens, secrets... etc
│ ├── fish
│ │ ├── config.fish # controlled by nix
│ │ ├── config.base.fish # common fish config
│ │ ├── config.user.fish # user fish config
| │ └── config.secret.fish # place for API tokens, secrets... etc
Just like what we did in our home.nix file, we just need to update each custom config files to add conditional file import.
For this to work, we are going to a few lines into .bashrc, .zshrc, and config.base.fish as following:
.bashrc
# DO NOT MODIFY this file unless, the change is required
# for all memebers.
#
# This file provide set of common bash configurations
# that are useful for all of us.
# set up PATH
export PATH=$PATH:$HOME/.config/bin
if [ -f $HOME/.config/bash/user.bash ]; then
source $HOME/.config/bash/user.bash
fi
if [ -f $HOME/.config/bash/secret.bash ]; then
source $HOME/.config/bash/secret.bash
fi
.zshrc
# DO NOT MODIFY this file unless, the change is required
# for all memebers.
#
# This file provide set of common zsh configurations
# that are useful for all of us.
# region: setup
# TODO:: add more configs
# endregion
if [ -f $HOME/.config/zsh/user.zsh ]; then
source $HOME/.config/zsh/user.zsh
fi
if [ -f $HOME/.config/zsh/secret.zsh ]; then
source $HOME/.config/zsh/secret.zsh
fi
config.base.fish
# DO NOT MODIFY this file unless, the change is required
# for all memebers.
#
# This file provide set of common fish configurations
# that are useful for all of us.
# region: setup
# TODO:: add more configs
# endregion
if test -f $HOME/.config/fish/config.user.fish
source $HOME/.config/fish/config.user.fish
end
if test -f $HOME/.config/fish/config.secret.fish
source $HOME/.config/fish/config.secret.fish
end
Now, we are ready test these new structure. For a demonstration, let’s add a custom zsh configuration
cat <<EOF > ~/.config/zsh/user.zsh
echo "My Custom config"
EOF
zsh # loading a new zsh session
And you should see that our zsh session prints out
My Custom config
Conclusion
By supplying a further hierarchy of configurations, we have successfully created a nix set up that can be used to supply common configurations for everyone in an organization, while providing an ability for each users to customize their own environment.
provided-pkgs.nix
file is used to provide common packages that we would like to distribute to everyone
in a team, while home.user.nix
can be used to provide further customization for an end user.
Similarly, common shell configurations can be managed by ~/.config/bash/.bashrc
, ~/.config/zsh/.zshrc
, and
~/.config/fish/config.base.fish
, while ~/.config/bash/user.bash
, ~/.config/zsh/user.zsh
, and
~/.config/fish/config.user.fish
provide place for further customization for an end user, without needing to
set up above shell environment using nix.