bundler 4.0.9 → 4.0.18

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +147 -0
  3. data/bundler.gemspec +2 -2
  4. data/lib/bundler/build_metadata.rb +1 -1
  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 +6 -0
  8. data/lib/bundler/cli/config.rb +8 -3
  9. data/lib/bundler/cli/gem.rb +2 -3
  10. data/lib/bundler/cli/install.rb +3 -0
  11. data/lib/bundler/cli/issue.rb +2 -2
  12. data/lib/bundler/cli/lock.rb +3 -0
  13. data/lib/bundler/cli/open.rb +12 -2
  14. data/lib/bundler/cli/outdated.rb +42 -2
  15. data/lib/bundler/cli/update.rb +2 -0
  16. data/lib/bundler/cli.rb +18 -12
  17. data/lib/bundler/compact_index_client/cache.rb +11 -0
  18. data/lib/bundler/compact_index_client/cache_file.rb +1 -1
  19. data/lib/bundler/compact_index_client/parser.rb +4 -1
  20. data/lib/bundler/definition.rb +51 -12
  21. data/lib/bundler/dsl.rb +10 -3
  22. data/lib/bundler/endpoint_specification.rb +17 -2
  23. data/lib/bundler/fetcher/base.rb +3 -0
  24. data/lib/bundler/fetcher/compact_index.rb +13 -0
  25. data/lib/bundler/fetcher/gem_remote_fetcher.rb +2 -0
  26. data/lib/bundler/fetcher.rb +12 -2
  27. data/lib/bundler/injector.rb +1 -2
  28. data/lib/bundler/installer/parallel_installer.rb +68 -23
  29. data/lib/bundler/installer.rb +12 -1
  30. data/lib/bundler/lockfile_generator.rb +39 -1
  31. data/lib/bundler/lockfile_parser.rb +23 -1
  32. data/lib/bundler/man/bundle-add.1 +6 -3
  33. data/lib/bundler/man/bundle-add.1.ronn +7 -2
  34. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  35. data/lib/bundler/man/bundle-cache.1 +4 -1
  36. data/lib/bundler/man/bundle-cache.1.ronn +6 -0
  37. data/lib/bundler/man/bundle-check.1 +1 -1
  38. data/lib/bundler/man/bundle-clean.1 +1 -1
  39. data/lib/bundler/man/bundle-config.1 +121 -157
  40. data/lib/bundler/man/bundle-config.1.ronn +33 -1
  41. data/lib/bundler/man/bundle-console.1 +3 -3
  42. data/lib/bundler/man/bundle-console.1.ronn +2 -2
  43. data/lib/bundler/man/bundle-doctor.1 +1 -1
  44. data/lib/bundler/man/bundle-env.1 +1 -1
  45. data/lib/bundler/man/bundle-exec.1 +1 -1
  46. data/lib/bundler/man/bundle-fund.1 +1 -1
  47. data/lib/bundler/man/bundle-gem.1 +1 -1
  48. data/lib/bundler/man/bundle-help.1 +1 -1
  49. data/lib/bundler/man/bundle-info.1 +1 -1
  50. data/lib/bundler/man/bundle-init.1 +2 -2
  51. data/lib/bundler/man/bundle-init.1.ronn +1 -1
  52. data/lib/bundler/man/bundle-install.1 +5 -2
  53. data/lib/bundler/man/bundle-install.1.ronn +9 -1
  54. data/lib/bundler/man/bundle-issue.1 +1 -1
  55. data/lib/bundler/man/bundle-licenses.1 +1 -1
  56. data/lib/bundler/man/bundle-list.1 +1 -1
  57. data/lib/bundler/man/bundle-lock.1 +5 -2
  58. data/lib/bundler/man/bundle-lock.1.ronn +6 -0
  59. data/lib/bundler/man/bundle-open.1 +1 -1
  60. data/lib/bundler/man/bundle-outdated.1 +17 -14
  61. data/lib/bundler/man/bundle-outdated.1.ronn +19 -12
  62. data/lib/bundler/man/bundle-platform.1 +1 -1
  63. data/lib/bundler/man/bundle-plugin.1 +2 -2
  64. data/lib/bundler/man/bundle-plugin.1.ronn +1 -1
  65. data/lib/bundler/man/bundle-pristine.1 +1 -1
  66. data/lib/bundler/man/bundle-remove.1 +1 -1
  67. data/lib/bundler/man/bundle-show.1 +1 -1
  68. data/lib/bundler/man/bundle-update.1 +5 -2
  69. data/lib/bundler/man/bundle-update.1.ronn +8 -0
  70. data/lib/bundler/man/bundle-version.1 +1 -1
  71. data/lib/bundler/man/bundle.1 +23 -2
  72. data/lib/bundler/man/bundle.1.ronn +22 -1
  73. data/lib/bundler/man/gemfile.5 +13 -2
  74. data/lib/bundler/man/gemfile.5.ronn +20 -1
  75. data/lib/bundler/mirror.rb +7 -7
  76. data/lib/bundler/plugin/unloaded_source.rb +25 -0
  77. data/lib/bundler/plugin.rb +12 -6
  78. data/lib/bundler/remote_specification.rb +1 -1
  79. data/lib/bundler/resolver/base.rb +9 -0
  80. data/lib/bundler/resolver/strategy.rb +6 -3
  81. data/lib/bundler/resolver.rb +80 -1
  82. data/lib/bundler/rubygems_ext.rb +22 -0
  83. data/lib/bundler/rubygems_gem_installer.rb +35 -2
  84. data/lib/bundler/runtime.rb +11 -11
  85. data/lib/bundler/self_manager.rb +6 -1
  86. data/lib/bundler/settings/validator.rb +7 -0
  87. data/lib/bundler/settings.rb +2 -0
  88. data/lib/bundler/shared_helpers.rb +6 -1
  89. data/lib/bundler/source/git/git_proxy.rb +13 -2
  90. data/lib/bundler/source/git.rb +1 -1
  91. data/lib/bundler/source/metadata.rb +4 -0
  92. data/lib/bundler/source/path.rb +3 -2
  93. data/lib/bundler/source/rubygems/remote.rb +12 -2
  94. data/lib/bundler/source/rubygems.rb +81 -13
  95. data/lib/bundler/source_list.rb +44 -3
  96. data/lib/bundler/spec_set.rb +2 -2
  97. data/lib/bundler/stub_specification.rb +1 -0
  98. data/lib/bundler/templates/newgem/newgem.gemspec.tt +10 -1
  99. data/lib/bundler/vendored_tsort.rb +19 -2
  100. data/lib/bundler/version.rb +1 -1
  101. data/lib/bundler.rb +11 -1
  102. metadata +5 -6
  103. data/lib/bundler/vendor/tsort/LICENSE.txt +0 -22
  104. data/lib/bundler/vendor/tsort/lib/tsort.rb +0 -455
