ククログ

株式会社クリアコード > ククログ > Windowsの32bit/64bit版Ruby用バイナリ入りgemをDebian GNU/Linux上で作る方法

Windowsの32bit/64bit版Ruby用バイナリ入りgemをDebian GNU/Linux上で作る方法

2013年10月時点での情報です。長いです。

Windowsの32bit/64bit版Ruby用のバイナリ入りgemをDebian GNU/Linux上で作る方法を紹介します。

背景

どうしてこの方法を紹介するのか、その動機を伝えるために背景を説明します。

Rubyから簡単に使えるGroongaという高性能な全文検索エンジンがあります。Groongaを簡単に使えるのはRroongaというgemがあるからです。Groongaは主にC言語で書かれているためWindowsでインストールするのは大変です。しかし、RroongaはWindows用のバイナリ入りのgem(作り方)も配布しているのでWindowsでも簡単に使うことができます。

Groongaは64bit環境を想定した設計になっていて、32bit環境では(主にサイズ面で)いくつか制限があります。そのため、Windows上でも64bit環境で簡単にGroongaを使えるように64bit環境用にビルドしたバイナリを配布しています。

ただし、RubyからGroongaを使う場合はこの恩恵を受けられませんでした。なぜなら、64bit版Ruby用のRroongaのバイナリが配布されていなかったからです1

どうして64bit版Ruby用のバイナリが配布されていなかったのか。それはMinGWでビルドされた64bit版Rubyのパッケージがなかったからです。そのため、まずはMinGWでビルドされた64bi版Rubyのパッケージを作ることからはじめました。これが2年前の話です。このときの成果が直接取り込まれたわけではありませんが、その後、RubyInstaller for WindowsはRuby 2.0.0用のパッケージから64bit版も配布するようになりました。8ヶ月前の話です。

MinGWでビルドされた64bit版Rubyのパッケージができたので、あとはRroongaの64bit版バイナリをDebian GNU/Linux上でビルドするだけです。これをするためにはrake-compiler 0.9.0の登場を待つ必要がありました。rake-compiler 0.9.0からWindowsの64bit版Rubyのサポートが始まったのです。rake-compiler 0.9.0の登場はWindowsの64bit版Rubyパッケージがリリースされてから半年後の今年の8月です。

これでWindowsの64bit版Ruby用のバイナリ入りのgemをリリースする準備が整いました。rake-compiler 0.9.0が登場した次の月、Rroonga 3.0.8はWindowsの32bit版Ruby用のバイナリ入りgemだけではなく64bit版Ruby用のバイナリ入りgemもリリースしました。

前置きがだいぶ長くなりましたが、Windowsの32bit/64bit版Ruby用のバイナリ入りgemをDebian GNU/Linux上で作る方法を紹介します。

仕組み

まず仕組みを説明します。次のように順を追って仕組みを説明します。

  1. バイナリ入りgemの仕組み

  2. 複数バージョンのRuby(1.9.3と2.0.0とか)に対応したバイナリ入りgemの仕組み

  3. 32bit版用と64bit版用のgemを提供する仕組み

バイナリ入りgemの仕組み

まず、バイナリ入りgemの仕組みについて説明します。

バイナリ入りgemを作る時のポイントは次の2点です。

  • gemの中に.so(バイナリ)を入れる
  • gemにextconf.rbを登録しない

「gemの中に.soを入れる」というのはGem::Specification#filesに.soを入れるということです。これを忘れるとバイナリが入っていないgemになってしまいます。そのため、忘れてはいけないとても大切なことです。

なお、.soを入れる前に.soを作る必要があります。Windows用のバイナリ入りgemをDebian GNU/Linux上で作るときはWindows用の.soをクロスコンパイルしなければいけません。

「gemにextconf.rbを登録しない」というのはGem::Specification#extensionsにextconf.rbを入れないということです。これを忘れるとせっかくバイナリがgemの中に入っているのにインストール時にバイナリをビルドしようとします。これではバイナリを入れている意味がありません。

クロスコンパイルも含めてこれら2つのことをやってくれるのがrake-copmilerというgem2です。rake-compilerを使うと、Rakefileに次のように書くだけでこれらのことをやってくれます。

