iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🐚

Managing Multiple Linux/Mac Environments with 1 Repository and 1 Command (For Nix Beginners)

に公開

Introduction

In this article, I will explain how I manage my user environment, which also serves as a personal memo.

When using multiple PCs, it is tedious to manually install tools like Git on each one or to copy and maintain .gitconfig files. Moreover, managing separate dotfiles repositories for WSL and Mac is also a hassle.

To address this, I use home-manager to manage my user environment.
I have structured my settings so that I can build user environments for multiple Linux/Mac machines using 1 repository + 1 command.

In this article, I will show you how to set up your environment so that by simply running git clone <dotfiles> and home-manager switch, you can construct your user environment (installing tools like Git, placing .gitconfig, etc.) tailored to your specific machine.

Article Structure

We will proceed in the following order:

    1. Preparing the New Environment (WSL Ubuntu 22.04)
    1. Installing Nix and home-manager
    1. Configuring home-manager
    1. Applying the User Environment
    1. Managing Mac and WSL Environments Simultaneously

In this article, we will set up a new Ubuntu environment in WSL as a sample. After the WSL user environment is complete, we will configure settings to manage both Mac and WSL environments simultaneously.

https://zenn.dev/trifolium/articles/dafb565c778ed5

https://nix-community.github.io/home-manager/index.xhtml#ch-nix-flakes

Verification Environment

I will use Windows 11 WSL2 Ubuntu 22.04. Details are collapsed as they are lengthy.

Verification Environment

Windows Information.

PowerShell
> systeminfo | findstr /B /C:"OS"
OS Name:                      Microsoft Windows 11 Home
OS Version:              10.0.26100 N/A Build 26100
# Omitted

> wsl --version
WSL Version: 2.6.1.0
Kernel Version: 6.6.87.2-1
WSLg Version: 1.0.66
MSRDC Version: 1.2.6353
Direct3D Version: 1.611.1-81528511
DXCore Version: 10.0.26100.1-240331-1435.ge-release
Windows Version: 10.0.26100.7171

WSL Ubuntu Information.

bash
$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 22.04.5 LTS"

# After installing Nix and home-manager
$ nix --version
nix (Determinate Nix 3.13.2) 2.32.4
$ home-manager --version
26.05-pre

1. Preparing the New Environment (WSL Ubuntu 22.04)

Install Ubuntu-22.04 on WSL.

PowerShell
> wsl --install Ubuntu-22.04
# ...
Enter new UNIX username: ryu_trifolium
# ...

From here on, operations will be within WSL.

PowerShell
wsl -d Ubuntu-22.04

https://zenn.dev/trifolium/articles/d695cebc50a888

2. Installing Nix and home-manager

2.1 Nix

We will use the installer from Determinate Systems to easily install Nix with a single command.

https://github.com/DeterminateSystems/nix-installer

Run the following command.
You will be prompted to confirm the installation; enter y.

bash
curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate

To activate the Nix command, either restart your shell or run the following command:

bash
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
Execution Log

Collapsed as it is long.

bash
$ curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate
info: downloading installer
 INFO nix-installer v3.13.2
`nix-installer` needs to run as `root`, attempting to escalate now via `sudo`...
[sudo] password for ryu_trifolium:
 INFO nix-installer v3.13.2
Nix install plan (v3.13.2)
Planner: linux

Configured settings:
* determinate_nix: true

Planned actions:
* Create directory `/nix`
* Install Determinate Nixd
* Extract the bundled Nix (originally from /nix/store/i130n1p2i0l1kyx0xa9fcn4d8jjrjb4b-nix-binary-tarball-3.13.2/nix-3.13.2-x86_64-linux.tar.xz) to `/nix/temp-install-dir`
* Create a directory tree in `/nix`
* Synchronize /nix/var ownership
* Move the downloaded Nix into `/nix`
* Synchronize /nix/store ownership
* Create build users (UID 30001-30032) and group (GID 30000)
* Setup the default Nix profile
* Place the Nix configuration in `/etc/nix/nix.conf`
* Configure the shell profiles
* Configure the Determinate Nix daemon
* Remove directory `/nix/temp-install-dir`


