bundler 4.1.0.beta1 → 4.1.0.beta2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bundler/build_metadata.rb +2 -2
  3. data/lib/bundler/cli/common.rb +4 -0
  4. data/lib/bundler/cli/doctor/ssl.rb +4 -8
  5. data/lib/bundler/cli/info.rb +1 -1
  6. data/lib/bundler/cli/lock.rb +14 -3
  7. data/lib/bundler/cli/open.rb +7 -1
  8. data/lib/bundler/cli/outdated.rb +8 -10
  9. data/lib/bundler/cli/platform.rb +15 -1
  10. data/lib/bundler/cli/show.rb +5 -1
  11. data/lib/bundler/cli/update.rb +1 -1
  12. data/lib/bundler/cli.rb +22 -7
  13. data/lib/bundler/constants.rb +1 -1
  14. data/lib/bundler/definition.rb +64 -14
  15. data/lib/bundler/dsl.rb +61 -3
  16. data/lib/bundler/fetcher/downloader.rb +5 -1
  17. data/lib/bundler/gem_helper.rb +1 -1
  18. data/lib/bundler/installer/parallel_installer.rb +2 -1
  19. data/lib/bundler/installer.rb +5 -1
  20. data/lib/bundler/lazy_specification.rb +1 -1
  21. data/lib/bundler/lockfile_parser.rb +1 -1
  22. data/lib/bundler/man/bundle-info.1 +4 -1
  23. data/lib/bundler/man/bundle-info.1.ronn +4 -0
  24. data/lib/bundler/man/bundle-lock.1 +2 -2
  25. data/lib/bundler/man/bundle-lock.1.ronn +4 -3
  26. data/lib/bundler/man/bundle-open.1 +4 -1
  27. data/lib/bundler/man/bundle-open.1.ronn +4 -1
  28. data/lib/bundler/man/bundle-outdated.1 +1 -1
  29. data/lib/bundler/man/bundle-outdated.1.ronn +1 -1
  30. data/lib/bundler/man/bundle-show.1 +4 -1
  31. data/lib/bundler/man/bundle-show.1.ronn +4 -0
  32. data/lib/bundler/man/bundle-update.1 +3 -3
  33. data/lib/bundler/man/bundle-update.1.ronn +5 -3
  34. data/lib/bundler/man/gemfile.5 +33 -1
  35. data/lib/bundler/man/gemfile.5.ronn +53 -2
  36. data/lib/bundler/materialization.rb +3 -1
  37. data/lib/bundler/override.rb +30 -2
  38. data/lib/bundler/resolver.rb +28 -4
  39. data/lib/bundler/self_manager.rb +15 -9
  40. data/lib/bundler/settings.rb +10 -26
  41. data/lib/bundler/source/git/git_proxy.rb +39 -2
  42. data/lib/bundler/source/git.rb +34 -7
  43. data/lib/bundler/source/rubygems_aggregate.rb +1 -1
  44. data/lib/bundler/source_map.rb +7 -3
  45. data/lib/bundler/spec_set.rb +8 -4
  46. data/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt +1 -1
  47. data/lib/bundler/version.rb +1 -1
  48. data/lib/bundler.rb +1 -3
  49. data/lib/rubygems/compact_index_client/http_fetcher.rb +5 -3
  50. metadata +391 -390
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64a16d95664584fa37c3a29d228d05929ff6ab31adfb8784a8625b242e27563d
4
- data.tar.gz: fcf44920ce08c9436bb0076a8a000f2344487858db6ee94644f4d6acced91cc4
3
+ metadata.gz: 3c0da83afaf51ba4984bf6afadad1dc16ba90dd523a7a70ed5ca4e353fb5a3ef
4
+ data.tar.gz: 252ab201fa16d64d654b43f3f642d622dbaa94c653b90bddea01af7f8dfdf6a7
5
5
  SHA512:
6
- metadata.gz: f39b20fc90c76be12671a4f4e5dfe73f697d9d46183786fbf0de925f977675e188fa74db50f3cf010053fdfc62724bdeca2ad65341167ee7f2b2c434a77b9773
7
- data.tar.gz: 0f7806fa9c0e09b79eeab58116f92d7f3b0bfd395a091de186a1c129e5cda7595ad02f891f993d0d1436cdfc993692783b8ff3a506d2bbbfbe7028976b0e53cd
6
+ metadata.gz: d3d0486d8ace2df28048406ccba0335f679c5d7bef8e3ab20c8c72628fb1500ddc3d80ac8ae897b63ecb268a33a7fcb9a826b5833414ee7e63fb46c37e674528
7
+ data.tar.gz: e9e6a99a38ff138d0fb3bc992843384bf06f732513c65e015c7758fa72dd210c5d369aced9826813799ca189b073270499a109cc4b5290183e67db5010430d3c
@@ -4,8 +4,8 @@ module Bundler
4
4
  # Represents metadata from when the Bundler gem was built.
5
5
  module BuildMetadata
6
6
  # begin ivars
7
- @built_at = "2026-09-09".freeze
8
- @git_commit_sha = "98e4d8083a7".freeze
7
+ @built_at = "2026-10-08".freeze
8
+ @git_commit_sha = "f8b8b02".freeze
9
9
  # end ivars
10
10
 
11
11
  # A hash representation of the build metadata.
@@ -122,6 +122,10 @@ module Bundler
122
122
  raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
123
123
  end
124
124
 
125
+ def self.select_spec_with_match_type(name, options)
126
+ select_spec(name, options["exact-match"] ? nil : :regex_match)
127
+ end
128
+
125
129
  def self.default_gem_spec(name)
126
130
  gem_spec = Gem::Specification.find_all_by_name(name).last
127
131
  gem_spec if gem_spec&.default_gem?
@@ -124,14 +124,10 @@ module Bundler
124
124
  ctx = OpenSSL::SSL::SSLContext.new
125
125
  supported = true
126
126
 
127
- if ctx.respond_to?(:min_version=)
128
- begin
129
- ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
130
- rescue OpenSSL::SSL::SSLError, NameError
131
- supported = false
132
- end
133
- else
134
- supported = OpenSSL::SSL::SSLContext::METHODS.include?(:TLSv1_2) # rubocop:disable Naming/VariableNumber
127
+ begin
128
+ ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
129
+ rescue OpenSSL::SSL::SSLError, NameError
130
+ supported = false
135
131
  end
136
132
 
137
133
  Bundler.ui.warn(<<~EOM) unless supported
@@ -26,7 +26,7 @@ module Bundler
26
26
  private
27
27
 
28
28
  def spec_for_gem(name)
29
- Bundler::CLI::Common.select_spec(name, :regex_match)
29
+ Bundler::CLI::Common.select_spec_with_match_type(name, options)
30
30
  end
31
31
 
32
32
  def print_gem_version(spec)
@@ -74,10 +74,21 @@ module Bundler
74
74
  puts definition.to_lock
75
75
  else
76
76
  file = options[:lockfile]
77
- file = file ? Pathname.new(file).expand_path : Bundler.default_lockfile
77
+ file = if file
78
+ Pathname.new(file).expand_path
79
+ elsif ENV["BUNDLE_LOCKFILE"].to_s.empty?
80
+ definition.lockfile
81
+ else
82
+ # BUNDLE_LOCKFILE outranks the lockfile method in the Gemfile, see gemfile(5)
83
+ Bundler.default_lockfile
84
+ end
78
85
 
79
- puts "Writing lockfile to #{file}"
80
- definition.write_lock(file, false)
86
+ if file
87
+ puts "Writing lockfile to #{file}"
88
+ definition.write_lock(file, false)
89
+ else
90
+ Bundler.ui.warn "Not writing a lockfile because the Gemfile sets `lockfile false`"
91
+ end
81
92
  end
82
93
 
83
94
  Bundler::CLI::Common.output_cooldown_skipped_summary(definition)
@@ -13,7 +13,7 @@ module Bundler
13
13
  raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty?
14
14
  editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }
15
15
  return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor
16
- return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match)
16
+ return unless spec = spec_for_gem(name)
17
17
  if spec.default_gem?
18
18
  Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist."
19
19
  else
@@ -35,5 +35,11 @@ module Bundler
35
35
  require "shellwords"
