require "fileutils" require "isolate/entry" require "isolate/events" require "rbconfig" require "rubygems/defaults" require "rubygems/uninstaller" require "rubygems/deprecate" # disable default gems getting in the way of everything. class Gem::BasicSpecification # :nodoc: class << self alias old_default_specifications_dir default_specifications_dir end def self.default_specifications_dir # :nodoc: "" end end module Isolate # An isolated environment. This class exposes lifecycle events for # extension, see Isolate::Events for more information. class Sandbox include Events DEFAULT_PATH = "tmp/isolate" # :nodoc: attr_reader :entries # :nodoc: attr_reader :environments # :nodoc: attr_reader :files # :nodoc: # 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. 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 # 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. 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 def cleanup # :nodoc: fire :cleaning gem_dir = Gem.dir global, local = Gem::Specification.partition { |s| s.base_dir != gem_dir } legit = legitimize! extra = (local - legit) + (local & global) self.remove(*extra) fire :cleaned end def cleanup? install? and @options.fetch(:cleanup, true) end 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! { |path| (path.start_with?("/") && # only full paths path.end_with?("/lib") && # and that end in lib path != isolate_lib && Gem.path.reject(&:empty?).any? { |gem_path| path.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 end