Nixでグローバルツール問題を根絶する:asdfユーザーが知るべき3つの概念


TL;DR

asdf/nvmでNodeバージョンを切り替えるとnpm i -gで入れたツールが消える。これは「ツールのインストール先がランタイムのバージョンディレクトリ内にある」という構造的問題で、pipも同じ。Nixはツールを依存ごと/nix/storeに閉じ込めることで、この問題をそもそも発生させない。覚えるべき概念は Store / Profile / nixpkgs の3つだけ。

問題:なぜ prettier が消えるのか

asdf/nvm/nodenvを使っている開発者なら、一度は経験があるはずだ。

asdf global nodejs 20.0.0
npm i -g prettier
prettier --version  # 動く

asdf global nodejs 22.0.0
prettier --version  # command not found

prettierは消えていない。見えなくなっただけだ。

npm i -g~/.asdf/installs/nodejs/20.0.0/lib/node_modules/に書き込む。バージョンを22に切り替えると、PATHのシンボリンクが22.0.0/に向き直るので、20.0.0/内のprettierはPATHから外れる。

~/.asdf/installs/nodejs/
  20.0.0/
    lib/node_modules/
      prettier/  ← ここにある
  22.0.0/
    lib/node_modules/
      (空)       ← PATHはこっちを見ている

これはnpm固有の問題ではない。pipも全く同じ構造を持つ。asdfでPythonバージョンを切り替えれば、pip installで入れたblackやruffも見えなくなる。

問題の本質は**「CLIツールのインストール先が、言語ランタイムのバージョンディレクトリの中にある」**ことだ。バージョンを切り替える=箱ごと取り替えるので、箱の中のツールも一緒に消える。

flowchart LR
    subgraph asdf["asdf (バージョン管理)"]
        N20["Node 20<br/>prettier ✓"]
        N22["Node 22<br/>(空)"]
    end
    Switch["asdf global nodejs 22"]
    N20 -->|切替| Switch
    Switch --> N22

解決策は明確だ。ツールを箱の外に出す

既存のアプローチとその限界

この問題に対する既存のアプローチはいくつかある。

アプローチ仕組み限界
~/.default-npm-packagesNode インストール時に自動で npm i -gバージョンごとに再インストールが走る。pip側は未対応
npx / pipxグローバルインストールを避ける頻繁に使うツールだと毎回のダウンロードが煩わしい
Homebrew言語ランタイムと独立してインストールパッケージがない場合やバージョンが古い場合がある
miseasdf互換で改善されたツール管理レジストリにないツールへの対応が限定的
VoltaグローバルツールをNodeバージョンから独立管理Node専用。Python製ツールはカバーしない

どれも部分的には解決するが、言語を問わず、完全に隔離された形でCLIツールを管理するという要求を満たすものは少ない。

ここでNixが登場する。

Nixの3つの概念

Nixには膨大な概念体系があるが、「グローバルCLIツールを言語ランタイムから独立させる」という用途に必要な知識は3つだけだ。Nix言語もflake.nixもNixOSも、今は全部無視していい。

1. Nix Store — すべてを閉じ込める倉庫

Nixの核心は/nix/storeという特殊なディレクトリにある。すべてのパッケージがハッシュ付きのユニークなパスに格納される。

/nix/store/abc123...-prettier-3.2.0/bin/prettier
/nix/store/def456...-nodejs-22.1.0/bin/node

重要なのは、このprettierが依存するnodeは上のdef456...-nodejs-22.1.0ハードコードされているということだ。asdfが~/.asdf/shims/nodeをどこに向けていようが、Nix Store内のprettierには一切関係がない。

これが「隔離が完全」と言われる理由だ。従来のnpm i -g$(asdf where nodejs)/lib/node_modules/に書き込むのに対し、Nixはそもそもこの仕組みの外にいる。

flowchart TB
    subgraph store["/nix/store"]
        P["prettier-3.2.0<br/>(同梱: nodejs-22.1.0)"]
        B["black-24.4.0<br/>(同梱: python-3.12.0)"]
        R["ruff-0.5.0"]
        J["jq-1.7"]
    end
    subgraph asdf["asdf (別レイヤー)"]
        AN["Node 20 / 22"]
        AP["Python 3.11 / 3.12"]
    end
    store -.-|"互いに干渉しない"| asdf

prettier の中に Node が入っている。black の中に Python が入っている。だから asdf 側で何を切り替えようと、Store 内のツールは影響を受けない。

2. Profile — 今使いたいものリスト

Store に入れただけでは PATH が通っていない。ここで Profile の出番だ。

~/.nix-profile/bin//nix/store 内のバイナリへのシンボリンクの集合体で、PATHに入っている。nix profile installするとリンクが追加され、nix profile removeすると削除される。

