bundler 4.0.10 → 4.0.20

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +174 -0
  3. data/bundler.gemspec +2 -2
  4. data/lib/bundler/build_metadata.rb +2 -2
  5. data/lib/bundler/cli/add.rb +3 -0
  6. data/lib/bundler/cli/check.rb +1 -3
  7. data/lib/bundler/cli/common.rb +17 -0
  8. data/lib/bundler/cli/config.rb +8 -3
  9. data/lib/bundler/cli/doctor/diagnose.rb +1 -1
  10. data/lib/bundler/cli/exec.rb +15 -1
  11. data/lib/bundler/cli/gem.rb +2 -3
  12. data/lib/bundler/cli/init.rb +4 -0
  13. data/lib/bundler/cli/install.rb +4 -0
  14. data/lib/bundler/cli/issue.rb +2 -2
  15. data/lib/bundler/cli/lock.rb +10 -2
  16. data/lib/bundler/cli/open.rb +12 -2
  17. data/lib/bundler/cli/outdated.rb +76 -6
  18. data/lib/bundler/cli/update.rb +3 -0
  19. data/lib/bundler/cli.rb +21 -15
  20. data/lib/bundler/compact_index_client/cache.rb +11 -0
  21. data/lib/bundler/compact_index_client/cache_file.rb +1 -1
  22. data/lib/bundler/compact_index_client/parser.rb +4 -1
  23. data/lib/bundler/definition.rb +64 -15
  24. data/lib/bundler/dsl.rb +11 -4
  25. data/lib/bundler/endpoint_specification.rb +30 -2
  26. data/lib/bundler/fetcher/base.rb +3 -0
  27. data/lib/bundler/fetcher/compact_index.rb +13 -0
  28. data/lib/bundler/fetcher/gem_remote_fetcher.rb +2 -0
  29. data/lib/bundler/fetcher.rb +12 -2
  30. data/lib/bundler/gem_helper.rb +2 -2
  31. data/lib/bundler/installer/parallel_installer.rb +68 -23
  32. data/lib/bundler/installer.rb +18 -1
  33. data/lib/bundler/lockfile_generator.rb +55 -1
  34. data/lib/bundler/lockfile_parser.rb +23 -1
  35. data/lib/bundler/man/bundle-add.1 +6 -3
  36. data/lib/bundler/man/bundle-add.1.ronn +7 -2
  37. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  38. data/lib/bundler/man/bundle-cache.1 +4 -1
  39. data/lib/bundler/man/bundle-cache.1.ronn +6 -0
  40. data/lib/bundler/man/bundle-check.1 +1 -1
  41. data/lib/bundler/man/bundle-clean.1 +1 -1
  42. data/lib/bundler/man/bundle-config.1 +121 -157
  43. data/lib/bundler/man/bundle-config.1.ronn +38 -6
  44. data/lib/bundler/man/bundle-console.1 +3 -3
  45. data/lib/bundler/man/bundle-console.1.ronn +2 -2
  46. data/lib/bundler/man/bundle-doctor.1 +1 -1
  47. data/lib/bundler/man/bundle-env.1 +1 -1
  48. data/lib/bundler/man/bundle-exec.1 +1 -1
  49. data/lib/bundler/man/bundle-fund.1 +1 -1
  50. data/lib/bundler/man/bundle-gem.1 +1 -1
  51. data/lib/bundler/man/bundle-help.1 +1 -1
  52. data/lib/bundler/man/bundle-info.1 +1 -1
  53. data/lib/bundler/man/bundle-init.1 +2 -2
  54. data/lib/bundler/man/bundle-init.1.ronn +1 -1
  55. data/lib/bundler/man/bundle-install.1 +5 -2
  56. data/lib/bundler/man/bundle-install.1.ronn +9 -1
  57. data/lib/bundler/man/bundle-issue.1 +1 -1
  58. data/lib/bundler/man/bundle-licenses.1 +1 -1
  59. data/lib/bundler/man/bundle-list.1 +1 -1
  60. data/lib/bundler/man/bundle-lock.1 +5 -2
  61. data/lib/bundler/man/bundle-lock.1.ronn +6 -0
  62. data/lib/bundler/man/bundle-open.1 +1 -1
  63. data/lib/bundler/man/bundle-outdated.1 +17 -14
  64. data/lib/bundler/man/bundle-outdated.1.ronn +25 -12
  65. data/lib/bundler/man/bundle-platform.1 +5 -1
  66. data/lib/bundler/man/bundle-platform.1.ronn +4 -0
  67. data/lib/bundler/man/bundle-plugin.1 +2 -2
  68. data/lib/bundler/man/bundle-plugin.1.ronn +1 -1
  69. data/lib/bundler/man/bundle-pristine.1 +1 -1
  70. data/lib/bundler/man/bundle-remove.1 +1 -1
  71. data/lib/bundler/man/bundle-show.1 +1 -1
  72. data/lib/bundler/man/bundle-update.1 +5 -2
  73. data/lib/bundler/man/bundle-update.1.ronn +8 -0
  74. data/lib/bundler/man/bundle-version.1 +1 -1
  75. data/lib/bundler/man/bundle.1 +23 -2
  76. data/lib/bundler/man/bundle.1.ronn +22 -1
  77. data/lib/bundler/man/gemfile.5 +14 -3
  78. data/lib/bundler/man/gemfile.5.ronn +23 -2
  79. data/lib/bundler/mirror.rb +7 -7
  80. data/lib/bundler/plugin/unloaded_source.rb +25 -0
  81. data/lib/bundler/plugin.rb +12 -6
  82. data/lib/bundler/remote_specification.rb +1 -1
  83. data/lib/bundler/resolver/base.rb +9 -0
  84. data/lib/bundler/resolver.rb +116 -2
  85. data/lib/bundler/rubygems_ext.rb +22 -0
  86. data/lib/bundler/rubygems_gem_installer.rb +35 -2
  87. data/lib/bundler/runtime.rb +11 -11
  88. data/lib/bundler/self_manager.rb +6 -1
  89. data/lib/bundler/settings.rb +1 -0
  90. data/lib/bundler/shared_helpers.rb +21 -1
  91. data/lib/bundler/source/git/git_proxy.rb +83 -10
  92. data/lib/bundler/source/git.rb +4 -4
  93. data/lib/bundler/source/metadata.rb +4 -0
  94. data/lib/bundler/source/path.rb +4 -3
  95. data/lib/bundler/source/rubygems/remote.rb +12 -2
  96. data/lib/bundler/source/rubygems.rb +81 -13
  97. data/lib/bundler/source_list.rb +44 -3
  98. data/lib/bundler/spec_set.rb +2 -2
  99. data/lib/bundler/stub_specification.rb +3 -1
  100. data/lib/bundler/templates/newgem/newgem.gemspec.tt +8 -2
  101. data/lib/bundler/vendored_tsort.rb +19 -2
  102. data/lib/bundler/version.rb +1 -1
  103. data/lib/bundler.rb +26 -6
  104. metadata +5 -6
  105. data/lib/bundler/vendor/tsort/LICENSE.txt +0 -22
  106. data/lib/bundler/vendor/tsort/lib/tsort.rb +0 -455
