bundler 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bundler might be problematic. Click here for more details.

@@ -119,7 +119,7 @@ to follow.
119
119
 
120
120
  * In your rails app, create a Gemfile and specify the gems that your
121
121
  application depends on. Make sure to specify rails as well:
122
-
122
+
123
123
  gem "rails", "2.1.2"
124
124
  gem "will_paginate"
125
125
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'spec/rake/spectask'
6
6
 
7
7
  spec = Gem::Specification.new do |s|
8
8
  s.name = "bundler"
9
- s.version = "0.3.0"
9
+ s.version = "0.3.1"
10
10
  s.author = "Yehuda Katz"
11
11
  s.email = "wycats@gmail.com"
12
12
  s.homepage = "http://github.com/wycats/bundler"
@@ -7,6 +7,7 @@ require "rubygems/remote_fetcher"
7
7
  require "rubygems/installer"
8
8
 
9
9
  require "bundler/gem_bundle"
10
+ require "bundler/source"
10
11
  require "bundler/finder"
11
12
  require "bundler/gem_ext"
12
13
  require "bundler/resolver"
@@ -4,7 +4,7 @@ class Gem::Commands::BundleCommand < Gem::Command
4
4
  super('bundle', 'Create a gem bundle based on your Gemfile', {:manifest => nil, :update => false})
5
5
 
6
6
  add_option('-m', '--manifest MANIFEST', "Specify the path to the manifest file") do |manifest, options|
7
- options[:manifest] = Pathname.new(manifest)
7
+ options[:manifest] = manifest
8
8
  end
9
9
 
10
10
  add_option('-u', '--update', "Force a remote check for newer gems") do
@@ -23,6 +23,8 @@ Bundle stuff
23
23
  end
24
24
 
25
25
  def execute
26
+ # Prevent the bundler from getting required unless it is actually being used
27
+ require 'bundler'
26
28
  Bundler::CLI.run(:bundle, options)
27
29
  end
28
30
 
@@ -4,7 +4,7 @@ class Gem::Commands::ExecCommand < Gem::Command
4
4
  super('exec', 'Run a command in context of a gem bundle', {:manifest => nil})
5
5
 
6
6
  add_option('-m', '--manifest MANIFEST', "Specify the path to the manifest file") do |manifest, options|
7
- options[:manifest] = Pathname.new(manifest)
7
+ options[:manifest] = manifest
8
8
  end
9
9
  end
10
10
 
@@ -20,9 +20,11 @@ class Gem::Commands::ExecCommand < Gem::Command
20
20
  <<-EOF.gsub(' ', '')
21
21
  Run in context of a bundle
22
22
  EOF
23
- end
23
+ end
24
24
 
25
25
  def execute
26
+ # Prevent the bundler from getting required unless it is actually being used
27
+ require 'bundler'
26
28
  Bundler::CLI.run(:exec, options)
27
29
  end
28
30
 
@@ -13,10 +13,9 @@ module Bundler
13
13
  # ==== Parameters
14
14
  # *sources<String>:: URI pointing to the gem repository
15
15
  def initialize(*sources)
16
- @results = {}
17
- @index = Hash.new { |h,k| h[k] = {} }
18
-
19
- sources.each { |source| fetch(source) }
16
+ @cache = {}
17
+ @index = {}
18
+ @sources = sources
20
19
  end
21
20
 
22
21
  # Figures out the best possible configuration of gems that satisfies
@@ -36,39 +35,6 @@ module Bundler
36
35
  resolved && GemBundle.new(resolved)
37
36
  end
38
37
 
39
- # Fetches the index from the remote source
40
- #
41
- # ==== Parameters
42
- # source<String>:: URI pointing to the gem repository
43
- #
44
- # ==== Raises
45
- # ArgumentError:: If the source is not a valid gem repository
46
- def fetch(source)
47
- Bundler.logger.info "Updating source: #{source}"
48
-
49
- deflated = Gem::RemoteFetcher.fetcher.fetch_path("#{source}/Marshal.4.8.Z")
50
- inflated = Gem.inflate deflated
51
-
52
- append(Marshal.load(inflated), source)
53
- rescue Gem::RemoteFetcher::FetchError => e
54
- raise ArgumentError, "#{source} is not a valid source: #{e.message}"
55
- end
56
-
57
- # Adds a new gem index linked to a gem source to the over all
58
- # gem index that gets searched.
59
- #
60
- # ==== Parameters
61
- # index<Gem::SourceIndex>:: The index to append to the list
62
- # source<String>:: The original source
63
- def append(index, source)
64
- index.gems.values.each do |spec|
65
- next unless Gem::Platform.match(spec.platform)
66
- spec.source = source
67
- @index[spec.name][spec.version] ||= spec
68
- end
69
- self
70
- end
71
-
72
38
  # Searches for a gem that matches the dependency
