Category Archives: ruby

6 Months of Avoiding Rails Controllers with DDC

My distaste for unnecessary Rails controllers is no secret. That’s why I wrote the ddc gem (Data Driven Controllers).

When DDC was released, it got mixed reviews in the comments section, so I thought I’d post a follow up with my results thus far.

Example


DDC::ControllerBuilder.build :monkeys, 
  actions: {
    show: {
      context: 'context_builder#user_and_id',
      service: 'monkey_service#find'
    },
    index: {
      context: 'context_builder#user_and_id',
      service: 'monkey_service#find_all'
    },
    update: {
      context: 'context_builder#monkey',
      service: 'monkey_service#update'
    },
    create: {
      context: 'context_builder#monkey',
      service: 'monkey_service#create'
    }
  }

Review

My current project’s Rails API has about 20 controllers, almost all of which are mere data describing how to glue the Rails pieces together. The overall approach has held up well. With decent naming conventions and DDC, I barely ever have to think about Rails controllers. When I did need a custom bit of something, DDC let me define the methods that actually required some thought. So far, working with DDC has been great: easy to use with very little mental overhead.

New Features

There have been a few features added along the way to round out the DDC API:

  1. :render_opts can now be specified. These options let you control which serializer will be used to render domain specific content to JSON. I actually haven’t needed this feature, but other projects using DDC have.
  2. :context will now take an optional array of function strings. The functions will be called in order, being passed the previous functions results and outputting its own. (Like building an onion from the inside-out.)
  3. DDC has been a great tool in my tool belt. I’d really like to see Rails find a clean way to make controllers optional the way Ember.js does.

    I’m curious what others are doing to streamline the building of Rails controllers and Rails sites in general. Leave a comment below.

    The post 6 Months of Avoiding Rails Controllers with DDC appeared first on Atomic Spin.

RSchema-Hamster: Schemas for Shaping Immutable Data

RSchema-Hamster is a Ruby gem that lets you use Hamster’s persistent data structures to define and validate the shape of your data via RSchema.

What for?

In order to clarify my designs, reduce mistakes, and leave code easier to change than when I found it, I:

  • Strive to program with immutable data
  • Learn to apply ideas from type-driven development

Using RSchema-Hamster, I can define a schema such as:

require 'rschema_hamster'
Name = String
PageNumber = Integer
Appearances = Hamster.vector(
  Hamster.hash(character: Name, page: PageNumber)
)

Build a data structure like this:

guide = Hamster.vector(
  Hamster.hash(character: "Arthur", page: 1), 
  Hamster.hash(character: "Zaphod", page: 98)
)

And assert the data conforms to the schema like this:

RSchema.validate!(Appearances, guide)

Immutable Data Structures via Hamster

I prefer to code using primarily immutable values because I’ve come to understand just how careful I need to be when mutating program state: plan for it, isolate it, and generally avoid changing internal object state unnecessarily.

Two up-front challenges have been to change my design habits to favor stateless, functional code (difficult, took lots of practice) and to find a powerful persistent data structures tool (not difficult; for Ruby, I picked Hamster).

The Hamster Ruby gem provides a set of persistent collections to supplement or replace Ruby’s mutable collections, such as Hash, Vector, lazy Lists, and so forth. It looks like this:

ford = Hamster.hash(name: "Ford Prefect", gender: :male)
# => Hamster::Hash[:name => "Ford Prefect", :gender => :male]
arthur = ford.put(:name, "Arthur Dent")
# => Hamster::Hash[:name => "Arthur Dent", :gender => :male]
# (ford remains unchanged)

Getting Lost

As I start moving back to using generic collections like Hashes, Vectors, and Sets to build my data structures, I’m getting a little nervous about inconsistency and accidental misuse. Immutable or not, these collections don’t care what you store in them and will give you no hint as to their intended use. There’s a lot of room for mistakes; this is why I had a hard time letting go of my descriptively-named classes and their attr_accessor declarations.

(A couple years back, Patrick Bacon and I created Hamsterdam as a first step toward alleviating this concern, using immutable record-like objects as a means of defining and documenting attributes.)

Schema-based Definition and Validation via RSchema

Whereas some functional languages like Haskell and F# have very expressive type systems for describing data, dynamic languages like Clojure and Ruby require me to take matters into my own hands. I discovered Tom Dalling’s RSchema (a Ruby implementation of Prismatic’s Schema for Clojure), and started tinkering with ad-hoc schema structures to help clarify and test some of my existing code as I refactored it. I now felt like I could start taking some queues from the Type-Driven crowd.

In RSchema, a schema is itself data, a value that describes the shape of other values:

OrderRow = {
  order_id: Integer,
  order_totals: Totals
}
row = { 
  order_id: 5, 
  order_totals: a_valid_totals_structure 
}
RSchema.validate! OrderRow, row

Read: An OrderRow is a Hash whose order_id key refers to an integer value, and whose order_totals key refers to a Totals object, presumably a Ruby class or perhaps another schema object.

It may not be compile-time type-checking, but it sure is expressive, and it gives me the means to programmatically connect variables to their intended types. I can easily discover and understand the data’s shape by reading the code, and at runtime (especially during automated unit and integration testing) assert that the data conforms. Executable documentation, y’see.

So if I want to insist that OrderRow is an immutable Hash, I want to be able to write:

OrderRow = Hamster.hash(
  order_id: Integer,
  order_totals: Totals
)

However, the above schema isn’t valid. In order for an object to be treated as a descriptive instances (in this case a Hash containing sub-schemas), the object must implement a method called “schema_walk”, and Hamster structures don’t.

Extending Hamster to Support RSchema

RSchema defines schemas in terms of classes, Hashes, and Arrays, plus a few configurable schemas available via its DSL, such as “enum,” “maybe,” and an optional Hash-key decorator. Ruby’s Hash and Array classes implement their respective “schema_walk” such that their instances may be used to describe internal structure.

In order to use Hamster instances to describe structure, I provided implementations of “schema_walk” to Hamster::Hash, Hamster::Vector and so on, plus a few generic schemas to give parity to RSchema’s “hash_of” and “set_of” DSL methods (“hamster_hash_of” and “hamster_set_of” respectively).

The big bonus: schemas are designed to be nestable in RSchema. Because of this, schemas I define using Hamster’s types may still contain (or be contained by) Ruby’s Hash and Array structures, as well as utilize (or be used by) RSchema’s “enum,” “maybe,” and “_?” (optional hash key) schema helpers, without any extra implementation.

