bundler08 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +284 -0
- data/Rakefile +81 -0
- data/lib/bundler08.rb +49 -0
- data/lib/bundler08/bundle.rb +314 -0
- data/lib/bundler08/cli.rb +89 -0
- data/lib/bundler08/commands/bundle_command.rb +83 -0
- data/lib/bundler08/commands/exec_command.rb +36 -0
- data/lib/bundler08/dependency.rb +62 -0
- data/lib/bundler08/dsl.rb +182 -0
- data/lib/bundler08/environment.rb +87 -0
- data/lib/bundler08/finder.rb +51 -0
- data/lib/bundler08/gem_bundle.rb +11 -0
- data/lib/bundler08/gem_ext.rb +34 -0
- data/lib/bundler08/remote_specification.rb +53 -0
- data/lib/bundler08/resolver.rb +250 -0
- data/lib/bundler08/runtime.rb +2 -0
- data/lib/bundler08/source.rb +365 -0
- data/lib/bundler08/templates/Gemfile +72 -0
- data/lib/bundler08/templates/app_script.erb +3 -0
- data/lib/bundler08/templates/environment.erb +156 -0
- data/lib/bundler08/templates/environment_picker.erb +4 -0
- data/lib/rubygems_plugin.rb +6 -0
- metadata +81 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
require "rubygems/source_index"
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
class InvalidCacheArgument < StandardError; end
|
5
|
+
class SourceNotCached < StandardError; end
|
6
|
+
|
7
|
+
class Environment
|
8
|
+
attr_reader :dependencies
|
9
|
+
attr_accessor :rubygems, :system_gems
|
10
|
+
|
11
|
+
def initialize(bundle)
|
12
|
+
@bundle = bundle # TODO: remove this
|
13
|
+
@default_sources = default_sources
|
14
|
+
@sources = []
|
15
|
+
@priority_sources = []
|
16
|
+
@dependencies = []
|
17
|
+
@rubygems = true
|
18
|
+
@system_gems = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def environment_rb(specs, options)
|
22
|
+
load_paths = load_paths_for_specs(specs, options)
|
23
|
+
bindir = @bundle.bindir.relative_path_from(@bundle.gem_path).to_s
|
24
|
+
filename = @bundle.gemfile.relative_path_from(@bundle.gem_path).to_s
|
25
|
+
|
26
|
+
template = File.read(File.join(File.dirname(__FILE__), "templates", "environment.erb"))
|
27
|
+
erb = ERB.new(template, nil, '-')
|
28
|
+
erb.result(binding)
|
29
|
+
end
|
30
|
+
|
31
|
+
def require_env(env = nil)
|
32
|
+
dependencies.each { |d| d.require_env(env) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def sources
|
36
|
+
sources = @priority_sources + [SystemGemSource.new(@bundle)] + @sources + @default_sources
|
37
|
+
sources.reject! {|s| !s.local? } if Bundler.local?
|
38
|
+
sources
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_source(source)
|
42
|
+
@sources << source
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_priority_source(source)
|
46
|
+
@priority_sources << source
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear_sources
|
50
|
+
@sources.clear
|
51
|
+
@default_sources.clear
|
52
|
+
end
|
53
|
+
|
54
|
+
alias rubygems? rubygems
|
55
|
+
alias system_gems? system_gems
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def default_sources
|
60
|
+
[GemSource.new(@bundle, :uri => "http://gems.rubyforge.org")]
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_paths_for_specs(specs, options)
|
64
|
+
load_paths = []
|
65
|
+
specs.each do |spec|
|
66
|
+
next if spec.no_bundle?
|
67
|
+
full_gem_path = Pathname.new(spec.full_gem_path)
|
68
|
+
|
69
|
+
if spec.bindir && full_gem_path.join(spec.bindir).exist?
|
70
|
+
load_paths << load_path_for(full_gem_path, spec.bindir)
|
71
|
+
end
|
72
|
+
spec.require_paths.each do |path|
|
73
|
+
load_paths << load_path_for(full_gem_path, path)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
load_paths
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_path_for(gem_path, path)
|
80
|
+
gem_path.join(path).relative_path_from(@bundle.gem_path).to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def spec_file_for(spec)
|
84
|
+
spec.loaded_from.relative_path_from(@bundle.gem_path).to_s
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Bundler
|
2
|
+
# Finder behaves like a rubygems source index in that it responds
|
3
|
+
# to #search. It also resolves a list of dependencies finding the
|
4
|
+
# best possible configuration of gems that satisifes all requirements
|
5
|
+
# without causing any gem activation errors.
|
6
|
+
class Finder
|
7
|
+
|
8
|
+
# Takes an array of gem sources and fetches the full index of
|
9
|
+
# gems from each one. It then combines the indexes together keeping
|
10
|
+
# track of the original source so that any resolved gem can be
|
11
|
+
# fetched from the correct source.
|
12
|
+
#
|
13
|
+
# ==== Parameters
|
14
|
+
# *sources<String>:: URI pointing to the gem repository
|
15
|
+
def initialize(*sources)
|
16
|
+
@cache = {}
|
17
|
+
@index = {}
|
18
|
+
@sources = sources
|
19
|
+
end
|
20
|
+
|
21
|
+
# Searches for a gem that matches the dependency
|
22
|
+
#
|
23
|
+
# ==== Parameters
|
24
|
+
# dependency<Gem::Dependency>:: The gem dependency to search for
|
25
|
+
#
|
26
|
+
# ==== Returns
|
27
|
+
# [Gem::Specification]:: A collection of gem specifications
|
28
|
+
# matching the search
|
29
|
+
def search(dependency)
|
30
|
+
@cache[dependency.hash] ||= begin
|
31
|
+
find_by_name(dependency.name).select do |spec|
|
32
|
+
dependency =~ spec
|
33
|
+
end.sort_by {|s| s.version }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def find_by_name(name)
|
40
|
+
matches = @index[name] ||= begin
|
41
|
+
versions = {}
|
42
|
+
@sources.reverse_each do |source|
|
43
|
+
versions.merge! source.specs[name] || {}
|
44
|
+
end
|
45
|
+
versions
|
46
|
+
end
|
47
|
+
matches.values
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Gem
|
2
|
+
class Installer
|
3
|
+
remove_method(:app_script_text) if method_defined?(:app_script_text)
|
4
|
+
|
5
|
+
def app_script_text(bin_file_name)
|
6
|
+
path = @gem_home
|
7
|
+
template = File.read(File.join(File.dirname(__FILE__), "templates", "app_script.erb"))
|
8
|
+
erb = ERB.new(template, nil, '-')
|
9
|
+
erb.result(binding)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Specification
|
14
|
+
attr_accessor :source, :location, :no_bundle
|
15
|
+
|
16
|
+
alias no_bundle? no_bundle
|
17
|
+
|
18
|
+
remove_method(:specification_version) if method_defined?(:specification_version)
|
19
|
+
|
20
|
+
# Hack to fix github's strange marshal file
|
21
|
+
def specification_version
|
22
|
+
@specification_version && @specification_version.to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
alias full_gem_path_without_location full_gem_path
|
26
|
+
def full_gem_path
|
27
|
+
if defined?(@location) && @location
|
28
|
+
@location
|
29
|
+
else
|
30
|
+
full_gem_path_without_location
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Bundler
|
2
|
+
# Represents a lazily loaded gem specification, where the full specification
|
3
|
+
# is on the source server in rubygems' "quick" index. The proxy object is to
|
4
|
+
# be seeded with what we're given from the source's abbreviated index - the
|
5
|
+
# full specification will only be fetched when necesary.
|
6
|
+
class RemoteSpecification
|
7
|
+
attr_reader :name, :version, :platform
|
8
|
+
attr_accessor :source
|
9
|
+
|
10
|
+
def initialize(name, version, platform, source_uri)
|
11
|
+
@name = name
|
12
|
+
@version = version
|
13
|
+
@platform = platform
|
14
|
+
@source_uri = source_uri
|
15
|
+
end
|
16
|
+
|
17
|
+
def full_name
|
18
|
+
if platform == Gem::Platform::RUBY or platform.nil? then
|
19
|
+
"#{@name}-#{@version}"
|
20
|
+
else
|
21
|
+
"#{@name}-#{@version}-#{platform}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Because Rubyforge cannot be trusted to provide valid specifications
|
26
|
+
# once the remote gem is donwloaded, the backend specification will
|
27
|
+
# be swapped out.
|
28
|
+
def __swap__(spec)
|
29
|
+
@specification = spec
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def _remote_uri
|
35
|
+
# "#{@source_uri}/quick/Marshal.4.8/#{@name}-#{@version}.gemspec.rz"
|
36
|
+
tuple = [@name, @version, @platform]
|
37
|
+
tuple = tuple - [nil, 'ruby', '']
|
38
|
+
"#{@source_uri}/quick/Marshal.4.8/#{tuple.join("-")}.gemspec.rz"
|
39
|
+
end
|
40
|
+
|
41
|
+
def _remote_specification
|
42
|
+
@specification ||= begin
|
43
|
+
deflated = Gem::RemoteFetcher.fetcher.fetch_path(_remote_uri)
|
44
|
+
inflated = Gem.inflate(deflated)
|
45
|
+
Marshal.load(inflated)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(method, *args, &blk)
|
50
|
+
_remote_specification.send(method, *args, &blk)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
# This is the latest iteration of the gem dependency resolving algorithm. As of now,
|
2
|
+
# it can resolve (as a success of failure) any set of gem dependencies we throw at it
|
3
|
+
# in a reasonable amount of time. The most iterations I've seen it take is about 150.
|
4
|
+
# The actual implementation of the algorithm is not as good as it could be yet, but that
|
5
|
+
# can come later.
|
6
|
+
|
7
|
+
# Extending Gem classes to add necessary tracking information
|
8
|
+
module Gem
|
9
|
+
class Dependency
|
10
|
+
def required_by
|
11
|
+
@required_by ||= []
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class Specification
|
15
|
+
def required_by
|
16
|
+
@required_by ||= []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Bundler
|
22
|
+
class GemNotFound < StandardError; end
|
23
|
+
class VersionConflict < StandardError; end
|
24
|
+
|
25
|
+
class Resolver
|
26
|
+
|
27
|
+
attr_reader :errors
|
28
|
+
|
29
|
+
# Figures out the best possible configuration of gems that satisfies
|
30
|
+
# the list of passed dependencies and any child dependencies without
|
31
|
+
# causing any gem activation errors.
|
32
|
+
#
|
33
|
+
# ==== Parameters
|
34
|
+
# *dependencies<Gem::Dependency>:: The list of dependencies to resolve
|
35
|
+
#
|
36
|
+
# ==== Returns
|
37
|
+
# <GemBundle>,nil:: If the list of dependencies can be resolved, a
|
38
|
+
# collection of gemspecs is returned. Otherwise, nil is returned.
|
39
|
+
def self.resolve(requirements, sources)
|
40
|
+
source_requirements = {}
|
41
|
+
|
42
|
+
requirements.each do |r|
|
43
|
+
next unless r.source
|
44
|
+
source_requirements[r.name] = r.source
|
45
|
+
end
|
46
|
+
|
47
|
+
resolver = new(sources, source_requirements)
|
48
|
+
result = catch(:success) do
|
49
|
+
resolver.resolve(requirements, {})
|
50
|
+
output = resolver.errors.inject("") do |o, (conflict, (origin, requirement))|
|
51
|
+
o << " Conflict on: #{conflict.inspect}:\n"
|
52
|
+
o << " * #{conflict} (#{origin.version}) activated by #{origin.required_by.first}\n"
|
53
|
+
o << " * #{requirement} required by #{requirement.required_by.first}\n"
|
54
|
+
o << " All possible versions of origin requirements conflict."
|
55
|
+
end
|
56
|
+
raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{output}"
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
if result
|
60
|
+
# Order gems in order of dependencies. Every gem's dependency is at
|
61
|
+
# a smaller index in the array.
|
62
|
+
ordered = []
|
63
|
+
result.values.each do |spec1|
|
64
|
+
spec1.no_bundle = true if source_requirements[spec1.name] == SystemGemSource.instance
|
65
|
+
index = nil
|
66
|
+
place = ordered.detect do |spec2|
|
67
|
+
spec1.dependencies.any? { |d| d.name == spec2.name }
|
68
|
+
end
|
69
|
+
place ?
|
70
|
+
ordered.insert(ordered.index(place), spec1) :
|
71
|
+
ordered << spec1
|
72
|
+
end
|
73
|
+
ordered.reverse
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize(sources, source_requirements)
|
78
|
+
@errors = {}
|
79
|
+
@stack = []
|
80
|
+
@specs = Hash.new { |h,k| h[k] = [] }
|
81
|
+
@by_gem = source_requirements
|
82
|
+
@cache = {}
|
83
|
+
@index = {}
|
84
|
+
|
85
|
+
sources.each do |source|
|
86
|
+
source.gems.each do |name, specs|
|
87
|
+
# Hack to work with a regular Gem::SourceIndex
|
88
|
+
specs = [specs] unless specs.is_a?(Array)
|
89
|
+
specs.compact.each do |spec|
|
90
|
+
next if @specs[spec.name].any? { |s| s.version == spec.version && s.platform == spec.platform }
|
91
|
+
@specs[spec.name] << spec
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def debug
|
98
|
+
puts yield if defined?($debug) && $debug
|
99
|
+
end
|
100
|
+
|
101
|
+
def resolve(reqs, activated)
|
102
|
+
# If the requirements are empty, then we are in a success state. Aka, all
|
103
|
+
# gem dependencies have been resolved.
|
104
|
+
throw :success, activated if reqs.empty?
|
105
|
+
|
106
|
+
debug { STDIN.gets ; print "\e[2J\e[f" ; "==== Iterating ====\n\n" }
|
107
|
+
|
108
|
+
# Sort dependencies so that the ones that are easiest to resolve are first.
|
109
|
+
# Easiest to resolve is defined by:
|
110
|
+
# 1) Is this gem already activated?
|
111
|
+
# 2) Do the version requirements include prereleased gems?
|
112
|
+
# 3) Sort by number of gems available in the source.
|
113
|
+
reqs = reqs.sort_by do |a|
|
114
|
+
[ activated[a.name] ? 0 : 1,
|
115
|
+
a.version_requirements.prerelease? ? 0 : 1,
|
116
|
+
@errors[a.name] ? 0 : 1,
|
117
|
+
activated[a.name] ? 0 : search(a).size ]
|
118
|
+
end
|
119
|
+
|
120
|
+
debug { "Activated:\n" + activated.values.map { |a| " #{a.name} (#{a.version})" }.join("\n") }
|
121
|
+
debug { "Requirements:\n" + reqs.map { |r| " #{r.name} (#{r.version_requirements})"}.join("\n") }
|
122
|
+
|
123
|
+
activated = activated.dup
|
124
|
+
# Pull off the first requirement so that we can resolve it
|
125
|
+
current = reqs.shift
|
126
|
+
|
127
|
+
debug { "Attempting:\n #{current.name} (#{current.version_requirements})"}
|
128
|
+
|
129
|
+
# Check if the gem has already been activated, if it has, we will make sure
|
130
|
+
# that the currently activated gem satisfies the requirement.
|
131
|
+
if existing = activated[current.name]
|
132
|
+
if current.version_requirements.satisfied_by?(existing.version)
|
133
|
+
debug { " * [SUCCESS] Already activated" }
|
134
|
+
@errors.delete(existing.name)
|
135
|
+
# Since the current requirement is satisfied, we can continue resolving
|
136
|
+
# the remaining requirements.
|
137
|
+
resolve(reqs, activated)
|
138
|
+
else
|
139
|
+
debug { " * [FAIL] Already activated" }
|
140
|
+
@errors[existing.name] = [existing, current]
|
141
|
+
debug { current.required_by.map {|d| " * #{d.name} (#{d.version_requirements})" }.join("\n") }
|
142
|
+
# debug { " * All current conflicts:\n" + @errors.keys.map { |c| " - #{c}" }.join("\n") }
|
143
|
+
# Since the current requirement conflicts with an activated gem, we need
|
144
|
+
# to backtrack to the current requirement's parent and try another version
|
145
|
+
# of it (maybe the current requirement won't be present anymore). If the
|
146
|
+
# current requirement is a root level requirement, we need to jump back to
|
147
|
+
# where the conflicting gem was activated.
|
148
|
+
parent = current.required_by.last || existing.required_by.last
|
149
|
+
# We track the spot where the current gem was activated because we need
|
150
|
+
# to keep a list of every spot a failure happened.
|
151
|
+
debug { " -> Jumping to: #{parent.name}" }
|
152
|
+
throw parent.name, existing.required_by.last.name
|
153
|
+
end
|
154
|
+
else
|
155
|
+
# There are no activated gems for the current requirement, so we are going
|
156
|
+
# to find all gems that match the current requirement and try them in decending
|
157
|
+
# order. We also need to keep a set of all conflicts that happen while trying
|
158
|
+
# this gem. This is so that if no versions work, we can figure out the best
|
159
|
+
# place to backtrack to.
|
160
|
+
conflicts = Set.new
|
161
|
+
|
162
|
+
# Fetch all gem versions matching the requirement
|
163
|
+
#
|
164
|
+
# TODO: Warn / error when no matching versions are found.
|
165
|
+
matching_versions = search(current)
|
166
|
+
|
167
|
+
if matching_versions.empty?
|
168
|
+
if current.required_by.empty?
|
169
|
+
location = @by_gem[current.name] ? @by_gem[current.name] : "any of the sources"
|
170
|
+
raise GemNotFound, "Could not find gem '#{current}' in #{location}"
|
171
|
+
end
|
172
|
+
Bundler.logger.warn "Could not find gem '#{current}' (required by '#{current.required_by.last}') in any of the sources"
|
173
|
+
end
|
174
|
+
|
175
|
+
matching_versions.reverse_each do |spec|
|
176
|
+
conflict = resolve_requirement(spec, current, reqs.dup, activated.dup)
|
177
|
+
conflicts << conflict if conflict
|
178
|
+
end
|
179
|
+
# If the current requirement is a root level gem and we have conflicts, we
|
180
|
+
# can figure out the best spot to backtrack to.
|
181
|
+
if current.required_by.empty? && !conflicts.empty?
|
182
|
+
# Check the current "catch" stack for the first one that is included in the
|
183
|
+
# conflicts set. That is where the parent of the conflicting gem was required.
|
184
|
+
# By jumping back to this spot, we can try other version of the parent of
|
185
|
+
# the conflicting gem, hopefully finding a combination that activates correctly.
|
186
|
+
@stack.reverse_each do |savepoint|
|
187
|
+
if conflicts.include?(savepoint)
|
188
|
+
debug { " -> Jumping to: #{savepoint}" }
|
189
|
+
throw savepoint
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def resolve_requirement(spec, requirement, reqs, activated)
|
197
|
+
# We are going to try activating the spec. We need to keep track of stack of
|
198
|
+
# requirements that got us to the point of activating this gem.
|
199
|
+
spec.required_by.replace requirement.required_by
|
200
|
+
spec.required_by << requirement
|
201
|
+
|
202
|
+
activated[spec.name] = spec
|
203
|
+
debug { " Activating: #{spec.name} (#{spec.version})" }
|
204
|
+
debug { spec.required_by.map { |d| " * #{d.name} (#{d.version_requirements})" }.join("\n") }
|
205
|
+
|
206
|
+
# Now, we have to loop through all child dependencies and add them to our
|
207
|
+
# array of requirements.
|
208
|
+
debug { " Dependencies"}
|
209
|
+
spec.dependencies.each do |dep|
|
210
|
+
next if dep.type == :development
|
211
|
+
debug { " * #{dep.name} (#{dep.version_requirements})" }
|
212
|
+
dep.required_by.replace(requirement.required_by)
|
213
|
+
dep.required_by << requirement
|
214
|
+
reqs << dep
|
215
|
+
end
|
216
|
+
|
217
|
+
# We create a savepoint and mark it by the name of the requirement that caused
|
218
|
+
# the gem to be activated. If the activated gem ever conflicts, we are able to
|
219
|
+
# jump back to this point and try another version of the gem.
|
220
|
+
length = @stack.length
|
221
|
+
@stack << requirement.name
|
222
|
+
retval = catch(requirement.name) do
|
223
|
+
resolve(reqs, activated)
|
224
|
+
end
|
225
|
+
# Since we're doing a lot of throw / catches. A push does not necessarily match
|
226
|
+
# up to a pop. So, we simply slice the stack back to what it was before the catch
|
227
|
+
# block.
|
228
|
+
@stack.slice!(length..-1)
|
229
|
+
retval
|
230
|
+
end
|
231
|
+
|
232
|
+
def search(dependency)
|
233
|
+
@cache[dependency.hash] ||= begin
|
234
|
+
pinned = @by_gem[dependency.name].gems if @by_gem[dependency.name]
|
235
|
+
specs = (pinned || @specs)[dependency.name]
|
236
|
+
|
237
|
+
wants_prerelease = dependency.version_requirements.prerelease?
|
238
|
+
only_prerelease = specs.all? {|spec| spec.version.prerelease? }
|
239
|
+
|
240
|
+
found = specs.select { |spec| dependency =~ spec }
|
241
|
+
|
242
|
+
unless wants_prerelease || (pinned && only_prerelease)
|
243
|
+
found.reject! { |spec| spec.version.prerelease? }
|
244
|
+
end
|
245
|
+
|
246
|
+
found.sort_by {|s| [s.version, s.platform.to_s == 'ruby' ? "\0" : s.platform.to_s] }
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|