bundler 1.9.10 → 1.10.0.pre

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.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +21 -13
  4. data/Rakefile +2 -2
  5. data/bin/bundle_ruby +2 -0
  6. data/bin/bundler +1 -1
  7. data/lib/bundler.rb +6 -10
  8. data/lib/bundler/cli.rb +23 -1
  9. data/lib/bundler/cli/gem.rb +5 -2
  10. data/lib/bundler/cli/install.rb +37 -5
  11. data/lib/bundler/cli/lock.rb +36 -0
  12. data/lib/bundler/cli/outdated.rb +9 -2
  13. data/lib/bundler/definition.rb +22 -7
  14. data/lib/bundler/dependency.rb +7 -6
  15. data/lib/bundler/deployment.rb +3 -0
  16. data/lib/bundler/dsl.rb +172 -39
  17. data/lib/bundler/endpoint_specification.rb +1 -1
  18. data/lib/bundler/fetcher.rb +90 -252
  19. data/lib/bundler/fetcher/base.rb +27 -0
  20. data/lib/bundler/fetcher/dependency.rb +88 -0
  21. data/lib/bundler/fetcher/downloader.rb +61 -0
  22. data/lib/bundler/fetcher/index.rb +31 -0
  23. data/lib/bundler/friendly_errors.rb +3 -0
  24. data/lib/bundler/inline.rb +50 -0
  25. data/lib/bundler/installer.rb +15 -60
  26. data/lib/bundler/installer/parallel_installer.rb +117 -0
  27. data/lib/bundler/lazy_specification.rb +1 -1
  28. data/lib/bundler/lockfile_parser.rb +26 -10
  29. data/lib/bundler/remote_specification.rb +21 -1
  30. data/lib/bundler/resolver.rb +2 -1
  31. data/lib/bundler/retry.rb +11 -10
  32. data/lib/bundler/rubygems_ext.rb +1 -1
  33. data/lib/bundler/rubygems_integration.rb +33 -6
  34. data/lib/bundler/settings.rb +58 -14
  35. data/lib/bundler/shared_helpers.rb +6 -3
  36. data/lib/bundler/source.rb +0 -10
  37. data/lib/bundler/source/git.rb +2 -2
  38. data/lib/bundler/source/path.rb +1 -1
  39. data/lib/bundler/source/path/installer.rb +8 -11
  40. data/lib/bundler/source/rubygems.rb +46 -16
  41. data/lib/bundler/source/rubygems/remote.rb +39 -0
  42. data/lib/bundler/templates/newgem/.travis.yml.tt +1 -0
  43. data/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt +2 -2
  44. data/lib/bundler/templates/newgem/Rakefile.tt +2 -0
  45. data/lib/bundler/templates/newgem/test/{test_newgem.rb.tt → newgem_test.rb.tt} +2 -2
  46. data/lib/bundler/templates/newgem/test/{minitest_helper.rb.tt → test_helper.rb.tt} +0 -0
  47. data/lib/bundler/version.rb +1 -1
  48. data/man/bundle-config.ronn +7 -0
  49. data/man/bundle-install.ronn +9 -0
  50. data/man/bundle.ronn +3 -3
  51. data/man/gemfile.5.ronn +9 -5
  52. metadata +13 -8
  53. data/UPGRADING.md +0 -103
  54. data/lib/bundler/anonymizable_uri.rb +0 -32
@@ -33,6 +33,7 @@ module Bundler
33
33
  set :bundle_dir, File.join(fetch(:shared_path), 'bundle')
34
34
  set :bundle_flags, "--deployment --quiet"
35
35
  set :bundle_without, [:development, :test]
36
+ set :bundle_with, [:mysql]
36
37
  set :bundle_cmd, "bundle" # e.g. "/opt/ruby/bin/bundle"
37
38
  set :bundle_roles, #{role_default} # e.g. [:app, :batch]
38
39
  DESC
@@ -42,6 +43,7 @@ module Bundler
42
43
  bundle_dir = context.fetch(:bundle_dir, File.join(context.fetch(:shared_path), 'bundle'))
43
44
  bundle_gemfile = context.fetch(:bundle_gemfile, "Gemfile")
44
45
  bundle_without = [*context.fetch(:bundle_without, [:development, :test])].compact
46
+ bundle_with = [*context.fetch(:bundle_with, [])].compact
45
47
  app_path = context.fetch(:latest_release)