Here’s an abridged real-world example of how I was able to untangle a document-like structure that was already on its way to generating headaches in a Rails app I’m working on:

require 'rschema_hamster'
module OrderReport::Schema
  Name    = String
  Id      = Integer
  Dollars = BigDecimal
  Totals = Hamster.hash(
    gross: Dollars,
    tax:   Dollars,
    fee:   Dollars,
    net:   Dollars,
  )
  OrderRow = Hamster.hash(
    order_id:     Id,
    order_number: Name,
    order_totals: Totals,
  )
  MarketBlock = Hamster.hash(
    market_id:        Id,
    market_name:      Name,
    account_dropdown: RSchemaHamster.schema {
      hamster_hash_of(Name => Id)
    },
    order_rows:       Hamster.vector(OrderRow),
    market_totals:    Totals
  )
end
def self.dollars(str); BigDecimal.new(str); end
market_block = Hamster.from(
  { 
    market_id: 42,
    market_name: "The Restaurant at the End of the Universe",
    account_dropdown: {
      "Hotblack Desiato" => 1, 
      "Zaphod Beeblebrox" => 3
    },
    order_rows: [
      { order_id: 101, order_number: "MILLIWAYS-00101", order_totals: { gross: dollars("120"), tax: dollars("14.4"), fee: dollars("20"), net: dollars("85.6") } },
      { order_id: 102, order_number: "MILLIWAYS-00102", order_totals: { gross: dollars("3030"), tax: dollars("363.6"), fee: dollars("505.10"), net: dollars("2161.3") } },
    ],
    market_totals: { gross: dollars("3150"), tax: dollars("378"), fee: dollars("525.10"), net: dollars("2246.9") }
  }
) 
RSchema.validate!(OrderReport::HamsterSchema::MarketBlock, market_block)

The post RSchema-Hamster: Schemas for Shaping Immutable Data appeared first on Atomic Spin.

Building an Infinite Procedurally-Generated World

I had a lot of fun writing my last blog post: All Work & No Play – Taking Time to Code for Fun. In it I talked about writing fun code that keeps you interested in programming and keeps you creative. I used the example of writing a 2D procedurally-generated, infinite world. In this post, I am going to explain details of how that example works.

proc_terrain_small

Bring the Noise

To build our terrain, we need something better than just randomly selecting a tile for each set of coordinates. Since we want the world to be infinite, we can’t design it by hand: that’s where Perlin noise comes in. Perlin noise is an algorithm for generating organic looking, multidimensional noise quickly (at least for 2D). You’ll want to know a few terms when playing with Perlin noise:

Octaves
The number of subsequent generations to run, usually double the frequency and half the amplitude.
Persistence
How to weight the additional octaves when adding them.

These can be fun to play with when generating terrain; they will make it smooth and flat or bumpy and spiky. Further explanation can be found here.

Homework assignment: try merging two noise datasets that have been generated with different octave / persistence values.

From Noise to Tiles

Once you have access to random 2D data, it’s pretty straight forward to convert that noise into usable data. Simply set up thresholds for each tile type you want to support, eg:

  • water if < 0.3
  • grass if >= 0.3 and <= 0.6
  • mountain if > 0.6

I recommend leaving the upper and lower cases open. If they are not left open, you may end up with holes in your map for unexpectedly high or low values. An alternative is to clamp or scale the noise values when you generate them.

Going Beyond Tiles

An infinite world of terrain isn’t all that interesting for very long without something in it. We want to add things like trees, caves, and towns to our game. We can simply use the same random number generator (RNG) that we used to generate our noise to determine when to place and how to build objects. We have to be careful though. If we use a single RNG for the whole world, the order in which we discover things will change the how they are generated.

chunks_small

The trick to deterministically creating the world based on a seed is to break it up into chunks (see screenshot above). A chunk is simply a range of tiles in the world. We keep 9 chunks loaded at a time. Each chunk is 50×50 grid of tiles. When the player enters a place with no chunk, we generate a new one. The first thing we do is seed a new RNG for that chunk with the global seed and the coordinates for that chunk. This allows us to throw away the chunk when the player leaves it, but know that it will be recreated the same when the player comes back. Just make sure your code is deterministic and that it only pulls random values from the chunk’s RNG and things should work out well.

The source for this project can be found on github. Please leave a comment if you are doing something similar and want an excuse to show off.

UPDATE:
Example uses tileset from Oryx Design Lab. Thanks!

The post Building an Infinite Procedurally-Generated World appeared first on Atomic Spin.

Some travlrmap updates

Been a while since I posted here about my travlrmap web app, I’ve been out of town the whole of February – first to Config Management Camp and then on holiday to Spain and Andorra.

I released version 1.5.0 last night which brought a fair few tweaks and changes like updating to latest Bootstrap, improved Ruby 1.9.3 UTF-8 support, give it a visual spruce up using the Map Icons Collection and gallery support.

I take a lot of photos and of course often these photos coincide with travels. I wanted to make it easy to put my travels and photos on the same map so have started adding a gallery ability to the map app. For now it’s very simplistic, it makes a point with a custom HTML template that just opens a new tab to the Flickr slideshow feature. This is not what I am after exactly, ideally when you click view gallery it would just open a overlay above the map and show the gallery with escape to close – that would take you right back to the map. There re some bootstrap plugins for this but they all seem to have some pain points so that’s not done now.

Today there’s only Flickr support and a gallery takes a spec like :gallery: flickr,user=ripienaar,set=12345 and from there it renders the Flickr set. Once I get the style of popup gallery figured out I’ll make that pluggable through gems so other photo gallery tools can be supported with plugins.

As you can see from above the trip to Spain was a Road Trip, I kept GPX tracks of almost the entire trip and will be adding support to show those on the map and render them. Again they’ll appear as a point just like galleries and clicking on them will show their details like a map view of the route and stats. This should be the aim for the 1.6.0 release hopefully.

Some travlrmap updates

Last weekend I finally got to a point of 1.0.0 of my travel map software, this week inbetween other things I made a few improvements:

  • Support 3rd party tile sets like Open Streetmap, Map Quest, Water Color, Toner, Dark Matter and Positron. These let you customise your look a bit, the Demo Site has them all enabled.
  • Map sets are supported, I use this to track my Travel Wishlist vs Places I’ve been.
  • Rather than list every individual yaml file in a directory to define a set you can now just point at a directory and everything will get loaded
  • You can designate a single yaml file as writable, the geocoder can then save points to disk directly without you having to do any YAML things.
  • The geocoder renders better on mobile devices and support geocoding based on your current position to make it easy to add points on the go.
  • Lots of UX improvements to the geocoder