Proceed? ([Y]es/[n]o/[e]xplain): y
 INFO Step: Create directory `/nix`
 INFO Step: Install Determinate Nixd
 INFO Step: Provision Nix
 INFO Step: Create build users (UID 30001-30032) and group (GID 30000)
 INFO Step: Configure Nix
 INFO Step: Create directory `/etc/tmpfiles.d`
 INFO Step: Configure the Determinate Nix daemon
 INFO Step: Remove directory `/nix/temp-install-dir`
Nix was installed successfully!
To get started using Nix, open a new shell or run `. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh`

2.2 home-manager

Install according to the "Nix Flakes - Standalone setup" section of the official manual.

https://nix-community.github.io/home-manager/index.xhtml#ch-nix-flakes

Run the following command:

bash
nix run home-manager/master -- init

This will generate home.nix and flake.nix inside the /home/ryu_trifolium/.config/home-manager/ folder.

3. Setting up home-manager

Define your user environment settings in home.nix.
Specify which .nix files to load in flake.nix.

Edit the files using your preferred editor.

bash
cd /home/ryu_trifolium/.config/home-manager
code .

3.1 home.nix

The automatically generated home.nix includes configuration instructions in the form of # comments, so please feel free to read them if you are interested.

Here, we will configure Git, Bash, and the .gitconfig, .bashrc, and .profile files.

I have also added an environment variable named GREETING for demonstration purposes.

home.nix
{ config, pkgs, ... }:

{
  home.username = "ryu_trifolium";
  home.homeDirectory = "/home/ryu_trifolium";

  home.stateVersion = "25.11";

  home.packages = [
    pkgs.git
    pkgs.bash
  ];

  home.file = {
    ".gitconfig".source = ./git/.gitconfig;
    ".bashrc".source = ./bash/.bashrc;
    ".profile".source = ./bash/.profile;
  };

  home.sessionVariables = {
    GREETING = "Hello Nix";
  };

  programs.home-manager.enable = true;
}

Detailed explanations of the settings follow below.

3.1.1 User and Version Information

home.nix
  home.username = "ryu_trifolium";
  home.homeDirectory = "/home/ryu_trifolium";

  home.stateVersion = "25.11";

You can leave these as they were automatically generated.

When updating home-manager, update the stateVersion. Please check the release notes for the appropriate value.

https://nix-community.github.io/home-manager/release-notes.xhtml

Note that I will explain how to perform updates later.

3.1.2 Managing Tools

home.nix
  home.packages = [
    pkgs.git
    pkgs.bash
  ];

List the tools you want to use in your user environment here.

You can search for available tools at NixOS Search - Packages. Since there are about 100,000 tools registered in the NixOS/nixpkgs repository, you should be able to find most of them.

https://search.nixos.org/packages

Example search

Open the site and enter vim in the search bar.
Select unstable for the Channel.

I will skip the explanation of Channels, but if you are not using NixOS, unstable is fine.

Click on vim (the part in blue text) and check the command listed under How to install vim?.
If you write the string following -p in nix-shell -p vim into your home.nix, you can install vim.

Example search

Nix automatically installs tools according to the platform, but some tools may not support every platform. Please double-check if your PC is supported, for example aarch64-darwin for M1 Macs.

Most cases are supported in standard Windows WSL environments (x86_64-linux).
I have the impression that some tools are occasionally unsupported on M1 Macs (aarch64-darwin). I am not familiar with others, but the level of support seems to be x86_64-linux > aarch64-darwin > others.

Supported platforms

How to write pkgs.git

If you see other people's settings, they might be written as follows:

home.nix
  home.packages = with pkgs; [
    git
    bash
  ];

This is equivalent to:

home.nix
  home.packages = [
    pkgs.git
    pkgs.bash
  ];

3.1.3 Managing Configuration Files

home.nix
  home.file = {
    ".gitconfig".source = ./git/.gitconfig;
    ".bashrc".source = ./bash/.bashrc;
    ".profile".source = ./bash/.profile;
  };