@@ -75,7 +75,12 @@ module Bundler
75
75
 
76
76
  argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0
77
77
  cmd = [argv0, *ARGV]
78
- cmd.unshift(Gem.ruby) unless File.executable?(argv0)
78
+ unless File.executable?(argv0)
79
+ # Gem.ruby is quoted if it contains whitespace, so split it into argv
80
+ # elements to keep the quotes out of the exec'd command.
81
+ require "shellwords"
82
+ cmd.unshift(*Shellwords.split(Gem.ruby))
83
+ end
79
84
 
80
85
  Bundler.with_original_env do
81
86
  Kernel.exec(
@@ -42,6 +42,7 @@ module Bundler
42
42
  ].freeze
43
43
 
44
44
  NUMBER_KEYS = %w[
45
+ cooldown
45
46
  jobs
46
47
  redirect
47
48
  retry
@@ -231,6 +231,21 @@ module Bundler
231
231
  destination
232
232
  end
233
233
 
234
+ # Globs for entries matching +glob+ inside of +base_path+, returning
235
+ # absolute paths. Glob metacharacters in +base_path+ are not treated as
236
+ # part of the pattern, and matched entries are joined literally, so an
237
+ # entry starting with `~` is not expanded into the home directory.
238
+ #
239
+ # Bundler runs against whatever RubyGems the host provides, so this cannot
240
+ # delegate to Gem::Util.glob_files_in_dir, which only gained the literal
241
+ # join in RubyGems 4.1.
242
+ def glob_files_in_dir(glob, base_path)
243
+ expanded_path = nil
244
+ Dir.glob(glob, base: base_path).map! do |f|
245
+ File.join(expanded_path ||= File.expand_path(base_path), f)
246
+ end
247
+ end
248
+
234
249
  private
235
250
 
236
251
  def validate_bundle_path
@@ -346,7 +361,12 @@ module Bundler
346
361
 
347
362
  def set_rubyopt
348
363
  rubyopt = [ENV["RUBYOPT"]].compact
349
- setup_require = "-r#{File.expand_path("setup", __dir__)}"
364
+ setup_path = File.expand_path("setup", __dir__)
365
+ # RUBYOPT is split on whitespace with no quoting mechanism, so an
366
+ # absolute path containing spaces would be torn apart. Fall back to
367
+ # requiring by feature name; set_rubylib puts our lib directory first
368
+ # on the child's load path.
369
+ setup_require = /\s/.match?(setup_path) ? "-rbundler/setup" : "-r#{setup_path}"
350
370
  return if !rubyopt.empty? && rubyopt.first.include?(setup_require)
351
371
  rubyopt.unshift setup_require
352
372
  Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ")
@@ -144,6 +144,12 @@ module Bundler
144
144
  FileUtils.rm_rf(p)
145
145
  end
146
146
  git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
147
+ # The copy is cloned from the local bare cache, which holds no Git LFS
148
+ # objects, so point origin back at the real remote and let git-lfs derive
149
+ # its endpoint from there when checking out. Use the credential-filtered
150
+ # URI to avoid persisting secrets in the copy's .git/config; auth is left
151
+ # to git's credential helper.
152
+ git "remote", "set-url", "origin", credential_filtered_uri, dir: destination
147
153
  File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination)