require "rake/extensiontask"
# specはGem::Specification
Rake::ExtensionTask.new(spec.name, spec) do |ext|
  ext.cross_compile = true
end

便利ですね。

rake-compilerの準備(後述)が済んでいれば、次のコマンドでWindowsの32bit版Ruby用のgemができます。

% rake RUBY_CC_VERSION=1.9.3:2.0.0:2.1.0 cross clean compile native gem

fat_gem_sampleというサンプル拡張ライブラリを用意しました。このサンプルを使うとすぐに試すことができます。次のコマンドでバイナリ入りgemができます。

% git clone https://github.com/kou/fat_gem_sample.git
% cd fat_gem_sample
% rake RUBY_CC_VERSION=1.9.3:2.0.0:2.1.0 cross clean compile native gem

pkg/fat_gem_sample-1.0.0-x86-mingw32.gemができているはずです。

複数バージョンのRubyに対応したバイナリ入りgemの仕組み

次に、複数バージョンのRubyに対応したバイナリ入りgemの仕組みについて説明します。

gemに.soを入れるのは単にGem::Specification#filesの中にファイルを1つ追加するだけです。そのため、1つのgemに複数の.soを入れることは簡単なことです。

複数バージョンのRubyに対応したバイナリ入りgemを作る時のポイントは次の2点です。

  • それぞれのRuby用の.soを入れる
  • requireされたときに対応するRuby用の.soだけを読み込む

「それぞれのRuby用の.soを入れる」というのはそれぞれのRuby用の.soをクロスコンパイルして、できた.soをすべてGem::Specification#filesに入れるということです。これは前述のrake-compilerがやってくれます。Rakefileの内容も前述のままで大丈夫です。

requireされたときに対応するRuby用の.soだけを読み込む」というのはRUBY_VERSIONを見て読み込む.soを変えるということです。具体的には次のようにします。

# lib/groonga.rb
begin
  major, minor, _ = RUBY_VERSION.split(/\./)
  require "#{major}.#{minor}/groonga.so"
rescue LoadError
  require "groonga.so"
end

これで、require "groonga"とすると使っているRubyのバージョン用の.soが読み込まれます。rescue LoadErrorしている部分は、バイナリ入りgemを使っているケース用の処理ではなく、通常のケース用の処理です。通常のケースとはバイナリの入っていないgemをインストールしているケースのことです。このときはインストール時に自分でビルドします。

requireしているパスについて少し補足します。rake-compilerはビルドした.soをlib/1.9/lib/2.0/のようにlib/#{メジャーバージョン}.#{マイナーバージョン}/以下のディレクトリに置きます。そのため、require "#{major}.#{minor}/groonga.so"と指定しています。

なお、このように1つのgemに複数バージョンのRuby用のバイナリが入っているgemをfat gemと呼びます。

32bit版用と64bit版用のgemを提供する仕組み

最後に32bit版用と64bit版用のgemを提供する仕組みについて説明します。

ポイントは次の1点です。

  • gemをわける

複数バージョンのRuby用のバイナリは1つのgemに入れられますが、複数プラットフォーム(32bit用と64bit用)に対応した1つのgemは作れません。これは、1つのgemには1つのプラットフォーム情報しか設定できないからです。

Windowsの32bit版Ruby用なら「x86-mingw32」というプラットフォームで、64bit版なら「x64-mingw32」というプラットフォームになります3

これもrake-compilerを使うといい感じにやってくれます。Rake::ExtensionTask#cross_platformにクロスコンパイルしたいプラットフォームを指定するだけです。

require "rake/extensiontask"
# specはGem::Specification
Rake::ExtensionTask.new(spec.name, spec) do |ext|
  ext.cross_compile = true
  ext.cross_platform = ["x86-mingw32", "x64-mingw32"]
end

簡単ですね。

gemを作るコマンドは変わりません。

% rake RUBY_CC_VERSION=1.9.3:2.0.0:2.1.0 cross clean compile native gem

これで次のファイルができます。

  • pkg/fat_gem_sample-1.0.0-x86-mingw32.gem
  • pkg/fat_gem_sample-1.0.0-x64-mingw32.gem

ここまでが仕組みの説明です。Windowsの32bit/64bit版Ruby用のバイナリ入りgemがどのような仕組みで実現されているかわかりましたか?また、複数バージョンのRubyのバイナリを1つのgemに入れる方法がどんな仕組みで実現されているかわかりましたか?

