class Isolate::Sandbox
An isolated environment. This class exposes lifecycle events for extension, see Isolate::Events
for more information.
Public Class Methods
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 39 def initialize options = {}, &block @enabled = false @entries = [] @environments = [] @files = [] @options = options path, name = options.values_at :path, :name raise ArgumentError, "can't specify both name and path!" if name && path options[:path] = File.expand_path("~/.gem/repos/#{name}") if name 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 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 84 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
# File lib/isolate/sandbox.rb, line 116 def cleanup? install? and @options.fetch(:cleanup, true) end
# File lib/isolate/sandbox.rb, line 120 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... $LOAD_PATH.reject! { |lpath| (lpath.start_with?("/") && # only full paths lpath.end_with?("/lib") && # and that end in lib lpath != isolate_lib && Gem.path.reject(&:empty?).any? { |gem_path| lpath.include?(gem_path) }) } # 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 install_missing environment rebuild_extensions fire :installed self end def install_missing environment 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 end def rebuild_extensions broken = entries.find_all { |e| e.specification && e.specification.missing_extensions? } unless broken.empty? padding = Math.log10(broken.size).to_i + 1 format = "[%0#{padding}d/%d] Building extensions for %s (ruby v%s)." broken.each_with_index do |e, i| spec = e.specification log format % [i + 1, broken.size, e.name, RUBY_VERSION] builder = Gem::Ext::Builder.new spec builder.build_extensions end Gem::Specification.reset end 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, false end def options options = nil @options.merge! options if options @options end def path base = @options.fetch :path, DEFAULT_PATH if multiruby? then 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
# File lib/isolate/sandbox.rb, line 191 def enabled? @enabled end
Restricts gem
calls inside block
to a set of environments
.
# File lib/isolate/sandbox.rb, line 197 def environment *environments, &block old = @environments @environments = @environments.dup.concat environments.map { |e| e.to_s } instance_eval(&block) ensure @environments = old end
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 212 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.
# File lib/isolate/sandbox.rb, line 222 def index @index ||= Gem::SourceIndex.from_gems_in File.join(path, "specifications") end
# File lib/isolate/sandbox.rb, line 237 def install_missing environment 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 end
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 345 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
# File lib/isolate/sandbox.rb, line 289 def multiruby? @options.fetch :multiruby, false end
# File lib/isolate/sandbox.rb, line 293 def options options = nil @options.merge! options if options @options end
# File lib/isolate/sandbox.rb, line 298 def path base = @options.fetch :path, DEFAULT_PATH if multiruby? then suffix = "#{Gem.ruby_engine}-#{RbConfig::CONFIG['ruby_version']}" base = File.join(base, suffix) unless base =~ /#{suffix}/ end File.expand_path base end
# File lib/isolate/sandbox.rb, line 255 def rebuild_extensions broken = entries.find_all { |e| e.specification && e.specification.missing_extensions? } unless broken.empty? padding = Math.log10(broken.size).to_i + 1 format = "[%0#{padding}d/%d] Building extensions for %s (ruby v%s)." broken.each_with_index do |e, i| spec = e.specification log format % [i + 1, broken.size, e.name, RUBY_VERSION] builder = Gem::Ext::Builder.new spec builder.build_extensions end Gem::Specification.reset end end
# File lib/isolate/sandbox.rb, line 309 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
# File lib/isolate/sandbox.rb, line 330 def system? @options.fetch :system, true end
# File lib/isolate/sandbox.rb, line 334 def verbose? @options.fetch :verbose, true end