36
36
  Shellwords.split(editor)
37
37
  end
38
+
39
+ private
40
+
41
+ def spec_for_gem(name)
42
+ Bundler::CLI::Common.select_spec_with_match_type(name, options)
43
+ end
38
44
  end
39
45
  end
@@ -90,7 +90,9 @@ module Bundler
90
90
  dependency = current_dependencies[current_spec.name]
91
91
  groups = ""
92
92
  if dependency && !options[:parseable]
93
- groups = dependency.groups.join(", ")
93
+ groups = dependency.groups
94
+ groups = groups.sort if options_include_groups
95
+ groups = groups.join(", ")
94
96
  end
95
97
 
96
98
  outdated_gems << {
@@ -101,13 +103,11 @@ module Bundler
101
103
  }
102
104
  end
103
105
 
104
- relevant_outdated_gems = if options_include_groups
105
- outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems|
106
- contains_group = groups.split(", ").include?(options[:group])
107
- next unless options[:groups] || contains_group
108
-
109
- gems
110
- end.compact
106
+ relevant_outdated_gems = if options[:groups]
107
+ without_groups, with_groups = outdated_gems.partition {|g| g[:groups].empty? }
108
+ with_groups.group_by {|g| g[:groups] }.sort.flat_map(&:last) + without_groups
109
+ elsif options_include_groups
110
+ outdated_gems.select {|g| g[:groups].split(", ").include?(options[:group]) }
111
111
  else
112
112
  outdated_gems
113
113
  end
@@ -333,8 +333,6 @@ module Bundler
333
333
 
334
334
  Bundler.ui.info justify(header, column_sizes)
335
335
 
336
- data.sort_by! {|row| row[0] }
337
-
338
336
  data.each do |row|
339
337
  Bundler.ui.info justify(row, column_sizes)
340
338
  end
@@ -25,7 +25,7 @@ module Bundler
25
25
  else
26
26
  platforms = Bundler.definition.platforms.map {|p| "* #{p}" }
27
27
 
28
- output << "Your platform is: #{Gem::Platform.local}"
28
+ output << platform_info
29
29
  output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}"
30
30
 
31
31
  if ruby_version
@@ -44,5 +44,19 @@ module Bundler
44
44
 
45
45
  Bundler.ui.info output.join("\n\n")
46
46
  end
47
+
48
+ private
49
+
50
+ def platform_info
51
+ info = "Your platform is: #{Gem::Platform.local}"
52
+ return info if Bundler.local_platform == Gem::Platform.local
53
+
54
+ info + "\nHowever, your effective platform is: #{Bundler.local_platform} " \
55
+ "(because force_ruby_platform is #{force_ruby_platform_origin})"
56
+ end
57
+
58
+ def force_ruby_platform_origin
59
+ Bundler.settings.pretty_values_for(:force_ruby_platform).first.sub(/\ASet /, "set ")
60
+ end
47
61
  end
48
62
  end
@@ -20,7 +20,7 @@ module Bundler
20
20
  if gem_name == "bundler"
21
21
  path = File.expand_path("../../..", __dir__)
22
22
  else
23
- spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match)
23
+ spec = spec_for_gem(gem_name)
24
24
  return unless spec
25
25
  path = spec.full_gem_path
26
26
  unless File.directory?(path)
@@ -55,6 +55,10 @@ module Bundler
55
55
 
56
56
  private
57
57
 
58
+ def spec_for_gem(name)
59
+ Bundler::CLI::Common.select_spec_with_match_type(name, options)
60
+ end
61
+
58
62
  def fetch_latest_specs
59
63
  definition = Bundler.definition(true)
60
64
  Bundler.ui.info "Fetching remote specs for outdated check...\n\n"
@@ -13,7 +13,7 @@ module Bundler
13
13
 
14
14
  update_bundler = options[:bundler]
15
15
 
16
- Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler
16
+ Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler, pre: options[:pre]) if update_bundler
17
17
 
18
18
  sources = Array(options[:source])
19
19
  groups = Array(options[:group]).map(&:to_sym)
data/lib/bundler/cli.rb CHANGED
@@ -177,7 +177,7 @@ module Bundler
177
177
  else