148
154
  rescue Errno::EEXIST => e
149
155
  file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1]
@@ -187,7 +193,13 @@ module Bundler
187
193
  return out if status.success?
188
194
 
189
195
  if err.include?("couldn't find remote ref") || err.include?("not our ref")
190
- raise MissingGitRevisionError.new(command_with_no_credentials, path, commit || explicit_ref, credential_filtered_uri)
196
+ default_branch = renamed_remote_default_branch if tracking_remote_default_branch?
197
+ if default_branch
198
+ out = follow_remote_default_branch(args, default_branch)
199
+ return out if out
200
+ end
201
+
202
+ raise MissingGitRevisionError.new(command_with_no_credentials, path, commit || explicit_ref || current_branch, credential_filtered_uri)
191
203
  else
192
204
  if shallow?
193
205
  args -= depth_args
@@ -292,6 +304,61 @@ module Bundler
292
304
  branch_option || ref.nil?
293
305
  end
294
306
 
307
+ def tracking_remote_default_branch?
308
+ explicit_ref.nil? && commit.nil?
309
+ end
310
+
311
+ # Returns nil on any failure, leaving HEAD untouched so the caller reports
312
+ # the original fetch failure. Runs inside the retry block, so it must not
313
+ # touch the caller's command locals.
314
+ def follow_remote_default_branch(args, default_branch)
315
+ reference = "refs/heads/#{default_branch}"
316
+ command = fetch_command(args, "#{reference}:#{reference}")
317
+ check_allowed(command)
318
+
319
+ out, err, status = capture(command, path)
320
+ unless status.success?
321
+ Bundler.ui.debug "Could not fetch #{reference} from #{credential_filtered_uri}: #{err}"
322
+ return
323
+ end
324
+
325
+ previous_branch = current_branch
326
+ begin
327
+ git "symbolic-ref", "HEAD", reference, dir: path
328
+ rescue GitError => e
329
+ Bundler.ui.debug "Could not repoint the cached clone at #{reference}: #{e.message}"
330
+ return
331
+ end
332
+ @current_branch = nil
333
+ Bundler.ui.warn "#{credential_filtered_uri} no longer has #{previous_branch}, " \
334
+ "now following its default branch #{default_branch}"
335
+ out
336
+ end
337
+
338
+ # The cached clone's HEAD branch is gone from the remote, so the remote's
339
+ # own idea of its default branch is the only thing left to follow.
340
+ def renamed_remote_default_branch
341
+ default_branch = remote_default_branch
342
+ return if default_branch.nil? || default_branch == current_branch
343
+
344
+ default_branch
345
+ end
346
+
347
+ def remote_default_branch
348
+ command = ["ls-remote", "--symref", "--", configured_uri, "HEAD"]
349
+ check_allowed(command)
350
+
351
+ out, err, status = capture(command, path)
352
+ unless status.success?
353
+ Bundler.ui.debug "Could not ask #{credential_filtered_uri} for its default branch: #{err}"
354
+ return
355
+ end
356
+
357
+ # A remote is free to advertise a ref name that is not valid UTF-8, and
358
+ # matching that as text raises out of the GitError family.
359
+ out.b[%r{^ref:\s+refs/heads/(.+?)\s+HEAD}, 1]
360
+ end
361
+
295
362
  def pinned_to_full_sha?