@@ -184,6 +184,9 @@ module Bundler
184
184
 
185
185
  platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : ""
186
186
  custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}"
187
+ if hint = cooldown_hint(specs_matching_other_platforms)
188
+ custom_explanation += " (#{hint})"
189
+ end
187
190
 
188
191
  label = "#{name} (#{constraint_string})"
189
192
  extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any?
@@ -353,9 +356,31 @@ module Bundler
353
356
  message << "\n#{other_specs_matching_message(specs, matching_part)}"
354
357
  end
355
358
 
359
+ if hint = cooldown_hint(specs_matching_requirement)
360
+ message << "\n\n#{hint}."
361
+ end
362
+
363
+ if specs_matching_requirement.any? && (hint = platform_mismatch_hint)
364
+ message << "\n\n#{hint}"
365
+ end
366
+
356
367
  raise GemNotFound, message
357
368
  end
358
369
 
370
+ def platform_mismatch_hint
371
+ locked_platforms = Bundler.locked_gems&.platforms
372
+ return unless locked_platforms
373
+
374
+ local_platform = Bundler.local_platform
375
+ return if locked_platforms.include?(local_platform)
376
+ return if locked_platforms.any? {|p| p == Gem::Platform::RUBY }
377
+
378
+ "Your current platform (#{local_platform}) is not included in the lockfile's platforms (#{locked_platforms.join(", ")}). " \
379
+ "Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
380
+ rescue GemfileNotFound
381
+ nil
382
+ end
383
+
359
384
  def filtered_versions_for(package)
360
385
  @gem_version_promoter.filter_versions(package, @all_versions[package])
361
386
  end
@@ -378,7 +403,7 @@ module Bundler
378
403
  end
379
404
 
380
405
  def filter_specs(specs, package)
381
- filter_remote_specs(filter_prereleases(specs, package), package)
406
+ filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package)
382
407
  end
383
408
 
384
409
  def filter_prereleases(specs, package)
@@ -387,6 +412,60 @@ module Bundler
387
412
  specs.reject {|s| s.version.prerelease? }
388
413
  end
389
414
 
