How to build native Ruby gems on OS X - step by step guide

I’ve recently begun porting our plugin to OS X and had to solve the problem of building native Ruby libraries for SketchUp on OS X. Since I couldn’t find a complete “how to” on the web, I’m here to share what I learned. This may not be the simplest possible way to do it, but I couldn’t get it working in any other way I tried. So here’s my step by step guide:

to be continued…

In Mountain Lion:

to be continued…

The most important step:

  • copy ruby-c-extension-examples/ThirdParty/lib/mac/2.0/Ruby.framework/Versions/2.0/libruby.2.0.0.dylib
    to ~/.rvm/rubies/ruby-2.0.0-p247/lib/ overwriting the original file. This needs to be done, because the version of Ruby which SketchUp uses is modified and the libraries have to be linked against this exact version. Otherwise you’ll get a BugSplat when SketchUp tries to load the library. This took me quite some time to figure out.

You are now ready to build Ruby gems with native extensions, e.g. yajl-ruby:

  • gem install yajl-ruby
  • copy ~/.rvm/gems/ruby-2.0.0-p247/gems/yajl-ruby-1.2.1/lib/*
    to your plugin’s library folder where SketchUp Ruby can find it. (Make sure it’s in $LOAD_PATH)

I hope this guide will help other developers.

If you’re distributing your plugin make sure to read Cannot install ruby gem in OSX as well and modify the gems to bring them into your own namespace to avoid potential conflicts with other plugins.

(sorry, I had to split the post because I’m not allowed to put more than two links into a post)

why install Parallels Desktop?

and why not use the OS X ruby installed version?

john

If you have a Mac with Mountain Lion you don’t need to use Parallels. I have El Capitan on my Mac and I couldn’t get it to build the libraries in a way that does not cause BugSplats. I think this guide shows a very safe way to build the libs correctly. As I said this may not be the simplest way but it worked for me where other methods didn’t.

That is not where SketchUp really wants gems. In SketchUp’s Ruby console type:

ENV["GEM_HOME"]

or

ENV["GEM_PATH"]

Sketchup sets these paths in the ENV hash (ie, the SketchUp process’ copy of the environment,) during it’s startup cycle (when it loads RubyGems.)

It would be nice if there was a SketchUp specific gem server on the sketchup server.

But (until then) you could place it at rubygems, but rename it “yajl-sketchup-ruby-2.0-mac-1.2.1” and then SketchUp (on Mac) could load the latest version it via:

Gem::install "yajl-sketchup-ruby-2.0-mac"

See if you go the plugin-sub-folder route, you risk the possibility of other authors doing the same, and having conflicting versions of the same gem loading from separate author’s plugins.

But I understand you need to keep moving forward, even if this mode is not the best. Perhaps next release things are deployed in a better manner.

Another option is to release the gem packaged specifically as a SketchUp extension, that adds it’s path into $LOAD_PATH, but does not actually load itself into memory. It lets other extensions require it to be loaded, (like we would any standard library module.)

I had an example of this with the ruby-zip gem. (But I cannot find where I posted it. I thought it was here in the Ruby API category.)

Ref other threads:

Dan, thank you for the additional info and clarification.

Does this call replace the require 'yajl' calls or do I need to call this only once and keep the require calls?

If I go the rubygems route, our users will need an internet connection when running our plugin for the first time, right? Would it be possible to ship the gem with our installer and just install it into the SketchUp gems folder? (We’re not using the extension warehouse, because our plugin is just a part of a bigger application)

No that call would be to install it from the RubyGems server. So you could first check if it had already been installed:

dirs = ""

gem_sought = "yajl-sketchup-ruby-2.0-mac"
# Note: the gem reference name omits the dash
# and version number, but the gem name has it.

Dir::chdir(File.join( ENV["GEM_HOME"], "gems" )) {
  dirs = Dir.glob("#{gem_sought}*").sort.select{|file| File.directory?(file) }
}
if dirs.empty?
  # no gem installed, install it:
  begin
    Gem::install gem_sought
  rescue Gem::InstallError
    # bailout code
  end

# else code could be here to test
# for a minimum installed version
  # the gem dirs will have a dash and version number:
  # "yajl-sketchup-ruby-2.0-mac-1.2.3",
  # "yajl-sketchup-ruby-2.0-mac-1.2.4", etc.

end

require gem_sought

Also, you see the other thread I referenced above?

In it we discuss compiling gems within your own company namespace, so you do not need to worry about conflicts.

There exists the possibility some other developer, or even the user, may install a newer version of the gem, and it could break your extension. (If you rely upon a global gem library.)

Are you trying to build gems or build Ruby C Extensions for SketchUp? If you only want to build Ruby C Extensions then all you need is in the SketchUp GitHub repo - no need to build Ruby yourself.

My post was about gems, not just Ruby C Extensions. I’ll see if I can change the topic to reflect that.

How are you using gems within SketchUp? Are you programmatically installing them on users machines?

Right now I’m putting them inside our own plugin directory and I’m adding the gem directories to $LOAD_PATH. I’m aware that this may not be the best solution. Dan already suggested alternatives, but I have not decided yet which way to go in the future.

Yea, that’s not ideal - that could cause clashes with other extensions running. Try to stay away from modifying the load path. Also beware that the Extension Warehouse require extensions to contain everything within a single namespace module. This means no globals, wrapping all your methods, modules, classes and constants under your namespace. Example:

module DeveloperName
  module ExtensionName
  end
end

Keep that in mind when you pick how you package and built the extension.