Open27

Nixでdotfiles管理できるようになるまでのメモ

airRnotairRnot

Nixに入門しようと思うので、リアルタイムに状況をメモしていきます。

目的

Nixでdotfiles管理ができるようになる

要件

  • 現状のchezmoiでのdotfilesでできてたこと全部できる
  • マルチプラットフォーム対応(x86_64-linuxaarch64-linuxは必須。できればaarch64-darwinも)
  • 美しいディレクトリ構造
    • パッケージごとにファイルを分けたい
airRnotairRnot

最初の一歩

以下の記事を読んだ。

これにより、Nixに対する一つの誤解が解けた。

before

  • Nixはnpmのようなパッケージマネージャ。プロジェクトの依存関係を宣言的に管理できる
  • NixOSを使用することで、ユーザ環境やOSの設定も宣言的に管理できるようになる

after

  • NixOSではなくNix単体でも、profileという概念によってユーザ環境を宣言的に管理できる
airRnotairRnot

Home Managerってなんだ?

home-managerは上記のNixパッケージマネージャを利用して、ローカルユーザー環境で利用するパッケージの管理とdotfilesを一元的に管理するための仕組みです。

home-managerを使わなくてもprofileによってパッケージをグローバルインストールすることは可能だが、home-managerを使うことによって.bashrcなどの設定ファイルなどもNixの管理下にできるという認識。

airRnotairRnot

環境構築に入る前に

自分のやりたいことがNixで実現できそうなことが分かったので、Nixの環境構築に入っていく。
ただし、私はchezmoiも気に入っているのでchezmoiと組み合わせることを検討する。
chezmoiにはNixの環境構築とusernameやemailなどの設定をお願いしたい。

airRnotairRnot

環境構築

Git & xz-utils

chezmoiはgit、Nixはgitとxz-utilsが必要みたいなのでこれだけはaptでインストールしておく。
なお、環境はm2のmacOSでOrbStackを用いて立ち上げたaarch64-linuxのUbuntuである。

sudo apt install -y git xz-utils

chezmoi

個人的に~/binにインストールされるのが気に入らないので、~/.local/binにインストールする。

sh -c "$(curl -fsLS get.chezmoi.io)" -- -b $HOME/.local/bin
chezmoi init

Nix

DeterminateSystemsが提供している方でインストールする。

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

chezmoiのディレクトリに移動する。

chezmoi cd

Nixはgit管理下にある場合、追跡中のファイルしか認識されないようになるので一旦gitを削除しておく。

rm -rf .git
airRnotairRnot

チュートリアル

一旦home-managerは置いておいて、以下の記事に沿って試してみる。

以降は基本的に~/.local/share/chezmoiディレクトリで操作を行っていく。

flake.nixの作成

neovimの設定ができるまではホストマシンのvscodeでコードを書いていく。
rootにflake.nixを作成し、以下のコードを書く。アーキテクチャはaarch64-linuxに変更している。

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

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

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

ここまでできたらprofileをinstallする。

nix profile install .#my-packages

(.#がなんなのかはよく分かっていない)

完了したら

ls ~/.nix-profile/bin

を実行し、中身があったら成功。

フォーマッタの設定

どうやらNixではフォーマッタを設定できるみたいなので、フォーマッタを設定する。

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

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

  outputs = { self, nixpkgs }: {
    formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.nixfmt-rfc-style;
    packages.aarch64-linux.my-packages = nixpkgs.legacyPackages.aarch64-linux.buildEnv {
      name = "my-packages-list";
      paths = [
        nixpkgs.legacyPackages.aarch64-linux.git
        nixpkgs.legacyPackages.aarch64-linux.curl
        nixpkgs.legacyPackages.aarch64-linux.nixfmt-rfc-style
      ];
    };
  };
}

nixfmt-rfc-styleというものをインストールするように変更。以下のコマンドで変更を反映させる。

nix flake update
nix profile upgrade my-packages
ls ~/.nix-profile/bin

してnixfmtがあったら成功。

nix fmt

でフォーマットできるようになる。

neovim