Seems like a huge amount of work but it was all quite small additions, mainly done in a hour or so after work.

Travlrmap 1.0.0

As mentioned in my previous 2 posts I’ve been working on rebuilding my travel tracker app. It’s now reached something I am happy to call version 1.0.0 so this post introduces it.

I’ve been tracking major travels, day trips etc since 1999 and plotting it on maps using various tools like the defunct Xerox Parc Map Viewer, XPlanet and eventually wrote a PHP based app to draw them on Google Maps. During the years I’ve also looked at various services to use instead so I don’t have to keep doing this myself but they all die, change business focus or hold data ransom so I am still fairly happy doing this myself.

The latest iteration of this can be seen at travels.devco.net. It’s a Ruby app that you can host on the free tier at Heroku quite easily. Features wise version 1.0.0 has:

  • Responsive design that works on mobile and PC
  • A menu of pre-defined views so you can draw attention to a certain area of the map
  • Points can be catagorized by type of visit like places you've lived, visited or transited through. Each with their own icon.
  • Points can have urls, text, images and dates associated with them
  • Point clustering that combines many points into one when zoomed out with extensive configuration options
  • Several sets of colored icons for point types and clusters. Ability to add your own.
  • A web based tool to help you visually construct the YAML snippets needed using search
  • Optional authentication around the geocoder
  • Google Analytics support
  • Export to KML for viewing in tools like Google Earth
  • Full control over the Google Map like enabling or disabling the street view options

It’s important to note the intended use isn’t something like a private Foursquare or Facebook checkin service, it’s not about tracking every coffee shop. Instead it’s for tracking major city or attraction level places you’ve been to. I’m contemplating adding a mobile app to make it easier to log visits while you’re out and about but it won’t become a checkin type service.

I specifically do not use a database or anything like that, it’s just YAML files that you can check into GitHub, easily backup and hopefully never loose. Data longevity is the most important aspect for me so the input format is simple and easy to convert to others like JSON or KML. This also means I do not currently let the app write into any data files where it’s hosted. I do not want to have to figure out the mechanics of not loosing some YAML file sat nowhere else but a webserver. Though I am planning to enable writing to a incoming YAML file as mentioned above.

Getting going with your own is really easy. Open up a free Heroku account and set up a free app with one dynamo. Clone the demo site into your own GitHub and push to Heroku. That’s it, you should have your own up and running with place holder content ready to start receiving your own points which you can make using the included geocoder. You can also host it on any Ruby based app server like Passenger without any modification from the Heroku one.

The code is on GitHub ripienaar/travlrmap under Apache 2. Docs for using it and configuration references are on it’s dedicated gh-pages page.

Marker clustering using GMaps.js

In a previous post I showed that I am using a KML file as input into GMaps.js to put a bunch of points on a map for my travels site. This worked great, but I really want to do some marker clustering since too many points is pretty bad looking as can be seen below.

I’d much rather do some clustering and expand out to multiple points when you zoom in like here:

Turns out there are a few libraries for this already, I started out with one called Marker Clusterer but ended up with a improved version of this called Marker Clusterer Plus. And GMaps.js supports cluster libraries natively so should be easy, right?

Turns out the way Google maps loads KML files is done using a layer over the map and the points just are not accessible to any libraries, so the cluster libraries does nothing. Ok, so back to drawing points using my code.

I added a endpoint to the app that emits my points as JSON:

[
  {"type":"visit",
   "country":"Finland",
   "title":"Helsinki",
   "lat":60.1333,
   "popup_html":"<p>\n<font size=\"+2\">Helsinki</font>\n<hr>\nBusiness trip in 2005<br /><br />\n\n</p>\n",
   "comment":"Business trip in 2005",
   "lon":25,
   "icon":"/markers/marker-RED-REGULAR.png"}
]

Now adding all the points and getting them clustered is pretty easy:

<script type="text/javascript">
    var map;
    function addPoints(data) {
      var markers_data = [];
      if (data.length > 0) {
        for (var i = 0; i < data.length; i++) {
          markers_data.push({
            lat: data[i].lat,
            lng: data[i].lon,
            title: data[i].title,
            icon: data[i].icon,
            infoWindow: {
              content: data[i].popup_html
            },
          });
        }
      }
      map.addMarkers(markers_data);
    }
 
    $(document).ready(function(){
      infoWindow = new google.maps.InfoWindow({});
      map = new GMaps({
        div: '#main_map',
        zoom: 15,
        lat: 0,
        lng: 20,
        markerClusterer: function(map) {
          options = {
            gridSize: 40
          }
 
          return new MarkerClusterer(map, [], options);
        }
      });
 
      points = $.getJSON("/points/json");
      points.done(addPoints);
    });
</script>

This is pretty simple, the GMaps() object takes a markerClusterer option that expects an instance of the clusterer. I fetch the JSON data and each row gets added as a point. Then it all just happens automagically. Marker Clusterer Plus can take a ton of options that lets you specify custom icons, grid sizes, tweak when to kick in clustering etc. Here I am just setting the gridSize to show how to do that. In this example I have custom icons used for the clustering, might blog about that later when I figured out how to get them to behave perfectly.

You can see this in action on my travel site. As an aside I’ve taken a bit of time to document how the Sinatra app works and put together a demo deployable to Heroku that should give people hints on how to get going if anyone wants to make a map of their own.

Ruby, Google Maps and KML

Since 1999 I kept record of most places I’ve traveled to. In the old days I used a map viewer from PARC Xerox to view these travels, I then used XPlanet which made a static image. Back in 2005 as Google Maps became usable from Javascript I made something to show my travels on an interactive map. It was using Gmaps EZ and PHP to draw points from a XML file.

Since then google made their v2 API defunct and something went bad with the old php code and so the time came to revisit all of this into the 4th iteration of a map tracking my travels.

Google Earth came out in 2005 as well – so just a bit late for me to use it’s data formats – but today it seems obvious that the data belong in a KML file. Hand building KML files though is not on, so I needed something to build the KML file in Ruby.

My new app maintains points in YAML files, have more or less an identical format to the old PHP system.

First to let people come up with categories of points you define a bunch of types of points first:

:types:
  :visit:
    :icon: http://your.site/markers/mini-ORANGE-BLANK.png
  :transit:
    :icon: http://your.site/markers/mini-BLUE-BLANK.png
  :lived:
    :icon: http://your.site/markers/mini-GREEN-BLANK.png

And then we have a series of points each referencing a type:

:points:
- :type: :visit
  :lon: -73.961334
  :title: New York
  :lat: 40.784506
  :country: United States
  :comment: Sample Data
  :href: http://en.wikipedia.org/wiki/New_York
  :linktext: Wikipedia
- :type: :transit
  :lon: -71.046524
  :title: Boston
  :lat: 42.363871
  :country: United States
  :comment: Sample Data
  :href: http://en.wikipedia.org/wiki/Boston
  :linkimg: https://pbs.twimg.com/profile_images/430836891198320640/_-25bnPr.jpeg

Here we have 2 points, both link to Wikipedia one using text and one using an image, one is a visit and one is a transit.

I use the ruby_kml Gem to convert this into KML:

First we set up the basic document and we set up the types using KML styles:

kml = KMLFile.new
document = KML::Document.new(:name => "Travlrmap Data")
 
@config[:types].each do |k, t|
  document.styles << KML::Style.new(
    :id         => "travlrmap-#{k}-style",
    :icon_style => KML::IconStyle.new(:icon => KML::Icon.new(:href => t[:icon]))
  )
end

This sets up the types and give them names like travlrmap-visited-style.

We’ll now reference these in the KML file for each point:

folder = KML::Folder.new(:name => "Countries")
folders = {}
 
@points.sort_by{|p| p[:country]}.each do |point|
  unless folders[point[:country]]
    folder.features << folders[point[:country]] = KML::Folder.new(:name => point[:country])
  end
 
  folders[point[:country]].features << KML::Placemark.new(
    :name        => point[:title],
    :description => point_comment(point),
    :geometry    => KML::Point.new(:coordinates => {:lat => point[:lat], :lng => point[:lon]}),
    :style_url   => "#travlrmap-#{point[:type]}-style"
  )
end
 
document.features << folder
kml.objects << document
 
kml.render

The points are put in folders by individual country. So in Google Earth I get a nice list of countries to enable/disable as I please etc.

I am not showing how I create the comment html here – it’s the point_comment method – it’s just boring code with a bunch of if’s around linkimg, linktext and href. KML documents does not support all of HTML but the basics are there so this is pretty easy.

So this is the basics of making a KML file from your own data, it’s fairly easy though the docs for ruby_kml isn’t that hot and specifically don’t tell you that you have to wrap all the points and styles and so forth in a document as I have done here – it seems a recent requirement of the KML spec though.

Next up we have to get this stuff onto a google map in a browser. As KML is the format Google Earth uses it’s safe to assume the Google Maps API support this stuff directly. Still, a bit of sugar around the Google APIs are nice because they can be a bit verbose. Previously I used GMapsEZ – which I ended up really hating as the author did all kinds of things like refuse to make it available for download instead hosting it on a unstable host. Now I’d say you must use gmaps.js to make it real easy.

For viewing a KML file, you basically just need this – more or less directly from their docs – there’s some ERB template stuff in here to set up the default view port etc:

<script type="text/javascript">
    var map;
    $(document).ready(function(){
      infoWindow = new google.maps.InfoWindow({});
      map = new GMaps({
        div: '#main_map',
        zoom: <%= @map_view[:zoom] %>,
        lat: <%= @map_view[:lat] %>,
        lng: <%= @map_view[:lon] %>,
      });
      map.loadFromKML({
        url: 'http://your.site/kml',
        suppressInfoWindows: true,
        preserveViewport: true,
        events: {
          click: function(point){
            infoWindow.setContent(point.featureData.infoWindowHtml);
            infoWindow.setPosition(point.latLng);
            infoWindow.open(map.map);
          }
        }
      });
    });
</script>

Make sure there’s a main_map div setup with your desired size and the map will show up there. Really easy.

You can see this working on my new travel site at travels.devco.net. The code is on Github as usual but it’s a bit early days for general use or release. The generated KML file can be fetched here.

Right now it supports a subset of older PHP code features – mainly drawing lines is missing. I hope to add a way to provide some kind of index to GPX files to show tracks as I have a few of those. Turning a GPX file into a KML file is pretty easy and the above JS code should show it without modification.

I’ll post a follow up here once the code is sharable, if you’re brave though and know ruby you can grab the travlrmap gem to install your own.

passenger native libs on CentOS 7

I'm setting up a new puppet master running under passenger on CentOS 7 using packages from the puppetlabs and foreman repos. I used a fork of Stephen Johnson's puppet module to set everything up (with puppet apply). All went swimmingly, except I would see this error in the logs the first time the puppet master app loaded (ie. the first time it got a request):

