rubygems-update 4.0.12 → 4.0.14

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/bundler/CHANGELOG.md +23 -0
  4. data/bundler/lib/bundler/build_metadata.rb +1 -1
  5. data/bundler/lib/bundler/cli/add.rb +3 -0
  6. data/bundler/lib/bundler/cli/common.rb +6 -0
  7. data/bundler/lib/bundler/cli/install.rb +3 -0
  8. data/bundler/lib/bundler/cli/outdated.rb +42 -2
  9. data/bundler/lib/bundler/cli/update.rb +2 -0
  10. data/bundler/lib/bundler/cli.rb +4 -0
  11. data/bundler/lib/bundler/dsl.rb +6 -2
  12. data/bundler/lib/bundler/endpoint_specification.rb +11 -1
  13. data/bundler/lib/bundler/installer.rb +5 -0
  14. data/bundler/lib/bundler/man/bundle-add.1 +4 -1
  15. data/bundler/lib/bundler/man/bundle-add.1.ronn +6 -1
  16. data/bundler/lib/bundler/man/bundle-config.1 +120 -156
  17. data/bundler/lib/bundler/man/bundle-config.1.ronn +30 -0
  18. data/bundler/lib/bundler/man/bundle-install.1 +4 -1
  19. data/bundler/lib/bundler/man/bundle-install.1.ronn +9 -1
  20. data/bundler/lib/bundler/man/bundle-outdated.1 +16 -13
  21. data/bundler/lib/bundler/man/bundle-outdated.1.ronn +19 -12
  22. data/bundler/lib/bundler/man/bundle-update.1 +4 -1
  23. data/bundler/lib/bundler/man/bundle-update.1.ronn +8 -0
  24. data/bundler/lib/bundler/remote_specification.rb +1 -1
  25. data/bundler/lib/bundler/resolver.rb +58 -1
  26. data/bundler/lib/bundler/rubygems_ext.rb +22 -0
  27. data/bundler/lib/bundler/rubygems_gem_installer.rb +1 -1
  28. data/bundler/lib/bundler/settings.rb +1 -0
  29. data/bundler/lib/bundler/source/git/git_proxy.rb +7 -2
  30. data/bundler/lib/bundler/source/rubygems/remote.rb +12 -2
  31. data/bundler/lib/bundler/source/rubygems.rb +52 -4
  32. data/bundler/lib/bundler/source_list.rb +10 -2
  33. data/bundler/lib/bundler/version.rb +1 -1
  34. data/lib/rubygems/ext/builder.rb +2 -1
  35. data/lib/rubygems/installer.rb +16 -3
  36. data/lib/rubygems/package.rb +21 -1
  37. data/lib/rubygems/resolver/api_set/gem_parser.rb +1 -1
  38. data/lib/rubygems/text.rb +10 -1
  39. data/lib/rubygems.rb +1 -1
  40. metadata +2 -2
@@ -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,6 +356,10 @@ 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
+
356
363
  if specs_matching_requirement.any? && (hint = platform_mismatch_hint)
357
364
  message << "\n\n#{hint}"
358
365
  end
@@ -396,7 +403,7 @@ module Bundler
396
403
  end
397
404
 
398
405
  def filter_specs(specs, package)
399
- filter_remote_specs(filter_prereleases(specs, package), package)
406
+ filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package)
400
407
  end
401
408
 
402
409
  def filter_prereleases(specs, package)
@@ -405,6 +412,56 @@ module Bundler
405
412
  specs.reject {|s| s.version.prerelease? }
406
413
  end