neovimのnightlyを入れるなら、nixpkgsじゃなくてneovim-nightly-overlayから入れるらしい。

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

  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,
    }:
    {
      formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.nixfmt-rfc-style;
      packages.aarch64-linux.my-packages = nixpkgs.legacyPackages.aarch64-linux.buildEnv {
        name = "my-packages-list";
        paths = [
          nixpkgs.legacyPackages.aarch64-linux.git
          nixpkgs.legacyPackages.aarch64-linux.curl
          nixpkgs.legacyPackages.aarch64-linux.nixfmt-rfc-style
          neovim-nightly-overlay.packages.aarch64-linux.neovim
        ];
      };
    };
}

nix flake update
nix profile upgrade my-packages
nvim

neovimが起動できたら成功。

変数の設定

aarch64-linuxnixpkgs.legacyPackagesという文字列を何回も書いているのが冗長なので、変数でまとめる。

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

  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,
    }:
    let
      system = "aarch64-linux";
      pkgs = nixpkgs.legacyPackages.${system}.extend (neovim-nightly-overlay.overlays.default);
    in
    {
      formatter.${system} = pkgs.nixfmt-rfc-style;
      packages.${system}.my-packages = pkgs.buildEnv {
        name = "my-packages-list";
        paths = with pkgs; [
          git
          curl
          nixfmt-rfc-style
          neovim
        ];
      };
    };
}

更新タスクの追加

nix flake update
nix profile upgrade my-packages

これを毎回書くのは面倒なので、一コマンドで完結するようにする。

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

  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,
    }:
    let
      system = "aarch64-linux";
      pkgs = nixpkgs.legacyPackages.${system}.extend (neovim-nightly-overlay.overlays.default);
    in
    {
      formatter.${system} = pkgs.nixfmt-rfc-style;
      packages.${system}.my-packages = pkgs.buildEnv {
        name = "my-packages-list";
        paths = with pkgs; [
          git
          curl
          nixfmt-rfc-style
          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!"
          ''
        );
      };
    };
}

以下のコマンドで実行。

nix run .#update
airRnotairRnot

ここで基礎へ

Nixがどういうものかふんわりと分かってきたので、ここで基礎をちゃんと読む。

そういえば、マルチプラットフォーム……

そういえば、マルチプラットフォーム対応したいのであった。現在はsystem = "aarch64-linux";と指定してしまっているので、これを無くしたい。
そこで、上記の記事で紹介されているflake-utilsを使うことにした。flake-utilsのflake-utils.lib.eachDefaultSystemを使用することで、マルチプラットフォーム対応することができる。

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

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

  outputs =
    {
      self,
      nixpkgs,
      neovim-nightly-overlay,
      flake-utils,
      ...
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system}.extend (neovim-nightly-overlay.overlays.default);
      in
      {
        formatter.${system} = pkgs.nixfmt-rfc-style;
        packages.${system}.my-packages = pkgs.buildEnv {
          name = "my-packages-list";
          paths = with pkgs; [
            git
            curl
            nixfmt-rfc-style
            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!"
            ''
          );
        };
      }
    );
}

これで反映させてみると、なんとエラー発生。

error: flake 'path:/home/.../.local/share/chezmoi' does not provide attribute 'apps.aarch64-linux.update', 'packages.aarch64-linux.update', 'legacyPackages.aarch64-linux.update' or 'update'

どうやらflake-utils.lib.eachDefaultSystem内では、formatter.${system}packages.${system}についていた${system}は要らないみたい。

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

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

  outputs =
    {
      self,
      nixpkgs,
      neovim-nightly-overlay,
      flake-utils,
      ...
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system}.extend (neovim-nightly-overlay.overlays.default);
      in
      {
        formatter = pkgs.nixfmt-rfc-style;
        packages.my-packages = pkgs.buildEnv {
          name = "my-packages-list";
          paths = with pkgs; [
            git
            curl
            nixfmt-rfc-style
            neovim
          ];
        };
        apps.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!"
            ''
          );
        };
      }
    );
}

これで再度実行。

nix run .#update
    Updating flake...
    Updating profile...
    Update complete!

無事成功。

airRnotairRnot

お片付け

Nixの環境を何度も作り直していると古い環境がたまっていくので、以下のコマンドで片付ける。

nix store gc
airRnotairRnot

さて、home-manager

