class Isolate::Sandbox

An isolated environment. This class exposes lifecycle events for extension, see Isolate::Events for more information.

Public Class Methods

new(options = {}) click to toggle source

Create a new Isolate::Sandbox instance. See Isolate.now! for the most common use of the API. You probably don't want to use this constructor directly. Fires :initializing and :initialized.

# File lib/isolate/sandbox.rb, line 28
def initialize options = {}, &block
  @enabled      = false
  @entries      = []
  @environments = []
  @files        = []
  @options      = options

  fire :initializing

  user = File.expand_path "~/.isolate/user.rb"
  load user if File.exist? user

  file, local = nil

  unless FalseClass === options[:file]
    file  = options[:file] || Dir["{Isolate,config/isolate.rb}"].first
    local = "#{file}.local" if file
  end

  load file if file

  if block_given?
    /\@(.+?):\d+/ =~ block.to_s
    files << ($1 || "inline block")
    instance_eval(&block)
  end

  load local if local && File.exist?(local)
  fire :initialized
end

Public Instance Methods

activate(environment = nil) click to toggle source

Activate this set of isolated entries, respecting an optional environment. Points RubyGems to a separate repository, messes with paths, auto-installs gems (if necessary), activates everything, and removes any superfluous gem (again, if necessary). If environment isn't specified, ISOLATE_ENV, RAILS_ENV, and RACK_ENV are checked before falling back to "development". Fires :activating and :activated.

# File lib/isolate/sandbox.rb, line 68
def activate environment = nil
  enable unless enabled?
  fire :activating

  env = (environment || Isolate.env).to_s

  install env if install?

  entries.each do |e|
    e.activate if e.matches? env
  end

  cleanup if cleanup?
  fire :activated

  self
end
cleanup?() click to toggle source
# File lib/isolate/sandbox.rb, line 100
def cleanup?
  install? and @options.fetch(:cleanup, true)
end
disable() { |ensure enable end| ... } click to toggle source
# File lib/isolate/sandbox.rb, line 104
  def disable &block
    return self if not enabled?
    fire :disabling

    ENV.replace @old_env
    $LOAD_PATH.replace @old_load_path

    @enabled = false

    Isolate.refresh
    fire :disabled

    begin; return yield ensure enable end if block_given?

    self
  end

  def enable # :nodoc:
    return self if enabled?
    fire :enabling

    @old_env       = ENV.to_hash
    @old_load_path = $LOAD_PATH.dup

    path = self.path

    FileUtils.mkdir_p path
    ENV["GEM_HOME"] = path

    unless system?
      isolate_lib = File.expand_path "../..", __FILE__

      # manually deactivate pre-isolate gems... is this just for 1.9.1?
      $LOAD_PATH.reject! do |p|
        p != isolate_lib && Gem.path.any? { |gp| p.include?(gp) }
      end

      # HACK: Gotta keep isolate explicitly in the LOAD_PATH in
      # subshells, and the only way I can think of to do that is by
      # abusing RUBYOPT.

      unless ENV["RUBYOPT"] =~ /\s+-I\s*#{Regexp.escape isolate_lib}\b/
        ENV["RUBYOPT"] = "#{ENV['RUBYOPT']} -I#{isolate_lib}"
      end

      ENV["GEM_PATH"] = path
    end

    bin = File.join path, "bin"

    unless ENV["PATH"].split(File::PATH_SEPARATOR).include? bin
      ENV["PATH"] = [bin, ENV["PATH"]].join File::PATH_SEPARATOR
    end

    ENV["ISOLATED"] = path

    if system? then
      Gem.path.unshift path # HACK: this is just wrong!
      Gem.path.uniq!        # HACK: needed for the previous line :(
    end
    Isolate.refresh

    @enabled = true
    fire :enabled

    self
  end

  def enabled?
    @enabled
  end

  # Restricts +gem+ calls inside +block+ to a set of +environments+.

  def environment *environments, &block
    old = @environments
    @environments = @environments.dup.concat environments.map { |e| e.to_s }

    instance_eval(&block)
  ensure
    @environments = old
  end

  alias_method :env, :environment

  # Express a gem dependency. Works pretty much like RubyGems' +gem+
  # method, but respects +environment+ and doesn't activate 'til
  # later.

  def gem name, *requirements
    entry = entries.find { |e| e.name == name }
    return entry.update(*requirements) if entry

    entries << entry = Entry.new(self, name, *requirements)
    entry
  end

  # A source index representing only isolated gems.

  def index
    @index ||= Gem::SourceIndex.from_gems_in File.join(path, "specifications")
  end

  def install environment # :nodoc:
    fire :installing

    installable = entries.select do |e|
      !e.specification && e.matches?(environment)
    end

    unless installable.empty?
      padding = Math.log10(installable.size).to_i + 1
      format  = "[%0#{padding}d/%s] Isolating %s (%s)."

      installable.each_with_index do |entry, i|
        log format % [i + 1, installable.size, entry.name, entry.requirement]
        entry.install
      end

      Gem::Specification.reset
    end

    fire :installed

    self
  end

  def install? # :nodoc:
    @options.fetch :install, true
  end

  def load file # :nodoc:
    files << file
    instance_eval IO.read(file), file, 1
  end

  def log s # :nodoc:
    $stderr.puts s if verbose?
  end

  def multiruby?
    @options.fetch :multiruby, true
  end

  def options options = nil
    @options.merge! options if options
    @options
  end

  def path
    base = @options.fetch :path, DEFAULT_PATH

    unless @options.key?(:multiruby) && @options[:multiruby] == false
      suffix = "#{Gem.ruby_engine}-#{RbConfig::CONFIG['ruby_version']}"
      base   = File.join(base, suffix) unless base =~ /#{suffix}/
    end

    File.expand_path base
  end

  def remove(*extra)
    unless extra.empty?
      padding = Math.log10(extra.size).to_i + 1
      format  = "[%0#{padding}d/%s] Nuking %s."

      extra.each_with_index do |e, i|
        log format % [i + 1, extra.size, e.full_name]

        Gem::DefaultUserInteraction.use_ui Gem::SilentUI.new do
          uninstaller =
            Gem::Uninstaller.new(e.name,
                                 :version     => e.version,
                                 :ignore      => true,
                                 :executables => true,
                                 :install_dir => e.base_dir)
          uninstaller.uninstall
        end
      end
    end
  end

  def system?
    @options.fetch :system, true
  end

  def verbose?
    @options.fetch :verbose, true
  end

  private

  # Returns a list of Gem::Specification instances that 1. exist in
  # the isolated gem path, and 2. are allowed to be there. Used in
  # cleanup. It's only an external method 'cause recursion is
  # easier.

  def legitimize! deps = entries
    specs = []

    deps.flatten.each do |dep|
      spec = case dep
             when Gem::Dependency then
               begin
                 dep.to_spec
               rescue Gem::LoadError
                 nil
               end
             when Isolate::Entry then
               dep.specification
             else
               raise "unknown dep: #{dep.inspect}"
             end

      if spec then
        specs.concat legitimize!(spec.runtime_dependencies)
        specs << spec
      end
    end

    specs.uniq
  end

  dep_module = defined?(Gem::Deprecate) ? Gem::Deprecate : Deprecate
  extend dep_module
  deprecate :index, :none, 2011, 11