46
48
  if app_path.to_s.empty?
47
49
  raise error_type.new("Cannot detect current release path - make sure you have deployed at least once.")
@@ -50,6 +52,7 @@ module Bundler
50
52
  args << "--path #{bundle_dir}" unless bundle_dir.to_s.empty?
51
53
  args << bundle_flags.to_s
52
54
  args << "--without #{bundle_without.join(" ")}" unless bundle_without.empty?
55
+ args << "--with #{bundle_with.join(" ")}" unless bundle_with.empty?
53
56
 
54
57
  run "cd #{app_path} && #{bundle_cmd} install #{args.join(' ')}"
55
58
  end
@@ -16,32 +16,30 @@ module Bundler
16
16
  attr_accessor :dependencies
17
17
 
18
18
  def initialize
19
- @source = nil
20
- @sources = SourceList.new
21
- @git_sources = {}
22
- @dependencies = []
23
- @groups = []
24
- @platforms = []
25
- @env = nil
26
- @ruby_version = nil
19
+ @source = nil
20
+ @sources = SourceList.new
21
+ @git_sources = {}
22
+ @dependencies = []
23
+ @groups = []
24
+ @install_conditionals = []
25
+ @optional_groups = []
26
+ @platforms = []
27
+ @env = nil
28
+ @ruby_version = nil
27
29
  add_git_sources
28
30
  end
29
31
 
30
32
  def eval_gemfile(gemfile, contents = nil)
31
33
  contents ||= Bundler.read_file(gemfile.to_s)
32
34
  instance_eval(contents, gemfile.to_s, 1)
33
- rescue SyntaxError => e
34
- syntax_msg = e.message.gsub("#{gemfile}:", 'on line ')
35
- raise GemfileError, "Gemfile syntax error #{syntax_msg}"
36
- rescue ScriptError, RegexpError, NameError, ArgumentError, RuntimeError => e
37
- e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})"
38
- Bundler.ui.warn e.backtrace.join("\n ")
39
- raise GemfileError, "There was an error in your Gemfile," \
40
- " and Bundler cannot continue."
35
+ rescue Exception => e
36
+ message = "There was an error parsing `#{File.basename gemfile.to_s}`: #{e.message}"
37
+ raise DSLError.new(message, gemfile, e.backtrace, contents)
41
38
  end
42
39
 
43
40
  def gemspec(opts = nil)
44
41
  path = opts && opts[:path] || '.'
42
+ glob = opts && opts[:glob]
45
43
  name = opts && opts[:name] || '{,*}'
46
44
  development_group = opts && opts[:development_group] || :development
47
45
  expanded_path = File.expand_path(path, Bundler.default_gemfile.dirname)
@@ -52,7 +50,7 @@ module Bundler
52
50
  when 1
53
51
  spec = Bundler.load_gemspec(gemspecs.first)
54
52
  raise InvalidOption, "There was an error loading the gemspec at #{gemspecs.first}." unless spec
55
- gem spec.name, :path => path
53
+ gem spec.name, :path => path, :glob => glob
56
54
  group(development_group) do
57
55
  spec.development_dependencies.each do |dep|
58
56
  gem dep.name, *(dep.requirement.as_list + [:type => :development])
@@ -158,16 +156,32 @@ module Bundler
158
156
  end
159
157
 
160
158
  def to_definition(lockfile, unlock)
161
- Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version)
159
+ Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups)
162
160
  end
163
161
 
164
162
  def group(*args, &blk)
163
+ opts = Hash === args.last ? args.pop.dup : {}
164
+ normalize_group_options(opts, args)
165
+
165
166
  @groups.concat args
167
+
168
+ if opts["optional"]
169
+ optional_groups = args - @optional_groups
170
+ @optional_groups.concat optional_groups
171
+ end
172
+
166
173
  yield
167
174
  ensure
168
175
  args.each { @groups.pop }
169
176
  end
170
177
 
178
+ def install_if(*args, &blk)
179
+ @install_conditionals.concat args
180
+ blk.call
181
+ ensure
182
+ args.each { @groups.pop }
183
+ end
184
+
171
185
  def platforms(*platforms)
172
186
  @platforms.concat platforms
173
187
  yield
@@ -184,9 +198,7 @@ module Bundler
184
198
  end
185
199
 
186
200
  def method_missing(name, *args)