407
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 pinned_by_lockfile_floor?(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 spec sitting exactly at a `>= locked_version` prevent-downgrade floor is
447
+ # the version the lockfile currently pins. `bundle update` and `bundle
448
+ # outdated` install that floor so resolution never moves a gem backwards.
449
+ # Filtering it out for cooldown would then make resolution impossible
450
+ # whenever the locked version is itself inside the cooldown window, which is
451
+ # exactly what happens to a lockfile written before cooldown was enabled.
452
+ # Keep it eligible; gems being explicitly updated carry an exact `=`
453
+ # requirement instead and stay subject to the cooldown filter.
454
+ def pinned_by_lockfile_floor?(spec)
455
+ return false unless defined?(@base) && @base
456
+ requirement = base_requirements[spec.name]
457
+ return false unless requirement && !requirement.exact?
458
+ requirement.requirements.any? {|op, version| op == ">=" && version == spec.version }
459
+ end
460
+
461
+ def cooldown_now
462
+ @cooldown_now ||= Time.now
463
+ end
464
+
408
465
  def filter_remote_specs(specs, package)
409
466
  if package.prefer_local?
410
467
  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"
@@ -20,7 +20,7 @@ module Bundler
20
20
  strict_rm_rf spec.extension_dir
21
21
 
22
22
  SharedHelpers.filesystem_access(gem_dir, :create) do
23
- FileUtils.mkdir_p gem_dir, mode: 0o755
23
+ FileUtils.mkdir_p gem_dir
24
24
  end
25
25
 
26
26
  SharedHelpers.filesystem_access(gem_dir, :write) do
@@ -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
@@ -432,9 +432,14 @@ module Bundler
432
432
  end
433
433
 
434
434
  def capture3_args_for(cmd, dir)
435
- return ["git", *cmd] unless dir
435
+ # Disable automatic maintenance so a background commit-graph write in
436
+ # the source repo can't race the hardlinking local clone and fail with
437
+ # "hardlink different from source".
438
+ opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
436
439
 
437
- ["git", "-C", dir.to_s, *cmd]
440
+ return ["git", *opts, *cmd] unless dir
441
+
442
+ ["git", "-C", dir.to_s, *opts, *cmd]
438
443
  end
439
444
 
440
445
  def extra_clone_args
@@ -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
@@ -25,7 +26,8 @@ module Bundler
25
26
  @gem_installers = {}
26
27
  @gem_installers_mutex = Mutex.new
27
28
 
28
- Array(options["remotes"]).reverse_each {|r| add_remote(r) }
29
+ cooldown = options["cooldown"]
30
+ Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
29
31
 
30
32
  @lockfile_remotes = @remotes if options["from_lockfile"]
31
33
  end
@@ -148,6 +150,13 @@ module Bundler
148
150
  # sources, and large_idx.merge! small_idx is way faster than
149
151
  # small_idx.merge! large_idx.
150
152
  index = @allow_remote ? remote_specs.dup : Index.new
153
+
154
+ # Snapshot per-version `created_at` from the remote info before installed
155
+ # / cached specs overwrite the EndpointSpecification objects that carry
156
+ # it. The cooldown filter consults `created_at` on every candidate, so
157
+ # local stubs need the published date back-filled to participate.
158
+ remote_created_at = collect_remote_created_at(index)
159
+
151
160
  index.merge!(cached_specs) if @allow_cached
152
161
  index.merge!(installed_specs) if @allow_local
153
162
 
@@ -161,6 +170,8 @@ module Bundler
161
170
  end
162
171
  end
163
172
 
173
+ backfill_created_at(index, remote_created_at) unless remote_created_at.empty?
174
+
164
175
  index
165
176
  end
166
177
  end
@@ -243,9 +254,14 @@ module Bundler
243
254
  cached_path
244
255
  end
245
256
 
246
- def add_remote(source)
257
+ def add_remote(source, cooldown: nil)
247
258
  uri = normalize_uri(source)
248
259
  @remotes.unshift(uri) unless @remotes.include?(uri)
260
+ @remote_cooldowns[uri] = cooldown if cooldown
261
+ end
262
+
263
+ def cooldown_for(uri)
264
+ @remote_cooldowns[uri]
249
265
  end
250
266
 
251
267
  def spec_names
@@ -266,7 +282,7 @@ module Bundler
266
282
 
267
283
  def remote_fetchers
268
284
  @remote_fetchers ||= remotes.to_h do |uri|
269
- remote = Source::Rubygems::Remote.new(uri)
285
+ remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri))
270
286
  [remote, Bundler::Fetcher.new(remote)]
271
287
  end.freeze
272
288
  end
@@ -314,6 +330,13 @@ module Bundler
314
330
  @allow_remote && api_fetchers.any?
315
331
  end
316
332
 
333
+ def clear_cache
334
+ @specs = nil
335
+ @installed_specs = nil
336
+ @default_specs = nil
337
+ @cached_specs = nil
338
+ end
339
+
317
340
  protected
318
341
 
319
342
  def remote_names
@@ -456,6 +479,31 @@ module Bundler
456
479
 
457
480
  private