end
enabled?() click to toggle source
# File lib/isolate/sandbox.rb, line 172
def enabled?
  @enabled
end
environment(*environments, &block) click to toggle source

Restricts gem calls inside block to a set of environments.

# File lib/isolate/sandbox.rb, line 178
def environment *environments, &block
  old = @environments
  @environments = @environments.dup.concat environments.map { |e| e.to_s }

  instance_eval(&block)
ensure
  @environments = old
end
gem(name, *requirements) click to toggle source

Express a gem dependency. Works pretty much like RubyGems' gem method, but respects environment and doesn't activate 'til later.

# File lib/isolate/sandbox.rb, line 193
def gem name, *requirements
  entry = entries.find { |e| e.name == name }
  return entry.update(*requirements) if entry

  entries << entry = Entry.new(self, name, *requirements)
  entry
end
index() click to toggle source

A source index representing only isolated gems.

# File lib/isolate/sandbox.rb, line 203
def index
  @index ||= Gem::SourceIndex.from_gems_in File.join(path, "specifications")
end
legitimize!(deps = entries) click to toggle source

Returns a list of Gem::Specification instances that 1. exist in the isolated gem path, and 2. are allowed to be there. Used in cleanup. It's only an external method 'cause recursion is easier.

# File lib/isolate/sandbox.rb, line 300
def legitimize! deps = entries
  specs = []

  deps.flatten.each do |dep|
    spec = case dep
           when Gem::Dependency then
             begin
               dep.to_spec
             rescue Gem::LoadError
               nil
             end
           when Isolate::Entry then
             dep.specification
           else
             raise "unknown dep: #{dep.inspect}"
           end

    if spec then
      specs.concat legitimize!(spec.runtime_dependencies)
      specs << spec
    end
  end

  specs.uniq
end
multiruby?() click to toggle source
# File lib/isolate/sandbox.rb, line 244
def multiruby?
  @options.fetch :multiruby, true
end
options(options = nil) click to toggle source
# File lib/isolate/sandbox.rb, line 248
def options options = nil
  @options.merge! options if options
  @options
end
path() click to toggle source
# File lib/isolate/sandbox.rb, line 253
def path
  base = @options.fetch :path, DEFAULT_PATH

  unless @options.key?(:multiruby) && @options[:multiruby] == false
    suffix = "#{Gem.ruby_engine}-#{RbConfig::CONFIG['ruby_version']}"
    base   = File.join(base, suffix) unless base =~ /#{suffix}/
  end

  File.expand_path base
end
remove(*extra) click to toggle source
# File lib/isolate/sandbox.rb, line 264
def remove(*extra)
  unless extra.empty?
    padding = Math.log10(extra.size).to_i + 1
    format  = "[%0#{padding}d/%s] Nuking %s."

    extra.each_with_index do |e, i|
      log format % [i + 1, extra.size, e.full_name]

      Gem::DefaultUserInteraction.use_ui Gem::SilentUI.new do
        uninstaller =
          Gem::Uninstaller.new(e.name,
                               :version     => e.version,
                               :ignore      => true,
                               :executables => true,
                               :install_dir => e.base_dir)
        uninstaller.uninstall
      end
    end
  end
end
system?() click to toggle source
# File lib/isolate/sandbox.rb, line 285
def system?
  @options.fetch :system, true
end
verbose?() click to toggle source
# File lib/isolate/sandbox.rb, line 289
def verbose?
  @options.fetch :verbose, true
end