187
- location = caller[0].split(':')[0..1].join(':')
188
- raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile\n" \
189
- " from #{location}"
201
+ raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile"
190
202
  end
191
203
 
192
204
  private
@@ -224,7 +236,7 @@ module Bundler
224
236
  end
225
237
 
226
238
  def valid_keys
227
- @valid_keys ||= %w(group groups git path name branch ref tag require submodules platform platforms type source)
239
+ @valid_keys ||= %w(group groups git path glob name branch ref tag require submodules platform platforms type source install_if)
228
240
  end
229
241
 
230
242
  def normalize_options(name, version, opts)
@@ -238,25 +250,19 @@ module Bundler
238
250
  normalize_hash(opts)
239
251
 
240
252
  git_names = @git_sources.keys.map(&:to_s)
241
-
242
- invalid_keys = opts.keys - (valid_keys + git_names)
243
- if invalid_keys.any?
244
- message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
245
- message << if invalid_keys.size > 1
246
- "as options for gem '#{name}', but they are invalid."
247
- else
248
- "as an option for gem '#{name}', but it is invalid."
249
- end
250
-
251
- message << " Valid options are: #{valid_keys.join(", ")}"
252
- raise InvalidOption, message
253
- end
253
+ validate_keys("gem '#{name}'", opts, valid_keys + git_names)
254
254
 
255
255
  groups = @groups.dup
256
256
  opts["group"] = opts.delete("groups") || opts["group"]
257
257
  groups.concat Array(opts.delete("group"))
258
258
  groups = [:default] if groups.empty?
259
259
 
260
+ install_if = @install_conditionals.dup
261
+ install_if.concat Array(opts.delete("install_if"))
262
+ install_if = install_if.reduce(true) do |memo, val|
263
+ memo && (val.respond_to?(:call) ? val.call : val)
264
+ end
265
+
260
266
  platforms = @platforms.dup
261
267
  opts["platforms"] = opts["platform"] || opts["platforms"]
262
268
  platforms.concat Array(opts.delete("platforms"))
@@ -289,10 +295,35 @@ module Bundler
289
295
  end
290
296
  end
291
297
 
292
- opts["source"] ||= @source
293
- opts["env"] ||= @env
294
- opts["platforms"] = platforms.dup
295
- opts["group"] = groups
298
+ opts["source"] ||= @source
299
+ opts["env"] ||= @env
300
+ opts["platforms"] = platforms.dup
301
+ opts["group"] = groups
302
+ opts["should_include"] = install_if
303
+ end
304
+
305
+ def normalize_group_options(opts, groups)
306
+ normalize_hash(opts)
307
+
308
+ groups = groups.map {|group| ":#{group}" }.join(", ")
309
+ validate_keys("group #{groups}", opts, %w(optional))
310
+
311
+ opts["optional"] ||= false
312
+ end
313
+
314
+ def validate_keys(command, opts, valid_keys)
315
+ invalid_keys = opts.keys - valid_keys
316
+ if invalid_keys.any?
317
+ message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
318
+ message << if invalid_keys.size > 1
319
+ "as options for #{command}, but they are invalid."
320
+ else
321
+ "as an option for #{command}, but it is invalid."
322
+ end
323
+
324
+ message << " Valid options are: #{valid_keys.join(", ")}"
325
+ raise InvalidOption, message
326
+ end
296
327
  end
297
328
 
298
329
  def normalize_source(source)
@@ -327,5 +358,107 @@ module Bundler
327
358
  end
328
359
  end
329
360
 
