Import from a URL in Nix

Michael Lynch

在 Nix 中從 URL 匯入

原文由 Michael Lynch 發布,訂閱此部落格

我還是個 Nix 新手,有一件事直到最近才搞懂,就是該如何把我的 configuration.nix 檔案中一部分納入版本控制。

我的目標

我希望我的 Nix 設定檔能夠模組化且可重複使用,這樣就能依照不同的系統或 flake,只引入需要的設定檔。我也希望所有的 Nix 設定檔都能納入版本控制,讓不同的系統可以依賴同一個檔案的不同版本,這樣就不用一次把每台系統上的每個設定檔都升級到最新版。

用本機檔案建立可重複使用的 bash 別名

我想做成可重複使用的 Nix 設定之一,就是我的 bash 別名。在我現有的系統上,我的 .bashrc 裡有這幾行:

alias gc='git commit --message'
alias gs='git status'
alias td='pushd $(mktemp -d)'

要在 Nix 中達到同樣的效果,我可以在 NixOS 系統上建立一個內容如下的檔案。我把它命名為 shell.nix,不過檔名可以隨意取:

{
  programs.bash.shellAliases = {
    gc = "git commit --message";
    gs = "git status";
    td = "pushd $(mktemp -d)";
  };
}

如果我把這個檔案存到 /tmp/shell.nix,就可以把它加到 imports 裡,匯入到我的 /etc/nixos/configuration.nix

{
  imports = [
    "/tmp/shell.nix"
  ];
}

如果我執行 sudo nixos-rebuild switch 然後重新啟動 shell,就能看到我的 bash 別名已經生效:

$ td
/tmp/tmp.awb7PW7aus ~

[/tmp/tmp.awb7PW7aus]$ pwd
/tmp/tmp.awb7PW7aus

把 Nix 檔案搬到 URL 上

上面的方法雖然可行,但我得在每台 Nix 系統上都複製同一個檔案。

我比較想把檔案放到一個可公開存取的 URL 上,這樣就能用一個標準的 configuration.nix 檔案去參照那個 URL。

以下是我的 configuration.nix 修改方式,用來從遠端 URL 引入我的 shell.nix 檔案

let
  shellAliases = builtins.fetchurl {
    url = "https://mtlynch.io/notes/nix-import-from-url/shell.nix";
  };
in {
  imports = [
    shellAliases
  ];
}

同樣地,如果我把這些修改存到 configuration.nix、執行 sudo nixos-rebuild switch 並重新啟動 shell,Nix 就會從該 URL 匯入我的 bash 別名。

使用 fetchGit(選用)

另一個取得遠端 Nix 檔案的方法,是把檔案放到公開的 Git 儲存庫中,再用 fetchGit 來取得。

以下是一個 configuration.nix 範例,會從公開的 GitHub 儲存庫取得我的 shell.nix

let
  repo = builtins.fetchGit {
    url = "https://github.com/mtlynch/nix-files";
    rev = "f98500a995cb5838e40be139a8327867faaff2d5";
  };
in {
  imports = [
    "${repo}/shell.nix"
  ];
}

其中 url 是我的 GitHub 儲存庫的公開 URL,而 rev 則是我想匯入的檔案版本的 Git commit hash。

configuration.nix 做了這些修改後,我只要重新執行 sudo nixos-rebuild switch,Nix 就會從我的 GitHub 儲存庫匯入 shell.nix 檔案。

更進階的 shell 設定

我的 bash 別名大多是簡單的一行指令,但有幾個比較複雜。對於這些情況,我會先定義 bash 函式,再為這些函式建立簡短的別名。

一個例子是我的 gcbm 別名。用法像是 gcbm some-branch,它會依序執行以下動作:

  1. 切換到 main 分支。
  2. 從遠端儲存庫拉取最新變更。
  3. 建立並切換到名為 some-branch 的新分支。

這個別名主要的工作是由一個叫做 git_sync_and_branch 的 bash 函式來完成。以下是我在 shell.nix 中的實作方式:

{
  programs.bash = {
    shellInit = ''
      function git_sync_and_branch {
        local readonly TARGET_BRANCH="''${1}"
        local readonly MAIN_BRANCH='master'

        git checkout "''${MAIN_BRANCH}" && \
          git pull origin "''${MAIN_BRANCH}" && \
          if [[ -n "''${TARGET_BRANCH}" ]]; then
            git checkout -b "''${TARGET_BRANCH}"
          fi
      }
      '';
    shellAliases = {
      gc = "git commit --message";
      gcbm = "git_sync_and_branch";
      gs = "git status";
      td = "pushd $(mktemp -d)";
    };
  };
}

注意:跳脫錢字號

我在嘗試把 bash 函式搬到 Nix 時踩到的一個坑,就是必須跳脫 ${ 序列。否則 Nix 會嘗試把它們當成區域變數來內插,但它們其實是 bash 變數,而不是 Nix 變數。

以下是我最初在 git_sync_and_branch bash 函式中嘗試寫的其中一行:

git checkout "${MAIN_BRANCH}"

Nix 回報了這個錯誤:

error: undefined variable 'MAIN_BRANCH'

我需要跳脫 ${,方法是在它前面加上兩個單引號(''),像這樣:

git checkout "''${MAIN_BRANCH}"

為什麼不用 Home Manager?

我想,更常見的 Nix 設定模組化方式是使用Home Manager

但老實說,我還是不太懂 Home Manager 的用途。根據專案的 README:

這個專案提供了一套基本系統,用於透過 Nix 套件管理器搭配 Nixpkgs 中的 Nix 函式庫來管理使用者環境。它可以對使用者專屬(非全域)的套件與 dotfiles 進行宣告式設定。

這讓我很困惑,因為聽起來這就是 Nix 本來就在做的事。

我已經看過 Home Manager 好幾次,但每次都覺得得花很多時間去學這個新工具,而能得到的好處卻不明顯。

或許我之後終究會發現用 Home Manager 管理 Nix 設定的好處,但就目前來說,我已經找到一個用標準 Nix 來管理遠端 Nix 檔案、相當直接的方法。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言