296
363
  full_sha_revision?(ref)
297
364
  end
@@ -358,9 +425,10 @@ module Bundler
358
425
  git("rev-parse", "--verify", reference, dir: path).strip
359
426
  end
360
427
 
361
- # Adds credentials to the URI
428
+ # Adds credentials to the URI. This is the URI given to git, so it's
429
+ # also the one command output must be filtered against.
362
430
  def configured_uri
363
- if /https?:/.match?(uri)
431
+ @configured_uri ||= if /https?:/.match?(uri)
364
432
  remote = Gem::URI(uri)
365
433
  config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host]
366
434
  remote.userinfo ||= config_auth
@@ -403,7 +471,7 @@ module Bundler
403
471
  raise GitNotInstalledError.new unless Bundler.git_present?
404
472
 
405
473
  require "shellwords"
406
- URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", uri)
474
+ URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", configured_uri)
407
475
  end
408
476
 
409
477
  def run_command(*command, dir: nil)
@@ -423,18 +491,23 @@ module Bundler
423
491
  require "open3"
424
492
  out, err, status = Open3.capture3(*capture3_args_for(cmd, dir))
425
493
 
426
- filtered_out = URICredentialsFilter.credential_filtered_string(out, uri)
494
+ filtered_out = URICredentialsFilter.credential_filtered_string(out, configured_uri)
427
495
  return [filtered_out, status] if ignore_err
428
496
 
429
- filtered_err = URICredentialsFilter.credential_filtered_string(err, uri)
497
+ filtered_err = URICredentialsFilter.credential_filtered_string(err, configured_uri)
430
498
  [filtered_out, filtered_err, status]
431
499
  end
432
500
  end
433
501
 
434
502
  def capture3_args_for(cmd, dir)
435
- return ["git", *cmd] unless dir
503
+ # Disable automatic maintenance so a background commit-graph write in
504
+ # the source repo can't race the hardlinking local clone and fail with
505
+ # "hardlink different from source".
506
+ opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
507
+
508
+ return ["git", *opts, *cmd] unless dir
436
509
 
437
- ["git", "-C", dir.to_s, *cmd]
510
+ ["git", "-C", dir.to_s, *opts, *cmd]
438
511
  end
439
512
 
440
513
  def extra_clone_args
@@ -453,8 +526,8 @@ module Bundler
453
526
  args
454
527
  end
455
528
 
456
- def fetch_command(args)
457
- ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact
529
+ def fetch_command(args, spec = refspec)
530
+ ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, spec].compact
458
531
  end
459
532
 
460
533
  def clone_command(args)
@@ -91,9 +91,9 @@ module Bundler
91
91
 
92
92
  def to_s
93
93
  begin
94
- at = humanized_ref || current_branch
95
-
96
- rev = "at #{at}@#{shortref_for_display(revision)}"
94
+ at = humanized_ref
95
+ at = "#{at}@" if at
96
+ rev = "at #{at}#{shortref_for_display(revision)}"
97
97
  rescue GitError
98
98
  ""
99
99
  end
@@ -320,7 +320,7 @@ module Bundler
320
320
 
321
321
  def serialize_gemspecs_in(destination)
322
322
  destination = destination.expand_path(Bundler.root) if destination.relative?
323
- Dir["#{destination}/#{@glob}"].each do |spec_path|
323
+ SharedHelpers.glob_files_in_dir(@glob, destination.to_s).each do |spec_path|
324
324
  # Evaluate gemspecs and cache the result. Gemspecs
325
325
  # in git might require git or other dependencies.
326
326
  # The gemspecs we cache should already be evaluated.
@@ -58,6 +58,10 @@ module Bundler
58
58
  def version_message(spec)
59
59
  "#{spec.name} #{spec.version}"
60
60
  end