[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] *** Phusion Passenger: no passenger_native_support.so found for the current Ruby interpreter. Compiling one (set PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY=0 to disable)...
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # mkdir -p /usr/share/gems/gems/passenger-4.0.18/lib/phusion_passenger/locations.ini/buildout/ruby/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Not a valid directory. Trying a different one...
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] -------------------------------
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # mkdir -p /var/lib/puppet/.passenger/native_support/4.0.18/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # cd /var/lib/puppet/.passenger/native_support/4.0.18/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # /usr/bin/ruby '/usr/share/gems/gems/passenger-4.0.18/ruby_extension_source/extconf.rb'
[ 2014-11-07 23:22:13.3048 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] /usr/bin/ruby: No such file or directory -- /usr/share/gems/gems/passenger-4.0.18/ruby_extension_source/extconf.rb (LoadError)
[ 2014-11-07 23:22:13.3156 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Compilation failed.
[ 2014-11-07 23:22:13.3156 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] -------------------------------
[ 2014-11-07 23:22:13.3157 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Ruby native_support extension not loaded. Continuing without native_support.

I double checked, and I do have the native libs installed – they're in the rubygem-passenger-native-libs rpm – the main library is in /usr/lib64/gems/ruby/passenger-4.0.18/native/passenger_native_support.so.

Digging in the passenger code, it tries to load the native libs by doing:

require 'native/passenger_native_support'

If I hacked this to:

require '/usr/lib64/gems/ruby/passenger-4.0.18/native/passenger_native_support'

then it loaded correctly.

It seems that /usr/lib64/gems/ruby/passenger-4.0.18 is not in the ruby load path.

Additional directories can be added to the ruby load path by setting an environment variable, RUBYLIB.

To set RUBYLIB for the apache process, I added the following line to /etc/sysconfig/httpd and restarted apache:

RUBYLIB=/usr/lib64/gems/ruby/passenger-4.0.18

The passenger native libraries now load correctly.

Acquiring a Modern Ruby (Part One)

Last month marked the 21st anniversary of the programming language Ruby. I use Ruby pretty much all the time. If I need to write a command line tool, a web app, pretty much anything, I'll probably start with Ruby. This means I've always got Ruby to hand on whatever machine I'm using. However, in practice, getting a recent Ruby on any platform isn't actually as simple as it sounds. Ruby is a fast-moving language, with frequent releases. Mainstream distributions often lag behind the current release. Let's have a quick look at the history of the releases of Ruby over the last year:

require 'nokogiri'
require 'open-uri'
news = Nokogiri::HTML(open("https://www.ruby-lang.org/en/news/2013/"))
news.xpath("//div[@class='post']/following-sibling::*").each do |item|
  match = item.text.match /Ruby (\S+) is released.*Posted by.*on ((?:\d{1,2} [a-zA-z]{3} \d{4}))/m
  if match
    puts "Ruby #{match[1]} was announced on #{match[2]}"
  end
end

Ruby 2.1.0-rc1 was announced on 20 Dec 2013
Ruby 2.1.0-preview2 was announced on 22 Nov 2013
Ruby 1.9.3-p484 was announced on 22 Nov 2013
Ruby 2.0.0-p353 was announced on 22 Nov 2013
Ruby 2.1.0-preview1 was announced on 23 Sep 2013
Ruby 2.0.0-p247 was announced on 27 Jun 2013
Ruby 1.9.3-p448 was announced on 27 Jun 2013
Ruby 1.8.7-p374 was announced on 27 Jun 2013
Ruby 1.9.3-p429 was announced on 14 May 2013
Ruby 2.0.0-p195 was announced on 14 May 2013
Ruby 2.0.0-p0 was announced on 24 Feb 2013
Ruby 1.9.3-p392 was announced on 22 Feb 2013
Ruby 2.0.0-rc2 was announced on 8 Feb 2013
Ruby 1.9.3-p385 was announced on 6 Feb 2013
Ruby 1.9.3-p374 was announced on 17 Jan 2013

So 15 releases in 2013, including a major version (2.0.0) in February, and a release candidate of 2.1 shortly before Christmas. For a Ruby developer today, the current releases are:

  • Ruby 2.1.1
  • 2.0.0-p451
  • 1.9.3-p545

Let's compare to what's available in Debian stable:

$ apt-cache show ruby
Package: ruby
Source: ruby-defaults
Version: 1:1.9.3
Installed-Size: 31
Maintainer: akira yamada <akira@debian.org>
Architecture: all
Replaces: irb, rdoc
Provides: irb, rdoc
Depends: ruby1.9.1 (>= 1.9.3.194-1)

So the current version in Debian is older than January 2013? What about Ubuntu? The latest 'saucy' offers us 2.0.0. That's fractionally better, but really, it's pretty old. What about my trusty Fedora? At the time of writing, that gives me 2.0.0p353. Still not exactly current, and not a sniff of a 2.1 package. Archlinux offers a Ruby 2.1, but even that's not right up to date:

$ ruby --version
ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-linux]

Now, I will grant that there might be third-party repositories, or backports or other providers of packages which might be more up-to-date, but my experience of this hasn't always been that positive. On the whole, it looks like we need to look somewhere else. On a Unix-derived system, this typically means building from source.

Building From Source

Building Ruby from source isn't difficult, not if you have some idea what you're doing, and can read documentation. However, equally, it's not entirely trivial. It does require you to know how to provide libraries for things like ffi, ncurses, readline, openssl, yaml etc. And, if you're using different systems, they have different names, and you might be working with different compilers and versions of make. In recognition of this, several tools have emerged to shield the complexity, and make it easier to get a modern Ruby on your system. The most popular are RVM, ruby-build, and ruby-install. Let's review each of them, briefly. I'm going to use a CentOS 6 machine as my example Linux machine, but each of these tools will work on all popular Linux distributions and Mac OSX. I've had success on FreeBSD and Solaris too, but I've not tested this recently, so YMMV.

RVM

RVM, the Ruby Version Manager, is the father of the tools designed to make managing modern Ruby versions easier. It's what one might call a monolithic tool - it does a huge range of things all in one place. We'll discuss it several times in this series, as it provides functionality beyond that of simply installing a modern Ruby, but for the purposes of this series, we're only going to use it to install Ruby.

RVM is a shell script. The most popular way to install it is via the fashionable 'curl pipe through bash' approach:

$ curl -sSL https://get.rvm.io | bash
Downloading https://github.com/wayneeseguin/rvm/archive/master.tar.gz
Creating group 'rvm'

Installing RVM to /usr/local/rvm/
Installation of RVM in /usr/local/rvm/ is almost complete:

  * First you need to add all users that will be using rvm to 'rvm' group,
    and logout - login again, anyone using rvm will be operating with `umask u=rwx,g=rwx,o=rx`.

  * To start using RVM you need to run `source /etc/profile.d/rvm.sh`
    in all your open shell windows, in rare cases you need to reopen all shell windows.

# Administrator,
#
#   Thank you for using RVM!
#   We sincerely hope that RVM helps to make your life easier and more enjoyable!!!
#
# ~Wayne, Michal & team.

In case of problems: http://rvm.io/help and https://twitter.com/rvm_io

Hmm, ok, let's try that.

# gpasswd -a sns rvm
Adding user sns to group rvm

I also added:

source /etc/profile.d/rvm.sh

to the end of ~/.bash_profile

$ rvm --version

rvm 1.25.19 (master) by Wayne E. Seguin <wayneeseguin@gmail.com>, Michal Papis <mpapis@gmail.com> [https://rvm.io/]

First let's see what Rubies it knows about:

$ rvm list known
# MRI Rubies
[ruby-]1.8.6[-p420]
[ruby-]1.8.7[-p374]
[ruby-]1.9.1[-p431]
[ruby-]1.9.2[-p320]
[ruby-]1.9.3[-p545]
[ruby-]2.0.0-p353
[ruby-]2.0.0[-p451]
[ruby-]2.1.1
[ruby-]2.1-head
ruby-head

# GoRuby
goruby

# Topaz
topaz

# TheCodeShop - MRI experimental patches
tcs

# jamesgolick - All around gangster
jamesgolick

# Minimalistic ruby implementation - ISO 30170:2012
mruby[-head]

# JRuby
jruby-1.6.8
jruby-1.7.6
jruby-1.7.9
jruby[-1.7.11]
jruby-head

# Rubinius
rbx-2.0.0
rbx-2.1.1
rbx[-2.2.5]
rbx-head

# Ruby Enterprise Edition
ree-1.8.6
ree[-1.8.7][-2012.02]

# Kiji
kiji

# MagLev
maglev[-head]
maglev-1.0.0

# Mac OS X Snow Leopard Or Newer
macruby-0.10
macruby-0.11
macruby[-0.12]
macruby-nightly
macruby-head

# Opal
opal

# IronRuby
ironruby[-1.1.3]
ironruby-head

Wow - that's a lot of Rubies. Let's just constrain ourselves to MRI, and install the latest stable 2.1:

$ rvm install ruby
Searching for binary rubies, this might take some time.
No binary rubies available for: centos/6/x86_64/ruby-2.1.1.
Continuing with compilation. Please read 'rvm help mount' to get more information on binary rubies.
Checking requirements for centos.
Installing requirements for centos.
Updating system.
Installing required packages: patch, libyaml-devel, libffi-devel, glibc-headers, gcc-c++, glibc-devel, patch, readline-devel, zlib-devel, openssl-devel, autoconf, automake, libtool, bisonsns password required for 'yum install -y patch libyaml-devel libffi-devel glibc-headers gcc-c++ glibc-devel patch readline-devel zlib-devel openssl-devel autoconf automake libtool bison':

The first thing to notice, and this is pretty cool, is that RVM will try to locate a precompiled binary. Unfortunately there isn't one for our platform, so it's going to go ahead and install one from source. It's going to install the various required packages, and then crack on.

This assumes that I've got sudo set up for my user. As it happens, I don't, but we can fix that. For a mac or an ubuntu machine this would be in place anyway. Please hold, caller... ...ok done. My sns user now has sudo privileges, and we can continue:

$ rvm install ruby
Searching for binary rubies, this might take some time.
No binary rubies available for: centos/6/x86_64/ruby-2.1.1.
Continuing with compilation. Please read 'rvm help mount' to get more information on binary rubies.
Checking requirements for centos.
Installing requirements for centos.
Updating system.
Installing required packages: patch, libyaml-devel, libffi-devel, glibc-headers, gcc-c++, glibc-devel, patch, readline-devel, zlib-devel, openssl-devel, autoconf, automake, libtool, bisonsns password required for 'yum install -y patch libyaml-devel libffi-devel glibc-headers gcc-c++ glibc-devel patch readline-devel zlib-devel openssl-devel autoconf automake libtool bison':
..........................
Requirements installation successful.
Installing Ruby from source to: /usr/local/rvm/rubies/ruby-2.1.1, this may take a while depending on your cpu(s)...
ruby-2.1.1 - #downloading ruby-2.1.1, this may take a while depending on your connection...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 11.4M  100 11.4M    0     0  61.0M      0 --:--:-- --:--:-- --:--:-- 64.9M
ruby-2.1.1 - #extracting ruby-2.1.1 to /usr/local/rvm/src/ruby-2.1.1.
ruby-2.1.1 - #configuring....................................................
ruby-2.1.1 - #post-configuration.
ruby-2.1.1 - #compiling...................................................................................
ruby-2.1.1 - #installing................................
ruby-2.1.1 - #making binaries executable.
Rubygems 2.2.2 already installed, skipping installation, use --force to reinstall.
ruby-2.1.1 - #gemset created /usr/local/rvm/gems/ruby-2.1.1@global
ruby-2.1.1 - #importing gemset /usr/local/rvm/gemsets/global.gems.....
ruby-2.1.1 - #generating global wrappers.
ruby-2.1.1 - #gemset created /usr/local/rvm/gems/ruby-2.1.1
ruby-2.1.1 - #importing gemsetfile /usr/local/rvm/gemsets/default.gems evaluated to empty gem list
ruby-2.1.1 - #generating default wrappers.
ruby-2.1.1 - #adjusting #shebangs for (gem irb erb ri rdoc testrb rake).
Install of ruby-2.1.1 - #complete
Ruby was built without documentation, to build it run: rvm docs generate-ri

Great - let's see what we have:

$ ruby --version
ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-linux]

In order to verify our installation, we're going to test OpenSSL and Nokogiri. If these two work, we can be pretty confident that we have a functional Ruby:

$ gem install nokogiri --no-ri --no-rdoc
Fetching: mini_portile-0.5.2.gem (100%)
Successfully installed mini_portile-0.5.2
Fetching: nokogiri-1.6.1.gem (100%)
Building native extensions.  This could take a while...
Successfully installed nokogiri-1.6.1
2 gems installed

Let's test this now. Here's a simple test to prove that both SSL and Nokogiri are working:

require 'nokogiri'
require 'open-uri'
require 'openssl'
puts OpenSSL::PKey::RSA.new(512).to_pem
https_url = 'https://google.com'
puts Nokogiri::HTML(open(https_url)).css('input')

If this works, we should see a PEM file printed to screen, and the HTML of various inputs on the Google homepage:

$ ruby ruby_test.rb
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAPyu4h5vvjmWam71iYoS/89gJGfknqekPEPZFIdO7TZeV117t4HO
nhJuyiDf5kNrHFL5KW/NhDf7AGb08MSPKvcCAwEAAQJAA8dJslLRcWMCbG5XhC0M
jjlN0g/lH3ShQhJ48B0KBwoSgRixbQ5EQJdJhlNq7LjADsrifKFzEBMTwWij35Kb
AQIhAP9UeYcEQBdDnrd993C8VuhYc9nvHXHFOcqTcp4HUoGhAiEA/VihZl5KRDua
OvdMcU/IxEtBDth50UQ4X37E3JwylZcCIQCm4jzIPcPyMLVFc2TOX5tiI6YdFIen
O+Ro/nSUy25m4QIhAMS+osEL8qLgopquWGga7LA8PO7ZgGjakOjgWuN0TfhLAiEA
lGlGv6ag4ay7xSZiLDVlngXWElBPALGfNPYrWHMphpo=
-----END RSA PRIVATE KEY-----
<input name="ie" value="ISO-8859-1" type="hidden">
<input value="en-GB" name="hl" type="hidden">
<input name="source" type="hidden" value="hp">
<input autocomplete="off" class="lst" value="" title="Google Search" maxlength="2048" name="q" size="57" style="color:#000;margin:0;padding:5px 8px 0 6px;vertical-align:top">
<input class="lsb" value="Google Search" name="btnG" type="submit">
<input class="lsb" value="I'm Feeling Lucky" name="btnI" type="submit" onclick="if(this.form.q.value)this.checked=1; else top.location='/doodles/'">
<input type="hidden" id="gbv" name="gbv" value="1">

So, RVM was fairly painless. We had to fiddle about with users and sudo, and add a line to our shell profile, but once that was done, we were easily able to install Ruby.

Ruby Build

Ruby-build is a dedicated tool, also written in shell, designed specifically to install Ruby, and provide fine-grained control over the configuration, build and installation. In order to get it we need to install Git. As with RVM, I'll set up a user with sudo access in order to permit this. Once we have Git installed, we can clone the project and install it.

$ git clone https://github.com/sstephenson/ruby-build.git
Initialized empty Git repository in /home/sns/ruby-build/.git/
remote: Reusing existing pack: 3077, done.
remote: Total 3077 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3077/3077), 510.82 KiB | 386 KiB/s, done.
Resolving deltas: 100% (1404/1404), done.
$ cd ruby-build
$ sudo ./install.sh