458
481
 
482
+ def collect_remote_created_at(index)
483
+ return {} unless @allow_remote
484
+
485
+ snapshot = {}
486
+ index.each do |spec|
487
+ next unless spec.respond_to?(:created_at) && spec.created_at
488
+ # Remember the remote that supplied the date too: when a source has
489
+ # several remotes with different per-URI cooldown settings we must
490
+ # restore the same one during backfill so `effective_cooldown` agrees.
491
+ snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote]
492
+ end
493
+ snapshot
494
+ end
495
+
496
+ def backfill_created_at(index, snapshot)
497
+ index.each do |spec|
498
+ next unless spec.respond_to?(:created_at=)
499
+ next if spec.created_at
500
+ remote_created_at, remote = snapshot[[spec.name, spec.version]]
501
+ next unless remote_created_at
502
+ spec.created_at = remote_created_at
503
+ spec.remote ||= remote if remote && spec.respond_to?(:remote=)
504
+ end
505
+ end
506
+
459
507
  def lockfile_remotes
460
508
  @lockfile_remotes || credless_remotes
461
509
  end
@@ -59,8 +59,8 @@ module Bundler
59
59
  add_source_to_list Plugin.source(source).new(options), @plugin_sources
60
60
  end
61
61
 
62
- def add_global_rubygems_remote(uri)
63
- global_rubygems_source.add_remote(uri)
62
+ def add_global_rubygems_remote(uri, cooldown: nil)
63
+ global_rubygems_source.add_remote(uri, cooldown: cooldown)
64
64
  global_rubygems_source
65
65
  end
66
66
 
@@ -136,6 +136,10 @@ module Bundler
136
136
  all_sources.each(&:remote!)
137
137
  end
138
138
 
139
+ def clear_cache
140
+ rubygems_sources.each(&:clear_cache)
141
+ end
142
+
139
143
  private
140
144
 
141
145
  def map_sources(replacement_sources)
@@ -165,6 +169,10 @@ module Bundler
165
169
  # locked sources never include credentials so always prefer remotes from the gemfile
166
170
  replacement_source.remotes = gemfile_source.remotes
167
171
 
172
+ # cooldowns are only ever declared in the Gemfile, so carry them over
173
+ # along with the remotes they apply to
174
+ replacement_source.remote_cooldowns = gemfile_source.remote_cooldowns
175
+
168
176
  yield replacement_source if block_given?
169
177
 
170
178
  replacement_source
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module Bundler
4
- VERSION = "4.0.12".freeze
4
+ VERSION = "4.0.14".freeze
5
5
 
6
6
  def self.bundler_major_version
7
7
  @bundler_major_version ||= gem_version.segments.first
@@ -102,7 +102,8 @@ class Gem::Ext::Builder
102
102
  # Set $SOURCE_DATE_EPOCH for the subprocess.
103
103
  build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env)
104
104
  output, status = begin
105
- Open3.popen2e(build_env, *command, chdir: dir) do |_stdin, stdouterr, wait_thread|
105
+ Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread|
106
+ stdin.close
106
107
  output = String.new
107
108
  while line = stdouterr.gets
108
109
  output << line
@@ -294,7 +294,7 @@ class Gem::Installer
294
294
 
295
295
  File.chmod(dir_mode, gem_dir) if dir_mode
296
296
 
297
- say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil?
297
+ say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil?
298
298
 
299
299
  Gem::Specification.add_spec(spec) unless @install_dir
300
300
 
@@ -707,6 +707,18 @@ class Gem::Installer
707
707
  if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ }
708
708
  raise Gem::InstallError, "#{spec} has an invalid dependencies"
709
709
  end
710
+
711
+ if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) }
712
+ raise Gem::InstallError, "#{spec} has an invalid executable"
713
+ end
714
+
715
+ raise Gem::InstallError, "#{spec} has an invalid bindir" unless spec.bindir.is_a?(String)
716
+
717
+ expanded_gem_dir = File.expand_path(gem_dir)
718
+ expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir))
719
+ unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/")
720
+ raise Gem::InstallError, "#{spec} has an invalid bindir"
721
+ end
710
722
  end
711
723
 
712
724
  ##
@@ -715,6 +727,7 @@ class Gem::Installer
715
727
  def app_script_text(bin_file_name)
716
728
  # NOTE: that the `load` lines cannot be indented, as old RG versions match
