❄️

Homebrew管理下のCLIをNixに移してみる

2024/07/25に公開

macOSにおけるパッケージマネージャといえばHomebrewが定番ですね。

https://brew.sh/

ところで、最近は純粋関数型パッケージマネージャのNixが流行りで激アツです。HomebrewのようにグローバルなCLIツールの管理もできるということを知り、やってみました。

https://nixos.org/

本記事はApple Silicon Mac環境での作業を基に書いているので、他環境の場合は適宜読み替えてください。

導入

とりあえずNixの基礎的な部分はこちらで勉強できます。

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

https://zero-to-nix.com/

上記のzero-to-nixを管理しているDeterminateSystemsが公式のインストーラをラップしたnix-installerというものを提供しています。これでインストールするのが便利です。

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

インストール完了したらターミナルを再起動し、$PATH~/.nix-profile/binが入っているか確認してください。
なお、筆者はシェル設定の最後にhomebrewのパスを定義する設定を入れていたため、nixで入れたものよりhomebrewで入れたものが優先される形になってしまっていました。パスの順序も注意してください。

flake.nixの作成

設定ファイルを置くディレクトリ(Git管理下)でnix flake initしましょう。flake.nixが作られます。
デフォルトではシステム指定がlinuxになっているので、aarch64-darwinに書き換えます。

flake.nix
{
  description = "A very basic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  };

  outputs = { self, nixpkgs }: {

    packages.aarch64-darwin.hello = nixpkgs.legacyPackages.aarch64-darwin.hello;

    packages.aarch64-darwin.default = self.packages.aarch64-darwin.hello;

  };
}

システム指定を修正したら、以下を実行して動作を確かめましょう。outputsdefaultに指定されているもの(ここではnixpkgsのhello)がビルドされます。

nix build

ビルド後にresultというディレクトリができていたら成功です。result/bin/helloにビルド結果のプログラムが入っています。

./result/bin/hello --help

確認できたらresultディレクトリは削除して構いません。

パッケージ定義の記述

flake.nixを修正して、パッケージの定義を書いていきます。

nixpkgsから取り込む

nixpkgsの提供しているパッケージは以下のページで検索できます。

https://search.nixos.org/packages

flakeのoutputsを修正します。今回は例としてgitとcurlを指定します。

flake.nix
{
  description = "Minimal package definition for aarch64-darwin";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  };

  outputs = { self, nixpkgs }: {
    packages.aarch64-darwin.my-packages = nixpkgs.legacyPackages.aarch64-darwin.buildEnv {
      name = "my-packages-list";
      paths = [
        nixpkgs.legacyPackages.aarch64-darwin.git
        nixpkgs.legacyPackages.aarch64-darwin.curl
        # ここにパッケージを追記していく
      ];
    };
  };
}

以下のコマンドでprofileを反映します。

nix profile install .#my-packages

nix profile listでinstallされた設定を見られます。筆者の例はこんな感じです。

❯ nix profile list
Name:               my-packages
Flake attribute:    packages.aarch64-darwin.my-packages
Original flake URL: git+file:///Users/kawarimidoll/dotfiles
Locked flake URL:   git+file:///Users/kawarimidoll/dotfiles
Store paths:        /nix/store/b5jphy5nm1ab1pz0xp08lmw7wf11pb7h-my-packages-list

ここで示されている/nix/store/{hash}-{package-name}/binにgitとcurlの実行ファイルがあるはずです。
こんな感じで実行ファイルが~/.nix-profile/bin/以下のファイルになっていればインストール成功です。できていない場合は前述の通り$PATHが上書きされている可能性があるので確認してください。

❯ which git
/Users/kawarimidoll/.nix-profile/bin/git

このprofileはnameを指定してアンインストールできます。

nix profile remove my-packages

設定を変更した場合は、nameを指定して更新します。nix flake updateもやっておくと良いみたいです。

nix flake update
nix profile upgrade my-packages

本記事ではgitとcurlだけ書いていますが、もちろん他のパッケージも追加できます。例えば、.nixファイルのフォーマッターであるalejandraなどが便利です。

nixpkgs以外から取り込む

nixpkgsで公開されているパッケージはバージョンが最新でないことがあります。たとえばNeovimは、記事執筆時点ではバージョンは0.9.5です。


nixpkgsのNeovimパッケージ

Neovimは機能追加・不具合修正が活発にされているため、(stableではなく)開発中の最新版を使いたいところです。幸い、nix-communityがneovim-nightly-overlayというパッケージを公開しています。これを導入します。

https://github.com/nix-community/neovim-nightly-overlay

inputsでneovim-nightly-overlayを読み込み、outputsの引数に指定し、pathsの配列に追加します。