rake-compilerの準備

仕組みがわかったということにして話を進めます。仕組みがわかったので実際にgemを作ってみましょう。

ここまでの説明でrake-compilerを使えばWindowsのRuby用バイナリ入りgemを簡単に作れることがわかっているはずです。ここからは、rake-compilerを使うために最初にしなければいけないことについて説明します。

まず、クロスコンパイル用のビルドツールをインストールします。以前はMinGWを使っていましたが、今はMinGW-w64を使います。MinGW-w64は32bit版のビルドにも64bit版のビルドにも対応しているからです。

% sudo apt-get install -y -V mingw-w64

次に、rake-compilerをインストールします。

% gem install rake-compiler

最後に、バイナリを作りたいプラットフォームのRubyをビルドして準備は完了です。複数バージョンのバイナリをビルドする場合はそれぞれのバージョンのRubyをビルドします。次のコマンドを実行するとRubyをビルドできます。プラットフォームとバージョンは引数で指定します。

% rake-compiler cross-ruby HOST=#{プラットフォーム} VERSION=#{Rubyのバージョン}

例えば、32bit版Rubyの2.0.0-p247をビルドしたい場合は次のようにします。

% rake-compiler cross-ruby HOST=i686-w64-mingw32 VERSION=2.0.0-p247

64bit版Rubyの2.1.0-preview1をビルドしたい場合は次のようにします。

% rake-compiler cross-ruby HOST=x86_64-w64-mingw32 VERSION=2.1.0-preview1

32bitと64bitの両方対応で、さらに、1.9.3、2.0.0、2.1.0用のバイナリ入りgemを作りたい場合は次のようにします。

% rake-compiler cross-ruby HOST=i686-w64-mingw32 VERSION=1.9.3-p448
% rake-compiler cross-ruby HOST=i686-w64-mingw32 VERSION=2.0.0-p247
% rake-compiler cross-ruby HOST=i686-w64-mingw32 VERSION=2.1.0-preview1
% rake-compiler cross-ruby HOST=x86_64-w64-mingw32 VERSION=1.9.3-p448
% rake-compiler cross-ruby HOST=x86_64-w64-mingw32 VERSION=2.0.0-p247
% rake-compiler cross-ruby HOST=x86_64-w64-mingw32 VERSION=2.1.0-preview1

これでrake-compilerの準備は完了です。

前述した次のコマンドが動くようになっているはずです。

% git clone https://github.com/kou/fat_gem_sample.git
% cd fat_gem_sample
% rake RUBY_CC_VERSION=1.9.3:2.0.0:2.1.0 cross clean compile native gem

次のgemができましたか?

  • pkg/fat_gem_sample-1.0.0-x86-mingw32.gem
  • pkg/fat_gem_sample-1.0.0-x64-mingw32.gem

このように、rake-compilerを準備すれば、rake一発でバイナリ入りgemが作れるようになります。拡張ライブラリーでも簡単にリリースできますね。

gemに依存ライブラリも入れる

実は、ここで説明している方法だけではRroongaのバイナリ入りgemは作れないのです。ただの拡張ライブラリーであればここまでの方法で大丈夫です。しかし、なにかのライブラリーのバインディング4ではそうはいきません。gemにバインディングのバイナリーは入っていても、「なにかのライブラリー」そのものは入っていないので動かないのです。そして、RroongaはGroongaのバインディングなのです。GroongaがないとRroongaは動きません。

これを解決する方法は2つあります。

  • gemとは別に「なにかのライブラリー」をインストールしてもらう
  • gemの中に「なにかのライブラリー」のバイナリも入れる

Rroongaは後者の方法を使っています。つまり、gemの中にGroongaのバイナリも入れているということです。こうすると、gemをインストールするだけで追加の手間は必要ありません。

ただし、このやり方はrake-compilerでは想定外のやり方なので、ひと手間かけないといけません。

rake-compilerはgemを作るときに一時ディレクトリーにファイルをコピーしてからそのディレクトリーを元にgemを作ります。この一時ディレクトリーを「stage」と呼んでいます。次のようなコマンドを実行しているイメージです。「tmp/stage/」ディレクトリーが「stage」に相当します。