717
729
  # against the beginning of the line
730
+ escaped_bin_file_name = bin_file_name.gsub(/[\\']/) {|c| "\\#{c}" }
718
731
  <<~TEXT
719
732
  #{shebang bin_file_name}
720
733
  #
@@ -738,9 +751,9 @@ class Gem::Installer
738
751
  end
739
752
 
740
753
  if Gem.respond_to?(:activate_and_load_bin_path)
741
- Gem.activate_and_load_bin_path('#{spec.name}', '#{bin_file_name}', version)
754
+ Gem.activate_and_load_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
742
755
  else
743
- load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version)
756
+ load Gem.activate_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
744
757
  end
745
758
  TEXT
746
759
  end
@@ -449,6 +449,11 @@ EOM
449
449
  directories << mkdir
450
450
  end
451
451
 
452
+ real_mkdir = File.realpath(mkdir)
453
+ unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/"))
454
+ raise Gem::Package::PathError.new(real_mkdir, destination_dir)
455
+ end
456
+
452
457
  if entry.file?
453
458
  File.open(destination, "wb") do |out|
454
459
  copy_stream(tar.io, out, entry.size)
@@ -466,7 +471,7 @@ EOM
466
471
 
467
472
  symlinks.each do |name, target, destination, real_destination|
468
473
  if File.exist?(real_destination)
469
- File.symlink(target, destination)
474
+ create_symlink(target, destination)
470
475
  else
471
476
  alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring"
472
477
  end
@@ -747,6 +752,21 @@ EOM
747
752
  raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit
748
753
  bytes
749
754
  end
755
+
756
+ if Gem.win_platform?
757
+ # Create a symlink and fallback to copy the file or directory on Windows,
758
+ # where symlink creation needs special privileges in form of the Developer Mode.
759
+ def create_symlink(old_name, new_name)
760
+ File.symlink(old_name, new_name)
761
+ rescue Errno::EACCES
762
+ from = File.expand_path(old_name, File.dirname(new_name))
763
+ FileUtils.cp_r(from, new_name)
764
+ end
765
+ else
766
+ def create_symlink(old_name, new_name)
767
+ File.symlink(old_name, new_name)
768
+ end
769
+ end
750
770
  end
751
771
 
752
772
  require_relative "package/digest_io"
@@ -13,7 +13,7 @@ class Gem::Resolver::APISet::GemParser
13
13
  private
14
14
 
15
15
  def parse_dependency(string)
16
- dependency = string.split(":")
16
+ dependency = string.split(":", 2)
17
17
  dependency[-1] = dependency[-1].split("&") if dependency.size > 1
18
18
  dependency[0] = -dependency[0]
19
19
  dependency
data/lib/rubygems/text.rb CHANGED
@@ -8,7 +8,16 @@ module Gem::Text
8
8
  # Remove any non-printable characters and make the text suitable for
9
9
  # printing.
10
10
  def clean_text(text)
11
- text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
11
+ text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
12
+
13
+ # Match C1 control characters (U+0080-U+009F) as codepoints. This requires
14
+ # a valid UTF-8 string so the regexp does not split a multibyte sequence;
15
+ # strings in other encodings are left unchanged.
16
+ if text.encoding == Encoding::UTF_8 && text.valid_encoding?
17
+ text = text.gsub(/[\u0080-\u009f]/, ".")
18
+ end
19
+
20
+ text
12
21
  end
13
22
 
14
23
  def truncate_text(text, description, max_length = 100_000)
data/lib/rubygems.rb CHANGED
@@ -9,7 +9,7 @@
9
9
  require "rbconfig"
10
10
 
11
11
  module Gem
12
- VERSION = "4.0.12"
12
+ VERSION = "4.0.14"
13
13
  end
14
14
 
15
15
  require_relative "rubygems/defaults"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubygems-update
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.12
4
+ version: 4.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Weirich
@@ -724,7 +724,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
724
724
  - !ruby/object:Gem::Version
725
725
  version: '0'
726
726
  requirements: []
727
- rubygems_version: 4.0.10
727
+ rubygems_version: 4.0.13
728
728
  specification_version: 4
729
729
  summary: RubyGems is a package management framework for Ruby. This gem is downloaded
730
730
  and installed by `gem update --system`, so that the `gem` CLI can update itself.