isolate 1.10.2 → 2.0.0.pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +28 -0
- data/Manifest.txt +8 -0
- data/README.rdoc +29 -39
- data/Rakefile +3 -0
- data/lib/hoe/isolate.rb +3 -3
- data/lib/isolate.rb +20 -261
- data/lib/isolate/entry.rb +107 -0
- data/lib/isolate/now.rb +2 -0
- data/lib/isolate/rake.rb +35 -10
- data/lib/isolate/sandbox.rb +256 -0
- data/test/fixtures/blort-0.0.gem +0 -0
- data/test/fixtures/override.rb +1 -0
- data/test/fixtures/override.rb.local +3 -0
- data/test/isolate/test.rb +53 -0
- data/test/test_isolate.rb +11 -241
- data/test/test_isolate_entry.rb +86 -0
- data/test/test_isolate_sandbox.rb +275 -0
- metadata +28 -12
@@ -0,0 +1,107 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rubygems/command"
|
3
|
+
require "rubygems/dependency_installer"
|
4
|
+
require "rubygems/requirement"
|
5
|
+
require "rubygems/version"
|
6
|
+
|
7
|
+
module Isolate
|
8
|
+
|
9
|
+
# An isolated Gem, with requirement, environment restrictions, and
|
10
|
+
# installation options. Internal use only.
|
11
|
+
|
12
|
+
class Entry
|
13
|
+
|
14
|
+
# Which environments does this entry care about? Generally an
|
15
|
+
# Array of Strings. An empty array means "all", not "none".
|
16
|
+
|
17
|
+
attr_reader :environments
|
18
|
+
|
19
|
+
# What's the name of this entry? Generally the name of a gem.
|
20
|
+
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
# Extra information or hints for installation. See +initialize+
|
24
|
+
# for well-known keys.
|
25
|
+
|
26
|
+
attr_reader :options
|
27
|
+
|
28
|
+
# What version of this entry is required? Expressed as a
|
29
|
+
# Gem::Requirement, which see.
|
30
|
+
|
31
|
+
attr_reader :requirement
|
32
|
+
|
33
|
+
# Create a new entry. Takes +sandbox+ (currently an instance of
|
34
|
+
# Isolate), +name+ (as above), and any number of optional version
|
35
|
+
# requirements (generally Strings). Options can be passed as a
|
36
|
+
# trailing hash. FIX: document well-known keys.
|
37
|
+
|
38
|
+
def initialize sandbox, name, *requirements
|
39
|
+
@environments = []
|
40
|
+
@file = nil
|
41
|
+
@name = name
|
42
|
+
@options = {}
|
43
|
+
@requirement = Gem::Requirement.default
|
44
|
+
@sandbox = sandbox
|
45
|
+
|
46
|
+
if /\.gem$/ =~ @name && File.file?(@name)
|
47
|
+
@file = File.expand_path @name
|
48
|
+
|
49
|
+
@name = File.basename(@file, ".gem").
|
50
|
+
gsub(/-#{Gem::Version::VERSION_PATTERN}$/, "")
|
51
|
+
end
|
52
|
+
|
53
|
+
update(*requirements)
|
54
|
+
end
|
55
|
+
|
56
|
+
def activate
|
57
|
+
Gem.activate name, *requirement.as_list
|
58
|
+
end
|
59
|
+
|
60
|
+
# Install this entry in the sandbox.
|
61
|
+
|
62
|
+
def install
|
63
|
+
old = Gem.sources.dup
|
64
|
+
|
65
|
+
begin
|
66
|
+
cache = File.join @sandbox.path, "cache"
|
67
|
+
|
68
|
+
installer = Gem::DependencyInstaller.new :development => false,
|
69
|
+
:generate_rdoc => false, :generate_ri => false,
|
70
|
+
:install_dir => @sandbox.path
|
71
|
+
|
72
|
+
Gem.sources += Array(options[:source]) if options[:source]
|
73
|
+
Gem::Command.build_args = Array(options[:args]) if options[:args]
|
74
|
+
|
75
|
+
installer.install @file || name, requirement
|
76
|
+
ensure
|
77
|
+
Gem.sources = old
|
78
|
+
Gem::Command.build_args = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Is this entry interested in +environment+?
|
83
|
+
|
84
|
+
def matches? environment
|
85
|
+
environments.empty? || environments.include?(environment)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Is this entry satisfied by +spec+ (generally a
|
89
|
+
# Gem::Specification)?
|
90
|
+
|
91
|
+
def matches_spec? spec
|
92
|
+
name == spec.name and requirement.satisfied_by? spec.version
|
93
|
+
end
|
94
|
+
|
95
|
+
# Updates this entry's environments, options, and
|
96
|
+
# requirement. Environments and options are merged, requirement is
|
97
|
+
# replaced.
|
98
|
+
|
99
|
+
def update *reqs
|
100
|
+
@environments |= @sandbox.environments
|
101
|
+
@options.merge! reqs.pop if Hash === reqs.last
|
102
|
+
@requirement = Gem::Requirement.new reqs unless reqs.empty?
|
103
|
+
|
104
|
+
self
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/isolate/now.rb
CHANGED
data/lib/isolate/rake.rb
CHANGED
@@ -1,18 +1,43 @@
|
|
1
1
|
namespace :isolate do
|
2
|
-
desc "
|
3
|
-
task :
|
4
|
-
|
2
|
+
desc "Show current isolated environment."
|
3
|
+
task :debug do
|
4
|
+
require "pathname"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
sandbox = Isolate.sandbox
|
7
|
+
here = Pathname Dir.pwd
|
8
|
+
path = Pathname(sandbox.path).relative_path_from here
|
9
|
+
files = sandbox.files.map { |f| Pathname(f) }
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
puts
|
12
|
+
puts " sandbox: #{path}"
|
13
|
+
puts " env: #{Isolate.env}"
|
13
14
|
|
14
|
-
|
15
|
+
files.collect! { |f| f.absolute? ? f.relative_path_from(here) : f }
|
16
|
+
puts " files: #{files.join ', '}"
|
17
|
+
puts
|
18
|
+
|
19
|
+
%w(cleanup? enabled? install? multiruby? system? verbose?).each do |flag|
|
20
|
+
printf "%10s %s\n", flag, sandbox.send(flag)
|
21
|
+
end
|
22
|
+
|
23
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
24
|
+
sandbox.entries.each { |e| grouped[e.environments] << e }
|
25
|
+
|
26
|
+
puts
|
27
|
+
|
28
|
+
grouped.keys.sort.each do |envs|
|
29
|
+
title = "all environments" if envs.empty?
|
30
|
+
title ||= envs.join ", "
|
31
|
+
|
32
|
+
puts "[#{title}]"
|
33
|
+
|
34
|
+
grouped[envs].each do |e|
|
35
|
+
gem = "gem #{e.name}, #{e.requirement}"
|
36
|
+
gem << ", #{e.options.inspect}" unless e.options.empty?
|
37
|
+
puts gem
|
15
38
|
end
|
39
|
+
|
40
|
+
puts
|
16
41
|
end
|
17
42
|
end
|
18
43
|
|
@@ -0,0 +1,256 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "isolate/entry"
|
3
|
+
require "rbconfig"
|
4
|
+
require "rubygems/uninstaller"
|
5
|
+
|
6
|
+
module Isolate
|
7
|
+
class Sandbox
|
8
|
+
attr_reader :entries # :nodoc:
|
9
|
+
attr_reader :environments # :nodoc:
|
10
|
+
attr_reader :files # :nodoc:
|
11
|
+
|
12
|
+
# Create a new Isolate instance. See Isolate.gems for the public
|
13
|
+
# API. You probably don't want to use this constructor directly.
|
14
|
+
|
15
|
+
def initialize options = {}, &block
|
16
|
+
@enabled = false
|
17
|
+
@entries = []
|
18
|
+
@environments = []
|
19
|
+
@files = []
|
20
|
+
@options = options
|
21
|
+
|
22
|
+
path options.fetch(:path, "tmp/isolate")
|
23
|
+
|
24
|
+
file, local = nil
|
25
|
+
|
26
|
+
unless FalseClass === options[:file]
|
27
|
+
file = options[:file] || Dir["{Isolate,config/isolate.rb}"].first
|
28
|
+
local = "#{file}.local" if file
|
29
|
+
end
|
30
|
+
|
31
|
+
load file if file
|
32
|
+
|
33
|
+
if block_given?
|
34
|
+
block.to_s =~ /\@([^:]+):/
|
35
|
+
files << ($1 || "inline block")
|
36
|
+
instance_eval(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
load local if local && File.exist?(local)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Activate this set of isolated entries, respecting an optional
|
43
|
+
# +environment+. Points RubyGems to a separate repository, messes
|
44
|
+
# with paths, auto-installs gems (if necessary), activates
|
45
|
+
# everything, and removes any superfluous gem (again, if
|
46
|
+
# necessary). If +environment+ isn't specified, +ISOLATE_ENV+,
|
47
|
+
# +RAILS_ENV+, and +RACK_ENV+ are checked before falling back to
|
48
|
+
# <tt>"development"</tt>.
|
49
|
+
|
50
|
+
def activate environment = nil
|
51
|
+
enable unless enabled?
|
52
|
+
|
53
|
+
env = (environment || Isolate.env).to_s
|
54
|
+
|
55
|
+
install env if install?
|
56
|
+
|
57
|
+
entries.each do |e|
|
58
|
+
e.activate if e.matches? env
|
59
|
+
end
|
60
|
+
|
61
|
+
cleanup if cleanup?
|
62
|
+
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def cleanup # :nodoc:
|
67
|
+
activated = Gem.loaded_specs.values.map { |s| s.full_name }
|
68
|
+
available = Gem.source_index.gems.values.sort
|
69
|
+
|
70
|
+
extra = available.reject do |spec|
|
71
|
+
active = activated.include? spec.full_name
|
72
|
+
entry = entries.detect { |e| e.matches_spec? spec }
|
73
|
+
system = !spec.loaded_from.include?(path)
|
74
|
+
|
75
|
+
active or entry or system
|
76
|
+
end
|
77
|
+
|
78
|
+
return if extra.empty?
|
79
|
+
|
80
|
+
padding = Math.log10(extra.size).to_i + 1
|
81
|
+
format = "[%0#{padding}d/%s] Nuking %s."
|
82
|
+
|
83
|
+
extra.each_with_index do |e, i|
|
84
|
+
log format % [i + 1, extra.size, e.full_name]
|
85
|
+
|
86
|
+
Gem::DefaultUserInteraction.use_ui Gem::SilentUI.new do
|
87
|
+
Gem::Uninstaller.new(e.name,
|
88
|
+
:version => e.version,
|
89
|
+
:ignore => true,
|
90
|
+
:executables => true,
|
91
|
+
:install_dir => path).uninstall
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def cleanup?
|
97
|
+
install? and @options.fetch(:cleanup, true)
|
98
|
+
end
|
99
|
+
|
100
|
+
def disable &block
|
101
|
+
return self if not enabled?
|
102
|
+
|
103
|
+
ENV["GEM_PATH"] = @old_gem_path
|
104
|
+
ENV["GEM_HOME"] = @old_gem_home
|
105
|
+
ENV["PATH"] = @old_path
|
106
|
+
ENV["RUBYOPT"] = @old_ruby_opt
|
107
|
+
|
108
|
+
$LOAD_PATH.replace @old_load_path
|
109
|
+
|
110
|
+
@enabled = false
|
111
|
+
|
112
|
+
Isolate.refresh
|
113
|
+
begin; return yield ensure enable end if block_given?
|
114
|
+
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
def enable # :nodoc:
|
119
|
+
return self if enabled?
|
120
|
+
|
121
|
+
@old_gem_path = ENV["GEM_PATH"]
|
122
|
+
@old_gem_home = ENV["GEM_HOME"]
|
123
|
+
@old_path = ENV["PATH"]
|
124
|
+
@old_ruby_opt = ENV["RUBYOPT"]
|
125
|
+
@old_load_path = $LOAD_PATH.dup
|
126
|
+
|
127
|
+
FileUtils.mkdir_p path
|
128
|
+
ENV["GEM_HOME"] = path
|
129
|
+
|
130
|
+
unless system?
|
131
|
+
$LOAD_PATH.reject! do |p|
|
132
|
+
p != File.dirname(__FILE__) &&
|
133
|
+
Gem.path.any? { |gp| p.include?(gp) }
|
134
|
+
end
|
135
|
+
|
136
|
+
# HACK: Gotta keep isolate explicitly in the LOAD_PATH in
|
137
|
+
# subshells, and the only way I can think of to do that is by
|
138
|
+
# abusing RUBYOPT.
|
139
|
+
|
140
|
+
dirname = Regexp.escape File.dirname(__FILE__)
|
141
|
+
|
142
|
+
unless ENV["RUBYOPT"] =~ /\s+-I\s*#{dirname}\b/
|
143
|
+
ENV["RUBYOPT"] = "#{ENV['RUBYOPT']} -I#{File.dirname(__FILE__)}"
|
144
|
+
end
|
145
|
+
|
146
|
+
ENV["GEM_PATH"] = path
|
147
|
+
end
|
148
|
+
|
149
|
+
bin = File.join path, "bin"
|
150
|
+
|
151
|
+
unless ENV["PATH"].split(File::PATH_SEPARATOR).include? bin
|
152
|
+
ENV["PATH"] = [bin, ENV["PATH"]].join File::PATH_SEPARATOR
|
153
|
+
end
|
154
|
+
|
155
|
+
Isolate.refresh
|
156
|
+
Gem.path.unshift path if system?
|
157
|
+
|
158
|
+
@enabled = true
|
159
|
+
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
def enabled?
|
164
|
+
@enabled
|
165
|
+
end
|
166
|
+
|
167
|
+
# Restricts +gem+ calls inside +block+ to a set of +environments+.
|
168
|
+
|
169
|
+
def environment *environments, &block
|
170
|
+
old = @environments
|
171
|
+
@environments = @environments.dup.concat environments.map { |e| e.to_s }
|
172
|
+
|
173
|
+
instance_eval(&block)
|
174
|
+
ensure
|
175
|
+
@environments = old
|
176
|
+
end
|
177
|
+
|
178
|
+
# Express a gem dependency. Works pretty much like RubyGems' +gem+
|
179
|
+
# method, but respects +environment+ and doesn't activate 'til
|
180
|
+
# later.
|
181
|
+
|
182
|
+
def gem name, *requirements
|
183
|
+
entry = entries.detect { |e| e.name == name }
|
184
|
+
return entry.update(*requirements) if entry
|
185
|
+
|
186
|
+
entries << entry = Entry.new(self, name, *requirements)
|
187
|
+
entry
|
188
|
+
end
|
189
|
+
|
190
|
+
def install environment # :nodoc:
|
191
|
+
installable = entries.select do |e|
|
192
|
+
!Gem.available?(e.name, *e.requirement.as_list) &&
|
193
|
+
e.matches?(environment)
|
194
|
+
end
|
195
|
+
|
196
|
+
return self if installable.empty?
|
197
|
+
|
198
|
+
padding = Math.log10(installable.size).to_i + 1
|
199
|
+
format = "[%0#{padding}d/%s] Isolating %s (%s)."
|
200
|
+
|
201
|
+
installable.each_with_index do |entry, i|
|
202
|
+
log format % [i + 1, installable.size, entry.name, entry.requirement]
|
203
|
+
entry.install
|
204
|
+
end
|
205
|
+
|
206
|
+
Gem.source_index.refresh!
|
207
|
+
|
208
|
+
self
|
209
|
+
end
|
210
|
+
|
211
|
+
def install? # :nodoc:
|
212
|
+
@options.fetch :install, true
|
213
|
+
end
|
214
|
+
|
215
|
+
def load file # :nodoc:
|
216
|
+
files << file
|
217
|
+
instance_eval IO.read(file), file, 1
|
218
|
+
end
|
219
|
+
|
220
|
+
def log s # :nodoc:
|
221
|
+
$stderr.puts s if verbose?
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
def multiruby?
|
226
|
+
@options.fetch :multiruby, true
|
227
|
+
end
|
228
|
+
def options options = nil
|
229
|
+
@options.merge! options if options
|
230
|
+
@options
|
231
|
+
end
|
232
|
+
|
233
|
+
def path path = nil
|
234
|
+
if path
|
235
|
+
unless @options.key?(:multiruby) && @options[:multiruby] == false
|
236
|
+
suffix = RbConfig::CONFIG.
|
237
|
+
values_at("ruby_install_name", "ruby_version").join "-"
|
238
|
+
|
239
|
+
path = File.join(path, suffix) unless path =~ /#{suffix}/
|
240
|
+
end
|
241
|
+
|
242
|
+
@path = File.expand_path path
|
243
|
+
end
|
244
|
+
|
245
|
+
@path
|
246
|
+
end
|
247
|
+
|
248
|
+
def system?
|
249
|
+
@options.fetch :system, true
|
250
|
+
end
|
251
|
+
|
252
|
+
def verbose?
|
253
|
+
@options.fetch :verbose, true
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
gem "monkey", :args => "--threaten"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "isolate"
|
2
|
+
require "minitest/autorun"
|
3
|
+
|
4
|
+
module Isolate
|
5
|
+
class Test < MiniTest::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
Isolate.refresh
|
8
|
+
|
9
|
+
@env = ENV.to_hash
|
10
|
+
@lp = $LOAD_PATH.dup
|
11
|
+
@lf = $LOADED_FEATURES.dup
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
Gem::DependencyInstaller.reset_value
|
16
|
+
Gem::Uninstaller.reset_value
|
17
|
+
|
18
|
+
ENV.replace @env
|
19
|
+
$LOAD_PATH.replace @lp
|
20
|
+
$LOADED_FEATURES.replace @lf
|
21
|
+
|
22
|
+
FileUtils.rm_rf "tmp/isolate"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module BrutalStub
|
28
|
+
@@value = []
|
29
|
+
def value; @@value end
|
30
|
+
def reset_value; value.clear end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Gem::DependencyInstaller
|
34
|
+
extend BrutalStub
|
35
|
+
|
36
|
+
alias old_install install
|
37
|
+
def install name, requirement
|
38
|
+
self.class.value << [name, requirement]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Gem::Uninstaller
|
43
|
+
extend BrutalStub
|
44
|
+
|
45
|
+
attr_reader :gem, :version, :gem_home
|
46
|
+
alias old_uninstall uninstall
|
47
|
+
|
48
|
+
def uninstall
|
49
|
+
self.class.value << [self.gem,
|
50
|
+
self.version.to_s,
|
51
|
+
self.gem_home.sub(Dir.pwd + "/", '')]
|
52
|
+
end
|
53
|
+
end
|