10.20.2008

Making Mr Bones work with jRuby

UPDATE : The fixes outlined bellow where incorporated by Tim in the main Mr Bones distribution. If you update to the latest gem, it will work out of the box with jRuby.

Want to make some GEM, Mr Bones (github) is going to help you setup your directory structure and comes with Rake tasks to package your application into a nice little Gem. The great thing about Mr Bones is that it does not add any dependencies to your Gem (like hoe does).


To create a new project, you just type bones [project_name] and you can start developping your application or library. You are done? type bones gem:install and your code is packaged and installed.


Want to do the same using jRuby instead of plain old Ruby (MRI)... You do just the same... But as of version 2.1.0, all you would just get errors when using Bones. The reason is that Mr Bones does things slightly differently dependings on the RUBY_PLATFORM value.

For example, if Bones detected that the platform was win32, it does it's redirection to nul: instead of /dev/null.

The problem with running Bones under jRuby, is that there is no win32 string in the value of RUBY_PLATFORM. Instead, you have java. So if you run Bones with jRuby under Windows, it does not beleive it's running under Windows and it does thing like it would if running under Unixes.

I really wanted Bones to work under jRuby, so I started working on a patch... Here is what I've done...

In lib/bones.rb, the existing code set a WIN32 constant to true if RUBY_PLATFORM contains win32. It then uses this constant to set DEV_NULL constant to the proper null device for redirection. I believe a more appropriate way of doing this is to check for the existence of /dev/null and then set DEV_NULL depending on the result. So on all Unix based OSes, the value would be /dev/null because it exists, else it would be nul: (these days, only Windows is not Unix based).

I also set a HOME constant that will point to the user home directory. To do this, I assign the value of the HOME environment variable and if its not set, I use the value of the USERPROFILE environment variable. Again, no need to check the platform, just check for things that should be there one way or another.

Finally, Bones will run things like rake or gem. It does so by using the system(...) method or Rake sh(...) method. This is fine if you have only one installation of Ruby on your system. But if you have many installation like I does, it will use the one it find in the PATH. So I use the same trick as Rake to find the ruby intepreter we are running on and set it to the RUBY constant. We will use this later on to make sure we use rake or gem for the same interpreter.

Here is the patch for lib/bones.rb :
diff --git a/lib/bones.rb b/lib/bones.rb
index 50a63b6..35dcd75 100644
--- a/lib/bones.rb
+++ b/lib/bones.rb
@@ -4,8 +4,9 @@ module Bones
   # :stopdoc:
   VERSION = '2.1.1'
   PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))
-  WIN32 = %r/win32/ =~ RUBY_PLATFORM
-  DEV_NULL = WIN32 ? 'NUL:' : '/dev/null'
+  HOME = File.expand_path(ENV['HOME'] || ENV['USERPROFILE'])
+  DEV_NULL = File.exist?("/dev/null") ? "/dev/null" : "nul:"
+  RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']). sub(/.*\s.*/m, '"\&"') # Ruby Interp
reter location - taken from Rake source code
   # :startdoc:

   # Returns the path for Mr Bones. If any arguments are given,
@@ -16,6 +17,7 @@ module Bones
     args.empty? ? PATH : File.join(PATH, args.flatten)
   end

+
   # call-seq:
   #    Bones.require_all_libs_relative_to( filename, directory = nil )
   #
@@ -46,7 +48,7 @@ module Bones
     load bones_setup

     rakefiles = Dir.glob(File.join(Dir.pwd, %w[tasks *.rake])).sort
-    import(*rakefiles)
+    import(*rakefiles) unless rakefiles.empty?
   end

   # TODO: fix file lists for Test::Unit and RSpec


Now onto lib/bones/main.rb. In main.rb, there are calls to rake. This is one place I use the RUBY constant mentionned earlier to execute rake from the same interpreter location and not from the system PATH. To do that, I just do
system("#{::Bones::RUBY} -S rake ...") instead of just system("rake ...")

Ther was also a place in this file that the old WIN32 constant was used to find the user home directory. So I replace this with the HOME constant we talked a bit earlier.

Here is the patch for lib/bones/main.rb :
diff --git a/lib/bones/main.rb b/lib/bones/main.rb
index b7ec28b..1cc074a 100644
--- a/lib/bones/main.rb
+++ b/lib/bones/main.rb
@@ -181,9 +181,9 @@ class Main
       if test(?f, File.join(output_dir, 'Rakefile'))
         begin
           FileUtils.cd output_dir
-          system "rake manifest:create 2>&1 > #{::Bones::DEV_NULL}"
+          system "#{::Bones::RUBY} -S rake manifest:create 2>&1 > #{::Bones::DEV_NULL}"
           @io.puts "now you need to fix these files"
