Featured image of post nix-shellでnodejsの切り替えを実現する

nix-shellでnodejsの切り替えを実現する

dotfilesをNix+home-managerでの管理へ移行するのをきっかけに、各開発環境の管理もNixへの移行を試行しています。

あけましておめでとうございます。 2023年気持ちも新たに、まずはdotfilesの見直しから開始しました。 これまでは、Emacs以外の fish や git
Nixとhome-managerにdotfiles管理を移行する

ここでは、JavaScriptのプロジェクト環境のためにnodejs/npmの環境を構築した際にやったことをまとめておきたいと思います。

JavaScriptの開発環境では、プロジェクト毎にnodeの要求versionが違うことがほとんどです。 nodejs環境の切り替えはnvmnodebrew、最近だとvoltaなどで行うのが一般的かと思います。 ちなみに自分は nodebrew を利用しています。

今回は、せっかくNixへ移行するので nix-shell による環境の切り替えで実現したいと思います。

なお、ここでの環境構築はLinuxではなく、MacOS上で実施しています。

nix-shellによる仮想開発環境

nix-shell はNixに付属しているコマンドです。 必要な依存derivationを構築して指定されたpathに配置した上で、interactive shellを起動してくれます。

このとき、これらの配置されたパッケージは実際のPATH配下のディレクトリにインストールされず、起動されたシェルだけで利用できるようになっています。 これはNixの仕組みによるものです。詳しくは、公式ドキュメントあたりを参照ください。

この仕組みを使うことで、再現可能な開発環境を構築することができます。

ちなみに同じようなツールに、Nix Flakesがあります。 こちらはNixの次世代パッケージ管理ツールで、依存パッケージのバージョンを固定するなどの機能があるようですが、まだあまり調査できていないです。 将来乗り替えていくかもしれません。

もうひとつdevboxがあります。先日v0.2がリリースされ、Nixのインストールも統合されたようです。 こちらはjetpack.io社が開発するNixの上で動作する開発環境構築ツールです。 JSONで依存関係を記述でき、環境をDodkerfileに書き出せることが特徴的な違いのようでした。

なお nix-shell を開発環境として利用するための情報は、公式WikiのDevelopment environment with nix-shellに詳しいです。

shell.nixで依存を記述する

例えば、Reactのプロジェクトでnodejs 18系とyarnを使い、TypeScriptで開発するために以下のようなパッケージが必要だとします。

  • nodejs 18.x/npm
  • yarn
  • typescript
  • typescript-language-server

これを shell.nix というファイルに記述して、プロジェクトルートなどに配置しておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let pkgs = import <nixpkgs> {};

in pkgs.mkShell rec {
  name = "devenv";

  buildInputs = with pkgs; [
    nodejs-18_x
    yarn
    nodePackages.typescript
    nodePackages.typescript-language-server
  ];
}

同ディレクトリで nix-shell コマンドを実行すると、ここに記載されているパッケージを配置してシェルを起動してくれます。 このようなプロジェクト毎の環境をそれぞれ用意しておくことで、開発時に nix-shell することで適宜必要な環境で開発をすることができるようになります。

古いnodejsのpackage作成

2023/1/7現在、nodejsのパッケージとしてNixpkgsで配布されているのはv14.x系より上のversionです。 そのため、これより古いv12やv10などはNixpkgsで取得できません。 従って、これらのパッケージは自分で補完する必要があります。

nodejsのソースを取得してビルドする

これは、Nixpkgsが実際に配布するときに利用している方法です。 これをそのまま再利用してローカルで必要なパッケージをビルドします。

以下の記事にその方法が紹介されていたので、参考にします。

ここでは複数のプロジェクトでの再利用を簡単にするために、 shell.nix に直接記述せずに ./config/nix/config.nix に記述することにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let pkgs = import <nixpkgs> {};

    buildNodejs = pkgs.callPackage <nixpkgs/pkgs/development/web/nodejs/nodejs.nix> {};

in {
  packageOverrides = pkgs: with pkgs; {
    nodejs-12_x = buildNodejs {
      enableNpm = true;
      version = "12.22.9";
      sha256 = "0jp2fdl73zj5lqjvw98i8pcf7m05cvjcab231zjvdhl4wl1jr66s";
    };
  };
  permittedInsecurePackages = [
    "nodejs-12.22.9"
  ];
}

packageOverridesnodejs-12_x というパッケージを追加します。上記ブログで紹介されていた、Nixpkgsの nodejs.nix を再利用しています。

ここで、 nix-env すると、追加したパッケージが確認できます。

1
2
3
4
$ nix-env -qaP '.*nodejs.*'
..
nixpkgs.nodejs-12                               nodejs-12.22.9
..