flake.nix
{
  description = "Minimal package definition for aarch64-darwin";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+   neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
  };

  outputs = {
    self,
    nixpkgs,
+   neovim-nightly-overlay,
  }: {
    packages.aarch64-darwin.my-packages = nixpkgs.legacyPackages.aarch64-darwin.buildEnv {
      name = "my-packages-list";
      paths = [
        nixpkgs.legacyPackages.aarch64-darwin.git
        nixpkgs.legacyPackages.aarch64-darwin.curl
+       neovim-nightly-overlay.packages.aarch64-darwin.neovim
      ];
    };
  };
}

GitHubから取り込む

Neovimは上記の通りneovim-nightly-overlayが公開されていますが、使いたいものが常にnix用に整備されているとは限りません。たとえばVimはこのような便利パッケージはありません。
したがって、GitHubから直接取り込み、自前でビルドする手段を取りました。

inputのvim-src.urlでURLを指定するのは前述のnixpkgsやneovim-nightly-overlayと同じですが、このリポジトリはflake.nixがないのでflake = falseを指定します。

そして、overrideAttrsでnixpkgsのvimの設定を上書きします。

  • versionにはバージョン名
    • ここでは、/nix/store/{hash}-vim-latest/bin/vimに実行ファイルがインストールされる
  • srcには読み込み元
  • configureFlagsにはビルド時のフラグ
  • buildInputsには依存関係
flake.nix
{
  description = "Minimal package definition for aarch64-darwin";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
+   vim-src = {
+     url = "github:vim/vim";
+     flake = false;
+   };
  };

  outputs = {
    self,
    nixpkgs,
    neovim-nightly-overlay,
+   vim-src,
  }: {
    packages.aarch64-darwin.my-packages = nixpkgs.legacyPackages.aarch64-darwin.buildEnv {
      name = "my-packages-list";
      paths = [
        nixpkgs.legacyPackages.aarch64-darwin.git
        nixpkgs.legacyPackages.aarch64-darwin.curl
+       (nixpkgs.legacyPackages.aarch64-darwin.vim.overrideAttrs (oldAttrs: {
+         version = "latest";
+         src = vim-src;
+         configureFlags =
+           oldAttrs.configureFlags
+           ++ [
+             "--enable-terminal"
+             "--with-compiledby=kawarimidoll-nix"
+             "--enable-luainterp"
+             "--with-lua-prefix=${nixpkgs.legacyPackages.aarch64-darwin.lua}"
+             "--enable-fail-if-missing"
+           ];
+         buildInputs =
+           oldAttrs.buildInputs
+           ++ [
+             nixpkgs.legacyPackages.aarch64-darwin.gettext
+             nixpkgs.legacyPackages.aarch64-darwin.lua
+             nixpkgs.legacyPackages.aarch64-darwin.libiconv
+           ];
+       }))
        neovim-nightly-overlay.packages.aarch64-darwin.neovim
      ];
    };
  };
}

luaのパスの指定(--with-lua-prefixオプション)が曲者だったのですが、変数展開することでnix内のluaのパスを使うことができます。ビルド後にvim --versionでLinkingの項目を見たところ、/nix/store/{hash}-lua-{version}/libのパスが指定されているのを確認できました。
なお、これらのフラグはhomebrewの設定を参考にしました。

https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/v/vim.rb

リファクタリング

ここまでで基本的なパッケージの導入はできました。
以降はファイル内の冗長な部分を修正していきます。

まず、パッケージ指定でnixpkgs.legacyPackages.aarch64-darwin.が繰り返されているのが気になります。これはwithを使ってまとめることができます。
(diff表示だとかえって見づらくなったので)まとめた結果を示します。neovim-nightly-overlayはnixpkgs配下ではないので、配列を分割し、++で連結しています。

flake.nix
{
  # description/inputsは省略
  outputs = {
    # 引数略...
  }: {
    packages.aarch64-darwin.my-packages = nixpkgs.legacyPackages.aarch64-darwin.buildEnv {
      name = "my-packages-list";
      paths = with nixpkgs.legacyPackages.aarch64-darwin;
        [
          git
          curl
          (vim.overrideAttrs (oldAttrs: {
            version = "latest";
            src = vim-src;
            configureFlags =
              oldAttrs.configureFlags
              ++ [
                "--enable-terminal"
                "--with-compiledby=kawarimidoll-nix"
                "--enable-luainterp"
                "--with-lua-prefix=${lua}"
                "--enable-fail-if-missing"
              ];
            buildInputs =
              oldAttrs.buildInputs
              ++ [ gettext lua libiconv ];
          }))
        ]
        ++ [neovim-nightly-overlay.packages.aarch64-darwin.neovim];
    };
  };
}

overlayはその名の通り上書き用の設定です。extendを使って、nixpkgsのneovimをnightlyに上書きしましょう。