61
+
62
+ def checksum_store
63
+ @checksum_store ||= Checksum::Store.new
64
+ end
61
65
  end
62
66
  end
63
67
  end
@@ -161,7 +161,7 @@ module Bundler
161
161
 
162
162
  if File.directory?(expanded_path)
163
163
  # We sort depth-first since `<<` will override the earlier-found specs
164
- Gem::Util.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file|
164
+ SharedHelpers.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file|
165
165
  next unless spec = load_gemspec(file)
166
166
  spec.source = self
167
167
 
@@ -220,10 +220,11 @@ module Bundler
220
220
  # Some gem authors put absolute paths in their gemspec
221
221
  # and we have to save them from themselves
222
222
  spec.files = spec.files.filter_map do |path|
223
- next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path)
223
+ pathname = Pathname.new(path)
224
+ next path unless pathname.absolute?
224
225
  next if File.directory?(path)
225
226
  begin
226
- Pathname.new(path).relative_path_from(gem_dir).to_s
227
+ pathname.relative_path_from(gem_dir).to_s
227
228
  rescue ArgumentError
228
229
  path
229
230
  end
@@ -4,9 +4,9 @@ module Bundler
4
4
  class Source
5
5
  class Rubygems
6
6
  class Remote
7
- attr_reader :uri, :anonymized_uri, :original_uri
7
+ attr_reader :uri, :anonymized_uri, :original_uri, :cooldown
8
8
 
9
- def initialize(uri)
9
+ def initialize(uri, cooldown: nil)
10
10
  orig_uri = uri
11
11
  uri = Bundler.settings.mirror_for(uri)
12
12
  @original_uri = orig_uri if orig_uri != uri
@@ -14,6 +14,16 @@ module Bundler
14
14
 
15
15
  @uri = apply_auth(uri, fallback_auth).freeze
16
16
  @anonymized_uri = remove_auth(@uri).freeze
17
+ @cooldown = cooldown
18
+ end
19
+
20
+ # Returns the cooldown days that apply to this remote, resolving the
21
+ # precedence CLI > config > Gemfile per-source. Returns nil if no
22
+ # cooldown applies.
23
+ def effective_cooldown
24
+ override = Bundler.settings[:cooldown]
25
+ return override if override
26
+ @cooldown
17
27
  end
18
28
 
19
29
  MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length
@@ -11,11 +11,12 @@ module Bundler
11
11
  API_REQUEST_SIZE = 100
12
12
  REQUIRE_MUTEX = Mutex.new
13
13
 
14
- attr_accessor :remotes
14
+ attr_accessor :remotes, :remote_cooldowns
15
15
 
16
16
  def initialize(options = {})
17
17
  @options = options
18
18
  @remotes = []
19
+ @remote_cooldowns = {}
19
20
  @dependency_names = []
20
21
  @allow_remote = false
21
22
  @allow_cached = false
@@ -24,8 +25,10 @@ module Bundler
24
25
  @checksum_store = Checksum::Store.new
25
26
  @gem_installers = {}
26
27
  @gem_installers_mutex = Mutex.new
28
+ @remote_specs_mutex = Mutex.new
27
29
 
28
- Array(options["remotes"]).reverse_each {|r| add_remote(r) }
30
+ cooldown = options["cooldown"]
31
+ Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
29
32
 
30
33
  @lockfile_remotes = @remotes if options["from_lockfile"]
31
34
  end
@@ -148,6 +151,13 @@ module Bundler
148
151
  # sources, and large_idx.merge! small_idx is way faster than
149
152
  # small_idx.merge! large_idx.
150
153
  index = @allow_remote ? remote_specs.dup : Index.new
154
+
155
+ # Snapshot per-version `created_at` from the remote info before installed
156
+ # / cached specs overwrite the EndpointSpecification objects that carry
157
+ # it. The cooldown filter consults `created_at` on every candidate, so
158
+ # local stubs need the published date back-filled to participate.
159
+ remote_created_at = collect_remote_created_at(index)
160
+
151
161
  index.merge!(cached_specs) if @allow_cached
152
162
  index.merge!(installed_specs) if @allow_local
153
163
 
@@ -161,6 +171,8 @@ module Bundler
161
171
  end
162
172
  end
163
173
 
174
+ backfill_created_at(index, remote_created_at) unless remote_created_at.empty?
175
+
164
176
  index
165
177
  end
166
178
  end
@@ -232,7 +244,7 @@ module Bundler
232
244
  def cached_built_in_gem(spec, local: false)