73
39
  #
74
40
  # ==== Parameters
@@ -78,12 +44,25 @@ module Bundler
78
44
  # [Gem::Specification]:: A collection of gem specifications
79
45
  # matching the search
80
46
  def search(dependency)
81
- @results[dependency.hash] ||= begin
82
- possibilities = @index[dependency.name].values
83
- possibilities.select do |spec|
47
+ @cache[dependency.hash] ||= begin
48
+ find_by_name(dependency.name).select do |spec|
84
49
  dependency =~ spec
85
50
  end.sort_by {|s| s.version }
86
51
  end
87
52
  end
53
+
54
+ private
55
+
56
+ def find_by_name(name)
57
+ matches = @index[name] ||= begin
58
+ versions = {}
59
+ @sources.reverse_each do |source|
60
+ versions.merge! source.specs[name] || {}
61
+ end
62
+ versions
63
+ end
64
+ matches.values
65
+ end
66
+
88
67
  end
89
68
  end
@@ -1,13 +1,8 @@
1
1
  module Bundler
2
2
  class GemBundle < Array
3
- def download(directory)
4
- FileUtils.mkdir_p(directory)
5
-
6
- each do |spec|
7
- unless directory.join("cache", "#{spec.full_name}.gem").file?
8
- Bundler.logger.info "Downloading #{spec.full_name}.gem"
9
- Gem::RemoteFetcher.fetcher.download(spec, spec.source, directory)
10
- end
3
+ def download(repository)
4
+ sort_by {|s| s.full_name.downcase }.each do |spec|
5
+ repository.download(spec)
11
6
  end
12
7
 
13
8
  self
@@ -12,8 +12,8 @@ module Gem
12
12
  attribute :source
13
13
 
14
14
  def source=(source)
15
- @source = source.is_a?(URI) ? source : URI.parse(source)
16
- raise ArgumentError, "The source must be an absolute URI" unless @source.absolute?
15
+ source = Bundler::Source.new(source) unless source.is_a?(Bundler::Source)
16
+ @source = source
17
17
  end
18
18
  end
19
19
  end
@@ -7,7 +7,6 @@ module Bundler
7
7
  attr_reader :sources, :dependencies, :path
8
8
 
9
9
  def initialize(sources, dependencies, bindir, repository_path, rubygems, system_gems)
10
- sources.map! {|s| s.is_a?(URI) ? s : URI.parse(s) }
11
10
  @sources = sources
12
11
  @dependencies = dependencies
13
12
  @bindir = bindir
@@ -58,7 +57,7 @@ module Bundler
58
57
  raise VersionConflict, "No compatible versions could be found for:\n#{gems}"
59
58
  end
60
59
 
61
- bundle.download(@repository.path)
60
+ bundle.download(@repository)
62
61
  end
63
62
 
64
63
  def gem_dependencies
@@ -11,7 +11,7 @@ module Bundler
11
11
 
12
12
  def initialize(filename)
13
13
  @filename = filename