178
178
  puts File.read("#{man_path}/#{File.basename(man_page)}.ronn")
179
179
  end
180
- elsif command_path = Bundler.which("bundler-#{cli}")
180
+ elsif command_path = self.class.bundler_command_path(cli)
181
181
  Kernel.exec(command_path, "--help")
182
182
  else
183
183
  super
@@ -189,11 +189,23 @@ module Bundler
189
189
  return Bundler::Plugin.exec_command(command, ARGV[1..-1])
190
190
  end
191
191
 
192
- return super unless command_path = Bundler.which("bundler-#{command}")
192
+ return super unless command_path = bundler_command_path(command)
193
193
 
194
194
  Kernel.exec(command_path, *ARGV[1..-1])
195
195
  end
196
196
 
197
+ # Only PATH is searched, unlike Bundler.which, which also finds a file in
198
+ # the current directory and returns a bare name that Kernel.exec cannot run.
199
+ def self.bundler_command_path(command)
200
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
201
+ path = path[1..-2] if path.start_with?('"') && path.end_with?('"')
202
+ command_path = Bundler.find_executable(File.expand_path("bundler-#{command}", path))
203
+ return command_path if command_path
204
+ end
205
+
206
+ nil
207
+ end
208
+
197
209
  desc "init [OPTIONS]", "Generates a Gemfile into the current working directory"
198
210
  long_desc <<-D
199
211
  Init generates a default Gemfile in the current working directory. When adding a
@@ -319,10 +331,10 @@ module Bundler
319
331
  method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)"
320
332
  method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed"
321
333
  method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock"
322
- method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
334
+ method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler"
323
335
  method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
324
336
  method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
325
- method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
337
+ method_option "major", type: :boolean, banner: "Prefer updating to latest major version (default)"
326
338
  method_option "pre", type: :boolean, banner: "Always choose the highest allowed version when updating gems, regardless of prerelease status"
327
339
  method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major"
328
340
  method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated."
@@ -342,6 +354,7 @@ module Bundler
342
354
  D
343
355
  method_option "paths", type: :boolean, banner: "List the paths of all gems that are required by your Gemfile."
344
356
  method_option "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)."
357
+ method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name"
345
358
  def show(gem_name = nil)
346
359
  if ARGV.include?("--outdated")
347
360
  removed_message = "the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`"
@@ -367,6 +380,7 @@ module Bundler
367
380
  desc "info GEM [OPTIONS]", "Show information for the given gem"
368
381
  method_option "path", type: :boolean, banner: "Print full path to gem"
369
382
  method_option "version", type: :boolean, banner: "Print gem version"
383
+ method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name"
370
384
  def info(gem_name)
371
385
  require_relative "cli/info"
372
386
  Info.new(options, gem_name).run
@@ -434,7 +448,7 @@ module Bundler
434
448
  method_option "filter-strict", type: :boolean, aliases: "--strict", banner: "Only list newer versions allowed by your Gemfile requirements"
435
449
  method_option "update-strict", type: :boolean, banner: "Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major"
436
450
  method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
437
- method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
451
+ method_option "major", type: :boolean, banner: "Prefer updating to latest major version (default)"
438
452
  method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
439
453
  method_option "filter-major", type: :boolean, banner: "Only list major newer versions"
440
454
  method_option "filter-minor", type: :boolean, banner: "Only list minor newer versions"
@@ -530,6 +544,7 @@ module Bundler
530
544
 
531
545
  desc "open GEM", "Opens the source directory of the given bundled gem"
532
546
  method_option "path", type: :string, lazy_default: "", banner: "Open relative path of the gem source."
547
+ method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name"
533
548
  def open(name)
534
549
  require_relative "cli/open"
535
550
  Open.new(options, name).run
@@ -640,11 +655,11 @@ module Bundler
640
655
  method_option "normalize-platforms", type: :boolean, default: false, banner: "Normalize lockfile platforms"
641
656
  method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version"
642
657
  method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version"
643
- method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)"
658
+ method_option "major", type: :boolean, banner: "If updating, prefer updating to latest major version (default)"
644
659
  method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status"
645
660
  method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major"
646
661
  method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated"