You can specify configuration files here. This implies that the file located at ./git/.gitconfig will be used as .gitconfig.

The files are specified using paths relative to home.nix.

In my case, I copied the .gitconfig I use daily. I also copied my existing ~/.bashrc for Bash. Since environment variables set by home-manager will not be reflected in Bash as-is, I added configuration to .bashrc.

bash
$ mkdir bash
$ cp ~/.bashrc bash/.bashrc
$ cat << 'EOF' >> bash/.bashrc

# for home-manager
. "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh"
EOF
Folder structure
~/.config/home-manager/
├── bash
│   ├── .bashrc
│   └── .profile
├── flake.nix
├── git
│   └── .gitconfig
└── home.nix

There are multiple ways to specify configuration files; for other methods, please see here:

https://zenn.dev/trifolium/articles/642043cbae5f21

Technical supplement

Adding . "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" to .profile is the official explanation, but on WSL, it is inconvenient because the environment is not reflected without re-logging in. Therefore, I added it to .bashrc.

Also, I found options like programs.bash.bashrcExtra and programs.bash.profileExtra.
If you use these, you might not need to add . "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh".

I haven't actually tried this, so I have no proof.

I wanted to keep the appearance as simple as possible this time, so I deliberately chose the method of using home.file.".profile".source.

3.1.4 Managing Environment Variables

home.nix
  home.sessionVariables = {
    GREETING = "Hello Nix";
  };

You can set environment variables as shown above.

3.2 flake.nix

The automatically generated state works fine as is.

Parts like system = "x86_64-linux" and homeConfigurations."ryu_trifolium" are automatically configured according to the execution environment.

When managing both WSL and Mac environments with a single flake.nix in the latter part of the article, we will manually edit these parts.

