Page 100 of 169

Creating own small ruby integration server – Part 2 – Handling Rubies and gemsets management easier (from Ruby)

Today we will create gemsets management stuff and we'll also make Ruby management easier. As previously, lets start from "low-level" bash scripts. Place them somewhere in your app (example: app_root/sript/tasks/rvm) and make them executable (chmod 755).

List available gemsets from a selected Ruby

#!/bin/bash       

# Returns list of selected ruby gemsets
# Requires ruby version as parameter
#
# Example: ./rvm_gemset_list.sh 1.9.2-p0

die () {
    echo >&2 "$@"
    exit 1
}

[ "$#" -gt 0 ] || die "Minimum 1 parameter required! $# provided"

ruby_version="$1"

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}

perform_task(){
  init_rvm
  rvm use $ruby_version > /dev/null
  rvm gemset list
}

perform_task

Creating gemset in selected Ruby

#!/bin/bash       

# Creates gemset in a given Ruby version
# Parameters:
#    1) Ruby version
#    2) Gemset name
#
# Example: ./rvm_gemset_list.sh 1.9.2-p0 example_gemset

die () {
    echo >&2 "$@"
    exit 1
}

[ "$#" -gt 1 ] || die "Minimum 2 parameters required! $# provided"

ruby_version="$1"
gemset_name="$2"

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}


perform_task(){
  init_rvm
  rvm use $ruby_version > /dev/null
  rvm gemset create $gemset_name
}

perform_task

Removing gemset from selected Ruby version

#!/bin/bash       

# Returns list of selected ruby gemsets
# Parameters:
#    1) Ruby version
#    2) Gemset name
#
# Example: ./rvm_gemset_list.sh 1.9.2-p0 example_gemset


die () {
    echo >&2 "$@"
    exit 1
}

[ "$#" -gt 0 ] || die "Minimum 1 parameter required! $# provided"

ruby_version="$1"
gemset_name="$2"

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}


perform_task(){
  init_rvm
  rvm use $ruby_version > /dev/null
  rvm --force gemset delete $gemset_name
}

perform_task

Creating RVM class

Ok, so we can now install/uninstall Rubies and create/destroy gemsets. Let's wrap it with some Ruby magic! We'll start from creating RVM class. It will represent RVM in our application:

class RVM

  class RVMNotInstalled < StandardError; end

  # Check if RVM is installed
  def self.installed?
    c = Script.new('rvm/list')
    res = c.run
    !res.include?('nie znale') && !res.include?('not f')
  rescue
    false
  end

  def self.check_for_rvm
    raise RVMNotInstalled unless self.installed?
  end

end

If RVM is not installed running list script will return something like "command not found". Thanks to this, we can check whether or not RVM is installed.

Let's make things better!

Ok, now the better stuff. Ruby stuff :) Each Ruby instance should have it's own representation in our application (not in AR - will wrap it with AR in next part of this tutorial):

class RVM

  class Ruby

    class UnknownRubyVersion          < StandardError; end
    class GemsetAlreadyExists         < StandardError; end
    class GemsetDoesNotExist          < StandardError; end
    class RubyVersionAlreadyInstalled < StandardError; end

    attr_reader :version
    attr_accessor :gemset

  end
end

Exceptions description:

  • UnknownRubyVersion - will be raised when trying ti install/uninstall unknown (not existing) Ruby version
  • GemsetAlreadyExists - when trying to install gemset which already exists
  • GemsetDoesNotExist - when trying to remove non existing gemset
  • RubyVersionAlreadyInstalled - when trying to install already installed Ruby version

Install/Uninstall Ruby

Now methods from RVM::Ruby:

class RVM

  class Ruby

    def self.install(ruby, log_path, lock_id)
      raise RubyVersionAlreadyInstalled, ruby if self.list.include?(ruby)
      raise UnknownRubyVersion, ruby unless self.known.include?(ruby)
      c = Script.new('rvm/install', ruby, log_path, lock_id)
      c.ignite
    end

    def self.uninstall(ruby, log_path)
      raise UnknownRubyVersion, ruby unless self.list.include?(ruby)
      c = Script.new('rvm/uninstall', ruby, log_path)
      c.run
    end

  end

end

Other helpful class methods

