CocoaPods 0.27 and improved installation UX

TL;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).

Cocoapods 0 27 Install

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:

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.