Like RVM, we can find out what Rubies ruby-build knows about:

$ ruby-build --definitions
1.8.6-p383
1.8.6-p420
1.8.7-p249
1.8.7-p302
1.8.7-p334
1.8.7-p352
1.8.7-p357
1.8.7-p358
1.8.7-p370
1.8.7-p371
1.8.7-p374
1.8.7-p375
1.9.1-p378
1.9.1-p430
1.9.2-p0
1.9.2-p180
1.9.2-p290
1.9.2-p318
1.9.2-p320
1.9.2-p326
1.9.3-dev
1.9.3-p0
1.9.3-p125
1.9.3-p194
1.9.3-p286
1.9.3-p327
1.9.3-p362
1.9.3-p374
1.9.3-p385
1.9.3-p392
1.9.3-p429
1.9.3-p448
1.9.3-p484
1.9.3-p545
1.9.3-preview1
1.9.3-rc1
2.0.0-dev
2.0.0-p0
2.0.0-p195
2.0.0-p247
2.0.0-p353
2.0.0-p451
2.0.0-preview1
2.0.0-preview2
2.0.0-rc1
2.0.0-rc2
2.1.0
2.1.0-dev
2.1.0-preview1
2.1.0-preview2
2.1.0-rc1
2.1.1
2.2.0-dev
jruby-1.5.6
jruby-1.6.3
jruby-1.6.4
jruby-1.6.5
jruby-1.6.5.1
jruby-1.6.6
jruby-1.6.7
jruby-1.6.7.2
jruby-1.6.8
jruby-1.7.0
jruby-1.7.0-preview1
jruby-1.7.0-preview2
jruby-1.7.0-rc1
jruby-1.7.0-rc2
jruby-1.7.1
jruby-1.7.10
jruby-1.7.11
jruby-1.7.2
jruby-1.7.3
jruby-1.7.4
jruby-1.7.5
jruby-1.7.6
jruby-1.7.7
jruby-1.7.8
jruby-1.7.9
jruby-9000-dev
jruby-9000+graal-dev
maglev-1.0.0
maglev-1.1.0-dev
maglev-2.0.0-dev
mruby-1.0.0
mruby-dev
rbx-1.2.4
rbx-2.0.0
rbx-2.0.0-dev
rbx-2.0.0-rc1
rbx-2.1.0
rbx-2.1.1
rbx-2.2.0
rbx-2.2.1
rbx-2.2.2
rbx-2.2.3
rbx-2.2.4
rbx-2.2.5
ree-1.8.6-2009.06
ree-1.8.7-2009.09
ree-1.8.7-2009.10
ree-1.8.7-2010.01
ree-1.8.7-2010.02
ree-1.8.7-2011.03
ree-1.8.7-2011.12
ree-1.8.7-2012.01
ree-1.8.7-2012.02
topaz-dev