233
245
  cached_path = cached_gem(spec)
234
246
  if cached_path.nil? && !local
235
- remote_spec = remote_specs.search(spec).first
247
+ remote_spec = remote_spec_for(spec)
236
248
  if remote_spec
237
249
  cached_path = fetch_gem(remote_spec)
238
250
  spec.remote = remote_spec.remote
@@ -243,9 +255,14 @@ module Bundler
243
255
  cached_path
244
256
  end
245
257
 
246
- def add_remote(source)
258
+ def add_remote(source, cooldown: nil)
247
259
  uri = normalize_uri(source)
248
260
  @remotes.unshift(uri) unless @remotes.include?(uri)
261
+ @remote_cooldowns[uri] = cooldown if cooldown
262
+ end
263
+
264
+ def cooldown_for(uri)
265
+ @remote_cooldowns[uri]
249
266
  end
250
267
 
251
268
  def spec_names
@@ -266,7 +283,7 @@ module Bundler
266
283
 
267
284
  def remote_fetchers
268
285
  @remote_fetchers ||= remotes.to_h do |uri|
269
- remote = Source::Rubygems::Remote.new(uri)
286
+ remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri))
270
287
  [remote, Bundler::Fetcher.new(remote)]
271
288
  end.freeze
272
289
  end
@@ -314,6 +331,19 @@ module Bundler
314
331
  @allow_remote && api_fetchers.any?
315
332
  end
316
333
 
334
+ def clear_cache
335
+ @specs = nil
336
+ @installed_specs = nil
337
+ @default_specs = nil
338
+ @cached_specs = nil
339
+ end
340
+
341
+ def release_resolution_memory!
342
+ @specs = nil
343
+ @remote_specs_mutex.synchronize { @remote_specs = nil }
344
+ @fetchers&.each(&:release_resolution_memory!)
345
+ end
346
+
317
347
  protected
318
348
 
319
349
  def remote_names
@@ -376,7 +406,7 @@ module Bundler
376
406
  @cached_specs ||= begin
377
407
  idx = Index.new
378
408
 
379
- Dir["#{cache_path}/*.gem"].each do |gemfile|
409
+ SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s).each do |gemfile|
380
410
  s ||= Bundler.rubygems.spec_from_gem(gemfile)
381
411
  s.source = self
382
412
  idx << s
@@ -391,17 +421,30 @@ module Bundler
391
421
  end
392
422
 
393
423
  def remote_specs
394
- @remote_specs ||= Index.build do |idx|
395
- index_fetchers = fetchers - api_fetchers
424
+ @remote_specs ||= @remote_specs_mutex.synchronize do
425
+ @remote_specs ||= Index.build do |idx|
426
+ index_fetchers = fetchers - api_fetchers
396
427
 
397
- if index_fetchers.empty?
398
- fetch_names(api_fetchers, dependency_names, idx)
399
- else
400
- fetch_names(fetchers, nil, idx)
428
+ if index_fetchers.empty?
429
+ fetch_names(api_fetchers, dependency_names, idx)
430
+ else
431
+ fetch_names(fetchers, nil, idx)
432
+ end
401
433
  end
402
434
  end
403
435
  end
404
436
 
437
+ # Looks up a single spec in the remote sources, fetching only its own
438
+ # name when the full remote index is not already materialized.
439
+ def remote_spec_for(spec)
440
+ return remote_specs.search(spec).first if @remote_specs || api_fetchers.empty?
441
+
442
+ index = Index.build do |idx|
443
+ fetch_names(api_fetchers, [spec.name], idx)
444
+ end
445
+ index.search(spec).first
446
+ end
447
+
405
448
  def fetch_names(fetchers, dependency_names, index)
406
449
  fetchers.each do |f|
407
450
  if dependency_names
@@ -456,6 +499,31 @@ module Bundler
456
499
 
457
500
  private
458
501
 