647
- method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
662
+ method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler"
648
663
  method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
649
664
  def lock
650
665
  require_relative "cli/lock"
@@ -3,7 +3,7 @@
3
3
  require "rbconfig"
4
4
 
5
5
  module Bundler
6
- WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/
6
+ WINDOWS = RbConfig::CONFIG["host_os"] =~ /(mswin|mingw)/
7
7
  deprecate_constant :WINDOWS
8
8
 
9
9
  FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd")
@@ -198,7 +198,7 @@ module Bundler
198
198
 
199
199
  sources.cached!
200
200
 
201
- if options[:add_checksums] || (!options[:local] && (install_needed? || refetch_needed?(options)))
201
+ if options[:add_checksums] || (!options[:local] && (install_needed? || refetch_needed?(options) || @locked_spec_with_empty_checksums))
202
202
  sources.remote!
203
203
  true
204
204
  else
@@ -346,7 +346,9 @@ module Bundler
346
346
  #
347
347
  # @return [SpecSet] resolved dependencies
348
348
  def resolve
349
- @resolve ||= if Bundler.frozen_bundle?
349
+ return @resolve if @resolve
350
+
351
+ @resolve = if Bundler.frozen_bundle?
350
352
  Bundler.ui.debug "Frozen, using resolution from the lockfile"
351
353
  @locked_specs
352
354
  elsif no_resolve_needed?
@@ -366,6 +368,8 @@ module Bundler
366
368
 
367
369
  start_resolution
368
370
  end
371
+ warn_inert_overrides(@resolve)
372
+ @resolve
369
373
  end
370
374
 
371
375
  def spec_git_paths
@@ -650,7 +654,7 @@ module Bundler
650
654
  @missing_lockfile_dep ||
651
655
  @unlocking_bundler ||
652
656
  @locked_spec_with_missing_checksums ||
653
- @locked_spec_with_empty_checksums ||
657
+ empty_checksums_actionable? ||
654
658
  @locked_spec_with_missing_deps ||
655
659
  @locked_spec_with_invalid_deps
656
660
  end
@@ -659,6 +663,16 @@ module Bundler
659
663
  unlocking? || something_changed?
660
664
  end
661
665
 
666
+ # Only a remote fetch can fill an empty CHECKSUMS entry, so it justifies a
667
+ # resolution only when one is coming. Resolving locally for it would repeat
668
+ # on every `Bundler.setup` without changing the lockfile. Frozen mode still
669
+ # has to refuse the entry.
670
+ def empty_checksums_actionable?
671
+ return false unless @locked_spec_with_empty_checksums
672
+
673
+ Bundler.frozen_bundle? || !sources.local_mode?
674
+ end
675
+
662
676
  def should_add_extra_platforms?
663
677
  !lockfile_exists? && Bundler::MatchPlatform.generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform]
664
678
  end
@@ -1095,25 +1109,61 @@ module Bundler
1095
1109
  end
1096
1110
 
1097
1111
  converge_overrides_outside_dependencies
1112
+ converge_rewritten_dependencies
1098
1113
 
1099
1114
  @changed_dependencies.any?
1100
1115
  end
1101
1116
 
1102
- def converge_overrides_outside_dependencies
1117
+ # The lockfile keeps the upstream dependencies of every gem, so a gem that
1118
+ # a `from:`/`to:` override cut loose stays locked until a resolution drops
1119
+ # it.
1120
+ def converge_rewritten_dependencies
1103
1121
  @overrides.each do |override|
1104
- # :all overrides are intentionally not pre-unlocked. They take effect on
1105
- # fresh resolution (no lockfile) or when the user runs `bundle update`.
1106
- # Forcing a full re-resolve from a single :all directive would surprise
1107
- # users with unrelated dependency churn.
1108
- next unless override.target.is_a?(String)
1122
+ next unless override.from
1109
1123
 
1110
1124
  name = override.target
1111
- next if @changed_dependencies.include?(name)
1112
1125
  next if @originally_locked_specs[name].empty?
