Building Binary Ruby Gems for Windows

I have two C extension based gems — Hitimes and Amalgalite — that I think would be useful to Windows users. This weekend I sat down, did the research and figured out a way to integrate building binary gems for Windows into my current setup.

Probably the biggest issue for developing binary gems for Windows is that the official Windows on Ruby builds are built built using VC6. This compiler is basically impossible to track down, and Luis has a great explanation of the issues developing Ruby for Windows.

I could have used the new One Click Dev-Kit, and it does look tempting. I already have rake scripts setup for all my building, testing, and distributing and I want to be able to say rake dist:rubyforge and all versions of the gem in question are pushed up to rubyforge in one fell swoop.

Fortunately Mauricio Fernandez had a great blog post about 2 years ago on Cross-compiling Ruby extensions for win32: rcovrt. His approach integrates better with my current setup and I was able to learn about cross-compilers in the process, which are something I've been meaning to experiment with for a while. Although the rcovrt blog post is 2 years old, it is still relevant and was able to point me in the right direction.

Install a Cross-Compiler

The first item we need is a cross compiler.

Right now I'm developing on a Mac so I need the mingw32 cross compiler so I can build binaries that will be compatible with the official VC6 built Ruby. Luckily the i386-mingw cross compiler is available in MacPorts.

sudo port install i386-mingw32-gcc

Unfortunately there is a problem with the i386-mingw32-binutils port. If you see an error relating to makeinfo in your build, use the patch I submitted to the ticket.

Once binutils is all happy and you've had a cup of coffee or two while the mingw32 tool-chain builds installs we can build our gem for Windows.

Building Ruby Using the MinGW Cross-Compiler

This piece was straight from Cross-compiling Ruby extensions for win32: rcovrt. I used Mauricio's cross-compile.sh as a base and altered it to work on OS X.

% cat cross-compile.sh
#!/bin/sh
env ac_cv_func_getpgrp_void=no \
    ac_cv_func_setpgrp_void=yes \
    rb_cv_negative_time_t=no \
    ac_cv_func_memcmp_working=yes \
    rb_cv_binary_elf=no \
    ./configure \
    --host=i386-mingw32 \
    --target=i386-mingw32 \
    --build=i686-darwin9.2.2 \
    --prefix=${HOME}/ruby-mingw32
make ruby
make rubyw.exe
make install

Stick this in a freshly extracted ruby source distribution and run it. This builds an i386-mingw32 version of ruby and installs it in ${HOME}/ruby-mingw32.

The whole purpose of this little exercise is a side effect. I don't really need to run this build of ruby, I only need it to exist so I may build against it.

Building a Windows Gem

Now that I have a mingw32 build of ruby I can build my extensions against it. Normally when building an extensions I do rake ext:build. Which under the covers does:

cd ext/
ruby extconf.rb
make

For a Windows build, we need to have the extconf.rb produced Makefile build against the i386-mingw32 ruby. This is done by taking the rbconfig.rb file from the ruby-mingw32 installation and putting it in the ext/ directory. I keep a copy of this as ext/rbconfig-mingw.rb. The build process then becomes:

cd ext
cp rbconfig-mingw.rb rbconfig.rb
ruby -I. extconf.rb
make
rm -f rbconfig.rb

Using -I. forces the current working directory to the front of the $LOAD_PATH and mkmf will therefore load the rbconfig.rb in the ext/ directory instead of the one from the global ruby installation. This forces all the Config::CONFIG accesses in mkmf to use the environment of the mingw build and make a i386-mingw Makefile.

The make command will then build against the i386-mingw32 ruby installation and the final loadable library can be shipped in a platform dependent gem.

Packaging the Gem

I have a top level gemspec.rb file in all of my projects which holds the global Gem Specification for the project. For the Windows binary gem I need an additional spec that is almost the same as the default spec, without the extensions. Here is the snippet I added for Amalgalite.

# create a new spec based upon the normal spec
Amalgalite::GEM_SPEC_WIN = Amalgalite::GEM_SPEC.clone

# set the platform to be compatible with the official 
# Windows release of Ruby
win_platform = ::Gem::Platform.new( "i386-mswin32_60" )
Amalgalite::GEM_SPEC_WIN.platform = win_platform

# turn off the extensions, since this is a binary release
Amalgalite::GEM_SPEC_WIN.extensions = []

# add the binary extension to the normal file list
Amalgalite::GEM_SPEC_WIN.files +=  ["lib/amalgalite3.so"]

The gem is then packaged using an additional rake gem packaging task.

desc "package the windows gem"
task :package_win => "ext:build_win" do
  cp "ext/amalgalite3.so", "lib", :verbose => true
  Gem::Builder.new( Amalgalite::GEM_SPEC_WIN ).build
  mv Dir["*.gem"].first, "pkg"
end 

This all culminates in the ability to publish a new version of a gem for my supported platforms to rubyforge with a simple rake dist:rubyforge

Clone the Amalgalite repo or Hitimes repo and look around. Let me know if you have any questions or comments.

Comments (View)

blog comments powered by Disqus