415
+ def filter_cooldown(specs)
416
+ return specs if specs.empty?
417
+ excluded_versions = cooldown_excluded_versions(specs)
418
+ return specs if excluded_versions.empty?
419
+ specs.reject {|s| excluded_versions.include?([s.name, s.version]) }
420
+ end
421
+
422
+ def cooldown_excluded_versions(specs)
423
+ excluded = {}
424
+ specs.each do |spec|
425
+ next unless cooldown_excluded?(spec)
426
+ excluded[[spec.name, spec.version]] = true
427
+ end
428
+ excluded
429
+ end
430
+
431
+ def cooldown_hint(specs)
432
+ excluded_versions = cooldown_excluded_versions(specs)
433
+ return nil if excluded_versions.empty?
434
+ "#{excluded_versions.size} version#{"s" if excluded_versions.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass"
435
+ end
436
+
437
+ def cooldown_excluded?(spec)
438
+ return false unless spec.respond_to?(:created_at) && spec.created_at
439
+ return false unless spec.respond_to?(:remote) && spec.remote
440
+ return false if locked_by_lockfile?(spec)
441
+ days = spec.remote.effective_cooldown
442
+ return false if days.nil? || days <= 0
443
+ (cooldown_now - spec.created_at) < (days * 86_400)
444
+ end
445
+
446
+ # A version already written to the lockfile has been adopted, and cooldown
447
+ # only governs the adoption of *new* versions, so it must never retract one
448
+ # the lockfile already pins. Keying this off the locked specs rather than the
449
+ # prevent-downgrade floor matters because that floor is absent on resolutions
450
+ # that re-pick a gem from scratch: the auxiliary full update run to compute
451
+ # `--update` targets, and the from-scratch retries after a conflict unlocks a
452
+ # gem. In those passes the locked version is the only candidate, so filtering
453
+ # it out makes an unrelated operation impossible whenever every published
454
+ # version matching the requirement sits inside the cooldown window.
455
+ #
456
+ # Gems named on a `bundle update GEM` command are the exception: the user
457
+ # asked to move them, so they stay subject to cooldown and a locked-but-fresh
458
+ # release is pushed back to an older one (or fails loudly when none exists).
459
+ def locked_by_lockfile?(spec)
460
+ return false unless defined?(@base) && @base
461
+ return false if @base.explicitly_unlocked?(spec.name)
462
+ @base.locked_specs[spec.name].any? {|locked| locked.version == spec.version }
463
+ end
464
+
465
+ def cooldown_now
466
+ @cooldown_now ||= Time.now
467
+ end
468
+
390
469
  def filter_remote_specs(specs, package)
391
470
  if package.prefer_local?
392
471
  local_specs = specs.select {|s| s.is_a?(StubSpecification) }
@@ -465,6 +465,28 @@ module Gem
465
465
  Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse)
466
466
  end
467
467
 
468
+ # RubyGems before 4.0.13 split compact index dependency/requirement entries
469
+ # on every colon, which mangles metadata values that contain colons such as
470
+ # the `created_at` timestamps the cooldown feature relies on. Split only on
471
+ # the first colon so those values survive on older RubyGems.
472
+ #
473
+ # The module is defined unconditionally so it stays testable on any RubyGems,
474
+ # but only prepended when the host RubyGems still has the buggy behavior.
475
+ module SplitCompactIndexEntryOnFirstColon
476
+ private
477
+
478
+ def parse_dependency(string)
479
+ dependency = string.split(":", 2)
480
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
481
+ dependency[0] = -dependency[0]
482
+ dependency
483
+ end
484
+ end
485
+
486
+ unless Gem.rubygems_version >= Gem::Version.new("4.0.13")
487
+ Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon)
488
+ end
489
+
468
490
  if Gem.rubygems_version < Gem::Version.new("3.6.0")
469
491
  class Package; end
470
492
  require "rubygems/package/tar_reader"
@@ -4,6 +4,11 @@ require "rubygems/installer"
4
4
 
5
5
  module Bundler
6
6
  class RubyGemsGemInstaller < Gem::Installer
7
+ # Cap how many jobserver slots a single gem's `make` may grab so that one
8
+ # gem with many recipes doesn't starve the others sharing the pool. Beyond
9
+ # a handful of jobs the extra parallelism rarely pays off in practice.
10
+ MAX_JOBS_PER_GEM = 3
11
+
7
12
  def check_executable_overwrite(filename)
8
13
  # Bundler needs to install gems regardless of binstub overwriting
9
14
  end