361
+ class DSLError < GemfileError
362
+ # @return [String] the description that should be presented to the user.
363
+ #
364
+ attr_reader :description
365
+
366
+ # @return [String] the path of the dsl file that raised the exception.
367
+ #
368
+ attr_reader :dsl_path
369
+
370
+ # @return [Exception] the backtrace of the exception raised by the
371
+ # evaluation of the dsl file.
372
+ #
373
+ attr_reader :backtrace
374
+
375
+ # @param [Exception] backtrace @see backtrace
376
+ # @param [String] dsl_path @see dsl_path
377
+ #
378
+ def initialize(description, dsl_path, backtrace, contents = nil)
379
+ @status_code = $!.respond_to?(:status_code) && $!.status_code
380
+
381
+ @description = description
382
+ @dsl_path = dsl_path
383
+ @backtrace = backtrace
384
+ @contents = contents
385
+ end
386
+
387
+ def status_code
388
+ @status_code || super
389
+ end
390
+
391
+ # @return [String] the contents of the DSL that cause the exception to
392
+ # be raised.
393
+ #
394
+ def contents
395
+ @contents ||= begin
396
+ dsl_path && File.exist?(dsl_path) && File.read(dsl_path)
397
+ end
398
+ end
399
+
400
+ # The message of the exception reports the content of podspec for the
401
+ # line that generated the original exception.
402
+ #
403
+ # @example Output
404
+ #
405
+ # Invalid podspec at `RestKit.podspec` - undefined method
406
+ # `exclude_header_search_paths=' for #<Pod::Specification for
407
+ # `RestKit/Network (0.9.3)`>
408
+ #
409
+ # from spec-repos/master/RestKit/0.9.3/RestKit.podspec:36
410
+ # -------------------------------------------
411
+ # # because it would break: #import <CoreData/CoreData.h>
412
+ # > ns.exclude_header_search_paths = 'Code/RestKit.h'
413
+ # end
414
+ # -------------------------------------------
415
+ #
416
+ # @return [String] the message of the exception.
417
+ #
418
+ def message
419
+ @message ||= begin
420
+ trace_line, description = parse_line_number_from_description
421
+
422
+ m = "\n[!] "
423
+ m << description
424
+ m << ". Bundler cannot continue.\n"
425
+
426
+ return m unless backtrace && dsl_path && contents
427
+
428
+ trace_line = backtrace.find { |l| l.include?(dsl_path.to_s) } || trace_line
429
+ return m unless trace_line
430
+ line_numer = trace_line.split(':')[1].to_i - 1
431
+ return m unless line_numer
432
+
433
+ lines = contents.lines.to_a
434
+ indent = ' # '
435
+ indicator = indent.gsub('#', '>')
436
+ first_line = (line_numer.zero?)
437
+ last_line = (line_numer == (lines.count - 1))
438
+
439
+ m << "\n"
440
+ m << "#{indent}from #{trace_line.gsub(/:in.*$/, '')}\n"
441
+ m << "#{indent}-------------------------------------------\n"
442
+ m << "#{indent}#{ lines[line_numer - 1] }" unless first_line
443
+ m << "#{indicator}#{ lines[line_numer] }"
444
+ m << "#{indent}#{ lines[line_numer + 1] }" unless last_line
445
+ m << "\n" unless m.end_with?("\n")
446
+ m << "#{indent}-------------------------------------------\n"
447
+ end
448
+ end
449
+
450
+ private
451
+
452
+ def parse_line_number_from_description
453
+ description = self.description
454
+ if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/
455
+ trace_line = Regexp.last_match[1]
456
+ description = description.sub(/#{Regexp.quote trace_line}:\s*/, '').sub("\n", ' - ')
457
+ end
458
+ [trace_line, description]
459
+ end
460
+ end
461
+
330
462
  end
463
+
331
464
  end
@@ -4,7 +4,7 @@ module Bundler
4
4
  include MatchPlatform
5
5
 
6
6
  attr_reader :name, :version, :platform, :dependencies
7
- attr_accessor :source, :source_uri
7
+ attr_accessor :source, :remote
8
8
 
9
9
  def initialize(name, version, platform, dependencies)
10
10
  @name = name
@@ -1,11 +1,15 @@
1
1
  require 'bundler/vendored_persistent'
2
- require 'securerandom'
3
2
  require 'cgi'
3
+ require 'securerandom'
4
4
 
5
5
  module Bundler
6
6
 
7
7
  # Handles all the fetching with the rubygems server
8
8
  class Fetcher
9
+ autoload :Downloader, 'bundler/fetcher/downloader'
10
+ autoload :Dependency, 'bundler/fetcher/dependency'
11
+ autoload :Index, 'bundler/fetcher/index'
12
+
9
13
  # This error is raised when it looks like the network is down
10
14
  class NetworkDownError < HTTPError; end
11
15
  # This error is raised if the API returns a 413 (only printed in verbose)
@@ -52,97 +56,21 @@ module Bundler
52
56
 
53
57
  class << self
54
58
  attr_accessor :disable_endpoint, :api_timeout, :redirect_limit, :max_retries
55
-
56
- def download_gem_from_uri(spec, uri)
57
- spec.fetch_platform
58
-
59
- download_path = Bundler.requires_sudo? ? Bundler.tmp(spec.full_name) : Bundler.rubygems.gem_dir
60
- gem_path = "#{Bundler.rubygems.gem_dir}/cache/#{spec.full_name}.gem"
61
-
62
- FileUtils.mkdir_p("#{download_path}/cache")
63
- Bundler.rubygems.download_gem(spec, uri, download_path)
64
-
65
- if Bundler.requires_sudo?
66
- Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/cache"
67
- Bundler.sudo "mv #{download_path}/cache/#{spec.full_name}.gem #{gem_path}"
68
- end
69
-
70
- gem_path
71
- ensure
72
- Bundler.rm_rf(download_path) if Bundler.requires_sudo?
73
- end
74
-
75
- def user_agent
76
- @user_agent ||= begin
77
- ruby = Bundler.ruby_version
78
-
79
- agent = "bundler/#{Bundler::VERSION}"
80
- agent << " rubygems/#{Gem::VERSION}"
81
- agent << " ruby/#{ruby.version}"
82
- agent << " (#{ruby.host})"
83
- agent << " command/#{ARGV.first}"
84
-
85
- if ruby.engine != "ruby"
86
- # engine_version raises on unknown engines
87
- engine_version = ruby.engine_version rescue "???"
88
- agent << " #{ruby.engine}/#{engine_version}"
89
- end
90
-
91
- agent << " options/#{Bundler.settings.all.join(",")}"
92
-
93
- # add a random ID so we can consolidate runs server-side
94
- agent << " " << SecureRandom.hex(8)
95
-
96
- # add any user agent strings set in the config
97
- extra_ua = Bundler.settings[:user_agent]
98
- agent << " " << extra_ua if extra_ua
99
-
100
- agent
101
- end
102
- end
103
-
104
59
  end
105
60
 
106
- def initialize(remote_uri)
107
- @redirect_limit = 5 # How many redirects to allow in one request
108
- @api_timeout = 10 # How long to wait for each API call
109
- @max_retries = 3 # How many retries for the API call
61
+ self.redirect_limit = Bundler.settings[:redirect] # How many redirects to allow in one request
62
+ self.api_timeout = Bundler.settings[:timeout] # How long to wait for each API call
63
+ self.max_retries = Bundler.settings[:retry] # How many retries for the API call
110
64
 
111
- @anonymizable_uri = configured_uri_for(remote_uri)
65
+ def initialize(remote)
66
+ @remote = remote
112
67
 
113
68
  Socket.do_not_reverse_lookup = true
114
69
  connection # create persistent connection
115
70
  end
116
71
 
117
- def connection
118
- @connection ||= begin
119
- needs_ssl = remote_uri.scheme == "https" ||
120
- Bundler.settings[:ssl_verify_mode] ||
121
- Bundler.settings[:ssl_client_cert]
122
- raise SSLError if needs_ssl && !defined?(OpenSSL::SSL)
123
-
124
- con = Net::HTTP::Persistent.new 'bundler', :ENV
125
-
126
- if remote_uri.scheme == "https"
127
- con.verify_mode = (Bundler.settings[:ssl_verify_mode] ||
128
- OpenSSL::SSL::VERIFY_PEER)
129
- con.cert_store = bundler_cert_store
130
- end
131
-
132
- if Bundler.settings[:ssl_client_cert]
133
- pem = File.read(Bundler.settings[:ssl_client_cert])
134
- con.cert = OpenSSL::X509::Certificate.new(pem)
135
- con.key = OpenSSL::PKey::RSA.new(pem)
136
- end
137
-
138
- con.read_timeout = @api_timeout
139
- con.override_headers["User-Agent"] = self.class.user_agent
140
- con
141
- end
142
- end
143
-
144
72
  def uri
145
- @anonymizable_uri.without_credentials
73
+ @remote.anonymized_uri
146
74
  end
147
75
 
148
76
  # fetch a gem specification
@@ -156,37 +84,26 @@ module Bundler
156
84
  elsif cached_spec_path = gemspec_cached_path(spec_file_name)
157
85
  Bundler.load_gemspec(cached_spec_path)
158
86
  else
159
- Bundler.load_marshal Gem.inflate(fetch(uri))
87
+ Bundler.load_marshal Gem.inflate(downloader.fetch uri)
160
88
  end
161
89
  rescue MarshalError
162
90
  raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \
163
91
  "Your network or your gem server is probably having issues right now."
164
92
  end
165
93
 
166
- # cached gem specification path, if one exists
167
- def gemspec_cached_path spec_file_name
168
- paths = Bundler.rubygems.spec_cache_dirs.map { |dir| File.join(dir, spec_file_name) }
169
- paths = paths.select {|path| File.file? path }
170
- paths.first
171
- end
172
-
173
94
  # return the specs in the bundler format as an index
174
95
  def specs(gem_names, source)
175
96
  old = Bundler.rubygems.sources
176
- index = Index.new
177
-
178
- if gem_names && use_api
179
- specs = fetch_remote_specs(gem_names)
180
- end
181
-
182
- if specs.nil?
183
- # API errors mean we should treat this as a non-API source
184
- @use_api = false
97
+ index = Bundler::Index.new
185
98
 
186
- specs = Bundler::Retry.new("source fetch", AUTH_ERRORS).attempts do
187
- fetch_all_remote_specs
99
+ specs = {}
100
+ fetchers.dup.each do |f|
101
+ unless f.api_fetcher? && !gem_names
102
+ break if specs = f.specs(gem_names)
188
103
  end
104
+ fetchers.delete(f)
189
105
  end
106
+ @use_api = false if fetchers.none?(&:api_fetcher?)
190
107
 
191
108
  specs[remote_uri].each do |name, version, platform, dependencies|
192
109
  next if name == 'bundler'
@@ -197,189 +114,111 @@ module Bundler
197
114
  spec = RemoteSpecification.new(name, version, platform, self)
198
115
  end
199
116
  spec.source = source
200
- spec.source_uri = @anonymizable_uri
117
+ spec.remote = @remote
201
118
  index << spec
202
119
  end
203
120
 
204
121
  index
205
- rescue CertificateFailureError => e
122
+ rescue CertificateFailureError
206
123
  Bundler.ui.info "" if gem_names && use_api # newline after dots
207
- raise e
124
+ raise
208
125
  ensure
209
126
  Bundler.rubygems.sources = old
210
127
  end
211
128
 
212
- # fetch index
213
- def fetch_remote_specs(gem_names, full_dependency_list = [], last_spec_list = [])
214
- query_list = gem_names - full_dependency_list
215
-
216
- # only display the message on the first run
217
- if Bundler.ui.debug?
218
- Bundler.ui.debug "Query List: #{query_list.inspect}"
219
- else
220
- Bundler.ui.info ".", false
221
- end
222
-
223
- return {remote_uri => last_spec_list} if query_list.empty?
224
-
225
- remote_specs = Bundler::Retry.new("dependency api", AUTH_ERRORS).attempts do
226
- fetch_dependency_remote_specs(query_list)
227
- end
228
-
229
- spec_list, deps_list = remote_specs
230
- returned_gems = spec_list.map {|spec| spec.first }.uniq
231
- fetch_remote_specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list)
232
- rescue HTTPError, MarshalError, GemspecError
233
- Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over
234
- Bundler.ui.debug "could not fetch from the dependency API, trying the full index"
235
- @use_api = false
236
- return nil
237
- end
238
-
239
129
  def use_api