class RVM

  class Ruby
    def self.selected
      RVM.check_for_rvm
      c = Script.new('rvm/list')
      list = c.run
      #list.delete!('rvm rubies')
      list = list.split("\n")
      selected = false
      list.each do |ruby|
        selected = ruby.delete('=> ').strip if ruby.include?('=> ')
      end
      selected.split('[').first
    end

    # Returns list of rvm rubies installed
    def self.list
      RVM.check_for_rvm

      c = Script.new('rvm/list')
      list = c.run
      #list.delete!('rvm rubies')
      list = list.split("\n")
      rubies = []
      list.each do |ruby|
        ruby = ruby.delete('=> ').strip
        next if ruby == 'rvmrubies' || ruby == ""
        rubies << ruby.split('[').first
      end
      rubies
    end

    # Returns list of known rubies
    def self.known
      RVM.check_for_rvm

      c = Script.new('rvm/known')
      list = c.run
      rubies = []
      list.split("\n").each do |r|
        next if r.include?('#')
        next if r.include?('iron')
        next if r.include?('macruby')
        next if r == ''
        r+='-head' unless r.include?('-')
        rubies << r.strip.delete('[').delete(']')
      end
      rubies
    end
  end

end

RVM::Ruby Instance methods

Ok, so now we have a bunch of useful class methods, however we still cannot manipulate on a single Ruby instance stuff such as gemsets. But, not for long. Lets start with initialization of an RVM::Ruby instance:

    def initialize(ruby, gemset = nil)
      RVM.check_for_rvm
      @version = ruby
      @gemset = gemset
      raise UnknownRubyVersion, ruby unless self.class.list.include?(ruby)
      if @gemset
        raise GemsetDoesNotExist, @gemset unless gemsets.include?(@gemset)
      end
    end

Now it is time for gemsets management:

    # Show all gemsets available in this ruby version
    def gemsets
      c = Script.new("rvm/gemsets", version)
      list = c.run
      list = list.split("\n")
      gemsets = []
      list[1..-1].each do |g|
        next if g.include?('gemsets for')
        g.delete!('=> ')
        gemsets << g.strip
      end
      gemsets
    end

    def gemset_selected
      unless @gemset
        c = Script.new("/rvm/gemsets", version)
        list = c.run
        list = list.split("\n")
        selected = nil
        list[1..-1].each do |g|
          next if g.include?('gemsets for')
          selected = g.strip if g.include?('=> ')
        end
        @gemset = selected
      end
      @gemset
    end

    def create_gemset(name)
      raise GemsetAlreadyExists, name if gemsets.include?(name)
      c = Script.new("rvm/gemset_create", version, name)
      c.run
      true
    end

    def delete_gemset(name)
      raise GemsetDoesNotExist, name unless gemsets.include?(name)
      c = Script.new("rvm/gemset_delete", version, name)
      c.run
      true
    end

and one additional method:

    def full_name
      "#{version}@#{gemset}"
    end

That's all for now! Managing RVM stuff now is much easier, for example we can do something like this:

RVM::Ruby.install('ruby-1.9.2-head', '/var/log/ruby_install.log', 1234)
# wait till installation ends
ruby = RVM::Ruby.new('ruby-1.9.2-head')
ruby.create_gemset('my_gemset')

Tutorial parts

  1. RVM Ruby version management directly from… Ruby
  2. Handling Rubies and gemsets management easier (from Ruby)

Creating own small ruby integration server – Part 1 – RVM Ruby version management directly from… Ruby

Introduction

As some of you might know, around one year ago I've developed small (around 1k PHP and bash code lines) integration software, to test Rails based apps "outside" of my own computer. I hate to wait when CPU is around 100% and it slows down my dev environment. Moving tests (for example tests of whole application after bigger re-factoring) to a different machine is really convenient:

  1. Don't need to wait when developing
  2. On a "non-gui" machine tests will probably run faster
  3. Everything can be "clicked" via web interface so it is fast
  4. Tests history and code coverage history available when needed

However my software had some disadvantages:

  1. PHP based
  2. Bad GUI (not working properly all the time)
  3. No Ruby versions management
  4. No gemsets (everything stored in one gemset per Ruby version)
  5.  No locks on applications - could start same tests more then one time simultaneously
  6. Messy output storage (needed to refer directly to files with output (not in GUI))
  7. Other annoying bugs ;)

Despite the disadvantages of this software it served me quite well. However I've decided to rewrite it to Rails to speed up development. I know about Hudson and other continuous integration servers, however I like to do stuff on my own when I can. That's why I've decided to write my own small ruby integration server. I'll be posting here tips and source codes so you can build your own dedicated server software. I am writing this at the same time as I create this software, so probably sometimes it will be inconsistent - sorry ;)

One Ruby to rule them all!

Ok - lets start! The basic purpose of this software is to test applications with different Ruby versions (REE, 1.9.2, 1.8.7, etc). But how to manage Rubies from Ruby? We need to (well maybe we don't need to, but it is idea I came up with) create some bash scripts and then we will enclose them with Ruby code.

We need to be able to do following things with our Ruby code:

  1. List Rubies
  2. List available Rubies
  3. Install Rubies
  4. Uninstall Rubies

Listing RVM Rubies

We will be creating some bash scripts and then execute them from our application. Lets start from listing Rubies:

#!/bin/bash       

# Returns list of RVM Rubies installed

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}

