bundler08 0.8.2
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/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
|