1113
- # version: overrides on direct deps are detected in the per-dep
1126
+ next if @dependencies.any? {|d| d.name == name }
1127
+
1128
+ still_required = @originally_locked_specs.any? do |s|
1129
+ Override.rewrite_dependencies(@overrides, s.name, s.runtime_dependencies).any? {|d| d.name == name }
1130
+ end
1131
+ next if still_required
1132
+
1133
+ @gems_to_unlock << name
1134
+ @changed_dependencies << name
1135
+ end
1136
+ end
1137
+
1138
+ # A misspelled `from:` gem leaves the override with nothing to rewrite.
1139
+ def warn_inert_overrides(specs)
1140
+ @overrides.each do |override|
1141
+ next unless override.from
1142
+ next if specs[override.from].any? {|s| s.dependencies.any? {|d| d.name == override.target } }
1143
+
1144
+ Bundler.ui.warn "#{override} has no effect because the bundle has no #{override.from} that depends on #{override.target}"
1145
+ end
1146
+ end
1147
+
1148
+ def converge_overrides_outside_dependencies
1149
+ @overrides.each do |override|
1150
+ # :all and metadata overrides are intentionally not pre-unlocked. The
1151
+ # lockfile does not record overrides, so unlocking for them would
1152
+ # re-resolve on every run and reject every frozen install. Install-time
1153
+ # compatibility checks already honor them for locked specs, and they
1154
+ # affect resolution on a fresh lock or when the user runs `bundle update`.
1155
+ next unless override.target.is_a?(String) && override.field == :version
1156
+
1157
+ name = override.target
1158
+ # Overrides on direct deps are detected in the per-dep
1114
1159
  # converge_dependencies loop via apply_override_to + matches_spec?.
1115
- # Other fields are not visible there, so they always reach here.
1116
- next if override.field == :version && @dependencies.any? {|d| d.name == name }
1160
+ next if @dependencies.any? {|d| d.name == name }
1161
+
1162
+ locked_specs = @originally_locked_specs[name]
1163
+ next if locked_specs.empty?
1164
+ # Only a version string constrains on its own. :ignore_upper and nil
1165
+ # loosen requirements the lockfile already satisfies.
1166
+ next if override.apply_to(Gem::Requirement.default).satisfied_by?(locked_specs.first.version)
1117
1167
 
1118
1168
  @gems_to_unlock << name
1119
1169
  @changed_dependencies << name
@@ -1342,7 +1392,7 @@ module Bundler
1342
1392
  end
1343
1393
 
1344
1394
  def source_map
1345
- @source_map ||= SourceMap.new(sources, dependencies, @locked_specs)
1395
+ @source_map ||= SourceMap.new(sources, dependencies, @locked_specs, @overrides.filter_map {|o| o.operation if o.from })
1346
1396
  end
1347
1397
 
1348
1398
  def new_resolver_for_full_update
data/lib/bundler/dsl.rb CHANGED
@@ -17,7 +17,7 @@ module Bundler
17
17
  VALID_PLATFORMS = Bundler::CurrentRuby::PLATFORM_MAP.keys.freeze
18
18
 
19
19
  VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules
20
- platform platforms source install_if force_ruby_platform].freeze
20
+ platform platforms source install_if force_ruby_platform sparse_checkout].freeze
21
21
 
22
22
  GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z}
23
23
  GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z}
@@ -200,6 +200,12 @@ module Bundler
200
200
 
201
201
  def override(target, **operations)
202
202
  validate_override_target!(target)
203
+ source_location = caller_locations(1, 1)&.first
204
+
205
+ if operations.key?(:from) || operations.key?(:to)
206
+ add_dependency_override(target, operations, source_location)
207
+ return
208
+ end
203
209
 
204
210
  if target == :all && operations.key?(:version)
205
211
  raise ArgumentError, "`override :all, version:` is not allowed; version requirements are per-gem"
@@ -211,7 +217,6 @@ module Bundler
211
217
  validate_override_uniqueness!(target, field)
212
218
  end
213
219
 
214
- source_location = caller_locations(1, 1)&.first
215
220
  operations.each do |field, operation|
216
221
  @overrides << Override.new(target, field, operation, source_location: source_location)
217
222
  end
@@ -219,6 +224,7 @@ module Bundler
219
224
 
220
225
  def to_definition(lockfile, unlock)