502
+ def collect_remote_created_at(index)
503
+ return {} unless @allow_remote
504
+
505
+ snapshot = {}
506
+ index.each do |spec|
507
+ next unless spec.respond_to?(:created_at) && spec.created_at
508
+ # Remember the remote that supplied the date too: when a source has
509
+ # several remotes with different per-URI cooldown settings we must
510
+ # restore the same one during backfill so `effective_cooldown` agrees.
511
+ snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote]
512
+ end
513
+ snapshot
514
+ end
515
+
516
+ def backfill_created_at(index, snapshot)
517
+ index.each do |spec|
518
+ next unless spec.respond_to?(:created_at=)
519
+ next if spec.created_at
520
+ remote_created_at, remote = snapshot[[spec.name, spec.version]]
521
+ next unless remote_created_at
522
+ spec.created_at = remote_created_at
523
+ spec.remote ||= remote if remote && spec.respond_to?(:remote=)
524
+ end
525
+ end
526
+
459
527
  def lockfile_remotes
460
528
  @lockfile_remotes || credless_remotes
461
529
  end
@@ -508,7 +576,7 @@ module Bundler
508
576
 
509
577
  # We are using a mutex to reaed and write from/to the hash.
510
578
  # The reason this double synchronization was added is for performance
511
- # and lock the mutex for the shortest possible amount of time. Otherwise,
579
+ # and to lock the mutex for the shortest possible amount of time. Otherwise,
512
580
  # all threads are fighting over this mutex and when it gets acquired it gets locked
513
581
  # until a threads finishes downloading a gem, leaving the other threads waiting
514
582
  # doing nothing.
@@ -50,7 +50,13 @@ module Bundler
50
50
 
51
51
  def add_rubygems_source(options = {})
52
52
  new_source = Source::Rubygems.new(options)
53
- return @global_rubygems_source if @global_rubygems_source == new_source
53
+ if @global_rubygems_source == new_source
54
+ warn_on_cooldown_conflict(new_source, @global_rubygems_source)
55
+ return @global_rubygems_source
56
+ end
57
+
58
+ existing_source = @rubygems_sources.find {|s| s == new_source }
59
+ warn_on_cooldown_conflict(new_source, existing_source) if existing_source
54
60
 
55
61
  add_source_to_list new_source, @rubygems_sources
56
62
  end
@@ -59,8 +65,15 @@ module Bundler
59
65
  add_source_to_list Plugin.source(source).new(options), @plugin_sources
60
66
  end
61
67
 
62
- def add_global_rubygems_remote(uri)
63
- global_rubygems_source.add_remote(uri)
68
+ def add_global_rubygems_remote(uri, cooldown: nil)
69
+ unless cooldown.nil?
70
+ new_source = source_class.new("remotes" => uri, "cooldown" => cooldown)
71
+ [global_rubygems_source, *@rubygems_sources].find do |existing_source|
72
+ warn_on_cooldown_conflict(new_source, existing_source)
73
+ end
74
+ end
75
+
76
+ global_rubygems_source.add_remote(uri, cooldown: cooldown)
64
77
  global_rubygems_source
65
78
  end
66
79
 
@@ -136,6 +149,14 @@ module Bundler
136
149
  all_sources.each(&:remote!)
137
150
  end
138
151
 
152
+ def clear_cache
153
+ rubygems_sources.each(&:clear_cache)
154
+ end
155
+
156
+ def release_resolution_memory!
157
+ rubygems_sources.each(&:release_resolution_memory!)
158
+ end
159
+
139
160
  private
140
161
 
141
162
  def map_sources(replacement_sources)
@@ -165,6 +186,10 @@ module Bundler
165
186
  # locked sources never include credentials so always prefer remotes from the gemfile
166
187
  replacement_source.remotes = gemfile_source.remotes
167
188
 
189
+ # cooldowns are only ever declared in the Gemfile, so carry them over
190
+ # along with the remotes they apply to
191
+ replacement_source.remote_cooldowns = gemfile_source.remote_cooldowns
192
+
168
193
  yield replacement_source if block_given?
169
194
 
170
195
  replacement_source
@@ -210,6 +235,22 @@ module Bundler
210
235
  end
211
236
  end
212
237
 
238
+ def warn_on_cooldown_conflict(new_source, existing_source)
239
+ new_source.remote_cooldowns.any? do |uri, cooldown|
240
+ next false unless existing_source.remotes.include?(uri)
241
+
242
+ existing_cooldown = existing_source.cooldown_for(uri)
243
+ next false if existing_cooldown == cooldown
244
+
245
+ previous = existing_cooldown ? "`cooldown: #{existing_cooldown}`" : "no cooldown"
246
+ Bundler.ui.warn "The source #{uri} is declared more than once with different cooldown " \
247
+ "values (`cooldown: #{cooldown}` here, #{previous} previously). All declarations of " \
248
+ "the same source URL share a single cooldown, so only one of these values will apply " \
249
+ "to all gems from this source."
250
+ true
251
+ end
252
+ end
253
+
213
254
  def warn_on_git_protocol(source)