14
- @sources = %w(http://gems.rubyforge.org)
14
+ @sources = [Source.new("http://gems.rubyforge.org")]
15
15
  @dependencies = []
16
16
  @rubygems = true
17
17
  @system_gems = true
@@ -57,7 +57,7 @@ module Bundler
57
57
  end
58
58
 
59
59
  def filename
60
- @filename ||= find_manifest_file
60
+ Pathname.new(@filename ||= find_manifest_file)
61
61
  end
62
62
 
63
63
  def find_manifest_file
@@ -22,6 +22,14 @@ module Bundler
22
22
  (Dir[@path.join("*")] - Dir[@path.join("{cache,doc,gems,environments,specifications}")]).empty?
23
23
  end
24
24
 
25
+ def download(spec)
26
+ FileUtils.mkdir_p(@path)
27
+
28
+ unless @path.join("cache", "#{spec.full_name}.gem").file?
29
+ spec.source.download(spec, @path)
30
+ end
31
+ end
32
+
25
33
  # Checks whether a gem is installed
26
34
  def install_cached_gems(options = {})
27
35
  cached_gems.each do |name, version|
@@ -33,6 +41,7 @@ module Bundler
33
41
 
34
42
  def install_cached_gem(name, version, options = {})
35
43
  cached_gem = cache_path.join("#{name}-#{version}.gem")
44
+ # TODO: Add a warning if cached_gem is not a file
36
45
  if cached_gem.file?
37
46
  Bundler.logger.info "Installing #{name}-#{version}.gem"
38
47
  installer = Gem::Installer.new(cached_gem.to_s, options.merge(
@@ -58,7 +67,7 @@ module Bundler
58
67
  def cached_gems
59
68
  cache_files.map do |f|
60
69
  full_name = File.basename(f).gsub(/\.gem$/, '')
61
- full_name.split(/-(?=[\d.]+$)/)
70
+ full_name.split(/-(?=[^-]+$)/)
62
71
  end
63
72
  end
64
73
 
@@ -1,3 +1,9 @@
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
+
1
7
  # Extending Gem classes to add necessary tracking information
2
8
  module Gem
3
9
  class Dependency
@@ -19,8 +25,8 @@ module Bundler
19
25
  attr_reader :errors
20
26
 
21
27
  def self.resolve(requirements, index = Gem.source_index)
28
+ resolver = new(index)
22
29
  result = catch(:success) do
23
- resolver = new(index)
24
30
  resolver.resolve(requirements, {})
25
31
  nil
26
32
  end
@@ -29,48 +35,108 @@ module Bundler
29
35
 
30
36
  def initialize(index)
31
37
  @errors = {}
38
+ @stack = []
32
39
  @index = index
33
40
  end
34
41
 
35
42
  def resolve(reqs, activated)
43
+ # If the requirements are empty, then we are in a success state. Aka, all
44
+ # gem dependencies have been resolved.
36
45
  throw :success, activated if reqs.empty?
37
46
 
47
+ # Sort requirements so that the ones that are easiest to resolve are first.
48
+ # Easiest to resolve is defined by: Is this gem already activated? Otherwise,
49
+ # check the number of child dependencies this requirement has.
38
50
  reqs = reqs.sort_by do |req|
39
51
  activated[req.name] ? 0 : @index.search(req).size
40
52
  end
41
53
 
42
54
  activated = activated.dup
55
+ # Pull off the first requirement so that we can resolve it
43
56
  current = reqs.shift
44
57
 
58
+ # Check if the gem has already been activated, if it has, we will make sure
59
+ # that the currently activated gem satisfies the requirement.
45
60
  if existing = activated[current.name]
46
61
  if current.version_requirements.satisfied_by?(existing.version)
47
62
  @errors.delete(existing.name)
63
+ # Since the current requirement is satisfied, we can continue resolving
64
+ # the remaining requirements.
48
65
  resolve(reqs, activated)
49
66
  else
50
67
  @errors[existing.name] = { :gem => existing, :requirement => current }
68
+ # Since the current requirement conflicts with an activated gem, we need
69
+ # to backtrack to the current requirement's parent and try another version
70
+ # of it (maybe the current requirement won't be present anymore). If the
71
+ # current requirement is a root level requirement, we need to jump back to
72
+ # where the conflicting gem was activated.
51
73
  parent = current.required_by.last || existing.required_by.last
52
- throw parent.name
74
+ # We track the spot where the current gem was activated because we need
75
+ # to keep a list of every spot a failure happened.
76
+ throw parent.name, existing.required_by.last.name
53
77
  end
54
78
  else
55
- @index.search(current).reverse_each do |spec|
56
- resolve_requirement(spec, current, reqs.dup, activated.dup)
79
+ # There are no activated gems for the current requirement, so we are going
80
+ # to find all gems that match the current requirement and try them in decending
81
+ # order. We also need to keep a set of all conflicts that happen while trying
82
+ # this gem. This is so that if no versions work, we can figure out the best
83
+ # place to backtrack to.
84
+ conflicts = Set.new
85
+
86
+ # Fetch all gem versions matching the requirement
87
+ #
88
+ # TODO: Warn / error when no matching versions are found.
89
+ matching_versions = @index.search(current)
90
+
91
+ matching_versions.reverse_each do |spec|
92
+ conflict = resolve_requirement(spec, current, reqs.dup, activated.dup)
93
+ conflicts << conflict if conflict
94
+ end
95
+ # If the current requirement is a root level gem and we have conflicts, we
96
+ # can figure out the best spot to backtrack to.
97
+ if current.required_by.empty? && !conflicts.empty?
98
+ # Check the current "catch" stack for the first one that is included in the
99
+ # conflicts set. That is where the parent of the conflicting gem was required.
100
+ # By jumping back to this spot, we can try other version of the parent of
101
+ # the conflicting gem, hopefully finding a combination that activates correctly.
102
+ @stack.reverse_each do |savepoint|
103
+ if conflicts.include?(savepoint)
104
+ throw savepoint
105
+ end
106
+ end
57
107
  end
58
108
  end
59
109
  end
60
110
 
61
111
  def resolve_requirement(spec, requirement, reqs, activated)
112
+ # We are going to try activating the spec. We need to keep track of stack of
113
+ # requirements that got us to the point of activating this gem.
62
114
  spec.required_by.replace requirement.required_by
115
+ spec.required_by << requirement
116
+
63
117
  activated[spec.name] = spec
64
118
 
119
+ # Now, we have to loop through all child dependencies and add them to our
120
+ # array of requirements.
65
121
  spec.dependencies.each do |dep|
66
122
  next if dep.type == :development
67
123
  dep.required_by << requirement
68
124
  reqs << dep
69
125
  end
70
126
 
71
- catch(requirement.name) do
127
+ # We create a savepoint and mark it by the name of the requirement that caused
128
+ # the gem to be activated. If the activated gem ever conflicts, we are able to
129
+ # jump back to this point and try another version of the gem.
130
+ length = @stack.length
131
+ @stack << requirement.name
132
+ retval = catch(requirement.name) do
72
133
  resolve(reqs, activated)
73
134
  end
135
+ # Since we're doing a lot of throw / catches. A push does not necessarily match
136
+ # up to a pop. So, we simply slice the stack back to what it was before the catch
137
+ # block.
138
+ @stack.slice!(length..-1)
139
+ retval
74
140
  end
75
141
 
76
142
  end
@@ -41,8 +41,10 @@ module Bundler
41
41
  end
42
42
 
43
43
  def source(source)
44
- @manifest_file.sources << source
45
- @manifest_file.sources.uniq!
44
+ source = Source.new(source)
45
+ unless @manifest_file.sources.include?(source)
46
+ @manifest_file.sources << source
47
+ end
46
48
  end
47
49
 
48
50
  def sources
@@ -0,0 +1,51 @@
1
+ module Bundler
2
+ # Represents a source of rubygems. Initially, this is only gem repositories, but
3
+ # eventually, this will be git, svn, HTTP
4
+ class Source
5
+ attr_reader :uri
6
+
7
+ def initialize(uri)
8
+ @uri = uri.is_a?(URI) ? uri : URI.parse(uri)
9
+ raise ArgumentError, "The source must be an absolute URI" unless @uri.absolute?
10
+ end
11
+
12
+ def specs
13
+ @specs ||= fetch_specs
14
+ end
15
+
16
+ def ==(other)
17
+ uri == other.uri
18
+ end
19
+
20
+ def to_s
21
+ @uri.to_s
22
+ end
23
+
24
+ def download(spec, destination)
25
+ Bundler.logger.info "Downloading #{spec.full_name}.gem"
26
+ Gem::RemoteFetcher.fetcher.download(spec, uri, destination)
27
+ end
28
+
29
+ private
30
+
31
+ def fetch_specs
32
+ Bundler.logger.info "Updating source: #{to_s}"
33
+
34
+ deflated = Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/Marshal.4.8.Z")
35
+ inflated = Gem.inflate deflated
36
+
37
+ index = Marshal.load(inflated)
38
+ specs = Hash.new { |h,k| h[k] = {} }
39
+
40
+ index.gems.values.each do |spec|
41
+ next unless Gem::Platform.match(spec.platform)
42
+ spec.source = self
43
+ specs[spec.name][spec.version] = spec
44
+ end
45
+
46
+ specs
47
+ rescue Gem::RemoteFetcher::FetchError => e
48
+ raise ArgumentError, "#{to_s} is not a valid source: #{e.message}"
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,4 @@
1
1
  require 'rubygems/command_manager'
2
- require 'bundler'
3
2
  require 'bundler/commands/bundle_command'
4
3
  require 'bundler/commands/exec_command'
5
4
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yehuda Katz
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-07-29 00:00:00 -07:00
12
+ date: 2009-08-02 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -38,6 +38,7 @@ files:
38
38
  - lib/bundler/repository.rb
39
39
  - lib/bundler/resolver.rb
40
40
  - lib/bundler/runtime.rb
41
+ - lib/bundler/source.rb
41
42
  - lib/bundler/templates/app_script.rb
42
43
  - lib/bundler/templates/environment.rb
43
44
  - lib/bundler/templates/rubygems.rb