240
130
  return @use_api if defined?(@use_api)
241
131
 
242
132
  if remote_uri.scheme == "file" || Bundler::Fetcher.disable_endpoint
243
133
  @use_api = false
244
- elsif fetch(dependency_api_uri)
245
- @use_api = true
134
+ else
135
+ fetchers.reject! { |f| f.api_fetcher? && !f.api_available? }
136
+ @use_api = fetchers.any?(&:api_fetcher?)
246
137
  end
247
- rescue NetworkDownError => e
248
- raise HTTPError, e.message
249
- rescue AuthenticationRequiredError
250
- # We got a 401 from the server. Don't fall back to the full index, just fail.
251
- raise
252
- rescue HTTPError
253
- @use_api = false
254
138
  end
255
139
 
256
- def inspect
257
- "#<#{self.class}:0x#{object_id} uri=#{uri}>"
258
- end
140
+ def user_agent
141
+ @user_agent ||= begin
142
+ ruby = Bundler.ruby_version
259
143
 
260
- private
144
+ agent = "bundler/#{Bundler::VERSION}"
145
+ agent << " rubygems/#{Gem::VERSION}"
146
+ agent << " ruby/#{ruby.version}"
147
+ agent << " (#{ruby.host})"
148
+ agent << " command/#{ARGV.first}"
261
149
 