@@ -20,7 +25,7 @@ module Bundler
20
25
  strict_rm_rf spec.extension_dir
21
26
 
22
27
  SharedHelpers.filesystem_access(gem_dir, :create) do
23
- FileUtils.mkdir_p gem_dir, mode: 0o755
28
+ FileUtils.mkdir_p gem_dir
24
29
  end
25
30
 
26
31
  SharedHelpers.filesystem_access(gem_dir, :write) do
@@ -101,10 +106,18 @@ module Bundler
101
106
  end
102
107
 
103
108
  def build_jobs
104
- Bundler.settings[:jobs] || super
109
+ @jobserver_read_io&.read_nonblock(MAX_JOBS_PER_GEM, @jobserver_tokens)
110
+ acquired_jobs = @jobserver_tokens.empty? ? nil : @jobserver_tokens.size
111
+
112
+ acquired_jobs || Bundler.settings[:jobs] || super
113
+ rescue IO::WaitReadable, EOFError
114
+ 1
105
115
  end
106
116
 
107
117
  def build_extensions
118
+ @jobserver_tokens = +""
119
+ @jobserver_read_io, @jobserver_write_io = connect_to_jobserver
120
+
108
121
  extension_cache_path = options[:bundler_extension_cache_path]
109
122
  extension_dir = spec.extension_dir
110
123
  unless extension_cache_path && extension_dir
@@ -128,6 +141,11 @@ module Bundler
128
141
  FileUtils.cp_r extension_dir, extension_cache_path
129
142
  end
130
143
  end
144
+ ensure
145
+ unless @jobserver_tokens.empty?
146
+ @jobserver_write_io.write(@jobserver_tokens)
147
+ @jobserver_write_io.flush
148
+ end
131
149
  end
132
150
 
133
151
  def spec
@@ -144,6 +162,21 @@ module Bundler
144
162
 
145
163
  private
146
164
 
165
+ def connect_to_jobserver
166
+ return unless ENV["MAKEFLAGS"]
167
+ # We append our own --jobserver-auth, so read the last one. Otherwise a
168
+ # parent jobserver's descriptors (e.g. `bundle install` run under
169
+ # `make -j`) would be picked up instead of the pool ParallelInstaller created.
170
+ read_fd, write_fd = ENV["MAKEFLAGS"].scan(/--jobserver-auth=(\d+),(\d+)/).last
171
+
172
+ return unless read_fd && write_fd
173
+
174
+ # Pass explicit modes. On POSIX, IO.new detects the descriptor's access
175
+ # mode, but on Windows it can't, so the write end would default to read
176
+ # mode and raise "IOError: not opened for writing" when releasing slots.
177
+ [IO.new(read_fd.to_i, "r", autoclose: false), IO.new(write_fd.to_i, "w", autoclose: false)]
178
+ end
179
+
147
180
  def prepare_extension_build(extension_dir)
148
181
  SharedHelpers.filesystem_access(extension_dir, :create) do
149
182
  FileUtils.mkdir_p extension_dir
@@ -141,7 +141,7 @@ module Bundler
141
141
  end
142
142
  end
143
143
 
144
- Dir[cache_path.join("*/.git")].each do |git_dir|
144
+ Gem::Util.glob_files_in_dir("*/.git", cache_path.to_s).each do |git_dir|
145
145
  FileUtils.rm_rf(git_dir)
146
146
  FileUtils.touch(File.expand_path("../.bundlecache", git_dir))
147
147
  end
@@ -159,13 +159,13 @@ module Bundler
159
159
  end
160
160
 
161
161
  def clean(dry_run = false)
162
- gem_bins = Dir["#{Gem.dir}/bin/*"]
163
- git_dirs = Dir["#{Gem.dir}/bundler/gems/*"]
164
- git_cache_dirs = Dir["#{Gem.dir}/cache/bundler/git/*"]
165
- gem_dirs = Dir["#{Gem.dir}/gems/*"]
166
- gem_files = Dir["#{Gem.dir}/cache/*.gem"]
167
- gemspec_files = Dir["#{Gem.dir}/specifications/*.gemspec"]
168
- extension_dirs = Dir["#{Gem.dir}/extensions/*/*/*"] + Dir["#{Gem.dir}/bundler/gems/extensions/*/*/*"]
162
+ gem_bins = Gem::Util.glob_files_in_dir("bin/*", Gem.dir)
163
+ git_dirs = Gem::Util.glob_files_in_dir("bundler/gems/*", Gem.dir)
164
+ git_cache_dirs = Gem::Util.glob_files_in_dir("cache/bundler/git/*", Gem.dir)
165
+ gem_dirs = Gem::Util.glob_files_in_dir("gems/*", Gem.dir)
166
+ gem_files = Gem::Util.glob_files_in_dir("cache/*.gem", Gem.dir)
167
+ gemspec_files = Gem::Util.glob_files_in_dir("specifications/*.gemspec", Gem.dir)
168
+ extension_dirs = Gem::Util.glob_files_in_dir("extensions/*/*/*", Gem.dir) + Gem::Util.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir)
169
169
  spec_gem_paths = []
