Nodeではchild_process.spawn()などで特定のコマンドを別プロセスで実行することができますが、前提としてそのコマンドが実行可能な状態でなければなりません。当該コマンドにPATHが通っているかどうかはnode-whichなどで確認できますし、ドキュメントに「事前に◯◯というプログラムをインストールしてください」とリクワイアメントを書いておけば利用者は対応できます。

しかし、できることなら利用者にそのような手間をかけさせたくはありません。npm installコマンドひとつでNodeプログラムに必要なファイルは全て揃ってほしいものです。ではどうするのかというと、npmのパッケージと同じように必要ならバイナリーもダウンロードしてしまえば良いのです。このような、一見強引ながら極めて合理的な解決策を提供するのがbin-wrapperです。

bin-wrapperを使う

bin-wrapper の基本的な役割は、以下の通りです。

  1. 指定されたURLからファイルをダウンロードし、
  2. ダウンロードしたファイルがアーカイブであれば展開し、
  3. 指定したファイル名のバイナリーを参照するパスを提供する

ほかにもバイナリーの実行やバージョン確認の機能を備えています。

今回はGithubを使いやすくするCLIツールhubをダウンロードして実行するまでをNodeでやってみましょう。hubにはコンパイル済みバイナリーを含んだtarファイルをダウンロードできるURLがあるので、それを利用します。

'use strict';

var path = require('path');

var BinWrapper = require('bin-wrapper');

var prefix = 'https://github.com/github/hub/releases/download/v2.2.0/hub-';
var suffix = '-2.2.0.tar.gz';

var bin = new BinWrapper();

// ダウンロードURLを指定
// OS、アーキテクチャごとに異なるURLを指定可能
bin.src(prefix + 'linux-386' + suffix, 'linux', 'x86');
bin.src(prefix + 'linux-amd64' + suffix, 'linux', 'x64');
bin.src(prefix + 'windows-386' + suffix, 'win32', 'x86');
bin.src(prefix + 'mac-amd64' + suffix, 'darwin');
bin.src(prefix + 'windows-386' + suffix, 'win32', 'x86');
bin.src(prefix + 'windows-amd64' + suffix, 'win32', 'x64');

// アーカイブの展開先を指定
bin.dest(path.resolve('vendor'));

// バイナリーとして使用するファイルの指定
bin.use(process.platform === 'win32' ? 'hub.exe' : 'hub');

// ダウンロード、アーカイブ展開の実行後、インストールの成否の確認。結果をコールバックに渡す
bin.run(function (err) {
  if (err) {
    throw err;
  }

  console.log('Installed to ' + bin.path());
});

bin-wrapperをインストール (npm install bin-wrapper) した後に上記スクリプトを実行してみましょう。Installed to (カレントディレクトリ)/hub(WindowsならInstalled to (カレントディレクトリ)\hub.exe)と表示されたら成功です。vendorディレクトリにファイルがダウンロードされているはずです。

$ ls vendor
LICENSE     README.md   etc     hub     man

ここまでで、hubのアーカイブのダウンロードと展開を行いました。当初の目的であるhubコマンドの実行までを行うには、.path()メソッドを利用します。

.path()メソッドは.dest()メソッドと.use()メソッドで指定したバイナリのパスを返します。このパスを使ってchild_process.exec()などでコマンドを実行すればいいのです。簡単にhubのバージョンを表示してみましょう。

// 先のコードの bin.run(function(err) { ... }) 内を書き換え

bin.run(function (err) {
  if (err) {
    throw err;
  }

  var exec = require('child_process').exec;

  exec(bin.path() + ' --version', function(err, stdout, stderr) {
    if (err) {
      throw err;
    }

    process.stdout.write(stdout);
    process.stderr.write(stderr);
  });
});

上記コードを実行すると、以下のようにhub --versionの結果が出力されます。

git version 2.3.0
hub version 2.2.0

以上、Nodeだけでプログラムに必要なバイナリーを取得し、それを実行することができました。このプログラムを実行するのに、事前にhubをインストールしておく必要はありません。

インストールスクリプトを分離する

上記スクリプトは、バイナリーが正常にダウンロードされているかどうかを実行時に毎回確認します。プログラムを何度も実行する上では無駄な処理と言えるでしょう。解決策としては、

  1. npm installの際にバイナリーを取得する
  2. プログラム実行時にはバイナリーの取得処理をせず、ダウンロード済みのバイナリーへのパスだけを提供する

というように、インストール時と実行時それぞれ別のスクリプトを用意することが挙げられます。そこで、package.jsonscriptsフィールドを利用します。

実例として、gifsicleのラッパーであるgifsicle-binのリポジトリが参考になります。

gifsicleバイナリーのインストールを行うlib/install.jsは、エントリーポイントであるindex.jsからはrequireされていません。代わりに、postinstallスクリプトで呼び出されるようpackage.jsonで指定されています

{
  "scripts": {
    "postinstall": "node lib/install.js"
  }
}

つまり、gifsicle-binパッケージがインストールされた直後に一度だけバイナリーのインストールが行われます。このようにインストール処理をpostinstallスクリプトとして切り離し、メインのスクリプトでは一切行わないのが、bin-wrapperを使う上でのベストプラクティスであると言えます。

更新の滞ったプロジェクトを引き継ぐ
sum-upの紹介記事の翻訳と、NodeのCLIユーティリティについて