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-packages | Node インストール時に自動で npm i -g | バージョンごとに再インストールが走る。pip側は未対応 |
| npx / pipx | グローバルインストールを避ける | 頻繁に使うツールだと毎回のダウンロードが煩わしい |
| Homebrew | 言語ランタイムと独立してインストール | パッケージがない場合やバージョンが古い場合がある |
| mise | asdf互換で改善されたツール管理 | レジストリにないツールへの対応が限定的 |
| 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
- nixpkgs からパッケージ定義を取得し、ビルド済みバイナリ(またはソースビルド結果)を Store に格納
- Profile が Store 内のバイナリへのシンボリンクを管理
- 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-manager | dotfilesの宣言的管理。別レイヤーの話 |
| NixOS | LinuxディストロとしてのNix。macOSユーザーには無関係 |
| overlay / override | パッケージのカスタマイズ。今回のユースケースでは出番なし |
Store、Profile、nixpkgs。この3つだけ覚えておけば、グローバルCLIツールの管理は完全に回る。深く学びたくなったらflakeやdevShellが待っているが、それは「必要になったとき」でいい。
以上。