% cp -a groogna.gemspec tmp/stage/
% cp -a lib/ tmp/stage/
% cp -a ext/ tmp/stage/
...
% cd tmp/stage
% gem build groonga.gemspec

Rroongaのgemの中にGroongaのバイナリも入れるためには上記の「cp -a ...」のタイミングでGroongaのバイナリも「stage」にコピーしなければいけません。それを実現すると以下のようになります。

require "find"

def collect_binary_files(spec)
  # 「vendor/#{プラットフォーム名}/」以下にGroongaのバイナリがあるとする
  Find.find("vendor/#{spec.platform}/").to_a
end

Rake::ExtensionTask.new(spec.name, spec) do |ext|
  ext.cross_platform = ["x86-mingw32", "x64-mingw32"]
  ext.cross_compile = true
  ext.cross_compiling do |_spec|
    binary_files = collect_binary_files(_spec)
    _spec.files += binary_files
    stage_path = "#{ext.tmp_dir}/#{_spec.platform}/stage"
    binary_files.each do |binary_file|
      stage_binary_file = "#{stage_path}/#{binary_file}"
      stage_binary_dir = File.dirname(stage_binary_file)
      directory stage_binary_dir
      file stage_binary_file => [stage_binary_dir, binary_file] do
        cp binary_file, stage_binary_file
      end
    end
  end
end

これでgemの「vendor/#{プラットフォーム名}/」以下にGroongaのバイナリが入ります。後は次のように環境変数PATHにGroongaのDLLがある「vendor/#{プラットフォーム名}/bin/」を加えれば完成です。この処理は「require "#{major}.#{minor}/groonga.so"」の前にやることがポイントです。groonga.soがGroongaのDLLを使うからです。

require "pathname"

base_dir = Pathname.new(__FILE__).dirname.dirname.expand_path
local_groonga_dir = base_dir + "vendor" + RUBY_PLATFORM
local_groonga_bin_dir = local_groonga_dir + "bin"
if local_groonga_bin_dir.exist?
  prepend_path = lambda do |environment_name, separator|
    paths = (ENV[environment_name] || "").split(separator)
    dir = local_groonga_bin_dir.to_s
    dir = dir.gsub(/\//, File::ALT_SEPARATOR) if File::ALT_SEPARATOR
    unless paths.include?(dir)
      paths = [dir] + paths
      ENV[environment_name] = paths.join(separator)
    end
  end

  prepend_path.call("PATH", File::PATH_SEPARATOR)
end

begin
  major, minor, _ = RUBY_VERSION.split(/\./)
  require "#{major}.#{minor}/groonga.so"
rescue LoadError
  require "groonga.so"
end

これで、ただの拡張ライブラリーだけでなく、バインディングもgemをインストールするだけですぐに使えるようになります。

ただし、「何かのライブラリー」(Rroongaの場合はGroongaに相当)のバイナリをどうやって用意するかについては触れていないので注意してください。これについてはrake-compilerは助けてくれません。Rroongaの場合はGroongaが配布しているバイナリをそのまま使っています。rcairoの場合は自分でクロスコンパイルしています。自分でクロスコンパイルする場合はDebian GNU/LinuxでWindows用バイナリをビルドする方法を参考にしてください。2年前の記事ですが今でも有効です。

まとめ

Windowsの32bit/64bit版Ruby用バイナリ入りgemをDebian GNU/Linux上で作る方法を説明しました。64bit版のWindowsでも簡単にRroongaを使えるように!と2年前からコツコツ進めていたことがようやく実現できました。Rubyの拡張ライブラリーやバインディングをWindowsでも簡単に使えるようにしたいという方はぜひrake-compilerを使ってみてください。

  1. 能楽堂にRroongaが同梱されているので64bit版RubyでもRroongaを使うことはできましたが、Groonga/Rroongaの方がバージョンアップが早いため最新のGroonga/Rroongaを使うことはできなくなっていました。

  2. RubyInstaller for Windowsをやっている人が作っているgemです。

  3. RubyInstaller for WindowsのRubyの話です。Ruby Microsoft Installer PackageのRubyなどでは違うプラットフォームになります。

  4. 「なにかのライブラリー」をRubyから使えるようにするための拡張ライブラリー