peritor-bundler 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ module Bundler
2
+ class GemBundle < Array
3
+ def download
4
+ sort_by {|s| s.full_name.downcase }.each do |spec|
5
+ spec.source.download(spec)
6
+ end
7
+
8
+ self
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
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
15
+ attr_accessor :location
16
+
17
+ remove_method(:specification_version) if method_defined?(:specification_version)
18
+
19
+ # Hack to fix github's strange marshal file
20
+ def specification_version
21
+ @specification_version && @specification_version.to_i
22
+ end
23
+
24
+ alias full_gem_path_without_location full_gem_path
25
+ def full_gem_path
26
+ if defined?(@location) && @location
27
+ @location
28
+ else
29
+ full_gem_path_without_location
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
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
+ end
37
+
38
+ def _remote_specification
39
+ @specification ||= begin
40
+ deflated = Gem::RemoteFetcher.fetcher.fetch_path(_remote_uri)
41
+ inflated = Gem.inflate(deflated)
42
+ Marshal.load(inflated)
43
+ end
44
+ end
45
+
46
+ def method_missing(method, *args, &blk)
47
+ _remote_specification.send(method, *args, &blk)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,256 @@
1
+ module Bundler
2
+ class InvalidRepository < StandardError ; end
3
+
4
+ class Repository
5
+ attr_reader :path
6
+
7
+ def initialize(path, bindir)
8
+ FileUtils.mkdir_p(path)
9
+
10
+ @path = Pathname.new(path)
11
+ @bindir = Pathname.new(bindir)
12
+
13
+ @cache = GemDirectorySource.new(:location => @path.join("cache"))
14
+ end
15
+
16
+ def install(dependencies, sources, options = {})
17
+ # TODO: clean this up
18
+ sources.each do |s|
19
+ s.repository = self
20
+ s.local = options[:cached]
21
+ end
22
+
23
+ source_requirements = {}
24
+ dependencies = dependencies.map do |dep|
25
+ source_requirements[dep.name] = dep.source if dep.source
26
+ dep.to_gem_dependency
27
+ end
28
+
29
+ # Check to see whether the existing cache meets all the requirements
30
+ begin
31
+ valid = nil
32
+ # valid = Resolver.resolve(dependencies, [source_index], source_requirements)
33
+ rescue Bundler::GemNotFound
34
+ end
35
+
36
+ sources = only_local(sources) if options[:cached]
37
+
38
+ # Check the remote sources if the existing cache does not meet the requirements
39
+ # or the user passed --update
40
+ if options[:update] || !valid
41
+ Bundler.logger.info "Calculating dependencies..."
42
+ bundle = Resolver.resolve(dependencies, [@cache] + sources, source_requirements)
43
+ download(bundle, options)
44
+ do_install(bundle, options)
45
+ valid = bundle
46
+ end
47
+
48
+ generate_bins(valid, options)
49
+ cleanup(valid, options)
50
+ configure(valid, options)
51
+ end
52
+
53
+ def cache(*gemfiles)
54
+ FileUtils.mkdir_p(@path.join("cache"))
55
+ gemfiles.each do |gemfile|
56
+ Bundler.logger.info "Caching: #{File.basename(gemfile)}"
57
+ FileUtils.cp(gemfile, @path.join("cache"))
58
+ end
59
+ end
60
+
61
+ def prune(dependencies, sources)
62
+ sources.each do |s|
63
+ s.repository = self
64
+ s.local = true
65
+ end
66
+
67
+ sources = only_local(sources)
68
+ bundle = Resolver.resolve(dependencies, [@cache] + sources)
69
+ @cache.gems.each do |name, specs|
70
+ specs.each do |spec|
71
+ unless bundle.any? { |s| s.name == spec.name && s.version == spec.version }
72
+ Bundler.logger.info "Pruning #{spec.name} (#{spec.version}) from the cache"
73
+ FileUtils.rm @path.join("cache", "#{spec.full_name}.gem")
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def gems
80
+ source_index.gems.values
81
+ end
82
+
83
+ def outdated_gems
84
+ source_index.outdated.sort
85
+ end
86
+
87
+ def source_index
88
+ index = Gem::SourceIndex.from_gems_in(@path.join("specifications"))
89
+ index.each { |n, spec| spec.loaded_from = @path.join("specifications", "#{spec.full_name}.gemspec") }
90
+ index
91
+ end
92
+
93
+ def download_path_for(type)
94
+ @repos[type].download_path_for
95
+ end
96
+
97
+ private
98
+
99
+ def only_local(sources)
100
+ sources.select { |s| s.can_be_local? }
101
+ end
102
+
103
+ def download(bundle, options)
104
+ bundle.sort_by {|s| s.full_name.downcase }.each do |spec|
105
+ next if options[:no_bundle].include?(spec.name)
106
+ spec.source.download(spec)
107
+ end
108
+ end
109
+
110
+ def do_install(bundle, options)
111
+ bundle.each do |spec|
112
+ next if options[:no_bundle].include?(spec.name)
113
+ spec.loaded_from = @path.join("specifications", "#{spec.full_name}.gemspec")
114
+ # Do nothing if the gem is already expanded
115
+ next if @path.join("gems", spec.full_name).directory?
116
+
117
+ case spec.source
118
+ when GemSource, GemDirectorySource, SystemGemSource
119
+ expand_gemfile(spec, options)
120
+ else
121
+ expand_vendored_gem(spec, options)
122
+ end
123
+ end
124
+ end
125
+
126
+ def generate_bins(bundle, options)
127
+ bundle.each do |spec|
128
+ next if options[:no_bundle].include?(spec.name)
129
+ # HAX -- Generate the bin
130
+ bin_dir = @bindir
131
+ path = @path
132
+ installer = Gem::Installer.allocate
133
+ installer.instance_eval do
134
+ @spec = spec
135
+ @bin_dir = bin_dir
136
+ @gem_dir = path.join("gems", "#{spec.full_name}")
137
+ @gem_home = path
138
+ @wrappers = true
139
+ @format_executable = false
140
+ @env_shebang = false
141
+ end
142
+ installer.generate_bin
143
+ end
144
+ end
145
+
146
+ def expand_gemfile(spec, options)
147
+ Bundler.logger.info "Installing #{spec.name} (#{spec.version})"
148
+
149
+ gemfile = @path.join("cache", "#{spec.full_name}.gem").to_s
150
+
151
+ if build_args = options[:build_options] && options[:build_options][spec.name]
152
+ Gem::Command.build_args = build_args.map {|k,v| "--with-#{k}=#{v}"}
153
+ end
154
+
155
+ installer = Gem::Installer.new(gemfile, options.merge(
156
+ :install_dir => @path,
157
+ :ignore_dependencies => true,
158
+ :env_shebang => true,
159
+ :wrappers => true,
160
+ :bin_dir => @bindir
161
+ ))
162
+ installer.install
163
+ ensure
164
+ Gem::Command.build_args = []
165
+ end
166
+
167
+ def expand_vendored_gem(spec, options)
168
+ add_spec(spec)
169
+ FileUtils.mkdir_p(@path.join("gems"))
170
+ File.symlink(spec.location, @path.join("gems", spec.full_name))
171
+ end
172
+
173
+ def add_spec(spec)
174
+ destination = path.join('specifications')
175
+ destination.mkdir unless destination.exist?
176
+
177
+ File.open(destination.join("#{spec.full_name}.gemspec"), 'w') do |f|
178
+ f.puts spec.to_ruby
179
+ end
180
+ end
181
+
182
+ def cleanup(valid, options)
183
+ to_delete = gems
184
+ to_delete.delete_if do |spec|
185
+ valid.any? { |other| spec.name == other.name && spec.version == other.version }
186
+ end
187
+
188
+ valid_executables = valid.map { |s| s.executables }.flatten.compact
189
+
190
+ to_delete.each do |spec|
191
+ Bundler.logger.info "Deleting gem: #{spec.name} (#{spec.version})"
192
+ FileUtils.rm_rf(@path.join("specifications", "#{spec.full_name}.gemspec"))
193
+ FileUtils.rm_rf(@path.join("gems", spec.full_name))
194
+ # Cleanup the bin directory
195
+ spec.executables.each do |bin|
196
+ next if valid_executables.include?(bin)
197
+ Bundler.logger.info "Deleting bin file: #{bin}"
198
+ FileUtils.rm_rf(@bindir.join(bin))
199
+ end
200
+ end
201
+ end
202
+
203
+ def expand(options)
204
+ each_repo do |repo|
205
+ repo.expand(options)
206
+ end
207
+ end
208
+
209
+ def configure(specs, options)
210
+ generate_environment(specs, options)
211
+ end
212
+
213
+ def generate_environment(specs, options)
214
+ FileUtils.mkdir_p(path)
215
+
216
+ load_paths = load_paths_for_specs(specs, options)
217
+ bindir = @bindir.relative_path_from(path).to_s
218
+ filename = options[:manifest].relative_path_from(path).to_s
219
+
220
+ File.open(path.join("environment.rb"), "w") do |file|
221
+ template = File.read(File.join(File.dirname(__FILE__), "templates", "environment.erb"))
222
+ erb = ERB.new(template, nil, '-')
223
+ file.puts erb.result(binding)
224
+ end
225
+ end
226
+
227
+ def load_paths_for_specs(specs, options)
228
+ load_paths = []
229
+ specs.each do |spec|
230
+ next if options[:no_bundle].include?(spec.name)
231
+ gem_path = Pathname.new(spec.full_gem_path)
232
+ load_paths << load_path_for(gem_path, spec.bindir) if spec.bindir
233
+ spec.require_paths.each do |path|
234
+ load_paths << load_path_for(gem_path, path)
235
+ end
236
+ end
237
+ load_paths
238
+ end
239
+
240
+ def load_path_for(gem_path, path)
241
+ gem_path.join(path).relative_path_from(@path).to_s
242
+ end
243
+
244
+ def spec_file_for(spec)
245
+ spec.loaded_from.relative_path_from(@path).to_s
246
+ end
247
+
248
+ def require_code(file, dep)
249
+ constraint = case
250
+ when dep.only then %{ if #{dep.only.inspect}.include?(env)}
251
+ when dep.except then %{ unless #{dep.except.inspect}.include?(env)}
252
+ end
253
+ "require #{file.inspect}#{constraint}"
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,234 @@
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, source_requirements = {})
40
+ resolver = new(sources, source_requirements)
41
+ result = catch(:success) do
42
+ resolver.resolve(requirements, {})
43
+ output = resolver.errors.inject("") do |o, (conflict, (origin, requirement))|
44
+ o << " Conflict on: #{conflict.inspect}:\n"
45
+ o << " * #{conflict} (#{origin.version}) activated by #{origin.required_by.first}\n"
46
+ o << " * #{requirement} required by #{requirement.required_by.first}\n"
47
+ o << " All possible versions of origin requirements conflict."
48
+ end
49
+ raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{output}"
50
+ nil
51
+ end
52
+ if result
53
+ # Order gems in order of dependencies. Every gem's dependency is at
54
+ # a smaller index in the array.
55
+ ordered = []
56
+ result.values.each do |spec1|
57
+ index = nil
58
+ place = ordered.detect do |spec2|
59
+ spec1.dependencies.any? { |d| d.name == spec2.name }
60
+ end
61
+ place ?
62
+ ordered.insert(ordered.index(place), spec1) :
63
+ ordered << spec1
64
+ end
65
+ ordered.reverse
66
+ end
67
+ end
68
+
69
+ def initialize(sources, source_requirements)
70
+ @errors = {}
71
+ @stack = []
72
+ @specs = Hash.new { |h,k| h[k] = [] }
73
+ @by_gem = source_requirements
74
+ @cache = {}
75
+ @index = {}
76
+
77
+ sources.each do |source|
78
+ source.gems.each do |name, specs|
79
+ # Hack to work with a regular Gem::SourceIndex
80
+ [specs].flatten.compact.each do |spec|
81
+ next if @specs[spec.name].any? { |s| s.version == spec.version }
82
+ @specs[spec.name] << spec
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def debug
89
+ puts yield if defined?($debug) && $debug
90
+ end
91
+
92
+ def resolve(reqs, activated)
93
+ # If the requirements are empty, then we are in a success state. Aka, all
94
+ # gem dependencies have been resolved.
95
+ throw :success, activated if reqs.empty?
96
+
97
+ debug { STDIN.gets ; print "\e[2J\e[f" ; "==== Iterating ====\n\n" }
98
+
99
+ # Sort dependencies so that the ones that are easiest to resolve are first.
100
+ # Easiest to resolve is defined by:
101
+ # 1) Is this gem already activated?
102
+ # 2) Do the version requirements include prereleased gems?
103
+ # 3) Sort by number of gems available in the source.
104
+ reqs = reqs.sort_by do |a|
105
+ [ activated[a.name] ? 0 : 1,
106
+ a.version_requirements.prerelease? ? 0 : 1,
107
+ @errors[a.name] ? 0 : 1,
108
+ activated[a.name] ? 0 : search(a).size ]
109
+ end
110
+
111
+ debug { "Activated:\n" + activated.values.map { |a| " #{a.name} (#{a.version})" }.join("\n") }
112
+ debug { "Requirements:\n" + reqs.map { |r| " #{r.name} (#{r.version_requirements})"}.join("\n") }
113
+
114
+ activated = activated.dup
115
+ # Pull off the first requirement so that we can resolve it
116
+ current = reqs.shift
117
+
118
+ debug { "Attempting:\n #{current.name} (#{current.version_requirements})"}
119
+
120
+ # Check if the gem has already been activated, if it has, we will make sure
121
+ # that the currently activated gem satisfies the requirement.
122
+ if existing = activated[current.name]
123
+ if current.version_requirements.satisfied_by?(existing.version)
124
+ debug { " * [SUCCESS] Already activated" }
125
+ @errors.delete(existing.name)
126
+ # Since the current requirement is satisfied, we can continue resolving
127
+ # the remaining requirements.
128
+ resolve(reqs, activated)
129
+ else
130
+ debug { " * [FAIL] Already activated" }
131
+ @errors[existing.name] = [existing, current]
132
+ debug { current.required_by.map {|d| " * #{d.name} (#{d.version_requirements})" }.join("\n") }
133
+ # debug { " * All current conflicts:\n" + @errors.keys.map { |c| " - #{c}" }.join("\n") }
134
+ # Since the current requirement conflicts with an activated gem, we need
135
+ # to backtrack to the current requirement's parent and try another version
136
+ # of it (maybe the current requirement won't be present anymore). If the
137
+ # current requirement is a root level requirement, we need to jump back to
138
+ # where the conflicting gem was activated.
139
+ parent = current.required_by.last || existing.required_by.last
140
+ # We track the spot where the current gem was activated because we need
141
+ # to keep a list of every spot a failure happened.
142
+ debug { " -> Jumping to: #{parent.name}" }
143
+ throw parent.name, existing.required_by.last.name
144
+ end
145
+ else
146
+ # There are no activated gems for the current requirement, so we are going
147
+ # to find all gems that match the current requirement and try them in decending
148
+ # order. We also need to keep a set of all conflicts that happen while trying
149
+ # this gem. This is so that if no versions work, we can figure out the best
150
+ # place to backtrack to.
151
+ conflicts = Set.new
152
+
153
+ # Fetch all gem versions matching the requirement
154
+ #
155
+ # TODO: Warn / error when no matching versions are found.
156
+ matching_versions = search(current)
157
+
158
+ if matching_versions.empty?
159
+ if current.required_by.empty?
160
+ raise GemNotFound, "Could not find gem '#{current}' in any of the sources"
161
+ end
162
+ Bundler.logger.warn "Could not find gem '#{current}' (required by '#{current.required_by.last}') in any of the sources"
163
+ end
164
+
165
+ matching_versions.reverse_each do |spec|
166
+ conflict = resolve_requirement(spec, current, reqs.dup, activated.dup)
167
+ conflicts << conflict if conflict
168
+ end
169
+ # If the current requirement is a root level gem and we have conflicts, we
170
+ # can figure out the best spot to backtrack to.
171
+ if current.required_by.empty? && !conflicts.empty?
172
+ # Check the current "catch" stack for the first one that is included in the
173
+ # conflicts set. That is where the parent of the conflicting gem was required.
174
+ # By jumping back to this spot, we can try other version of the parent of
175
+ # the conflicting gem, hopefully finding a combination that activates correctly.
176
+ @stack.reverse_each do |savepoint|
177
+ if conflicts.include?(savepoint)
178
+ debug { " -> Jumping to: #{savepoint}" }
179
+ throw savepoint
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def resolve_requirement(spec, requirement, reqs, activated)
187
+ # We are going to try activating the spec. We need to keep track of stack of
188
+ # requirements that got us to the point of activating this gem.
189
+ spec.required_by.replace requirement.required_by
190
+ spec.required_by << requirement
191
+
192
+ activated[spec.name] = spec
193
+ debug { " Activating: #{spec.name} (#{spec.version})" }
194
+ debug { spec.required_by.map { |d| " * #{d.name} (#{d.version_requirements})" }.join("\n") }
195
+
196
+ # Now, we have to loop through all child dependencies and add them to our
197
+ # array of requirements.
198
+ debug { " Dependencies"}
199
+ spec.dependencies.each do |dep|
200
+ next if dep.type == :development
201
+ debug { " * #{dep.name} (#{dep.version_requirements})" }
202
+ dep.required_by.replace(requirement.required_by)
203
+ dep.required_by << requirement
204
+ reqs << dep
205
+ end
206
+
207
+ # We create a savepoint and mark it by the name of the requirement that caused
208
+ # the gem to be activated. If the activated gem ever conflicts, we are able to
209
+ # jump back to this point and try another version of the gem.
210
+ length = @stack.length
211
+ @stack << requirement.name
212
+ retval = catch(requirement.name) do
213
+ resolve(reqs, activated)
214
+ end
215
+ # Since we're doing a lot of throw / catches. A push does not necessarily match
216
+ # up to a pop. So, we simply slice the stack back to what it was before the catch
217
+ # block.
218
+ @stack.slice!(length..-1)
219
+ retval
220
+ end
221
+
222
+ def search(dependency)
223
+ @cache[dependency.hash] ||= begin
224
+ collection = @by_gem[dependency.name].gems if @by_gem[dependency.name]
225
+ collection ||= @specs
226
+ collection[dependency.name].select do |spec|
227
+ match = dependency =~ spec
228
+ match &= dependency.version_requirements.prerelease? if spec.version.prerelease?
229
+ match
230
+ end.sort_by {|s| s.version }
231
+ end
232
+ end
233
+ end
234
+ end