あとは、 shell.nix でこのパッケージを利用するだけです。このとき yarn で利用するnodejsを指定するようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let pkgs = import <nixpkgs> {};

in pkgs.mkShell rec {
  name = "devenv";

  buildInputs = with pkgs; [
    nodejs-12_x
    (yarn.override { nodejs = nodejs-12_x; })
    pkgs.nodePackages.typescript
    pkgs.nodePackages.typescript-language-server
  ];
}

ここで nix-shell を実行すると、最初はnodeのビルドが走ります。一度ビルドされればcacheから利用されるようになります。

nodejsのビルド済みバイナリを取得する

一方でnodejsはビルド済みバイナリも配布されているので、時短やビルドエラーのためにそちらを利用したいケースもあるでしょう。

こちらも、以下の記事でバイナリを利用する方法が紹介されていたので参考にします。

Nix recipe to setup nodejs in a cross platform virtual nix environment
Nix Recipe: Setup Nodejs

nodejs.nix というファイルを用意して、以下のように記述します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{ nixpkgs ? import <nixpkgs> {}, version, sha256 }:

let
  inherit (nixpkgs) lib stdenv autoPatchelfHook fetchurl patchelf;
  inherit (stdenv) mkDerivation;

in
mkDerivation {
  inherit version;

  name = "nodejs-${version}-binary";
  src = fetchurl {
    url = "https://nodejs.org/dist/v${version}/node-v${version}${if stdenv.isDarwin then "-darwin-x64" else "-linux-x64"}.tar.xz"; # this darwin/linux check doesn't work since sha is different for packages
    inherit sha256;
  };

  buildInputs = with nixpkgs; lib.optional stdenv.isLinux [ patchelf ];
  nativeBuildInputs = with nixpkgs; lib.optional stdenv.isLinux [ autoPatchelfHook ];

  installPhase = ''
  echo "installing nodejs"
  mkdir -p $out
  cp -r ./ $out/
  '';

  meta = with lib; {
    description = "Event-driven I/O framework for the V8 JavaScript engine";
    homepage = "https://nodejs.org";
    license = licenses.mit;
  };

}

元記事のコードではビルドに必要になるツールを指定していますが、これはバイナリ配布物なので実際はtarを解凍して配置するだけです。 なので、不要な依存は削除しています。

また、 stdenv.lib は現在利用している22.11ではもう利用できなくなっているようなので、そちらも直接libを参照するように変更しています。 なお元のコードは20.03を利用しているのでそのままで動作します。NixOSのissueを参照すると21.x系ではもうdeperecatedになっているようです。

この nodejs.nix を先のソースコード提供の例と同じように .config/nix/config.nix から利用してパッケージを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let pkgs = import <nixpkgs> {};

in {
  packageOverrides = pkgs: with pkgs; {
    nodejs-10_x-binary = pkgs.callPackage ./nodejs.nix {
      version = "10.24.1";
      sha256 = "1nh2a97c023y590psi9sg8mnbbm5mqfvdnscfs7dw4vm1h3fbnlf";
    };
  };
}

あとは、上記の利用例と同じです。 shell.nix から nodejs-10_x-binary で利用できます。

direnvとfishの設定

ここまでで必要な実行環境はできていると思いますが、利用しているエディタやシェルとの統合に関する設定も必要です。

実は、 nix-shell はbashしか利用できません。 私はインタラクティブシェルとしては普段fishを利用しているので、できればfishを使いたいところです。

先に紹介した公式WikiのDevelopment environment with nix-shellでも触れられていますが、これを回避するためにdirenvを利用する方法があります。 またdirenvを利用することで、EmacsからLSPの利用を行うこともできるようになります。

direnvの設定とシェル

まずはdirenvのinstallを以下公式のガイドの通りに実施します。fish向けのhookの設定まで行います。

続いてfish向けのhookの設定を行います。 これは上記のWikiの通り、direnv公式のNix integrationの設定なのですが、以下の設定を記述した .envrc を用意します。

1
use_nix

あとは $direnv allow . とすることでfish環境にいながら、配置した shell.nix の内容をそのディレクトリでだけ反映することができます。 以降は cd でそのディレクトリに入るだけで shell.nix の環境に切りかわるようになります。もちろん他のディレクトリに出れば元に戻ります。

まとめ

Nixを利用して、JavaScriptプロジェクトの開発環境を管理する方法についてまとめてみました。 特に古いversionを用意するところが少し手間ですが、一度用意してしまえばそれほど面倒ではないかなと思います。

もしNixをチーム全体で共有できれば、全員で同じ環境を再現することも可能である、というメリットもあると思います。 その場合versionの固定なども検討したいところなので、Flakesの調査も進めたいところです。

Built with Hugo
テーマ StackJimmy によって設計されています。