ここからはhome-managerに入っていきたい。流れとしては、以下のようになると思われる。

  1. 最小構成でhome-manager導入
  2. 現在のdotfilesでの設定の移植
  3. パッケージごとにディレクトリを分ける

ちなみに、3が実現できるのかは確認しきれていない。ただ、GitHubで検索したところそれっぽいことをしてるリポジトリはいくつか見つけた。

この記事と合わせてhome-managerを導入していく。

とりあえず導入

以下の設定でビルドしてみる。

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

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      home-manager,
      neovim-nightly-overlay,
      flake-utils,
      ...
    }@inputs:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        inherit (import ./home/options.nix) username;
        pkgs = import nixpkgs { inherit system; };
      in
      {
        formatter = pkgs.nixfmt-rfc-style;
        apps.update = {
          type = "app";
          program = toString (
            pkgs.writeShellScript "update-script" ''
              set -e
              echo "Updating flake..."
              nix flake update
              echo "Updating home-manager..."
              nix run nixpkgs#home-manager -- switch --flake .#${username}
              echo "Update complete!"
            ''
          );
        };
        homeConfigurations."${username}" = home-manager.lib.homeManagerConfiguration {
          pkgs = pkgs;
          extraSpecialArgs = {
            inherit inputs;
          };
          modules = [
            ./home/home.nix
          ];
        };
      }
    );
}

./home/options.nix
rec {
  username = "airrnot";
  gitUsername = "airrnot";
  gitEmail = "airrnot@example.com";
}

./home/home.nix
{
  inputs,
  lib,
  config,
  pkgs,
  ...
}:
let
  inherit (import ./options.nix) username;
in
{
  nixpkgs = {
    config = {
      allowUnfree = true;
    };
  };

  home = {
    username = username;
    homeDirectory = "/home/${username}";

    # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
    stateVersion = "24.05";
  };

  programs.home-manager.enable = true;
}

以下のコマンドでビルド実行。

nix run nixpkgs#home-manager -- switch --flake .#airrnot

と、ここで、エラー発生。

error: flake 'path:/home/airrnot/.local/share/chezmoi' does not provide attribute 'packages.aarch64-linux.homeConfigurations."airrnot".activationPackage', 'legacyPackages.aarch64-linux.homeConfigurations."airrnot".activationPackage' or 'homeConfigurations."airrnot".activationPackage'

んー、よく分からない。とりあえずここで詰み。

airRnotairRnot

うわ、flake-utils.lib.eachDefaultSystemを外して

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

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      home-manager,
      neovim-nightly-overlay,
      flake-utils,
      ...
    }@inputs:
      let
        inherit (import ./home/options.nix) username;
        system = "aarch64-linux";
        pkgs = import nixpkgs { inherit system; };
      in
      {
        formatter.${system} = pkgs.nixfmt-rfc-style;
        apps.${system}.update = {
          type = "app";
          program = toString (
            pkgs.writeShellScript "update-script" ''
              set -e
              echo "Updating flake..."
              nix flake update
              echo "Updating home-manager..."
              nix run nixpkgs#home-manager -- switch --flake .#${username}
              echo "Update complete!"
            ''
          );
        };
        homeConfigurations."${username}" = home-manager.lib.homeManagerConfiguration {
          pkgs = pkgs;
          extraSpecialArgs = {
            inherit inputs;
          };
          modules = [
            ./home/home.nix
          ];
        };
      };
}

だと成功した。
どういうことですか。

airRnotairRnot

解決

GitHubで実装例を調べまくってなんとか解決した。

flake-utils.lib.eachDefaultSystemを使用している場合は、homeConfigurationsをlegacyPackagesで囲まなければならなかった。

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

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      home-manager,
      neovim-nightly-overlay,
      flake-utils,
      ...
    }@inputs:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        inherit (import ./home/options.nix) username;
        pkgs = import nixpkgs { inherit system; };
      in
      {
        formatter = pkgs.nixfmt-rfc-style;
        apps.update = {
          type = "app";
          program = toString (
            pkgs.writeShellScript "update-script" ''
              set -e
              echo "Updating flake..."
              nix flake update
              echo "Updating home-manager..."
              nix run nixpkgs#home-manager -- switch --flake .#${username}
              echo "Update complete!"
            ''
          );
        };
        legacyPackages = {
          inherit (pkgs) home-manager;
          homeConfigurations."${username}" = home-manager.lib.homeManagerConfiguration {
            pkgs = pkgs;
            extraSpecialArgs = {
              inherit inputs;
            };
            modules = [
              ./home/home.nix
            ];
          };
        };
      }
    );
}