https://nixos.wiki/wiki/Overlays#In_a_Nix_flake

let ... inで、nixpkgsにneovim-nightly-overlayを適用し、pkgsという変数にします。以下のように書けます。

flake.nix
{
  # description/inputsは省略
  outputs = {
    # 引数略...
- }: {
+ }:
+   let
+     pkgs = nixpkgs.legacyPackages.aarch64-darwin.extend (
+       neovim-nightly-overlay.overlays.default
+     );
+   in
+ {
-   packages.aarch64-darwin.my-packages = nixpkgs.legacyPackages.aarch64-darwin.buildEnv {
+   packages.aarch64-darwin.my-packages = pkgs.buildEnv {
      name = "my-packages-list";
-     paths = with nixpkgs.legacyPackages.aarch64-darwin;
+     paths = with pkgs;
        [
          # neovim以外のパッケージは省略
-       ]
-       ++ [neovim-nightly-overlay.packages.aarch64-darwin.neovim];
+         neovim
+       ];
    };
  };
}

システム指定も変数にします。

flake.nix
    # ここまで略...
    let
-     pkgs = nixpkgs.legacyPackages.aarch64-darwin.extend (
+     system = "aarch64-darwin";
+     pkgs = nixpkgs.legacyPackages.${system}.extend (
        neovim-nightly-overlay.overlays.default
      );
    in
  {
-   packages.aarch64-darwin.my-packages = pkgs.buildEnv {
+   packages.${system}.my-packages = pkgs.buildEnv {
      name = "my-packages-list";
    # 以下省略

これで冗長な記述を圧縮することができました。

更新タスクの追加

設定更新時には以下のコマンドを実行すると書きましたが:

nix flake update
nix profile upgrade my-packages

これを一括で実行できるようにします。appsという項目を追加します。

flake.nix
  {
    packages.${system}.my-packages = pkgs.buildEnv {
      # 略...
    };

+   apps.${system}.update = {
+     type = "app";
+     program = toString (pkgs.writeShellScript "update-script" ''
+       set -e
+       echo "Updating flake..."
+       nix flake update
+       echo "Updating profile..."
+       nix profile upgrade my-packages
+       echo "Update complete!"
+     '');
+   };

  }

appsprogramには既存の実行ファイルのパスを指定するのが原則らしいのですが↓

https://nix.dev/manual/nix/2.22/command-ref/new-cli/nix3-run

pkgs.writeShellScript {script-name} {content}を使うことで、シェルスクリプトを直接記述して実行することができます。これで、以下の1コマンドで更新をかけられます。

nix run .#update

豆知識

flakeを更新→反映を繰り返しているとnixの環境を都度作り直すことになります。古いバージョンが溜まっている可能性があるので、設定が整ったら掃除しておきましょう。

nix store gc

また、flakeを問い合わせるためにGitHub APIを叩くので、何度も調整しているとRate Limitに引っかかる場合があります(筆者は引っかかりました)。設定ファイルにトークンを追加すると回数を増やすことができます。

~/.config/nix/nix.conf
access-tokens = github.com=abcd1234

https://discourse.nixos.org/t/flakes-provide-github-api-token-for-rate-limiting/18609

この記事で作ったflake.nixの完成版
flake.nix
{
  description = "Minimal package definition for aarch64-darwin";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
    vim-src = {
      url = "github:vim/vim";
      flake = false;
    };
  };

  outputs = {
    self,
    nixpkgs,
    neovim-nightly-overlay,
    vim-src,
  }: let
    system = "aarch64-darwin";
    pkgs = nixpkgs.legacyPackages.${system}.extend (
      neovim-nightly-overlay.overlays.default
    );
  in {
    packages.${system}.my-packages = pkgs.buildEnv {
      name = "my-packages-list";
      paths = with pkgs; [
        git
        curl

        (vim.overrideAttrs (oldAttrs: {
          version = "latest";
          src = vim-src;
          configureFlags =
            oldAttrs.configureFlags
            ++ [
              "--enable-terminal"
              "--with-compiledby=kawarimidoll-nix"
              "--enable-luainterp"
              "--with-lua-prefix=${lua}"
              "--enable-fail-if-missing"
            ];
          buildInputs = oldAttrs.buildInputs ++ [gettext lua libiconv];
        }))

        neovim
      ];
    };

    apps.${system}.update = {
      type = "app";
      program = toString (pkgs.writeShellScript "update-script" ''
        set -e
        echo "Updating flake..."
        nix flake update
        echo "Updating profile..."
        nix profile upgrade my-packages
        echo "Update complete!"
      '');
    };
  };
}

続編

以下の記事に続きます。

https://zenn.dev/kawarimidoll/articles/9c44ce8b60726f

Discussion