262
- HTTP_ERRORS = [
263
- Timeout::Error, EOFError, SocketError, Errno::ENETDOWN,
264
- Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN,
265
- Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
266
- Net::HTTP::Persistent::Error
267
- ]
150
+ if ruby.engine != "ruby"
151
+ # engine_version raises on unknown engines
152
+ engine_version = ruby.engine_version rescue "???"
153
+ agent << " #{ruby.engine}/#{engine_version}"
154
+ end
268
155
 
269
- def fetch(uri, counter = 0)
270
- raise HTTPError, "Too many redirects" if counter >= @redirect_limit
156
+ agent << " options/#{Bundler.settings.all.join(",")}"
271
157
 
272
- response = request(uri)
273
- Bundler.ui.debug("HTTP #{response.code} #{response.message}")
158
+ # add a random ID so we can consolidate runs server-side
159
+ agent << " " << SecureRandom.hex(8)
274
160
 
275
- case response
276
- when Net::HTTPRedirection
277
- new_uri = URI.parse(response["location"])
278
- if new_uri.host == uri.host
279
- new_uri.user = uri.user
280
- new_uri.password = uri.password
281
- end
282
- fetch(new_uri, counter + 1)
283
- when Net::HTTPSuccess
284
- response.body
285
- when Net::HTTPRequestEntityTooLarge
286
- raise FallbackError, response.body
287
- when Net::HTTPUnauthorized
288
- raise AuthenticationRequiredError, remote_uri.host
289
- else
290
- raise HTTPError, "#{response.class}: #{response.body}"
161
+ # add any user agent strings set in the config
162
+ extra_ua = Bundler.settings[:user_agent]
163
+ agent << " " << extra_ua if extra_ua
164
+
165
+ agent
291
166
  end