221
226
  check_primary_source_safety
227
+ check_dropped_dependencies
222
228
  lockfile = @lockfile unless @lockfile.nil?
223
229
  Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, @overrides)
224
230
  end
@@ -317,6 +323,54 @@ module Bundler
317
323
  raise ArgumentError, "duplicate override for #{target.inspect} `#{field}:`"
318
324
  end
319
325
 
326
+ def add_dependency_override(target, operations, source_location)
327
+ unless operations.key?(:from)
328
+ raise ArgumentError, "`override #{target.inspect}, to:` requires `from:` naming the gem whose dependency is rewritten"
329
+ end
330
+ unless operations.key?(:to)
331
+ raise ArgumentError, "`override #{target.inspect}, from:` requires `to:`; use `to: nil` to drop the dependency or `to: \"<gem>\"` to replace it"
332
+ end
333
+
334
+ from = operations[:from]
335
+ to = operations[:to]
336
+ requirement = operations[:version]
337
+
338
+ raise ArgumentError, "`override :all` does not accept `from:`" if target == :all
339
+ raise ArgumentError, "`from:` must be a gem name string, got #{from.inspect}" unless from.is_a?(String)
340
+ raise ArgumentError, "`to:` must be a gem name string or nil, got #{to.inspect}" unless to.nil? || to.is_a?(String)
341
+ raise ArgumentError, "`to:` must name a gem other than #{target.inspect} and #{from.inspect}" if to == target || to == from
342
+ validate_gem_name!(from)
343
+ validate_gem_name!(to) if to
344
+
345
+ unsupported = operations.keys - [:from, :to, :version]
346
+ if unsupported.any?
347
+ raise ArgumentError, "unsupported override field `#{unsupported.first}:` with `from:`; supported fields: `to:`, `version:`"
348
+ end
349
+
350
+ if operations.key?(:version)
351
+ raise ArgumentError, "`version:` with `from:` requires `to:` to name a replacement gem" if to.nil?
352
+ raise ArgumentError, "`version:` with `from:` must be a version requirement string, got #{requirement.inspect}" unless requirement.is_a?(String)
353
+ validate_override_operation!(requirement)
354
+ end
355
+
356
+ if @overrides.any? {|o| o.target == target && o.from == from }
357
+ raise ArgumentError, "duplicate override for #{target.inspect} `from: #{from.inspect}`"
358
+ end
359
+
360
+ @overrides << Override.new(target, :to, to, from: from, requirement: requirement, source_location: source_location)
361
+ end
362
+
363
+ # Dropping a gem the Gemfile requires directly would leave it installed
364
+ # anyway, with only the parent's requirement gone.
365
+ def check_dropped_dependencies
366
+ @overrides.each do |override|
367
+ next unless override.from && override.operation.nil?
368
+ next unless @dependencies.any? {|d| d.name == override.target }
369
+
370
+ raise GemfileError, "#{override} cannot drop #{override.target} because the Gemfile depends on it directly"
371
+ end
372
+ end
373
+
320
374
  def add_dependency(name, version = nil, options = {})
321
375
  options["gemfile"] = @gemfile
322
376
  options["source"] ||= @source
@@ -460,7 +514,7 @@ module Bundler
460
514
  @valid_keys ||= VALID_KEYS
461
515
  end
462
516
 
463
- def normalize_options(name, version, opts)
517
+ def validate_gem_name!(name)
464
518
  if name.is_a?(Symbol)
465
519
  raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead)
466
520
  end
@@ -468,6 +522,10 @@ module Bundler
468
522
  raise GemfileError, %('#{name}' is not a valid gem name because it contains whitespace)
469
523
  end
470
524
  raise GemfileError, %(an empty gem name is not valid) if name.empty?
525
+ end
526
+
527
+ def normalize_options(name, version, opts)
528
+ validate_gem_name!(name)
471
529
 
472
530
  normalize_hash(opts)
473
531
 
@@ -56,9 +56,13 @@ module Bundler
56
56
  if https?(uri) && !https?(new_uri)
57
57
  raise HTTPError, "Redirecting to a non-https URI is not allowed: #{URICredentialsFilter.credential_filtered_uri(new_uri)}"