flake.nix
{
  description = "Home Manager configuration of ryu_trifolium";

  inputs = {
    # Specify the source of Home Manager and Nixpkgs.
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    { nixpkgs, home-manager, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in
    {
      homeConfigurations."ryu_trifolium" = home-manager.lib.homeManagerConfiguration {
        inherit pkgs;

        # Specify your home configuration modules here, for example,
        # the path to your home.nix.
        modules = [ ./home.nix ];

        # Optionally use extraSpecialArgs
        # to pass through arguments to home.nix
      };
    };
}

4. Applying the User Environment

Run the following command.

bash
nix run home-manager/master -- init --switch -b backup
Explanation of the command

The official reference specifies nix run home-manager/master -- init --switch, but I have added -b backup.

In this case, I copied .bashrc from ~/.bashrc.
If you write home.file.".bashrc".source = ./bash/.bashrc;, home-manager will try to place .bashrc in ~/.

Therefore, this would cause an error due to a collision with the existing file.
By adding -b backup, it renames the original file to <filename>.backup before placing the new .bashrc.

From next time, you can update your user environment after updating home.nix with the following command:

bash
home-manager switch

4.1 Updating home-manager and tools

To update the home-manager base and the tools introduced via home-manager, run the following commands.

Execute these in the directory containing home-manager/flake.nix.

bash
$ nix flake update

# To update only home-manager
$ nix flake lock --update-input home-manager

# To update only tools introduced by home-manager
$ nix flake lock --update-input nixpkgs

After updating the tools with the commands above, run switch to apply them to your user environment.

bash
home-manager switch

When updating the home-manager base, be sure to check the release notes and update the stateVersion.

home.nix
  home.stateVersion = "25.11";

https://nix-community.github.io/home-manager/release-notes.xhtml

5. Managing Mac and WSL Environments Simultaneously

Assuming that we will manage the Mac environment as well, in addition to the WSL environment created earlier.

We will proceed with the following steps:

  • Prepare the dotfiles repository
  • Create a file defining common settings (common.nix)
  • Create files defining environment-specific settings (wsl.nix, mac.nix)
  • Verify the USER and HOSTNAME of each environment
  • Edit flake.nix
  • Apply changes with home-manager switch
Folder Structure
home-manager/
├─ home/
│   ├─ common.nix
│   ├─ mac.nix
│   └─ wsl.nix
├─ flake.nix
├─ git/
├─ bash/
└─ zsh/

5.1 Preparing the dotfiles repository

Manage the ~/.config/home-manager/ directory we worked on earlier using Git.

You can manage it directly within the folder, or as I do, manage it in ~/work/dotfiles and create a symbolic link from dotfiles/home-manager to ~/.config/home-manager.
Feel free to use whichever method you prefer.

5.2 Creating the file for common settings

Create a file named common.nix that extracts only the common elements among settings described in home.nix.

Folder Structure
 home-manager/
 └─ home/
+     └─ common.nix

common.nix
{ config, pkgs, ... }:

{
  programs.home-manager.enable = true;
  home.stateVersion = "25.11";

  home.packages = with pkgs; [
    git
  ];

  home.file = {
    ".gitconfig".source = ../git/.gitconfig;
  };
}

5.3 Creating files for environment-specific settings

Create wsl.nix and mac.nix, which are configuration files dedicated to WSL and MacBook respectively.

Specify the appropriate values for home.username and home.homeDirectory according to your environment.

Folder Structure
 home-manager/
 └─ home/
      ├─ common.nix
+     ├─ mac.nix
+     └─ wsl.nix
wsl.nix
{ config, pkgs, ... }:

{
  home.username = "ryu_trifolium";
  home.homeDirectory = "/home/ryu_trifolium";

  home.packages = with pkgs; [
    bash
    # Specify other tools used only on WSL
  ];

  home.file = {
    ".bashrc".source = ../bash/.bashrc;
    ".profile".source = ../bash/.profile;
  };
}
mac.nix
{ config, pkgs, ... }:

{
  home.username = "ryu";
  home.homeDirectory = "/Users/ryu";

  home.packages = with pkgs; [
    zsh
    # Specify other tools used only on Mac
  ];

  home.file = {
    ".zshrc".source = ../zsh/.zshrc;
    ".profile".source = ../zsh/.profile;
  };
}

5.4 Verifying USER and HOSTNAME for each environment

The following commands are examples from my environment.
Note them down as they will be used in the next step.

bash WSL Environment
$ echo $USER
ryu_trifolium

$ hostname
main
zsh Mac Environment
$ echo $USER
ryu

$ hostname
MacBook.local

5.5 Editing flake.nix

By writing homeConfigurations.USER@HOSTNAME in flake.nix, you can specify configurations for a particular environment.
For example, my WSL environment looks like this:

# USER = ryu_trifolium, HOSTNAME = main
homeConfigurations."ryu_trifolium@main" = home-manager.lib.homeManagerConfiguration {
  # Specify the system for your environment; for M1 Mac, use aarch64-darwin
  pkgs = nixpkgs.legacyPackages.x86_64-linux;
  # Specify the configuration files to load
  modules = [
    ./home/common.nix
    ./home/wsl.nix
  ];
};

Here is the full example code for flake.nix:

Folder Structure
 home-manager/
 ├─ home/
 │   ├─ common.nix
 │   ├─ mac.nix
 │   └─ wsl.nix
+└─ flake.nix
flake.nix
{
  description = "Home Manager configuration";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      nixpkgs,
      home-manager,
      ...
    }:
    {
      homeConfigurations = {
        # Main desktop PC, Windows 11, WSL (Ubuntu 22.04.5 LTS)
        "ryu_trifolium@main" = home-manager.lib.homeManagerConfiguration {
          pkgs = nixpkgs.legacyPackages.x86_64-linux;
          modules = [
            ./home/common.nix
            ./home/wsl.nix
          ];
        };
        # MacBook Pro M1
        "ryu@MacBook.local" = home-manager.lib.homeManagerConfiguration {
          pkgs = nixpkgs.legacyPackages.aarch64-darwin;
          modules = [
            ./home/common.nix
            ./home/mac.nix
          ];
        };
      };
    };
}

5.6 Applying the environment

You can update the environment with the following command:

It automatically detects and loads the homeConfigurations that matches the USER and HOSTNAME of the command execution environment.

bash
home-manager switch

In this case, since we are executing it on WSL, the settings written in wsl.nix and common.nix are applied.

5.7 Building the user environment on Mac

Assuming Nix is not already present on your Mac, user environment setup can be completed in a few commands:

zsh
# Install Nix
curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate

# Place the contents of ~/.config/home-manager/ into the home-manager/ managed by dotfiles
git clone <your_dotfiles>
ln -s ~/work/dotfiles/home-manager ~/.config/home-manager

# Apply environment
home-manager switch

Conclusion

This completes the setup.

While "package manager Nix" might sound intimidating due to the need to write settings in the purely functional "Nix language," I hope this example conveys that it is not that difficult when simply managing a user environment with home-manager.

As an aside, using the devShell feature of Flakes, you can also build development environments with a similar syntax.

Part of flake.nix
devShells.default = pkgs.mkShell {
  packages = [
    pkgs.terraform
    pkgs.azure-cli
    pkgs.go-task
  ];
};

If you are interested, the following books are very easy to understand, so please read them.

https://zenn.dev/asa1984/books/nix-introduction

https://zenn.dev/asa1984/books/nix-hands-on


I believe my dotfiles management is not fully polished yet, so if there are any parts that seem "off," I would appreciate your comments.


(Reference) About homeConfigurations

I searched for details on homeConfigurations using human and AI, but could not find official documentation.

The following is an old thread from 2022, which mentions that the logic is not written in the manual, and I was in the same situation.

It should be explained in the manual: https://nix-community.github.io/home-manager/index.html#ch-nix-flakes
The extra logic around managing user+hostname has gone undocumented though. The scripts for this are easy to read if you need more detail.

https://discourse.nixos.org/t/get-hostname-in-home-manager-flake-for-host-dependent-user-configs/18859

Therefore, I checked the source code.
It appears to be a feature of the home-manager switch command.

The following content is based on my speculation, so please take it with a grain of salt.

This file: home-manager/home-manager from which the following code excerpt is taken seems to be the relevant logic.

home-manager
#!@bash@/bin/bash
# Omitted...
function setFlakeAttribute() {
# Omitted...
  local name="$USER"
  # Check FQDN, long, and short hostnames; long first to preserve
  # pre-existing behaviour in case both happen to be defined.
  for n in "$USER@$(hostname -f)" "$USER@$(hostname)" "$USER@$(hostname -s)"; do
      if [[ "$(nix eval "$flake#homeConfigurations" --apply "x: x ? \"$(escapeForNix "$n")\"")" == "true" ]]; then
          name="$n"
          if [[ -v VERBOSE ]]; then
              echo "Using flake homeConfiguration for $name"
          fi
      fi
  done
# Omitted...

In function doSwitch(), function setFlakeAttribute() and function doBuildFlake() are called in that order.

Based on the processing of function setFlakeAttribute(), it seems the rules are as follows:

  • It checks if the name in flake.nix's homeConfigurations matches any of the following 3 patterns:
    • $USER@$(hostname -f)
    • $USER@$(hostname)
    • $USER@$(hostname -s)
  • If there is a match, it assigns the name to name.
  • -> Uses the information in homeConfiguration.name in subsequent processes.

This mechanism allows it to automatically read the USER and HOSTNAME of the environment and switch the configuration file to load.

(Reference) Other methods

I considered other methods while finding this one.
I'll leave a brief memo here for my own reference.

I investigated how to implement Method 3 more easily, and this led me to my current approach.

1. Create a dotfiles repository per OS and prepare home.nix for each

  • Cannot share common settings between WSL and Mac; management costs are high.

2. Prepare home.nix in a single dotfiles repository and try to use if-statements

  • Not elegant (subjective)
    • Poor readability because descriptions for each environment are mixed together.
  • Reference: Section "I want to write branches for each OS?"

https://apribase.net/2023/08/22/nix-home-manager-qa/

3. Support multiple platforms in a single home.nix

  • The home-manager switch --flake .#hoge-user --impure command is long, and it becomes impure.
  • Settings cannot be separated by environment.
  • Reference: Section "Trying to make it look nice"

https://zenn.dev/ymat19/articles/beac3c1beccac4#良い感じにしてみる

Discussion