292
167
  end
293
168
 
294
- def request(uri)
295
- Bundler.ui.debug "HTTP GET #{uri}"
296
- req = Net::HTTP::Get.new uri.request_uri
297
- if uri.user
298
- user = CGI.unescape(uri.user)
299
- password = uri.password ? CGI.unescape(uri.password) : nil
300
- req.basic_auth(user, password)
301
- end
302
- connection.request(uri, req)
303
- rescue OpenSSL::SSL::SSLError
304
- raise CertificateFailureError.new(uri)
305
- rescue *HTTP_ERRORS => e
306
- Bundler.ui.trace e
307
- case e.message
308
- when /host down:/, /getaddrinfo: nodename nor servname provided/
309
- raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \
310
- "connection and try again."
311
- else
312
- raise HTTPError, "Network error while fetching #{uri}"
313
- end
169
+ def fetchers
170
+ @fetchers ||= FETCHERS.map { |f| f.new(downloader, remote_uri, fetch_uri, uri) }
314
171
  end
315
172
 
316
- def dependency_api_uri(gem_names = [])
317
- uri = fetch_uri + "api/v1/dependencies"
318
- uri.query = "gems=#{URI.encode(gem_names.join(","))}" if gem_names.any?
319
- uri
173
+ def inspect
174
+ "#<#{self.class}:0x#{object_id} uri=#{uri}>"
320
175
  end
321
176
 
322
- # fetch from Gemcutter Dependency Endpoint API
323
- def fetch_dependency_remote_specs(gem_names)
324
- Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(',')}"
325
- gem_list = []
326
- deps_list = []
177
+ private
327
178
 
328
- gem_names.each_slice(Source::Rubygems::API_REQUEST_SIZE) do |names|
329
- marshalled_deps = fetch dependency_api_uri(names)
330
- gem_list += Bundler.load_marshal(marshalled_deps)
331
- end
179
+ FETCHERS = [Dependency, Index]
332
180
 
333
- spec_list = gem_list.map do |s|
334
- dependencies = s[:dependencies].map do |name, requirement|
335
- dep = well_formed_dependency(name, requirement.split(", "))
336
- deps_list << dep.name
337
- dep
338
- end
181
+ def connection
182
+ @connection ||= begin
183
+ needs_ssl = remote_uri.scheme == "https" ||
184
+ Bundler.settings[:ssl_verify_mode] ||
185
+ Bundler.settings[:ssl_client_cert]
186
+ raise SSLError if needs_ssl && !defined?(OpenSSL::SSL)
339
187
 
