Nodeのバージョンを上げたらnode-sassがエラーを吐いたのでコードリーディングしてみた

起きた事象

フロントエンドのビルド環境としてNode v8で動いていたプロジェクトをNode v10にアップデートしました。その結果、次のような Node Sass could not find a binding for your current environment というエラーメッセージを吐いて落ちました。

どうやら使用していたモジュールのnode-sassでエラーが起きたようです。

Error: Missing binding /path/to/project/node_modules/node-sass/vendor/darwin-x64-64/binding.node
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 10.x

Found bindings for the following environments:
  - OS X 64-bit with Node.js 8.x

This usually happens because your environment has changed since running `npm install`.
Run `npm rebuild node-sass` to download the binding for your current environment.
    at module.exports (/path/to/project/node_modules/node-sass/lib/binding.js:15:13)
    at Object.<anonymous> (/path/to/project/node_modules/node-sass/lib/index.js:14:35)
    at Module._compile (internal/modules/cjs/loader.js:778:30)

解決方法

エラーメッセージを読むと書いてありますが、次のコマンドをたたけば動きました。

$ npm rebuild node-sass

おしまい。

始めに

こんにちは。サービスグループの武田です。

今回遭遇したエラーは上記のとおり、コマンド一発たたけば解決しました。ただこれで終わってしまうのもなんですので、もう少し原因周りを調べてみました。

検証環境

次の環境で検証をしました。Node v8でnode-sassをインストールし、そのあとNode v10に切り替えると再現するはずです。

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.5
BuildVersion:	18F132

$ node -v
v8.16.1

# switch node version
$ node -v
v10.16.3

エラーメッセージから探る原因

エラーはnode_modules/node-sass/vendor/darwin-x64-64/binding.nodeが見つからないというメッセージでした。ディレクトリを確認してみます。

$ tree node_modules/node-sass/vendor/
node_modules/node-sass/vendor/
└── darwin-x64-57
    └── binding.node

1 directory, 1 file

たしかにdarwin-x64-64ではなくdarwin-x64-57でした。darwin-x64はわかります。OSプラットフォームとアーキテクチャです。では最後の57や64とはなんでしょうか。わからないならソースを見よう、ということで確認しました。

最初に掲載したスタックトレースから、最後に実行されたのはnode-sass/lib/binding.js:15ということがわかります。

  if (!ext.hasBinary(ext.getBinaryPath())) {
    if (!ext.isSupportedEnvironment()) {
      throw new Error(errors.unsupportedEnvironment());
    } else {
      throw new Error(errors.missingBinary());
    }
  }

node-sass/binding.js at v4.12.0 · sass/node-sass

15行目は単にErrorを投げているだけですし、12行目のif文はエラー種別の分岐をしているだけです。ということは直前の!ext.hasBinary(ext.getBinaryPath())がtrueを返していることが原因のようです。このgetBinaryPath()が怪しいですね。次にこれを確認してみましょう(ちなみにhasBinary()はファイルの存在チェックをしているだけです)。

function getBinaryPath() {
  var binaryPath;

  if (getArgument('--sass-binary-path')) {
    binaryPath = getArgument('--sass-binary-path');
  } else if (process.env.SASS_BINARY_PATH) {
    binaryPath = process.env.SASS_BINARY_PATH;
  } else if (process.env.npm_config_sass_binary_path) {
    binaryPath = process.env.npm_config_sass_binary_path;
  } else if (pkg.nodeSassConfig && pkg.nodeSassConfig.binaryPath) {
    binaryPath = pkg.nodeSassConfig.binaryPath;
  } else {
    binaryPath = path.join(getBinaryDir(), getBinaryName().replace(/_(?=binding\.node)/, '/'));
  }

node-sass/extensions.js at v4.12.0 · sass/node-sass

注目すべきは308行目のgetBinaryName().replace(/_(?=binding\.node)/, '/')です。replaceメソッドはxxx_binding.nodexxx/binding.nodeに置換している部分で、かなりそれっぽい処理をしています。ではgetBinaryName()が何をしているか、ですが、次のような処理をこの関数内で行っています。

    binaryName = [
      platform, '-',
      process.arch, '-',
      process.versions.modules
    ].join('');
  }

  return [binaryName, 'binding.node'].join('_');
}

platformprocess.archprocess.versions.modules-で結合し、最後に_binding.nodeを結合しています。どうやら57や64という数字はここから来ていたようです。

process.versions.modulesとは

公式ドキュメントを参照します。

Process | Node.js v10.16.3 Documentation

ドキュメントによると、process.versions.modulescurrent ABI version を示す値のようです。ABIとはApplication Binary Interfaceのことで、このバージョンによってC++アドオンを再コンパイルすることなくロード可能かをチェックするようです。

ではこのバージョンがどこで確認できるかというと、リリース一覧のページにありました *1

Previous Releases | Node.js

NODE_MODULE_VERSION列がそうです。Node.js 10.16.3は64、Node.js 8.16.1は57ということが確認でき、今回の検証環境と一致します。

rebuildの結果を確認

さて、冒頭の解決策でも記載したrebuildの結果をあらためて確認してみましょう。

$ node -v
v10.16.3

$ node -p 'process.versions.modules'
64

$ npm rebuild node-sass

$ tree node_modules/node-sass/vendor/
node_modules/node-sass/vendor/
├── darwin-x64-57
│   └── binding.node
└── darwin-x64-64
    └── binding.node

2 directories, 2 files

なるほど、rebuildすることで対応したnode-sassのバイナリが生成されていることが確認できました。

まとめ

node-sassはプラットフォームおよびNodeのバージョンに合わせてバイナリを生成します。そのため、環境が変われば新しい環境用にバイナリを用意する必要があります。そのためのrebuildだったわけですね。

Nodeの環境について詳しい方なら常識だったでしょうか。また少し理解が深まりました。

おまけ

同じくnode-sassのエラーで Error: Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime (64) の場合はrebuildでは解決しません。使用しているモジュールのバージョンがサポートしている環境を確認し、アップデートで解決するようであればアップデートしましょう。

Releases · sass/node-sass

脚注

  1. ちなみにこのURLもソースコードに書いてありました。node/node_version.h at v10.16.3 · nodejs/node