Let's opt for ruby 2.1 again. Now, unlike RVM, ruby-build doesn't make any attempt to install development tools suitable to allow Ruby to be built from source. It does provide a wiki with information on what is needed, but if you expect ruby-build to magically install your toolkit, you're going to be disappointed. Additionally, ruby-build requires you to state exactly where you want Ruby to be installed. It doesn't have a default, so you need to work out where it should go. Realistically this boils down to two choices - do you want to try to install it somewhere global, where everyone can see it, or just locally for your own use. The former adds some more complications around permissions and so forth, so in this instance we'll install to a local directory.

First, let's check the wiki for the build dependencies:

$ yum install gcc-c++ glibc-headers openssl-devel readline libyaml-devel readline-devel zlib zlib-devel

Now that's all installed, we can go ahead with building Ruby:

$ ruby-build 2.1.1 ~/local/mri-2.1.1
Downloading ruby-2.1.1.tar.gz...
-> http://dqw8nmjcqpjn7.cloudfront.net/e57fdbb8ed56e70c43f39c79da1654b2
Installing ruby-2.1.1...
Installed ruby-2.1.1 to /home/sns/local/mri-2.1.1

Not a very verbose output... in fact one might be forgiven for wondering at times if anything's happening at all! But, now we have a Ruby, let's add it to our shell path, and do our nokogiri/openssl test.

$ export PATH=$PATH:/home/sns/local/mri-2.1.1/bin/
$ ruby --version
ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-linux]

$ gem install nokogiri --no-rdoc --no-ri
$ vi ruby_test.rb
$ ruby ruby_test.rb
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAMmE1PQWOrX2YUOAJuGWYvUXxCHbNFa8cpUAtSX/evBbWeJsxi/5
PfNlFsMSrVqbrHXmw/StK2E3SMTuINB/0lMCAwEAAQJBAKVZVpqI+td/N8N5DW4g
RUYxn/7frAxXR/U2xWxOOoOoHEZlupt9L5ClZ05TtHG2H4HQ+2aTHcBzEkg6UELX
RjECIQDnU0us05BaDJZ6SqQgj0J5nPerb6gnZwkgLXgCeF8ivwIhAN8Dn2xctLQd
ZdRGt1Y9aUirqtAB3F4PhfEV674inrltAiEAzxOOm8Cr6SB5mDG3KPp1jH5Ka6pB
gjxHPAnX84Yjy+sCIQCbIVYAduxsbhardxWJM+bM2j1TvDeiZoHxliIKfOKUcQIg
BVsL1xcTqgZZM7RWwxe/n/ujThspC8SZvHKaWOLwb2Y=
-----END RSA PRIVATE KEY-----
<input name="ie" value="ISO-8859-1" type="hidden">
<input value="en-GB" name="hl" type="hidden">
<input name="source" type="hidden" value="hp">
<input autocomplete="off" class="lst" value="" title="Google Search" maxlength="2048" name="q" size="57" style="color:#000;margin:0;padding:5px 8px 0 6px;vertical-align:top">
<input class="lsb" value="Google Search" name="btnG" type="submit">
<input class="lsb" value="I'm Feeling Lucky" name="btnI" type="submit" onclick="if(this.form.q.value)this.checked=1; else top.location='/doodles/'">
<input type="hidden" id="gbv" name="gbv" value="1">

