bowline-bundler 0.0.1

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.
Files changed (43) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +291 -0
  3. data/Rakefile +45 -0
  4. data/VERSION +1 -0
  5. data/bin/bowline-bundle +59 -0
  6. data/lib/bowline/bundler/bundle.rb +323 -0
  7. data/lib/bowline/bundler/cli.rb +87 -0
  8. data/lib/bowline/bundler/dependency.rb +62 -0
  9. data/lib/bowline/bundler/dsl.rb +182 -0
  10. data/lib/bowline/bundler/environment.rb +87 -0
  11. data/lib/bowline/bundler/finder.rb +51 -0
  12. data/lib/bowline/bundler/gem_bundle.rb +11 -0
  13. data/lib/bowline/bundler/gem_ext.rb +34 -0
  14. data/lib/bowline/bundler/remote_specification.rb +53 -0
  15. data/lib/bowline/bundler/resolver.rb +250 -0
  16. data/lib/bowline/bundler/runtime.rb +2 -0
  17. data/lib/bowline/bundler/source.rb +361 -0
  18. data/lib/bowline/bundler/templates/app_script.erb +3 -0
  19. data/lib/bowline/bundler/templates/environment.erb +156 -0
  20. data/lib/bowline/bundler/templates/environment_picker.erb +4 -0
  21. data/lib/bowline/bundler.rb +35 -0
  22. data/spec/bundler/cli_spec.rb +558 -0
  23. data/spec/bundler/directory_spec.rb +255 -0
  24. data/spec/bundler/dsl_spec.rb +126 -0
  25. data/spec/bundler/fetcher_spec.rb +138 -0
  26. data/spec/bundler/git_spec.rb +266 -0
  27. data/spec/bundler/installer_spec.rb +155 -0
  28. data/spec/bundler/manifest_file_spec.rb +105 -0
  29. data/spec/bundler/manifest_spec.rb +257 -0
  30. data/spec/bundler/runtime_spec.rb +141 -0
  31. data/spec/bundler/system_gems_spec.rb +42 -0
  32. data/spec/quality_spec.rb +57 -0
  33. data/spec/resolver/engine_spec.rb +112 -0
  34. data/spec/resolver/error_spec.rb +50 -0
  35. data/spec/resolver/fake_source_index_spec.rb +43 -0
  36. data/spec/resolver/prerelease_spec.rb +113 -0
  37. data/spec/spec_helper.rb +46 -0
  38. data/spec/support/builders.rb +257 -0
  39. data/spec/support/core_ext.rb +18 -0
  40. data/spec/support/helpers.rb +126 -0
  41. data/spec/support/matchers.rb +201 -0
  42. data/spec/support/path_utils.rb +63 -0
  43. metadata +126 -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
+ @priority_sources + [SystemGemSource.new(@bundle)] + @sources + @default_sources
37
+ end
38
+
39
+ def add_source(source)
40
+ @sources << source
41
+ end
42
+
43
+ def add_priority_source(source)
44
+ @priority_sources << source
45
+ end
46
+
47
+ def clear_sources
48
+ @sources.clear
49
+ @default_sources.clear
50
+ end
51
+
52
+ def gem_dependencies
53
+ @gem_dependencies ||= dependencies.map { |d| d.to_gem_dependency }
54
+ end
55
+
56
+ alias rubygems? rubygems
57
+ alias system_gems? system_gems
58
+
59
+ private
60
+
61
+ def default_sources
62
+ [GemSource.new(@bundle, :uri => "http://gems.rubyforge.org")]
63
+ end
64
+
65
+ def load_paths_for_specs(specs, options)
66
+ load_paths = []
67
+ specs.each do |spec|
68
+ next if spec.no_bundle?
69
+ full_gem_path = Pathname.new(spec.full_gem_path)
70
+
71
+ load_paths << load_path_for(full_gem_path, spec.bindir) if spec.bindir
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,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,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
@@ -0,0 +1,2 @@
1
+ require File.join(File.dirname(__FILE__), "runtime", "dsl")
2
+ require File.join(File.dirname(__FILE__), "runtime", "dependency")