340
- [s[:name], Gem::Version.new(s[:number]), s[:platform], dependencies]
341
- end
188
+ con = Net::HTTP::Persistent.new 'bundler', :ENV
342
189
 
343
- [spec_list, deps_list.uniq]
344
- end
190
+ if remote_uri.scheme == "https"
191
+ con.verify_mode = (Bundler.settings[:ssl_verify_mode] ||
192
+ OpenSSL::SSL::VERIFY_PEER)
193
+ con.cert_store = bundler_cert_store
194
+ end
345
195
 
346
- # fetch from modern index: specs.4.8.gz
347
- def fetch_all_remote_specs
348
- old_sources = Bundler.rubygems.sources
349
- Bundler.rubygems.sources = [remote_uri.to_s]
350
- Bundler.rubygems.fetch_all_remote_specs
351
- rescue Gem::RemoteFetcher::FetchError, OpenSSL::SSL::SSLError => e
352
- case e.message
353
- when /certificate verify failed/
354
- raise CertificateFailureError.new(uri)
355
- when /401/
356
- raise AuthenticationRequiredError, remote_uri
357
- when /403/
358
- if remote_uri.userinfo
359
- raise BadAuthenticationError, remote_uri
360
- else
361
- raise AuthenticationRequiredError, remote_uri
196
+ if Bundler.settings[:ssl_client_cert]
197
+ pem = File.read(Bundler.settings[:ssl_client_cert])
198
+ con.cert = OpenSSL::X509::Certificate.new(pem)
199
+ con.key = OpenSSL::PKey::RSA.new(pem)
362
200
  end
363
- else
364
- Bundler.ui.trace e
365
- raise HTTPError, "Could not fetch specs from #{uri}"
201
+
202
+ con.read_timeout = Fetcher.api_timeout
203
+ con.override_headers["User-Agent"] = user_agent
204
+ con
366
205
  end
367
- ensure
368
- Bundler.rubygems.sources = old_sources
369
206
  end
370
207
 
371
- def well_formed_dependency(name, *requirements)
372
- Gem::Dependency.new(name, *requirements)
373
- rescue ArgumentError => e
374
- illformed = 'Ill-formed requirement ["#<YAML::Syck::DefaultKey'
375
- raise e unless e.message.include?(illformed)
376
- puts # we shouldn't print the error message on the "fetching info" status line
377
- raise GemspecError,
378
- "Unfortunately, the gem #{s[:name]} (#{s[:number]}) has an invalid " \
379
- "gemspec. \nPlease ask the gem author to yank the bad version to fix " \
380
- "this issue. For more information, see http://bit.ly/syck-defaultkey."
208
+ # cached gem specification path, if one exists
209
+ def gemspec_cached_path spec_file_name
210
+ paths = Bundler.rubygems.spec_cache_dirs.map { |dir| File.join(dir, spec_file_name) }
211
+ paths = paths.select {|path| File.file? path }
212
+ paths.first
381
213
  end
382
214
 
215
+ HTTP_ERRORS = [
216
+ Timeout::Error, EOFError, SocketError, Errno::ENETDOWN,
217
+ Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN,
218
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
219
+ Net::HTTP::Persistent::Error
220
+ ]
221
+
383
222
  def bundler_cert_store
384
223
  store = OpenSSL::X509::Store.new
385
224
  if Bundler.settings[:ssl_ca_cert]
@@ -398,12 +237,6 @@ module Bundler
398
237
 
399
238
  private
400
239
 
401
- def configured_uri_for(uri)
402
- uri = Bundler::Source.mirror_for(uri)
403
- config_auth = Bundler.settings[uri.to_s] || Bundler.settings[uri.host]
404
- AnonymizableURI.new(uri, config_auth)
405
- end
406
-
407
240
  def fetch_uri
408
241
  @fetch_uri ||= begin
409
242
  if remote_uri.host == "rubygems.org"
@@ -417,7 +250,12 @@ module Bundler
417
250
  end
418
251
 
419
252
  def remote_uri
420
- @anonymizable_uri.original_uri
253
+ @remote.uri
254
+ end
255
+
256
+ def downloader
257
+ @downloader ||= Downloader.new(connection, self.class.redirect_limit)
421
258
  end
259
+
422
260
  end
423
261
  end