So, ruby-build did what it said on the tin. It didn't need us to create a group, or source any files in our profile. All we needed to do was update our shell path, which, of course, we'd add to our shell profile to make permanent. We did have to install the build dependencies manually, but this is documented on the wiki. It's a lighter-weight solution than RVM, optimised for local users.

Ruby Install

The third of our 'install from source' options is 'Ruby Install'. Another shell utility, it is more aligned with 'Ruby Build' than RVM, as it only really has one purpose in life - to install Ruby. It doesn't have all the extra features of RVM, but it does install build dependencies for most platforms.

To install, we obtain a tarball of the latest release:

$ wget -O ruby-install-0.4.0.tar.gz https://github.com/postmodern/ruby-install/archive/v0.4.0.tar.gz
$tar xzvf ruby-install-0.4.0.tar.gz
$ cd ruby-install-0.4.0
$ sudo make install
[sudo] password for sns:
for dir in `find etc lib bin sbin share -type d 2>/dev/null`; do mkdir -p /usr/local/$dir; done
for file in `find etc lib bin sbin share -type f 2>/dev/null`; do cp $file /usr/local/$file; done
mkdir -p /usr/local/share/doc/ruby-install-0.4.0
cp -r *.md *.txt /usr/local/share/doc/ruby-install-0.4.0/

This will make the ruby-install tool available. Again, we can see which Rubies are available:

$ ruby-install
Known ruby versions:
  ruby:
    1:      1.9.3-p484
    1.9:    1.9.3-p484
    1.9.1:  1.9.1-p431
    1.9.2:  1.9.2-p320
    1.9.3:  1.9.3-p484
    2.0:    2.0.0-p353
    2.0.0:  2.0.0-p353
    2:      2.1.0
    2.1:    2.1.0
    stable: 2.1.0
  jruby:
    1.7:    1.7.10
    stable: 1.7.10
  rbx:
    2.1:    2.1.1
    2.2:    2.2.5
    stable: 2.2.5
  maglev:
    1.0:    1.0.0
    1.1:    1.1RC1
    stable: 1.0.0
  mruby:
    1.0:    1.0.0
    stable: 1.0.0

Because we obtained the stable release of the ruby-install tool, we don't have the very latest version available. We could simply do as we did with ruby-build, and get the latest version straight from Git, but the process would be the same. For now, we're going to opt for the latest stable release the tool offers us, which is 2.1.0:

$ ruby-install ruby
>>> Installing ruby 2.1.0 into /home/sns/.rubies/ruby-2.1.0 ...
>>> Installing dependencies for ruby 2.1.0 ...
[sudo] password for sns:
... grisly details ...
<snip>

The install is substantially more verbose than either ruby-build or RVM, showing the grisly details of the compiling and linking. Once installed, let's check the version and run our test:

$ export PATH=$PATH:/home/sns/.rubies/ruby-2.1.0/bin/
$ ruby --version
ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-linux]

$ gem install nokogiri --no-ri --no-rdoc
Building native extensions.  This could take a while...
Successfully installed nokogiri-1.6.1
1 gem installed
$ vi ruby_test.rb
$ ruby ruby_test.rb
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAJtMl+dNUykAmbG5FIJosgaMEobLqZ8fQ7JOPILRks+ehEx/kVEk
JiLLEdoGdWSo5vv68m9mZPCfRn3r3TBe2lMCAwEAAQJBAIqJ6XkOIgGR54oD8afk
fewZSO23AbpRybJm2NRa4P0UqFHnQjurr9MU/Wao+2pTPc459oq4iAIs8uUJb2x5
zAECIQDMtUyp/RIb8hRFPGvK30J0iQYD+xTl0vH2e7bzjU5yIwIhAMI2BvUv/6I5
8OCII0ZAHdQ/JfPmB7YQ+XIehu8BdwIRAiEAkPUxPI6AbFmYEQ8gJaDDJlKBeAAs
ZDJkNO6WNxExI5ECIFYanfsWzL+f8KubsAf1ZUQ0ux7nXYPbAr0Vy70raRgBAiA3
FocBYyjk04tOhCPIFr0Xd+YkBE5zDPJEYZ8ILBbA2A==
-----END RSA PRIVATE KEY-----
<input name="ie" value="ISO-8859-1" type="hidden">
<input value="en-GB" name="hl" type="hidden">
<input name="source" type="hidden" value="hp">
<input autocomplete="off" class="lst" value="" title="Google Search" maxlength="2048" name="q" size="57" style="color:#000;margin:0;padding:5px 8px 0 6px;vertical-align:top">
<input class="lsb" value="Google Search" name="btnG" type="submit">
<input class="lsb" value="I'm Feeling Lucky" name="btnI" type="submit" onclick="if(this.form.q.value)this.checked=1; else top.location='/doodles/'">
<input type="hidden" id="gbv" name="gbv" value="1">

So, ruby-install was pretty easy to use. We didn't need to mess about with groups or sourcing extra files in our shell profile. There was a default, local path for the Rubies, the dependencies were identified and installed for us, and everything worked. The only downside was that, on account of using the stable version of the tool, we didn't get access to the very latest Ruby. That's easily fixed by simply getting the tool straight from Git - which we'll look at when we come to discuss managing multiple versions of Ruby.

Conclusion

So at this stage in our journey, we've explored three approaches to simplifying the process of installing a Ruby from source - RVM, ruby-build and ruby-install. Given that the stated objective was to install a recent version of Ruby, from source, as simply as possible, in my view the order of preference goes:

  1. Ruby Install: This does the simplest thing that could possibly work. It has sane defaults, solves and installs dependencies, and requires nothing more than a path setting.

  2. RVM: This requires only slightly more setup than Ruby Install, and certainly meets the requirements. It's much more heavyweight, as it does many more things than just install Ruby, and these capabilities will come into consideration later in the series.

  3. Ruby Build: This brings up the rear. It works, but it doesn't have sane defaults, and needs extra manual steps to install build dependencies.

As we continue the series, we'll look into ways to manage multiple versions of Ruby on a workstation, strategies for getting the relevant versions of Ruby onto servers, running Ruby under Microsoft Windows, and automating the whole process. Next time we'll talk about Ruby on Windows. Until then, bye for now.