~/.nix-profile/bin/prettier → /nix/store/abc123...-prettier-3.2.0/bin/prettier
~/.nix-profile/bin/black    → /nix/store/def456...-black-24.4.0/bin/black
~/.nix-profile/bin/ruff     → /nix/store/ghi789...-ruff-0.5.0/bin/ruff

Store が倉庫だとすれば、Profile はショーケースだ。倉庫には大量のものが入っているが、ショーケースには「今使いたいもの」だけが並んでいる。

3. nixpkgs — 言語を問わない巨大レジストリ

パッケージをどこから持ってくるか。nixpkgsという10万以上のパッケージ定義を持つ単一のGitリポジトリがある。

npmレジストリやPyPIに相当するものだが、決定的な違いは言語を問わないこと。Node製のprettierも、Python製のblackも、Rust製のripgrepも、Go製のghも、すべて同じレジストリから同じコマンドでインストールできる。

nix profile install nixpkgs#prettier   # Node製
nix profile install nixpkgs#black      # Python製
nix profile install nixpkgs#ripgrep    # Rust製
nix profile install nixpkgs#gh         # Go製

パッケージの検索はsearch.nixos.orgが最も手軽だ。名前で検索してPackagesタブで絞り込めば一発で見つかる。nix search nixpkgs prettierのようにCLIでも探せるが、非推奨パッケージの警告やvimプラグイン等の無関係なヒットが大量に混じるため、実用的にはnix search nixpkgs prettier 2>/dev/null | grep -E '^\* .*\.prettier 'のようにフィルタが必要になる。パッケージ名の確認はWeb、インストールはCLIという使い分けが一番ストレスがない。メジャーなCLIツールはほぼ揃っている。

3つの概念の関係

ここまでの3概念を整理するとこうなる。

flowchart LR
    NP["nixpkgs<br/>(レジストリ)"]
    NS["/nix/store<br/>(倉庫)"]
    PR["~/.nix-profile/bin/<br/>(ショーケース)"]
    U["ユーザーの PATH"]
    NP -->|"nix profile install"| NS
    NS -->|"シンボリンク"| PR
    PR -->|"PATH に含まれる"| U
  1. nixpkgs からパッケージ定義を取得し、ビルド済みバイナリ(またはソースビルド結果)を Store に格納
  2. Profile が Store 内のバイナリへのシンボリンクを管理
  3. Profile の bin/ が PATH に入っているので、ユーザーはツールを直接実行できる

実践:asdfとNixの棲み分け

概念がわかったところで、実際の運用に移ろう。

インストール

Determinate Systemsのインストーラを使う。公式インストーラより設定が楽で、最初からflakesとnix-commandが有効になる。

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

シェルを再起動して nix --version が通れば完了。

日常コマンド

覚えるべきコマンドは6つだけだ。

# ツールを探す(search.nixos.org が楽。CLI なら↓)
nix search nixpkgs prettier 2>/dev/null

# インストール
nix profile install nixpkgs#prettier

# 何が入っているか確認
nix profile list

# 全パッケージを最新に更新
nix profile upgrade --all

# 一時実行(npx / pipx run 相当)
nix run nixpkgs#cowsay -- "hello"

# 掃除
nix profile wipe-history && nix store gc

棲み分けのルール

運用のルールはシンプルだ。

asdf  → 開発用ランタイム(Node, Python)
         プロジェクトごとに .tool-versions で切替

Nix   → CLIツール(prettier, black, ruff, awscli, jq...)
         言語バージョンに依存させない

この2つは完全に別レイヤーとして動く。同じツール名をasdfとNixの両方に入れなければ、PATHの衝突も起きない。asdfを捨てる必要はないし、Nixに全部移行する必要もない。

知らなくていいこと

最後に、この用途では不要な知識を明示しておく。「Nixは学習コストが高い」とよく言われるが、それはNixのすべてを学ぼうとするからだ。

概念なぜ不要か
Nix言語パッケージを自作しない限り書かない
Flakes / flake.nixプロジェクト単位の環境管理に必要。グローバルツール管理だけなら意識不要
Derivationパッケージのビルドレシピ。使う側は中身を知る必要なし
home-managerdotfilesの宣言的管理。別レイヤーの話
NixOSLinuxディストロとしてのNix。macOSユーザーには無関係
overlay / overrideパッケージのカスタマイズ。今回のユースケースでは出番なし

Store、Profile、nixpkgs。この3つだけ覚えておけば、グローバルCLIツールの管理は完全に回る。深く学びたくなったらflakeやdevShellが待っているが、それは「必要になったとき」でいい。

以上。