-
Notifications
You must be signed in to change notification settings - Fork 2
/
minigems.rb
348 lines (301 loc) · 12.4 KB
/
minigems.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
module Gem
unless const_defined?(:MiniGems)
module MiniGems
VERSION = "0.9.7"
# The next line needs to be kept exactly as shown; it's being replaced
# during minigems installation.
FULL_RUBYGEMS_METHODS = []
def self.camel_case(str)
return str if str !~ /_/ && str =~ /[A-Z]+.*/
str.split('_').map{|e| e.capitalize}.join
end
end
end
end
# Enable minigems unless rubygems has already loaded.
unless $LOADED_FEATURES.include?("rubygems.rb")
$MINIGEMS_SKIPPABLE ||= []
$LOADED_FEATURES << "rubygems.rb"
require 'minigems/core'
require 'rubygems/specification'
require 'pathname'
unless Gem::MiniGems.const_defined?(:INLINE_REGEXP)
Gem::MiniGems::INLINE_REGEXP = /^Inline_.*?\.#{Config::CONFIG['DLEXT']}/
end
module Kernel
def gem(name, *versions)
Gem.activate(name, *versions)
end
if RUBY_VERSION < '1.9' then
alias :gem_original_require :require
# We replace Ruby's require with our own, which is capable of
# loading gems on demand.
#
# When you call <tt>require 'x'</tt>, this is what happens:
# * If the file can be loaded from the existing Ruby loadpath, it
# is.
# * Otherwise, installed gems are searched for a file that matches.
# If it's found in gem 'y', that gem is activated (added to the
# loadpath).
#
# The normal <tt>require</tt> functionality of returning false if
# that file has already been loaded is preserved.
#
def require(path) # :nodoc:
gem_original_require path
rescue LoadError => load_error
if File.basename(path).match(Gem::MiniGems::INLINE_REGEXP)
# RubyInline dynamically created .so/.bundle
return gem_original_require(File.join(Inline.directory, path))
elsif path == 'Win32API' && !Gem.win_platform?
raise load_error
elsif load_error.message =~ /#{Regexp.escape path}\z/
if !path.include?('/') && (match = Gem.find_name(path))
Gem.activate_gem_from_path(match.first)
return gem_original_require(path)
elsif $MINIGEMS_SKIPPABLE.include?(path)
raise load_error
elsif spec = Gem.searcher.find(path)
Gem.activate(spec.name, "= #{spec.version}")
return gem_original_require(path)
end
end
raise load_error
end
end
end
module Gem
CORE_GEM_METHODS = Gem.methods(false)
class Exception < RuntimeError; end
# Keep track of loaded gems, maps gem name to full_name.
def self.loaded_gems
@loaded_gems ||= {}
end
# Refresh the current 'cached' gems - in this case
# just the list of loaded gems.
def self.refresh
self.loaded_gems.clear
end
# See if a given gem is available.
def self.available?(name, *version_requirements)
version_requirements = Gem::Requirement.default if version_requirements.empty?
gem = Gem::Dependency.new(name, version_requirements)
not find_name(gem).nil?
end
# Activates an installed gem matching +gem+. The gem must satisfy
# +version_requirements+.
#
# Returns true if the gem is activated, false if it is already
# loaded, or an exception otherwise.
#
# Gem#activate adds the library paths in +gem+ to $LOAD_PATH. Before a Gem
# is activated its required Gems are activated. If the version information
# is omitted, the highest version Gem of the supplied name is loaded. If a
# Gem is not found that meets the version requirements or a required Gem is
# not found, a Gem::LoadError is raised.
#
# More information on version requirements can be found in the
# Gem::Requirement and Gem::Version documentation.
def self.activate(gem, *version_requirements)
if match = find_name(gem, *version_requirements)
activate_gem_from_path(match.first)
elsif match = find_name(MiniGems.camel_case(gem), *version_requirements)
activate_gem_from_path(match.first)
else
unless gem.respond_to?(:name) && gem.respond_to?(:version_requirements)
gem = Gem::Dependency.new(gem, version_requirements)
end
report_activate_error(gem)
end
end
# Helper method to find all current gem paths.
#
# Find the most recent gem versions' paths just by looking at the gem's
# directory version number. This is faster than parsing gemspec files
# at the expense of being less complete when it comes to require paths.
# That's why Gem.activate actually parses gemspecs instead of directories.
def self.latest_gem_paths
lookup = {}
gem_path_sets = self.path.map { |path| [path, Dir["#{path}/gems/*"]] }
gem_path_sets.each do |root_path, gems|
unless gems.empty?
gems.each do |gem_path|
if matches = File.basename(File.basename(gem_path)).match(/^(.*?)-([\d\.]+)$/)
name, version_no = matches.captures[0,2]
version = Gem::Version.new(version_no)
if !lookup[name] || (lookup[name] && lookup[name][1] < version)
lookup[name] = [gem_path, version]
end
end
end
end
end
lookup.collect { |name,(gem_path, version)| gem_path }.sort
end
# Array of paths to search for Gems.
def self.path
@path ||= begin
paths = [ENV['GEM_PATH'] ? ENV['GEM_PATH'] : default_path]
paths << APPLE_GEM_HOME if defined?(APPLE_GEM_HOME) && !ENV['GEM_PATH']
paths
end
end
# Default gem load path.
def self.default_path
@default_path ||= if defined? RUBY_FRAMEWORK_VERSION then
File.join File.dirname(RbConfig::CONFIG["sitedir"]), 'Gems',
RbConfig::CONFIG["ruby_version"]
elsif defined?(RUBY_ENGINE) && File.directory?(
File.join(RbConfig::CONFIG["libdir"], RUBY_ENGINE, 'gems',
RbConfig::CONFIG["ruby_version"])
)
File.join RbConfig::CONFIG["libdir"], RUBY_ENGINE, 'gems',
RbConfig::CONFIG["ruby_version"]
else
File.join RbConfig::CONFIG["libdir"], 'ruby', 'gems',
RbConfig::CONFIG["ruby_version"]
end
end
# Reset the +path+ value. The next time +path+ is requested,
# the values will be calculated from scratch.
def self.clear_paths
@path = nil
end
# Catch calls to full rubygems methods - once accessed
# the current Gem methods are overridden.
def self.method_missing(m, *args, &blk)
if Gem::MiniGems::FULL_RUBYGEMS_METHODS.include?(m.to_s)
load_full_rubygems!
return send(m, *args, &blk)
end
super
end
# Catch references to full rubygems constants - once accessed
# the current Gem constants are merged.
def self.const_missing(const)
load_full_rubygems!
if Gem.const_defined?(const)
Gem.const_get(const)
else
super
end
end
protected
# Activate a gem by specifying a path to a gemspec.
def self.activate_gem_from_path(gem_path, gem_spec = nil)
# Load and initialize the gemspec
gem_spec ||= Gem::Specification.load(gem_path)
gem_spec.loaded_from = gem_path
# Raise an exception if the same spec has already been loaded - except for identical matches
if (already_loaded = self.loaded_gems[gem_spec.name]) && gem_spec.full_name != already_loaded
raise Gem::Exception, "can't activate #{gem_spec.name}, already activated #{already_loaded}"
# If it's an identical match, we're done activating
elsif already_loaded
return false
end
# Keep track of loaded gems - by name instead of full specs (memory!)
self.loaded_gems[gem_spec.name] = gem_spec.full_name
# Load dependent gems first
gem_spec.runtime_dependencies.each { |dep_gem| activate(dep_gem) }
# bin directory must come before library directories
gem_spec.require_paths.unshift(gem_spec.bindir) if gem_spec.bindir
# Add gem require paths to $LOAD_PATH
gem_spec.require_paths.reverse.each do |require_path|
$LOAD_PATH.unshift File.join(gem_spec.full_gem_path, require_path)
end
return true
end
# Find a file in the source path and activate its gem (best/highest match).
def self.find_in_source_path(path)
if ['.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar'].include?(File.extname(path))
file_path = path
else
file_path = "#{path}.rb"
end
matched_paths = self.path.map do |gem_path|
[Pathname.new("#{gem_path}/gems"), Dir["#{gem_path}/gems/**/#{file_path}"]]
end
versions = matched_paths.inject([]) do |versions, (root_path, paths)|
paths.each do |matched_path|
dir_name = Pathname.new(matched_path).relative_path_from(root_path).to_s.split('/').first
gemspec_path = File.join(File.dirname(root_path), 'specifications', "#{dir_name}.gemspec")
if File.exists?(gemspec_path)
# Now check if the file was in a valid require_path
gem_spec = Gem::Specification.load(gemspec_path)
gem_spec.loaded_from = gemspec_path
gem_dir = Pathname.new("#{root_path}/#{dir_name}")
relative_file = Pathname.new(matched_path).relative_path_from(gem_dir).to_s
if gem_spec.require_paths.any? { |req| File.join(req, file_path) == relative_file }
versions << gem_spec
end
end
end
versions
end
versions.max { |a, b| a.version <=> b.version }
end
# Find a file in the Gem source index - loads up the full rubygems!
def self.find_in_source_index(path)
show_notification "Switching from minigems to full rubygems..."
Gem.searcher.find(path)
end
# Find the best (highest) matching gem version.
def self.find_name(gem, *version_requirements)
version_requirements = Gem::Requirement.default if version_requirements.empty?
unless gem.respond_to?(:name) && gem.respond_to?(:version_requirements)
gem = Gem::Dependency.new(gem, version_requirements)
end
gemspec_sets = self.path.map { |path| [path, Dir["#{path}/specifications/#{gem.name}-*.gemspec"]] }
versions = gemspec_sets.inject([]) do |versions, (root_path, gems)|
unless gems.empty?
gems.each do |gemspec_path|
if (version_no = gemspec_path[/-([\d\.]+)\.gemspec$/, 1]) &&
gem.version_requirements.satisfied_by?(version = Gem::Version.new(version_no))
versions << [gemspec_path, version]
end
end
end
versions
end
versions.max { |a, b| a.last <=> b.last }
end
# Report a load error during activation.
def self.report_activate_error(gem)
error = Gem::LoadError.new("Could not find RubyGem #{gem.name} (#{gem.version_requirements})\n")
error.name = gem.name
error.version_requirement = gem.version_requirements
raise error
end
# Load the full rubygems suite, at which point all minigems logic
# is being overridden, so all regular methods and classes are available.
def self.load_full_rubygems!
show_notification 'Loaded full RubyGems instead of MiniGems'
if !caller.first.to_s.match(/`const_missing'$/) && (require_entry = get_require_caller(caller))
show_notification "A gem was possibly implicitly loaded from #{require_entry}"
end
# Clear out any minigems methods
class << self
(MINIGEMS_METHODS - CORE_GEM_METHODS).each do |method_name|
undef_method method_name
end
end
# Fix some constants from throwing already initialized warnings
Gem.send(:remove_const, :RubyGemsPackageVersion)
Gem.send(:remove_const, :WIN_PATTERNS)
# Re-alias the 'require' method back to its original.
::Kernel.module_eval { alias_method :require, :gem_original_require }
require $LOADED_FEATURES.delete("rubygems.rb")
end
def self.get_require_caller(callstack)
require_entry = callstack.find { |c| c =~ /`require'$/ }
if require_entry && (idx = callstack.index(require_entry)) && (entry = callstack[idx + 1])
entry
end
end
def self.show_notification(msg)
puts "\033[1;31m#{msg}\033[0m"
end
# Record all minigems methods - except the minigems? predicate method.
MINIGEMS_METHODS = Gem.methods(false) - ["minigems?"]
end
end