なんでこれで解決できるのかはよく分からない。
が、とりあえず良かった。

airRnotairRnot

自分でビルドするには?

nixpkgsやneovimのような他のコミュニティでも公開されていないようなパッケージはどうやってビルドするんだ? ということで、Chat GPTに聞いてみた。
ここではオシャレな時計を表示するcliツールであるeraを例に行う。eraはdenoで書かれている。

ChatGPT's answer

{
  description = "Build the 'era' project using Deno";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs"; # nixpkgs のソース
  };

  outputs = { self, nixpkgs }: {
    packages = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: let
      pkgs = import nixpkgs { inherit system; };
    in
      {
        era = pkgs.stdenv.mkDerivation rec {
          pname = "era";
          version = "1.0.0"; # 必要に応じてバージョンを変更

          src = pkgs.fetchFromGitHub {
            owner = "kyoheiu";
            repo = "era";
            rev = "main"; # 特定のバージョンやコミットハッシュを指定するならここを変更
            sha256 = lib.fakeSha256; # 実際のsha256は `nix-prefetch-git` を使用して取得
          };

          buildInputs = [ pkgs.deno ];

          buildPhase = ''
            deno task build
          '';

          installPhase = ''
            mkdir -p $out/bin
            cp -r ./dist/* $out/bin/
          '';

          meta = with pkgs.lib; {
            description = "ERA - A tool built on Deno";
            license = licenses.mit;
            platforms = platforms.all;
          };
        };
      }
    );
  };
}

どうやら、pkgs.stdenv.mkDerivationが肝らしい。
そもそも、flakeってどうやって書くんだっけ?

outputsは関数になっており、Flakeの評価時にinputsに指定したFlakeのoutputsが引数に渡されます。outputsはAttribute Setを返します。
実は、outputsで返せるのはパッケージだけではありません。outputsには以下のようなものを指定できます。

{
  inputs = {
    # 依存するFlake
  };

  outputs = inputs: {
    packages."<システムアーキテクチャ>"."<パッケージ名>" = derivation;
    devShells."<システムアーキテクチャ>"."<devShellの名前>" = derivation;
    formatter."<システムアーキテクチャ>"."<パッケージの名前>" = derivation;
    templates."<テンプレートの名前>" = {
      path = "<ストアパス>";
      description = "テンプレートの説明";
    };

    # ...その他多数
  };
}

packagesにはderivationを渡すと書かれている。では、derivationとは何か?

ここでpkgs.stdenv.mkDerivationに戻ってきた。
何となくビルドの流れが理解できた。
では次に、ソースの取得までNixで完結させる方法について調べる。

airRnotairRnot

なんかだめっぽい

nix run .#update
Updating flake...
warning: updating lock file '/home/airrnot/.local/share/chezmoi/flake.lock':
• Updated input 'era':
    'path:./packages/era?lastModified=1&narHash=sha256-AD8Gu7WYoGKr0JjQ9idWFQObxPGr0siccQ8/a0q2H4s%3D' (1970-01-01)
  → 'path:./packages/era?lastModified=1&narHash=sha256-3yFKi6f4QNy/WX%2Br3AkSN9ZxjFv56qb0u/%2BIjYieyq4%3D' (1970-01-01)
Updating home-manager...
error: builder for '/nix/store/9a932srxsnfc7x3a5rzda7qwqsqm7jgx-era-0.1.3.drv' failed with exit code 2;
       last 25 log lines:
       >
       > Caused by:
       >     Error code 14: Unable to open the database file)
       > Failed to open cache file '/homeless-shelter/.cache/deno/dep_analysis_cache_v2', opening in-memory cache.
       > Could not initialize cache database '/homeless-shelter/.cache/deno/check_cache_v2', deleting and retrying... (unable to open database file: /homeless-shelter/.cache/deno/check_cache_v2
       >
       > Caused by:
       >     Error code 14: Unable to open the database file)
       > Failed to open cache file '/homeless-shelter/.cache/deno/check_cache_v2', performance may be degraded.
       > Check file:///build/source/src/main.ts
       > Could not initialize cache database '/homeless-shelter/.cache/deno/fast_check_cache_v2', deleting and retrying... (unable to open database file: /homeless-shelter/.cache/deno/fast_check_cache_v2
       >
       > Caused by:
       >     Error code 14: Unable to open the database file)
       > Failed to open cache file '/homeless-shelter/.cache/deno/fast_check_cache_v2', performance may be degraded.
       > Compile file:///build/source/src/main.ts to era
       > Download https://dl.deno.land/release/v1.46.2/denort-aarch64-unknown-linux-gnu.zip
       > error: Writing temporary file 'era.tmp-cea1351b875beb7b'
       >
       > Caused by:
       >     0: error sending request for url (https://dl.deno.land/release/v1.46.2/denort-aarch64-unknown-linux-gnu.zip): client error (Connect): dns error: failed to lookup address information: Temporary failure in name resolution: failed to lookup address information: Temporary failure in name resolution
       >     1: client error (Connect)
       >     2: dns error: failed to lookup address information: Temporary failure in name resolution
       >     3: failed to lookup address information: Temporary failure in name resolution
       > make: *** [Makefile:7: install] Error 1
       For full logs, run 'nix log /nix/store/9a932srxsnfc7x3a5rzda7qwqsqm7jgx-era-0.1.3.drv'.
error: 1 dependencies of derivation '/nix/store/vi4b6w729zk5nd5xc3dpfj54hscpfaz7-home-manager-path.drv' failed to build
error: 1 dependencies of derivation '/nix/store/v1lwv35yvd6wlw0z8l9qrwpx1ba6al0f-home-manager-generation.drv' failed to build

よく考えればdenoもネットを介して依存関係を解決するわけで、普通の方法じゃ無理に決まっている。

airRnotairRnot

いけた

Denoでコンパイルするのは一旦諦めて、とりあえずコンパイル済みのバイナリをfetchしてきてインストールすることにした。
以下のようなディレクトリ構造だとして、

.
├── flake.lock
├── flake.nix
├── home
│   ├── home.nix
│   └── options.nix
└── packages
    └── era.nix

それぞれのソースは以下の通り。

./packages/era.nix
{
  pkgs ? import <nixpkgs> { },
}:
pkgs.stdenv.mkDerivation {
  name = "era";
  src = builtins.fetchTarball {
    url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-aarch64-linux.tar.gz";
    sha256 = "0r804l7l5xlxzyap32i5q2l706xfgwsdqiylw71rk6hk2m487w79";
  };
  phases = [ "installPhase" ];
  installPhase = ''
    mkdir -p $out/bin
    cp $src/era $out/bin/era
    chmod +x $out/bin/era
  '';
}

./home/home.nix
{
  inputs,
  lib,
  config,
  pkgs,
  ...
}:
let
  inherit (import ./options.nix) username;
  era = import ../packages/era.nix { inherit pkgs; };
in
{
  nixpkgs = {
    config = {
      allowUnfree = true;
    };
  };

  home = {
    username = username;
    homeDirectory = "/home/${username}";

    # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
    stateVersion = "24.05";

    packages = with pkgs; [
      nixfmt-rfc-style
      era
    ];
  };

  programs.home-manager.enable = true;
}

home.nixでera.nixをimportして、home.packagesに加えればインストールすることができた。
しかし、現状ではaarch64-linuxにしか対応していないので、マルチプラットフォーム対応を考える。

airRnotairRnot

モジュール化した場合のマルチプラットフォーム対応

目標は、flake.nixでflake-utils.lib.eachDefaultSystemで取得しているsystemをera.nixに渡すことである。そのためには、flake.nix -> home.nix -> era.nixという橋渡しが必要である。

home.nixにsystemを渡す

extraSpecialArgsに指定した値は、home.nixで引数として受け取ることが出来る。

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

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      home-manager,
      neovim-nightly-overlay,
      flake-utils,
      ...
    }@inputs:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        inherit (import ./home/options.nix) username;
        pkgs = import nixpkgs { inherit system; };
      in
      {
        formatter = pkgs.nixfmt-rfc-style;
        apps.update = {
          type = "app";
          program = toString (
            pkgs.writeShellScript "update-script" ''
              set -e
              echo "Updating flake..."
              nix flake update
              echo "Updating home-manager..."
              nix run nixpkgs#home-manager -- switch --flake .#${username} --show-trace
              echo "Update complete!"
            ''
          );
        };
        legacyPackages = {
          inherit (pkgs) home-manager;
          homeConfigurations."${username}" = home-manager.lib.homeManagerConfiguration {
            pkgs = pkgs;
            extraSpecialArgs = {
              inherit inputs system;
            };
            modules = [
              ./home/home.nix
            ];
          };
        };
      }
    );
}

era.nixにsystemを渡す

import関数は第ニ引数に指定した値を引数として渡すことが出来る。そのため、以下のようにしてsystemをera.nixに渡す。

./home/home.nix
{
  inputs,
  lib,
  config,
  pkgs,
  system,
  ...
}:
let
  inherit (import ./options.nix) username;
  era = import ../packages/era.nix { inherit pkgs system; };
in
{
  nixpkgs = {
    config = {
      allowUnfree = true;
    };
  };

  home = {
    username = username;
    homeDirectory = "/home/${username}";

    # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
    stateVersion = "24.05";

    packages = with pkgs; [
      nixfmt-rfc-style
      era
    ];
  };

  programs.home-manager.enable = true;
}

era.nixでsrcをsystemによって分岐させる

if式で頑張って分岐させる。

./packages/era.nix
{
  pkgs ? import <nixpkgs> { },
  system,
}:
pkgs.stdenv.mkDerivation {
  name = "era";
  src =
    if system == "x86_64-linux" then
      builtins.fetchTarball {
        url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-x86_64-linux.tar.gz";
        sha256 = "0inf53m863vavvh6fg4dqys0sffrignsq426ybv3sfdfyi9g75jd";
      }
    else if system == "aarch64-linux" then
      builtins.fetchTarball {
        url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-aarch64-linux.tar.gz";
        sha256 = "0r804l7l5xlxzyap32i5q2l706xfgwsdqiylw71rk6hk2m487w79";
      }
    else if system == "x86_64-darwin" then
      builtins.fetchTarball {
        url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-x86_64-darwin.tar.gz";
        sha256 = "1g5jxlzpl955730p8b5gqrzvdnk92nfyn3fqwhyissh00iysqdiq";
      }
    else if system == "aarch64-darwin" then
      builtins.fetchTarball {
        url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-aarch64-darwin.tar.gz";
        sha256 = "165p4qp20q6wy19fxa40w2108xa45ri3g1h9jwrhqrd29vmbxvh4";
      }
    else
      throw "not supported system";
  phases = [ "installPhase" ];
  installPhase = ''
    mkdir -p $out/bin
    cp $src/era $out/bin/era
    chmod +x $out/bin/era
  '';
}

追記: と思いきや

systemはpkgs.systemで大丈夫だった。

airRnotairRnot

importsってのがあるらしい

home.packagesとprograms.*は同じファイルで設定できないのかと思ってたが、普通にできるらしい。以下の記事を参照。

なので、eraの設定をhome/packages/に移動した。ついでにbashの設定も入れて現在のディレクトリ構造はこんな感じ。

.
├── flake.lock
├── flake.nix
└── home
    ├── home.nix
    ├── modules
    │   ├── bash.nix
    │   └── era.nix
    └── options.nix

./home/modules/bash.nix
{ pkgs, ... }:
{
  programs.bash = {
    enable = true;
  };
}

./home/modules/era.nix
{
  pkgs,
  system,
}:
{
  home.packages = with pkgs; [
    (pkgs.stdenv.mkDerivation {
      name = "era";
      src =
        if system == "x86_64-linux" then
          builtins.fetchTarball {
            url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-x86_64-linux.tar.gz";
            sha256 = "0inf53m863vavvh6fg4dqys0sffrignsq426ybv3sfdfyi9g75jd";
          }
        else if system == "aarch64-linux" then
          builtins.fetchTarball {
            url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-aarch64-linux.tar.gz";
            sha256 = "0r804l7l5xlxzyap32i5q2l706xfgwsdqiylw71rk6hk2m487w79";
          }
        else if system == "x86_64-darwin" then
          builtins.fetchTarball {
            url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-x86_64-darwin.tar.gz";
            sha256 = "1g5jxlzpl955730p8b5gqrzvdnk92nfyn3fqwhyissh00iysqdiq";
          }
        else if system == "aarch64-darwin" then
          builtins.fetchTarball {
            url = "https://github.com/airRnot1106/era/releases/download/v0.1.3/era-v0.1.3-aarch64-darwin.tar.gz";
            sha256 = "165p4qp20q6wy19fxa40w2108xa45ri3g1h9jwrhqrd29vmbxvh4";
          }
        else
          throw "not supported system";
      phases = [ "installPhase" ];
      installPhase = ''
        mkdir -p $out/bin
        cp $src/era $out/bin/era
        chmod +x $out/bin/era
      '';
    })
  ];
}

./home/home.nix
{
  inputs,
  lib,
  config,
  pkgs,
  system,
  ...
}:
let
  inherit (import ./options.nix) username;
in
{
  nixpkgs = {
    overlays = [
      inputs.neovim-nightly-overlay.overlays.default
    ];
    config = {
      allowUnfree = true;
    };
  };

  home = {
    username = username;
    homeDirectory = "/home/${username}";

    # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
    stateVersion = "24.05";

  };

  imports = [
    ./modules/bash.nix
    (import ./modules/era.nix {
      inherit pkgs system;
    })
  ];

  programs.home-manager.enable = true;
}

importsの中で追加で引数を渡したいなら、上記のように書けばいける。

airRnotairRnot

と、思いきや普通に

./home/home.nix
{
  inputs,
  lib,
  config,
  pkgs,
  system,
  ...
}:
let
  inherit (import ./options.nix) username;
in
{
  nixpkgs = {
    overlays = [
      inputs.neovim-nightly-overlay.overlays.default
    ];
    config = {
      allowUnfree = true;
    };
  };

  home = {
    username = username;
    homeDirectory = "/home/${username}";

    # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
    stateVersion = "24.05";
  };

  imports = [
    ./modules/bash.nix
    ./modules/era.nix
  ];

  programs.home-manager.enable = true;
}

でも行けた。勝手にsystemも渡してくれているらしい。era.nixで引数に...を追加するのを忘れずに。

airRnotairRnot

というか、programs.*.enable = tureにすれば、home.packagesに書かなくてもインストールしてくれるのか。

airRnotairRnot

現在のディレクトリ構造

.
├── flake.lock
├── flake.nix
└── home
    ├── home.nix
    ├── modules
    │   ├── bash.nix
    │   ├── era.nix
    │   ├── eza.nix
    │   ├── fd.nix
    │   ├── genact.nix
    │   ├── git.nix
    │   ├── neovim.nix
    │   ├── nix.nix
    │   ├── oh-my-posh.nix
    │   └── tree.nix
    └── options.nix

次のステップ

現状はmodules直下にすべてを詰め込んでいるので、カテゴリ分けをしたい。カテゴリの粒度は慎重に決めていきたい。

airRnotairRnot

カテゴリー案

  • shell
    • bashとかzshとかoh-my-poshとか
  • dev
    • プログラミング言語系。languageにするかは迷いどころ
  • misc
    • genactとかのおまけツール
airRnotairRnot

PATHを追加する

ところで、bashをenableにすると、PATHから~/.local/binが消滅してしまい、chezmoiが使えなくなる。よって、自分でPATHを通す必要がある。
home.sessionPathにPATHを書くと、PATHを通してくれる。

./home/home.nix
{
  inputs,
  lib,
  config,
  pkgs,
  system,
  mkPnpmPackages,
  ...
}:
let
  inherit (import ./options.nix) username;
in
{
  nixpkgs = {
    overlays = [
      inputs.neovim-nightly-overlay.overlays.default
    ];
    config = {
      allowUnfree = true;
    };
  };

  home = {
    username = username;
    homeDirectory = "/home/${username}";

    sessionPath = [ "$HOME/.local/bin" ];

    # https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
    stateVersion = "24.05";

  };

  imports = [
    ./modules/bash.nix
    ./modules/era.nix
    ./modules/eza.nix
    ./modules/fd.nix
    ./modules/genact.nix
    ./modules/git.nix
    ./modules/neovim.nix
    ./modules/nix.nix
    ./modules/oh-my-posh.nix
    ./modules/tree.nix
  ];

  programs.home-manager.enable = true;
}

airRnotairRnot

NPM Packages

npmでグローバルインストールするパッケージたちもNixの管理下に置きたい。
ここに含まれているものなら、

home.packages = with pkgs; [
    nodePackages.${パッケージ名}
];

でインストールできるのだが、含まれていないものは自分でどうにかする必要がある。今回はaicommitsをインストールしたい。

こちらの設定を参考に、以下のように設定を書いた。

./home/modules/aicommits.nix
{
  pkgs,
  ...
}:
let
  nodejs = pkgs.nodejs;
  pnpm = pkgs.pnpm;
in
{
  home.packages = with pkgs; [
    (pkgs.stdenv.mkDerivation rec {
      pname = "aicommits";
      version = "1.11.0";
      src = fetchFromGitHub {
        owner = "Nutlope";
        repo = "aicommits";
        rev = "604def8284361b8827087350fe6fcb6d9e2de836";
        hash = "sha256-JWZywM/pJNG2HbIuM8jqOVEMomvFmLnZjmkJfy9M1j8=";
      };
      nativeBuildInputs = [
        nodejs
        pnpm.configHook
        makeWrapper
      ];
      pnpmDeps = pnpm.fetchDeps {
        inherit pname version src;
        hash = "sha256-uRCQOdF2Lki3e71hMq4vDFp1921+0Ety/T+WsUmoxGA=";
      };
      buildPhase = ''
        runHook preBuild

        pnpm build

        runHook postBuild
      '';
      installPhase = ''
        runHook preInstall

        mkdir -p $out/{lib,bin}
        cp -r {node_modules,dist} $out/lib

        makeWrapper $out/lib/dist/cli.mjs $out/bin/aicommits

        runHook postInstall
      '';
    })
  ];
}

流れとしては、fetchFromGitHubでソースを持ってきて、pnpmでbuildするというもの。
正直中身はよくわかってない。具体的にはpnpm.fetchDepsで指定するhashとmakeWrapperとは何かという部分。

airRnotairRnot

色々整理

意地のモジュール化を行い、現在のディレクトリ構造は以下の通り。

.
├── flake.lock
├── flake.nix
└── home
    ├── home.nix
    ├── modules
    │   ├── default.nix
    │   ├── dev
    │   │   ├── default.nix
    │   │   ├── languages
    │   │   │   ├── default.nix
    │   │   │   ├── deno.nix
    │   │   │   ├── erlang.nix
    │   │   │   ├── gleam.nix
    │   │   │   ├── go.nix
    │   │   │   ├── node.nix
    │   │   │   └── python.nix
    │   │   ├── lsps
    │   │   │   ├── bash-language-server.nix
    │   │   │   └── default.nix
    │   │   └── tools
    │   │       ├── cmake.nix
    │   │       ├── default.nix
    │   │       ├── docker.nix
    │   │       ├── gcc.nix
    │   │       └── pnpm.nix
    │   ├── editor
    │   │   ├── default.nix
    │   │   └── neovim.nix
    │   ├── git
    │   │   ├── aicommits.nix
    │   │   ├── default.nix
    │   │   ├── git.nix
    │   │   └── lazygit.nix
    │   ├── nix
    │   │   ├── default.nix
    │   │   └── nixfmt-rfc-style.nix
    │   ├── shell
    │   │   ├── bash.nix
    │   │   ├── default.nix
    │   │   └── oh-my-posh.nix
    │   └── util
    │       ├── curl.nix
    │       ├── default.nix
    │       ├── era.nix
    │       ├── eza.nix
    │       ├── fd.nix
    │       ├── genact.nix
    │       ├── tree.nix
    │       ├── unzip.nix
    │       └── wget.nix
    └── options.nix

airRnotairRnot

aicommits用のopenaiKeyを直接書きたくないので、機密情報を含むやつはchezmoiに任せる方針
本当はusernameとかも書きたくないけど、流石に不便なのでそこは妥協。

airRnotairRnot

一段落

とりあえず形にはなったのでdotfilesを更新。

airRnotairRnot

その後もなんやかんやあってひとまず完成!