-          system "rake notes"
+          system "#{::Bones::RUBY} -S rake notes"
         ensure
           FileUtils.cd pwd
         end
@@ -363,7 +363,7 @@ class Main
   def mrbones_dir
     return @mrbones_dir if defined? @mrbones_dir

-    path = (::Bones::WIN32 ? ENV['HOMEPATH'].tr("\\", "/") : ENV['HOME'])
+    path = ::Bones::HOME
     path = File.join(path, '.mrbones')
     @mrbones_dir = File.expand_path(path)
   end

It was now lib/bones/tasks/setup.rb b/lib/bones/tasks/setup.rb turn to get dissected. There, it was again seting the value of a WIN32 constant with a slightly different logic than in bones.rb. It used this constant to set DEV_NULL again and then to use diff.exe instead of just diff when running under Windows. I removed the WIN32 constant. Got rid of the if used to add the ".exe" extension as they are not necessary. If ther is a diff.exe on Windows, system("diff ...") will work just nice.  When checking for an appropriate diff utility, the existing code does some system commands inside a quiet context. the quiet(...) method just changed the stderr and stdout to the null device before running the block. There is a problem when calling the reopen() method under jRuby that made the quite method effect stay there until the end of the program. So I removed that method and used redirection in the system(...) call to acheive the same result.

I then set the value of the RCOV, RDOC and GEM constant to "#{RUBY} -S rcov", ... so that these commands execute under the same interpreter we are currently running.

Here is the lib/bones/tasks/setup.rb b/lib/bones/tasks/setup.rb patch :
diff --git a/lib/bones/tasks/setup.rb b/lib/bones/tasks/setup.rb
index 27d659b..a722c13 100644
--- a/lib/bones/tasks/setup.rb
+++ b/lib/bones/tasks/setup.rb
@@ -5,7 +5,7 @@ require 'rake/clean'
 require 'fileutils'
 require 'ostruct'

-class OpenStruct; undef :gem; end
+class OpenStruct; undef :gem if defined? gem; end

 # TODO: make my own openstruct type object that includes descriptions
 # TODO: use the descriptions to output help on the available bones options
@@ -123,36 +123,17 @@ import(*rakefiles)
 %w(lib ext).each {|dir| PROJ.libs << dir if test ?d, dir}

 # Setup some constants
-WIN32 = %r/djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM unless defined? WIN32
-
-DEV_NULL = WIN32 ? 'NUL:' : '/dev/null'
-
-def quiet( &block )
-  io = [STDOUT.dup, STDERR.dup]
-  STDOUT.reopen DEV_NULL
-  STDERR.reopen DEV_NULL
-  block.call
-ensure
-  STDOUT.reopen io.first
-  STDERR.reopen io.last
-  $stdout, $stderr = STDOUT, STDERR
-end
+DEV_NULL = File.exist?("/dev/null") ? "/dev/null" : "nul:"
+
+DIFF = if system("gdiff '#{__FILE__}' '#{__FILE__}' > #{DEV_NULL} 2>&1") then 'gdiff'
+       else 'diff' end unless defined? DIFF
+
+SUDO = if system("which sudo > #{DEV_NULL} 2>&1") then 'sudo'
+       else '' end

-DIFF = if WIN32 then 'diff.exe'
-       else
-         if quiet {system "gdiff", __FILE__, __FILE__} then 'gdiff'
-         else 'diff' end
-       end unless defined? DIFF
-
-SUDO = if WIN32 then ''
-       else
-         if quiet {system 'which sudo'} then 'sudo'
-         else '' end
-       end
-
-RCOV = WIN32 ? 'rcov.bat' : 'rcov'
-RDOC = WIN32 ? 'rdoc.bat' : 'rdoc'
-GEM  = WIN32 ? 'gem.bat'  : 'gem'
+RCOV = "#{RUBY} -S rcov"
+RDOC = "#{RUBY} -S rdoc"
+GEM  = "#{RUBY} -S gem"

 %w(rcov spec/rake/spectask rubyforge bones facets/ansicode).each do |lib|
   begin

There is one last change that should be done to be able to run the specifications, we need to require 'rubygems' so the trick we took from Rake to get the current ruby interpreter work :
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5a60dc1..1965e1c 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,7 @@
 unless defined? BONES_SPEC_HELPER
 BONES_SPEC_HELPER = true

+require 'rubygems'
 require File.expand_path(
     File.join(File.dirname(__FILE__), %w[.. lib bones]))

--


We can nw enjoy Bones with jRuby on Windows just like we would enjoy it under MRI.

I've submitted this patch to Tim, the auther of Mr Bones. While he review it, you can download it from here and apply it to your source tree to check it out.


No comments:

AdSense Links