peritor-bundler 0.7.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.
@@ -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