214
255
  return if Bundler.settings["git.allow_insecure"]
215
256
 
@@ -5,7 +5,7 @@ require_relative "vendored_tsort"
5
5
  module Bundler
6
6
  class SpecSet
7
7
  include Enumerable
8
- include TSort
8
+ include Gem::TSort
9
9
 
10
10
  def initialize(specs)
11
11
  @specs = specs
@@ -315,7 +315,7 @@ module Bundler
315
315
 
316
316
  def sorted
317
317
  @sorted ||= ([@specs.find {|s| s.name == "rake" }] + tsort).compact.uniq
318
- rescue TSort::Cyclic => error
318
+ rescue Gem::TSort::Cyclic => error
319
319
  cgems = extract_circular_gems(error)
320
320
  raise CyclicDependencyError, "Your bundle requires gems that depend" \
321
321
  " on each other, creating an infinite loop. Please remove either" \
@@ -126,7 +126,9 @@ module Bundler
126
126
  def _remote_specification
127
127
  @_remote_specification ||= begin
128
128
  rs = stub.to_spec
129
- if rs.equal?(self) # happens when to_spec gets the spec from Gem.loaded_specs
129
+ # Gem::StubSpecification#to_spec may return an activated specification
130
+ # with the same name and version from a different gem installation.
131
+ if rs.equal?(self) || rs.loaded_from != loaded_from
130
132
  rs = Gem::Specification.load(loaded_from)
131
133
  Bundler.rubygems.stub_set_spec(stub, rs)
132
134
  end
@@ -22,6 +22,12 @@ Gem::Specification.new do |spec|
22
22
  spec.metadata["changelog_uri"] = "<%= config[:changelog_uri] %>"
23
23
  <%- end -%>
24
24
 
25
+ # Uncomment the line below to require MFA for gem pushes.
26
+ # This helps protect your gem from supply chain attacks by ensuring
27
+ # no one can publish a new version without multi-factor authentication.
28
+ # See: https://guides.rubygems.org/mfa-requirement-opt-in/
29
+ # spec.metadata["rubygems_mfa_required"] = "true"
30
+
25
31
  # Specify which files should be added to the gem when it is released.
26
32
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
33
  gemspec = File.basename(__FILE__)
@@ -41,12 +47,12 @@ Gem::Specification.new do |spec|
41
47
  # Uncomment to register a new dependency of your gem
42
48
  # spec.add_dependency "example-gem", "~> 1.0"
43
49
  <%- if config[:ext] == 'rust' -%>
44
- spec.add_dependency "rb_sys", "~> 0.9.91"
50
+ spec.add_dependency "rb_sys", "~> 0.9.128"
45
51
  <%- end -%>
46
52
  <%- if config[:ext] == 'go' -%>
47
53
  spec.add_dependency "go_gem", "~> 0.2"
48
54
  <%- end -%>
49
55
 
50
56
  # For more information and examples about making a new gem, check out our
51
- # guide at: https://bundler.io/guides/creating_gem.html
57
+ # guide at: https://guides.rubygems.org/make-your-own-gem/
52
58
  end
@@ -1,4 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Bundler; end
4
- require_relative "vendor/tsort/lib/tsort"
3
+ # The defined? guard avoids reopening Gem::TSort when an old RubyGems has
4
+ # already loaded its own copy, e.g. through rubygems/request_set from
5
+ # Gem.activate_bin_path in binstubs.
6
+ #
7
+ unless defined?(Gem::TSort)
8
+ begin
9
+ require "rubygems/vendored_tsort"
10
+ rescue LoadError
11
+ begin
12
+ # RubyGems 3.4 and 3.5 ship the same file under its pre-3.6 name.
13
+ # Requiring the real tsort here instead would activate the tsort
14
+ # default gem, and `bundler/setup` must not activate any gems.
15
+ require "rubygems/tsort"
16
+ rescue LoadError
17
+ require "tsort"
18
+ Gem::TSort = TSort
19
+ end
20
+ end
21
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module Bundler
4
- VERSION = "4.0.10".freeze
4
+ VERSION = "4.0.20".freeze
5
5
 
6
6
  def self.bundler_major_version
7
7
  @bundler_major_version ||= gem_version.segments.first