💧

aqua が Node.js をサポート

2024/09/01に公開

CLI ツールを YAML でバージョン管理できるツール aqua を開発しています。

https://zenn.dev/shunsuke_suzuki/books/aqua-handbook

本記事では aqua が Node.js のバージョン管理をサポートした件について紹介します。

https://aquaproj.github.io/docs/reference/nodejs-support/

aqua で Node.js を管理する方法

aqua-registry v4.216.0 以上が必要です。
aqua は最新じゃなくても大丈夫です。

nodejs/node を aqua.yaml に追加します。

aqua g -i nodejs/node
aqua i -l

これで node や npm コマンドは使えるようになります。

node -v
npm -v

npm i -g コマンドでツールをインストールするには、 npm の prefix config と、環境変数 PATH を設定します。
npm の prefix config は npm i -g でパッケージをインストールする際のインストール先です。
prefix config の設定方法は幾つかありますが、ここでは環境変数 NPM_CONFIG_PREFIX を設定します。
prefix config の値は何でも良いです。

export NPM_CONFIG_PREFIX=${XDG_DATA_HOME:-$HOME/.local/share}/npm-global # 値は好きに変更してください
export PATH=$NPM_CONFIG_PREFIX/bin:$PATH

実際に Node.js をインストールし、 npm で zx というツールをインストールしてみます。
勿論、 zx じゃなくても構いません。

npm i -g zx
command -v zx
zx -v

Node.js のバージョンを変更してみます。

aqua up node@v20.16.0
zx -v # Node.js のバージョンを変えても、以前のバージョンでインストールしたツールが実行できる
node -v
npm -v

以上、問題なく動くことが確認できたかと思います。

prefix config を指定しているため、 npm i -g でインストールしたツールが複数の Node.js のバージョンで共有されます。
これには Pros, Cons 両方あると思います。

Pros:

  • Node.js のバージョンを変更しても改めてツールをインストールし直す必要がない

Cons:

  • Node.js のバージョンの違いにより、別のバージョンでインストールしたツールがうまく動かない可能性がある

Cons に関してはこれがどの程度問題になるかは未知数です。
うまく動かないようであればツールをインストールし直す必要があるでしょう。
あまりに問題になるようだったら対策を考える必要があるかもしれませんが、現状は許容しています。

なぜ npm の prefix config が必要なのか

npm に関してはこちらのコメントも参照してください。

https://github.com/aquaproj/aqua/issues/2996#issuecomment-2304011654

npm install の global mode npm i -g は package を prefix directory にインストールし、実行ファイルは {prefix}/bin にリンクされます。
prefix config はデフォルトで Node.js のインストール先と同じであり、このディレクトリを $PATH に追加しない限り npm i -g でインストールしたツールを実行することができません。
Node.js のインストール先は Node.js のバージョンに依存するため、このディレクトリを $PATH に追加するには動的に $PATH を書き換える必要がありますが、現状 aqua は動的な $PATH の書き換えに対応していないため、 aqua は長らく Node.js のサポートをしてきませんでした。
では同じディレクトリにある nodenpm コマンドはなぜ実行できるかというと、これらのコマンドは aqua によって管理されており、 aqua が $(aqua root-dir)/bin 配下に symbolic link (Windows では hard link) を作成し、 aqua-proxy を経由して実行しているからです。

https://zenn.dev/shunsuke_suzuki/books/aqua-handbook/viewer/how-aqua-changes-tool-versions-dynamically

npm i -g でインストールしたツールは aqua で管理されていないため、 aqua が $(aqua root-dir)/bin 配下に link を作ることはできませんし、 link を作ったとしても aqua がインストール先を特定することはできないので結局実行することはできません。
なぜインストール先を特定できないかと言うと、 aqua は aqua.yaml に書かれたツールのバージョンと Registry の情報を元にツールのインストール先を特定しますが、 npm でインストールされたツールの情報は aqua.yaml や Registry にはないからです。

この問題を解決するために、動的な $PATH の書き換えを検討・実装しました。
しかし、この方法には動作環境(主に shell) に依存するという問題がありました。
動的に $PATH を書き換えるにはシェルの機能を使う必要があります。
例えば Zsh では hook function, Bash では $PROMPT_COMMAND を使う必要があり、シェルごとに実装を変える必要があります。
これは実装や検証・トラブルシューティング・問い合わせ対応を考えると非常にコストが大きいです。
当初この機能を実装するうえで direnv の実装を参考にしましたが、 direnv は非常に多くの shell に個別対応しています。 shell_*.go を見てみてください。

https://github.com/direnv/direnv/blob/68978efd9e0051bf9c4db10ac0e50afe0b3e7823/internal/cmd/shell_zsh.go

このような対応を aqua ではやりたくありませんでした。
Windows 対応も非常に面倒ですし、すべてのシェルがこれに対応しているとも限りません。
また、 interactive な shell 以外では動作しないという問題もあります。
例えばシェルスクリプト内でディレクトリを移動して Node.js のバージョンを切り替えても $PATH は書き換わってくれません。
そう考えると非常に微妙な対応だと言わざるを得ません。

そこで npm の設定で prefix config を指定し、 {prefix}/bin ディレクトリを $PATH に追加することでこの問題を解決しました。
この解決策の良いところは動作環境に依存しないことと aqua に全く手を入れる必要がなかったことです。
自分は npm には明るくなかったので preset config でインストール先を変えられるとか知らなかったのですが、 contributor の方が教えてくれました。
こういう自分の知らないことを知れるのは OSS の良いところと言えるでしょう。

Discussion