24 October 2013
Follow @alloyTL;DR: Compilation as part of the CocoaPods gem install process is dead. Long live precompiled gems!
The CocoaPods gems will now be installable without the need for installing the Xcode Command Line Tools and fixing the Ruby headers location on OS X (Mountain Lion and Mavericks).
History on the underlying problem
While CocoaPods and most of its dependencies are implemented in pure Ruby there is one dependency that requires a bit of C code, or –in Ruby parlance– a ‘Ruby C extension’.
Typically, a ‘Ruby C extension’ will be compiled on the user’s machine during installation of the gem it belongs to. While this leads to better portability, it does add the requirement of having a compiler and the Ruby C headers, both of which are not available on OS X by default.
To be fair, Mavericks does come with the Command Line Tools pre-installed, and thus with a compiler, but it does not place the Ruby C headers in the right location and manual intervention is required.
The dependency with a C extension that we have is Xcodeproj, the library that powers our Xcode project parsing and generation. It does this by utilising the OS X Core Foundation API called CFPropertyList. This API allows us to read any type of PList, including the deprecated ASCII format that Xcode still uses, and generate a PList readable by Xcode. While there were –at the time of implementing Xcodeproj– some pure Ruby libraries to read or write one of the PList formats, there was none that implemented them all to our satisfaction. Finally, as a Xcode project is an undocumented format, we did not want to spend a lot of time on implementing and possibly having to replace it in the future if Apple would ever decide to change the format.
UX Perspective
You can skip to ‘The solution’ if you’re not interested in a discussion on the ramifications of how to get things done with Ruby and the status quo.
These sorts of issues make the out-of-the-box UX for pure Ruby users really bad. CocoaPods is such a tool that has many more users whom are not Ruby developers. I.e. Ruby is somewhat of an implementation detail. This understandably leads to frustrated users.
To add insult to injury, such users –in their willingness to give CocoaPods a second chance– will usually google for a solution leading them into, what I call, the ‘developer bubble’ of Ruby developers.
In this ‘developer bubble’ there is no sane Ruby environment unless you at least build your own Ruby, but preferably including one or more of the following tools: homebrew, RVM, rbenv, and many other alternatives.
Here’s a recent blog post that gives this illusion and here’s a typical tweet that illustrates how this suggestion leads a pure Ruby user astray:
Soooo... I need to install homebrew for rvm, ruby-install, and rbenv, and then will be able to `gem install cocoa pods`? #shitshow
— ¡ɜɿoɾɪɹℲ (@frijole) October 21, 2013
Shitshow indeed.
The sad part is, it isn’t even necessary. Not if you are a Ruby developer, let alone if you are a pure user. We in the Ruby community need to fix this perspective and look at the mess we’ve gotten ourselves in from the outside and better frame our suggestions in context. But I digress, for now.
Solution
The solution is simple and, in our case, two-fold:
Recommend the use of the Ruby version that comes with OS X.
The only remaining –but very valid– concern is that, by default, you will
have to use sudo
when installing gems. (This is only an issue for the
duration of the gem installation, though.)
If you do not want to grant RubyGems admin privileges for this process,
you can tell RubyGems to install into your user directory by passing either
the --user-install
flag to gem install
or by configuring the RubyGems
environment. The latter is in our opinion the best solution. To do this,
edit the .profile
file in your home directory and add or amend it to
include these lines:
export GEM_HOME=$HOME/gems
export PATH=$GEM_HOME/bin:$PATH
Note that if you choose to use the --user-install
option, you will still
have to configure your .profile
file to set the PATH
or use the command
prepended by the full path. You can find out where a gem is installed with
gem which cocoapods
. E.g.
$ gem install cocoapods --user-install
$ gem which cocoapods
/Users/eloy/.gem/ruby/1.8/gems/cocoapods-0.27.0/lib/cocoapods.rb
$ /Users/eloy/.gem/ruby/1.8/gems/cocoapods-0.27.0/bin/pod install
Provide prebuilt binaries for the Ruby versions that come with OS X.
In our case this means a prebuilt version of the C extension for Ruby 1.8.7 on OS X 10.8.x and for Ruby 2.0.0 on OS X 10.9.x. On these versions, the user does not have to install the Xcode Command Line Tools, nor fix the Ruby C headers.
If you are using a custom Ruby version, however, regardless of it being the exact same Ruby version for which we provide prebuilt binaries, you will have to compile the C extension and thus need the Xcode Command Line Tools.
This all works transparently, so you don’t need to think about it much. If,
however, for whatever reason you wish to control this then you can use the
XCODEPROJ_BUILD
environment variable. Set it to 1
to always compile or
to 0
to never compile.
Note that you can recognize that you’re using the prebuilt binary by the
following sentence in the gem install
output:
[!] You are using the prebuilt binary version of the xcodeproj gem.
Notes for RubyGems developers
Over the course of figuring out the best way to cater to our users, I came to the following insights:
RubyGems built-in prebuilt binaries support.
RubyGems automagically installs a CPU-architecture + OS-version specific
version of your gem if one exists with the following filename:
[NAME]-[VERSION]-[ARCH]-[PLATFORM].gem
.
E.g.: xcodeproj-0.14.0-universal.x86_64-darwin13.gem
.
While this allows you to prebuilt for specific architectures and OS versions, it doesn’t allow for control over which Ruby version you have. This means it is not an option for us, as we have to check if the Ruby you are using is in fact a pre-installed OS X Ruby version. (In case there is a modifier to control the Ruby version, please let me know and I will update this section.)
RubyGems installation without the need for make
.
Because we have to check if the Ruby version you are using is a version that
comes pre-installed with OS X, we have to perform these checks at the time
that the gem is being installed. Typically, RubyGems authors ‘abuse’ their
extconf.rb
to generate a no-op Makefile
in the case that no compilation
is required. This, however, means that the user will still need to have
make
installed, which is not the case on OS X 10.8.
Luckily Eric Hodel, one of the RubyGems maintainers, pointed me to an undocumented feature. Instead of specifying the extension like so:
s.extensions = "ext/xcodeproj/extconf.rb"
You can alternatively specify a Rakefile
, in which case RubyGems will leave
running make
, or whatever you need to run, up to your rake task:
s.extensions = "ext/xcodeproj/Rakefile"
Prebuilt binary packaging helpers.
The rake-compiler library is
very helpful in prebuilding extensions and understanding how RubyGems handles
these gem filename formats. However, if you are going to target very specific
Ruby versions which are the same regardless of user/machine –like we do– then
it’s much simpler to just vendor the result of your extconf.rb
(i.e. the
Makefile
and possibly extconf.h
) and use those to build your extensions.
Here’s our final solution.