58
58
  end
59
- if new_uri.host == uri.host
59
+ if [new_uri.scheme, new_uri.host, new_uri.port] == [uri.scheme, uri.host, uri.port]
60
60
  new_uri.user = uri.user
61
61
  new_uri.password = uri.password
62
+ else
63
+ # see Gem::RemoteFetcher#fetch_http. A nil value removes the header
64
+ # that ConnectionPools adds from its override_headers.
65
+ headers = headers.merge("X-Gemfile-Source" => nil)
62
66
  end
63
67
  fetch(new_uri, headers, counter + 1)
64
68
  when Gem::Net::HTTPRequestedRangeNotSatisfiable
@@ -216,7 +216,7 @@ module Bundler
216
216
  Bundler.ui.debug(cmd)
217
217
  SharedHelpers.chdir(base) do
218
218
  outbuf = IO.popen(cmd, err: [:child, :out], &:read)
219
- status = $?
219
+ status = Process.last_status
220
220
  block&.call(outbuf) if status.success?
221
221
  [outbuf, status]
222
222
  end
@@ -69,7 +69,8 @@ module Bundler
69
69
  @specs = all_specs.map {|s| SpecInstallation.new(s) }
70
70
  specs_by_name = @specs.to_h {|s| [s.name, s] }
71
71
  @specs.each do |spec_install|
72
- spec_install.dependencies = spec_install.spec.dependencies.filter_map do |dep|
72
+ dependencies = Override.rewrite_dependencies(installer.definition.overrides, spec_install.name, spec_install.spec.dependencies)
73
+ spec_install.dependencies = dependencies.filter_map do |dep|
73
74
  specs_by_name[dep.name] unless dep.type == :development || dep.name == spec_install.name
74
75
  end
75
76
  end
@@ -103,7 +103,7 @@ module Bundler
103
103
 
104
104
  if options[:binstubs_cmd] && spec.executables.empty?
105
105
  options = {}
106
- spec.runtime_dependencies.each do |dep|
106
+ Override.rewrite_dependencies(@definition.overrides, spec.name, spec.runtime_dependencies).each do |dep|
107
107
  bins = @definition.specs[dep].first.executables
108
108
  options[dep.name] = bins unless bins.empty?
109
109
  end
@@ -127,6 +127,8 @@ module Bundler
127
127
  template_path = File.expand_path("templates/Executable", __dir__)
128
128
  template = File.read(template_path)
129
129
 
130
+ Bundler.mkdir_p(bin_path)
131
+
130
132
  exists = []
131
133
  spec.executables.each do |executable|
132
134
  binstub_path = "#{bin_path}/#{executable}"
@@ -173,6 +175,8 @@ module Bundler
173
175
  ruby_command = Thor::Util.ruby_command
174
176
  ruby_command = ruby_command
175
177
 
178
+ Bundler.mkdir_p(bin_path)
179
+
176
180
  spec.executables.each do |executable|
177
181
  next if executable == "bundle"
178
182
  executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path)
@@ -286,7 +286,7 @@ module Bundler
286
286
  # which would affect `bundler/setup` performance.
287
287
  def validate_dependencies(spec)
288
288
  if !@materialization_options[:strict] && spec.is_a?(StubSpecification)
289
- spec.dependencies = dependencies
289
+ spec.dependencies = Override.rewrite_dependencies(overrides, name, dependencies)
290
290
  else
291
291
  if !source.is_a?(Source::Path) && spec.runtime_dependencies.sort != dependencies.sort
292
292
  raise IncorrectLockfileDependencies.new(self, spec.runtime_dependencies, dependencies)
@@ -49,7 +49,7 @@ module Bundler
49
49
  PATH = "PATH"
50
50
  PLUGIN = "PLUGIN SOURCE"
51
51
  SPECS = " specs:"
52
- OPTIONS = /^ ([a-z]+): (.*)$/i
52
+ OPTIONS = /^ ([a-z_]+): (.*)$/i
53
53
  SOURCE = [GIT, GEM, PATH, PLUGIN].freeze
54
54
 
55
55
  SECTIONS_BY_VERSION_INTRODUCED = {