perform_task(){
  init_rvm
  rvm list
}
perform_task

Nothing special - now something more fancy.

Installing RVM Rubies

#!/bin/bash       

# Install selected Ruby version
# 3 parameters required:
#   1) RVM ruby version to be installed
#   2) Absolute path to place (file) where save output
#   3) Unique id for lock file name

die () {
    echo >&2 "$@"
    exit 1
}

[ "$#" -gt 2 ] || die "Minimum 3 parameter required! $# provided"

ruby_version="$1"
output_path="$2"
lock_file="/tmp/$3_rvm_install.lock"

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}

perform_task(){
  init_rvm
  touch $lock_file
  rvm install $ruby_version
  rm $lock_file
}

perform_task | tee $output_path

Why use tee instead of redirecting stream? Well I wanted to save output into a file but also I would like to be able to fetch it directly in Ruby. Of course this task can take a lot of time, so it will be ignited in background (how to do so - later ;) ). All the scripts parameters will come directly from application. We put a lock when we start to install and we will remove it after it's done, so we will know when everything started and ended.

Uninstalling Ruby versions

This script is similar to the one above:

#!/bin/bash       

# Uninstall selected Ruby version
# Parameters:
#  1) Ruby version to be uninstalled
#  2) Output file path

die () {
    echo >&2 "$@"
    exit 1
}

[ "$#" -gt 1 ] || die "Minimum 2 parameter required! $# provided"

ruby_version="$1"
output_path="$2"

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}

perform_task(){
  init_rvm
  rvm uninstall $ruby_version
}

perform_task | tee $output_path

Uninstalling is quite fast so we don't need to add lock mechanism. We will run script and wait till it's finished (will not be executed in background).

List available Rubies

#!/bin/bash       

# Returns list of RVM Rubies installed

init_rvm(){
  if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
    source "$HOME/.rvm/scripts/rvm"
  elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
    source "/usr/local/rvm/scripts/rvm"
  else
    printf "ERROR: An RVM installation was not found.\n"
  fi
}


perform_task(){
  init_rvm
  rvm list known
}

perform_task

Executing bash scripts in Ruby

Store those scripts somewhere in your app (for example in script/tasks/rvm) and make them all executable (chmod 755 ./scriptname). You can also test them however they work fine :)

Executing system commands in Ruby is easy (if you're not familiar with this topic read this and this).

I will be using two commands:

  • system - will run command and wait till it's done
  • `command` - will run command in a sub shell so we don't need to wait till it's done

Let's create a class to manage commands executing:

# Will execute system command as a user
# If use_sudo set to true - will use sudo su - user -c syntax
class Command
  attr_reader :command

  def initialize(command, *p)
    @parameters = p
    @prefix = prefix
    @command = "#{@prefix} #{exec_command(command)}"
  end

  # Ignites command (will start it but will not wait till its ended
  def ignite
    IO.popen(@command+parameters)
  end

  # Will run command and return result
  def run
    `#{@command+parameters}`
  end

  private

  def parameters
    out = ''
    @parameters.each {|p| out += " #{p}" }
    out
  end

  def exec_command(command)
    command
  end

  def prefix
    user = 'user'
    use_sudo = false
    "#{"sudo su - #{user} -c " if use_sudo}"
  end

end

It just wraps executing into a nice small class. This piece of code is easy - the only "weird" stuff can be this:

sudo su - #{user} -c 

This syntax can be helpful (but it is not safe at all!!!!) when we want to execute code as a different user than a current one.

Lets test it:

c1 = Command.new('ls')
c1.run => "command.rb\nrvm.rb\nscript.rb\n"

# We can add some parameters
c2 = Command.new('du', '-sh')
c2.run => "132K\t.\n"

# Start application
c3 = Command.new('gedit')
c3.ignite => #<IO:fd 3>

If you still don't understand difference between run and ignite try running following stuff and watch console:

c1 = Command.new('gedit')
c1.run
# Don't close Gedit and see script console
c1.ignite

So now we can execute our scripts like this:

c = Command.new(File.join(Rails.root, path, script.sh), *params)
c.run / c.ignite

But writing this all the time can be inconvenient so let's wrap it with an another class:

class Script < Command

  private

  def exec_command(command)
    File.join(Rails.root, 'script', 'tasks', "#{command}.sh")
  end

end

Now we can execute our RVM scripts much easier:

s = Script.new('ruby_install', '1.9.2-p0', '/path/rvm.log', 12345)
s.ignite

Still it is "low level" and we need to make it better, but we will do this in a next part of this tutorial.

In next part we will create gemsets management stuff:

RVM::Ruby.create_gemset('name')
RVM::Ruby.delete_gemset('name')

Tutorial parts

  1. RVM Ruby version management directly from... Ruby
  2. Handling Rubies and gemsets management easier (from Ruby)

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