170
170
  # need to keep git sources around
171
171
  spec_git_paths = @definition.spec_git_paths
@@ -232,7 +232,7 @@ module Bundler
232
232
  private
233
233
 
234
234
  def prune_gem_cache(resolve, cache_path)
235
- cached = Dir["#{cache_path}/*.gem"]
235
+ cached = Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s)
236
236
 
237
237
  cached = cached.delete_if do |path|
238
238
  spec = Bundler.rubygems.spec_from_gem path
@@ -257,7 +257,7 @@ module Bundler
257
257
  end
258
258
 
259
259
  def prune_git_and_path_cache(resolve, cache_path)
260
- cached = Dir["#{cache_path}/*/.bundlecache"]
260
+ cached = Gem::Util.glob_files_in_dir("*/.bundlecache", cache_path.to_s)
261
261
 
262
262
  cached = cached.delete_if do |path|
263
263
  name = File.basename(File.dirname(path))
@@ -283,7 +283,7 @@ module Bundler
283
283
  # Add man/ subdirectories from activated bundles to MANPATH for man(1)
284
284
  manuals = $LOAD_PATH.filter_map do |path|
285
285
  man_subdir = path.sub(/lib$/, "man")
286
- man_subdir unless Dir[man_subdir + "/man?/"].empty?
286
+ man_subdir unless Dir.glob("man?/", base: man_subdir).empty?
287
287
  end
288
288
 
289
289
  return if manuals.empty?
@@ -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(
@@ -74,6 +74,13 @@ module Bundler
74
74
  fail!(key, value, "`#{other_key}` is current set to #{other_setting.inspect}", "the `#{conflicting.join("`, `")}` groups conflict")
75
75
  end
76
76
  end
77
+
78
+ rule %w[default_cli_command], "default_cli_command must be either 'install' or 'cli_help'" do |key, value, _settings|
79
+ valid_values = %w[install cli_help]
80
+ if !value.nil? && !valid_values.include?(value.to_s)
81
+ fail!(key, value, "must be one of: #{valid_values.join(", ")}")
82
+ end
83
+ end
77
84
  end
78
85
  end
79
86
  end
@@ -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
@@ -59,6 +60,7 @@ module Bundler
59
60
  bin
60
61
  cache_path
61
62
  console
63
+ default_cli_command
62
64
  gem.ci
63
65
  gem.github_username
64
66
  gem.linter
@@ -346,7 +346,12 @@ module Bundler
346
346
 
347
347
  def set_rubyopt
348
348
  rubyopt = [ENV["RUBYOPT"]].compact
349
- setup_require = "-r#{File.expand_path("setup", __dir__)}"
349
+ setup_path = File.expand_path("setup", __dir__)
350
+ # RUBYOPT is split on whitespace with no quoting mechanism, so an
351
+ # absolute path containing spaces would be torn apart. Fall back to
352
+ # requiring by feature name; set_rubylib puts our lib directory first
353
+ # on the child's load path.
354
+ setup_require = /\s/.match?(setup_path) ? "-rbundler/setup" : "-r#{setup_path}"
350
355
  return if !rubyopt.empty? && rubyopt.first.include?(setup_require)
351
356
  rubyopt.unshift setup_require
352
357
  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]
@@ -432,9 +438,14 @@ module Bundler
432
438
  end
433
439
 
434
440
  def capture3_args_for(cmd, dir)
435
- return ["git", *cmd] unless dir
441
+ # Disable automatic maintenance so a background commit-graph write in
442
+ # the source repo can't race the hardlinking local clone and fail with
443
+ # "hardlink different from source".
444
+ opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
436
445
 
437
- ["git", "-C", dir.to_s, *cmd]
446
+ return ["git", *opts, *cmd] unless dir
447
+
448
+ ["git", "-C", dir.to_s, *opts, *cmd]
438
449
  end
439
450
 
440
451
  def extra_clone_args
@@ -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
+ Gem::Util.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
@@ -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
+ Gem::Util.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.