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
@@ -230,6 +230,16 @@ module Bundler
230
230
  sources.prefer_local!
231
231
  end
232
232
 
233
+ # Releases memory only needed during resolution, such as remote spec
234
+ # indexes and resolver state. Only safe to call once resolution is
235
+ # complete and the result has been materialized, since any further
236
+ # resolution will need to refetch remote specs.
237
+ def release_resolution_memory!
238
+ @resolver = nil
239
+ @resolution_base = nil
240
+ sources.release_resolution_memory!
241
+ end
242
+
233
243
  # For given dependency list returns a SpecSet with Gemspec of all the required
234
244
  # dependencies.
235
245
  # 1. The method first resolves the dependencies specified in Gemfile
@@ -387,10 +397,6 @@ module Bundler
387
397
 
388
398
  contents = to_lock
389
399
 
390
- # Convert to \r\n if the existing lock has them
391
- # i.e., Windows with `git config core.autocrlf=true`
392
- contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match?("\r\n")
393
-
394
400
  if @locked_bundler_version
395
401
  locked_major = @locked_bundler_version.segments.first
396
402
  current_major = bundler_version_to_lock.segments.first
@@ -411,6 +417,14 @@ module Bundler
411
417
  return
412
418
  end
413
419
 
420
+ # Convert to \r\n if the existing lock has them, i.e., Windows with
421
+ # `git config core.autocrlf=true`. Detect from the bytes on disk because
422
+ # reading in text mode strips carriage returns on Windows, which would
423
+ # otherwise defeat this check and rewrite a `\r\n` lockfile with `\n`.
424
+ if File.exist?(file) && SharedHelpers.filesystem_access(file, :read) {|p| File.binread(p).include?("\r\n") }
425
+ contents.gsub!(/\n/, "\r\n")
426
+ end
427
+
414
428
  begin
415
429
  SharedHelpers.filesystem_access(file) do |p|
416
430
  File.open(p, "wb") {|f| f.puts(contents) }
@@ -688,9 +702,10 @@ module Bundler
688
702
  "available locally before rerunning Bundler."
689
703
  else
690
704
  "Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \
691
- "no longer be found in that source. That means the author of #{locked_gem} has removed it. " \
692
- "You'll need to update your bundle to a version other than #{locked_gem} that hasn't been " \
693
- "removed in order to install."
705
+ "no longer be found in that source. That means either the author of #{locked_gem} has removed it, " \
706
+ "or you no longer have access to that source. You'll need to update your bundle to a version other " \
707
+ "than #{locked_gem} that hasn't been removed, or check your credentials and access rights for " \
708
+ "#{locked_gem.source}, in order to install."
694
709
  end
695
710
 
696
711
  raise GemNotFound, message
@@ -783,7 +798,25 @@ module Bundler
783
798
  end
784
799
 
785
800
  def precompute_source_requirements_for_indirect_dependencies?
786
- sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
801
+ if sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
802
+ true
803
+ else
804
+ non_dependency_api_warning
805
+ false
806
+ end
807
+ end
808
+
809
+ def non_dependency_api_warning
810
+ non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
811
+ non_api_source_names = non_api_sources.map {|d| " * #{d}" }.join("\n")
812
+
813
+ msg = String.new
814
+ msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
815
+ msg << non_api_source_names
816
+ msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
817
+ "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
818
+ "such as gemstash or geminabox gem servers."
819
+ Bundler.ui.warn msg
787
820
  end
788
821
 
789
822
  def current_platform_locked?
@@ -988,6 +1021,8 @@ module Bundler
988
1021
  end
989
1022
  end
990
1023
 
1024
+ sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems
1025
+
991
1026
  changes
992
1027
  end
993
1028
 
@@ -1157,16 +1192,20 @@ module Bundler
1157
1192
  def find_source_requirements
1158
1193
  preload_git_sources
1159
1194
 
1195
+ # Only safe to exclude when locked_requirements (merged below) backfills the gap.
1196
+ nothing_changed = nothing_changed?
1197
+ excluded = nothing_changed ? excluded_git_sources : []
1198
+
1160
1199
  # Record the specs available in each gem's source, so that those
1161
1200
  # specs will be available later when the resolver knows where to
1162
1201
  # look for that gemspec (or its dependencies)
1163
1202
  source_requirements = if precompute_source_requirements_for_indirect_dependencies?
1164
- all_requirements = source_map.all_requirements(excluded_git_sources)
1203
+ all_requirements = source_map.all_requirements(excluded)
1165
1204
  { default: default_source }.merge(all_requirements)
1166
1205
  else
1167
- { default: Source::RubygemsAggregate.new(sources, source_map, excluded_git_sources) }.merge(source_map.direct_requirements)
1206
+ { default: Source::RubygemsAggregate.new(sources, source_map, excluded) }.merge(source_map.direct_requirements)
1168
1207
  end
1169
- source_requirements.merge!(source_map.locked_requirements) if nothing_changed?
1208
+ source_requirements.merge!(source_map.locked_requirements) if nothing_changed
1170
1209
  metadata_dependencies.each do |dep|
1171
1210
  source_requirements[dep.name] = sources.metadata_source
1172
1211
  end
@@ -1259,7 +1298,7 @@ module Bundler
1259
1298
 
1260
1299
  def new_resolution_base(last_resolve:, unlock:)
1261
1300
  new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms
1262
- Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms)
1301
+ Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms, explicit_unlocks: @explicit_unlocks)
1263
1302
  end
1264
1303
 
1265
1304
  def new_resolver(base)
data/lib/bundler/dsl.rb CHANGED
@@ -116,6 +116,10 @@ module Bundler
116
116
  options = args.last.is_a?(Hash) ? args.pop.dup : {}
117
117
  options = normalize_hash(options)
118
118
  source = normalize_source(source)
119
+ cooldown = options["cooldown"]
120
+ if cooldown && !(cooldown.is_a?(Integer) && cooldown >= 0)
121
+ raise InvalidOption, "Expected `cooldown` to be a non-negative integer, got #{cooldown.inspect}"
122
+ end
119
123
 
120
124
  if options.key?("type")
121
125
  options["type"] = options["type"].to_s
@@ -130,9 +134,9 @@ module Bundler
130
134
  source_opts = options.merge("uri" => source)
131
135
  with_source(@sources.add_plugin_source(options["type"], source_opts), &blk)
132
136
  elsif block_given?
133
- with_source(@sources.add_rubygems_source("remotes" => source), &blk)
137
+ with_source(@sources.add_rubygems_source("remotes" => source, "cooldown" => cooldown), &blk)
134
138
  else
135
- @sources.add_global_rubygems_remote(source)
139
+ @sources.add_global_rubygems_remote(source, cooldown: cooldown)
136
140
  end
137
141
  end
138
142
 
@@ -598,8 +602,11 @@ module Bundler
598
602
 
599
603
  trace_line = backtrace.find {|l| l.include?(dsl_path) } || trace_line
600
604
  return m unless trace_line
601
- line_number = trace_line.split(":")[1].to_i - 1
605
+ # Match the line number right before `:in` or the end of the line so a
606
+ # Windows drive letter like `C:` does not get mistaken for the number.
607
+ line_number = trace_line[/:(\d+)(?::in\b|\z)/, 1]
602
608
  return m unless line_number
609
+ line_number = line_number.to_i - 1
603
610
 
604
611
  lines = contents.lines.to_a
605
612
  indent = " # "
@@ -5,7 +5,7 @@ module Bundler
5
5
  class EndpointSpecification < Gem::Specification
6
6
  include MatchRemoteMetadata
7
7
 
8
- attr_reader :name, :version, :platform, :checksum
8
+ attr_reader :name, :version, :platform, :checksum, :created_at
9
9
  attr_writer :dependencies
10
10
  attr_accessor :remote, :locked_platform
11
11
 
@@ -145,6 +145,7 @@ module Bundler
145
145
  unless data
146
146
  @required_ruby_version = nil
147
147
  @required_rubygems_version = nil
148
+ @created_at = nil
148
149
  return
149
150
  end
150
151
 
@@ -152,8 +153,13 @@ module Bundler
152
153
  next unless v
153
154
  case k.to_s
154
155
  when "checksum"
156
+ # Some registries send empty checksum values, treat them as if the
157
+ # checksum was not included at all
158
+ checksum = v.last
159
+ next unless checksum
160
+
155
161
  begin
156
- @checksum = Checksum.from_api(v.last, @spec_fetcher.uri)
162
+ @checksum = Checksum.from_api(checksum, @spec_fetcher.uri)
157
163
  rescue ArgumentError => e
158
164
  raise ArgumentError, "Invalid checksum for #{full_name}: #{e.message}"
159
165
  end
@@ -161,6 +167,15 @@ module Bundler
161
167
  @required_rubygems_version = Gem::Requirement.new(v)
162
168
  when "ruby"
163
169
  @required_ruby_version = Gem::Requirement.new(v)
170
+ when "created_at"
171
+ value = v.is_a?(Array) ? v.last : v
172
+ if value.is_a?(String)
173
+ @created_at = begin
174
+ Time.new(value)
175
+ rescue ArgumentError
176
+ nil
177
+ end
178
+ end
164
179
  end
165
180
  end
166
181
  rescue StandardError => e
@@ -38,6 +38,9 @@ module Bundler
38
38
  false
39
39
  end
40
40
 
41
+ def release_resolution_memory!
42
+ end
43
+
41
44
  private
42
45
 
43
46
  def log_specs(&block)
@@ -63,6 +63,13 @@ module Bundler
63
63
  true
64
64
  end
65
65
 
66
+ # The client holds the parsed checksums of all info files in the
67
+ # index. Dropping it is always safe because it is rebuilt from the
68
+ # local cache on demand.
69
+ def release_resolution_memory!
70
+ @compact_index_client = nil
71
+ end
72
+
66
73
  private
67
74
 
68
75
  def compact_index_client
@@ -73,6 +80,12 @@ module Bundler
73
80
  end
74
81
 
75
82
  def fetch_gem_infos(names)
83
+ # Create the client and update the versions file on this thread.
84
+ # Otherwise the workers race to lazily create the client and update
85
+ # the versions file concurrently, e.g. when the client was released
86
+ # after resolution and is being rebuilt for `bundle cache`.
87
+ compact_index_client.available?
88
+
76
89
  in_parallel(names) {|name| compact_index_client.info(name) }
77
90
  rescue TooManyRequestsError # rubygems.org is rate limiting us, slow down.
78
91
  @bundle_worker&.stop
@@ -9,6 +9,8 @@ module Bundler
9
9
  super
10
10
 
11
11
  @pool_size = Bundler.settings.installation_parallelization
12
+ ssl_ca_cert = Bundler.settings[:ssl_ca_cert]
13
+ @cert_files << ssl_ca_cert if ssl_ca_cert
12
14
  end
13
15
 
14
16
  def request(*args)
@@ -215,7 +215,13 @@ module Bundler
215
215
  agent << " #{ruby.engine}/#{ruby.versions_string(engine_version)}"
216
216
  end
217
217
 
218
- agent << " options/#{Bundler.settings.all.join(",")}"
218
+ # Some settings keys embed a URI (e.g. `mirror.<uri>`) whose userinfo can
219
+ # carry credentials. Strip the userinfo before advertising the setting
220
+ # names so we don't leak secrets to every gem source we talk to.
221
+ options = Bundler.settings.all.map do |key|
222
+ key.include?("://") ? key.gsub(%r{//[^/@]+@}, "//") : key
223
+ end
224
+ agent << " options/#{options.join(",")}"
219
225
 
220
226
  agent << " ci/#{cis.join(",")}" if cis.any?
221
227
 
@@ -243,6 +249,10 @@ module Bundler
243
249
  fetchers.first.api_fetcher?
244
250
  end
245
251
 
252
+ def release_resolution_memory!
253
+ @fetchers&.each(&:release_resolution_memory!)
254
+ end
255
+
246
256
  def gem_remote_fetcher
247
257
  @gem_remote_fetcher ||= begin
248
258
  require_relative "fetcher/gem_remote_fetcher"
@@ -314,7 +324,7 @@ module Bundler
314
324
  if ssl_client_cert
315
325
  pem = File.read(ssl_client_cert)
316
326
  con.cert = OpenSSL::X509::Certificate.new(pem)
317
- con.key = OpenSSL::PKey::RSA.new(pem)
327
+ con.key = OpenSSL::PKey.read(pem)
318
328
  end
319
329
 
320
330
  con.read_timeout = Fetcher.api_timeout
@@ -80,11 +80,10 @@ module Bundler
80
80
  def conservative_version(spec)
81
81
  version = spec.version
82
82
  return ">= 0" if version.nil?
83
- segments = version.segments
84
83
  seg_end_index = version >= Gem::Version.new("1.0") ? 1 : 2
85
84
 
86
85
  prerelease_suffix = version.to_s.delete_prefix(version.release.to_s) if version.prerelease?
87
- "#{version_prefix}#{segments[0..seg_end_index].join(".")}#{prerelease_suffix}"
86
+ "#{version_prefix}#{version.segments[0..seg_end_index].join(".")}#{prerelease_suffix}"
88
87
  end
89
88
 
90
89
  def version_prefix
@@ -6,7 +6,7 @@ require_relative "gem_installer"
6
6
  module Bundler
7
7
  class ParallelInstaller
8
8
  class SpecInstallation
9
- attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error
9
+ attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error, :dependencies
10
10
  def initialize(spec)
11
11
  @spec = spec
12
12
  @name = spec.name
@@ -46,25 +46,11 @@ module Bundler
46
46
  !post_install_message.empty?
47
47
  end
48
48
 
49
- def ignorable_dependency?(dep)
50
- dep.type == :development || dep.name == @name
51
- end
52
-
53
- # Checks installed dependencies against spec's dependencies to make
54
- # sure needed dependencies have been installed.
49
+ # Recursively checks that all dependencies (direct and transitive) have been installed.
55
50
  def dependencies_installed?(installed_specs)
56
- dependencies.all? {|d| installed_specs.include? d.name }
57
- end
58
-
59
- # Represents only the non-development dependencies, the ones that are
60
- # itself and are in the total list.
61
- def dependencies
62
- @dependencies ||= all_dependencies.reject {|dep| ignorable_dependency? dep }
63
- end
64
-
65
- # Represents all dependencies
66
- def all_dependencies
67
- @spec.dependencies
51
+ dependencies.all? do |dep|
52
+ installed_specs.include?(dep.name) && dep.dependencies_installed?(installed_specs)
53
+ end
68
54
  end
69
55
 
70
56
  def to_s
@@ -85,6 +71,12 @@ module Bundler
85
71
  @force = force
86
72
  @local = local
87
73
  @specs = all_specs.map {|s| SpecInstallation.new(s) }
74
+ specs_by_name = @specs.to_h {|s| [s.name, s] }
75
+ @specs.each do |spec_install|
76
+ spec_install.dependencies = spec_install.spec.dependencies.filter_map do |dep|
77
+ specs_by_name[dep.name] unless dep.type == :development || dep.name == spec_install.name
78
+ end
79
+ end
88
80
  @specs.each do |spec_install|
89
81
  spec_install.state = :installed if skip.include?(spec_install.name)
90
82
  end if skip
@@ -118,10 +110,55 @@ module Bundler
118
110
  end
119
111
 
120
112
  def install_with_worker
121
- installed_specs = {}
122
- enqueue_specs(installed_specs)
113
+ with_jobserver do
114
+ installed_specs = {}
115
+ enqueue_specs(installed_specs)
123
116
 
124
- process_specs(installed_specs) until finished_installing?
117
+ process_specs(installed_specs) until finished_installing?
118
+ end
119
+ end
120
+
121
+ def with_jobserver
122
+ # The jobserver hands tokens to child `make` processes through MAKEFLAGS
123
+ # using the GNU make `--jobserver-auth` protocol. nmake, the default make
124
+ # on mswin, instead reads MAKEFLAGS as bare option letters and aborts
125
+ # every native extension build with `fatal error U1065: invalid option
126
+ # '-'`. Skip the jobserver when nmake is in use. Other Windows toolchains
127
+ # such as mingw use GNU make and keep working through the inherited pipe.
128
+ return yield if nmake? || bsd_make?
129
+
130
+ begin
131
+ r, w = IO.pipe
132
+ r.close_on_exec = false
133
+ w.close_on_exec = false
134
+ w.write("*" * @size)
135
+
136
+ old_makeflags = ENV["MAKEFLAGS"]
137
+ ENV["MAKEFLAGS"] = [old_makeflags, "--jobserver-auth=#{r.fileno},#{w.fileno}"].compact.join(" ")
138
+
139
+ yield
140
+ ensure
141
+ # Restore MAKEFLAGS before closing the pipe so a close failure can't
142
+ # leave the process with descriptors that point at a closed pipe.
143
+ old_makeflags ? ENV["MAKEFLAGS"] = old_makeflags : ENV.delete("MAKEFLAGS")
144
+
145
+ r&.close
146
+ w&.close
147
+ end
148
+ end
149
+
150
+ # Mirror how RubyGems' extension builder picks the make program so the
151
+ # jobserver is only set up when a GNU-compatible make will consume it.
152
+ def nmake?
153
+ make = ENV["MAKE"] || ENV["make"]
154
+ make ||= "nmake" if RUBY_PLATFORM.include?("mswin")
155
+ /\bnmake/i.match?(make.to_s)
156
+ end
157
+
158
+ def bsd_make?
159
+ return false unless Gem.freebsd_platform?
160
+ make = ENV["MAKE"] || ENV["make"] || "make"
161
+ !/\bgmake/i.match?(make)
125
162
  end
126
163
 
127
164
  def install_serially
@@ -218,7 +255,15 @@ module Bundler
218
255
 
219
256
  def require_tree_for_spec(spec)
220
257
  tree = @spec_set.what_required(spec)
221
- t = String.new("In #{File.basename(SharedHelpers.default_gemfile)}:\n")
258
+ gemfile_name = begin
259
+ File.basename(SharedHelpers.default_gemfile)
260
+ rescue GemfileNotFound
261
+ # This runs while reporting an install error. When no Gemfile can be
262
+ # located (e.g. Bundler used as a library), raising here would mask
263
+ # the original error, so fall back to a generic header instead.
264
+ "Gemfile"
265
+ end
266
+ t = String.new("In #{gemfile_name}:\n")
222
267
  tree.each_with_index do |s, depth|
223
268
  t << " " * depth.succ << s.name
224
269
  unless tree.last == s
@@ -63,6 +63,11 @@ module Bundler
63
63
  Bundler.create_bundle_path
64
64
 
65
65
  ProcessLock.lock do
66
+ # Invalidate any stale gem specification cache from before we acquired the lock.
67
+ # Another process may have installed gems while we were waiting.
68
+ Gem::Specification.reset
69
+ @definition.sources.clear_cache
70
+
66
71
  @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment])
67
72
 
68
73
  if @definition.dependencies.empty?
@@ -190,7 +195,13 @@ module Bundler
190
195
  force = options[:force]
191
196
  local = options[:local] || options[:"prefer-local"]
192
197
  jobs = Bundler.settings.installation_parallelization
193
- spec_installations = ParallelInstaller.call(self, @definition.specs, jobs, standalone, force, local: local)
198
+ specs = @definition.specs
199
+ # Installing default gems may need the remote index again to cache
200
+ # their .gem files, so keep resolution memory around in that case.
201
+ # The bundler spec itself is excluded because it comes from the
202
+ # metadata source and never goes through that path.
203
+ @definition.release_resolution_memory! if specs.none? {|s| s.default_gem? && s.source.is_a?(Source::Rubygems) }
204
+ spec_installations = ParallelInstaller.call(self, specs, jobs, standalone, force, local: local)
194
205
  spec_installations.each do |installation|
195
206
  post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message?
196
207
  end
@@ -71,7 +71,8 @@ module Bundler
71
71
  checksums = definition.resolve.map do |spec|
72
72
  spec.source.checksum_store.to_lock(spec)
73
73
  end
74
- add_section("CHECKSUMS", checksums)
74
+
75
+ add_section("CHECKSUMS", checksums + bundler_checksum)
75
76
  end
76
77
 
77
78
  def add_locked_ruby_version
@@ -100,5 +101,42 @@ module Bundler
100
101
  raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile"
101
102
  end
102
103
  end
104
+
105
+ def bundler_checksum
106
+ # `.dev` versions and `SKIP_BUNDLER_CHECKSUM` are deliberate opt-outs (used
107
+ # by Bundler/RubyGems' own development and release tasks): never record a
108
+ # checksum for Bundler itself in those cases.
109
+ return [] if Bundler.gem_version.to_s.end_with?(".dev") || ENV["SKIP_BUNDLER_CHECKSUM"]
110
+
111
+ bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last
112
+
113
+ # Record a fresh checksum from the locally cached gem when it's available.
114
+ # When it isn't (e.g. a fresh checkout/CI that never downloaded the bundler
115
+ # gem), fall back to whatever checksum is already locked rather than
116
+ # dropping it, so the entry stays consistent across environments.
117
+ if File.exist?(bundler_spec.cache_file)
118
+ require "rubygems/package"
119
+
120
+ package = Gem::Package.new(bundler_spec.cache_file)
121
+ definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package))
122
+ elsif bundled_with_changing?
123
+ # We can't compute a fresh checksum (the bundler gem isn't cached) and the
124
+ # BUNDLED WITH version is changing. Keeping the previously locked checksum
125
+ # would leave a `bundler (<old version>) sha256=...` entry that no longer
126
+ # matches the new BUNDLED WITH version, so drop it instead.
127
+ return []
128
+ end
129
+
130
+ return [] if definition.sources.metadata_source.checksum_store.missing?(bundler_spec)
131
+
132
+ [definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)]
133
+ end
134
+
135
+ def bundled_with_changing?
136
+ locked_gems = definition.locked_gems
137
+ return false unless locked_gems
138
+
139
+ locked_gems.bundler_version != definition.bundler_version_to_lock
140
+ end
103
141
  end
104
142
  end
@@ -28,6 +28,7 @@ module Bundler
28
28
 
29
29
  attr_reader(
30
30
  :sources,
31
+ :metadata_source,
31
32
  :dependencies,
32
33
  :specs,
33
34
  :platforms,
@@ -97,6 +98,7 @@ module Bundler
97
98
  def initialize(lockfile, strict: false)
98
99
  @platforms = []
99
100
  @sources = []
101
+ @metadata_source = Source::Metadata.new
100
102
  @dependencies = {}
101
103
  @parse_method = nil
102
104
  @specs = {}
@@ -113,6 +115,17 @@ module Bundler
113
115
  "Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock."
114
116
  end
115
117
 
118
+ @valid = lockfile.strip.empty? ||
119
+ lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) }
120
+
121
+ unless @valid
122
+ SharedHelpers.feature_deprecated!(
123
+ "Your #{@lockfile_path} does not appear to be a valid lockfile. " \
124
+ "Run `rm #{@lockfile_path}` and then `bundle install` to generate a new lockfile. " \
125
+ "This will raise a LockfileError in a future version of Bundler."
126
+ )
127
+ end
128
+
116
129
  lockfile.split(/((?:\r?\n)+)/) do |line|
117
130
  # split alternates between the line and the following whitespace
118
131
  next @pos.advance!(line) if line.match?(/^\s*$/)
@@ -162,6 +175,10 @@ module Bundler
162
175
  bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2")
163
176
  end
164
177
 
178
+ def valid?
179
+ @valid
180
+ end
181
+
165
182
  private
166
183
 
167
184
  TYPES = {
@@ -252,7 +269,12 @@ module Bundler
252
269
  version = Gem::Version.new(version)
253
270
  platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
254
271
  full_name = Gem::NameTuple.new(name, version, platform).full_name
255
- return unless spec = @specs[full_name]
272
+ spec = @specs[full_name]
273
+
274
+ if name == "bundler"
275
+ spec ||= LazySpecification.new(name, version, platform, @metadata_source)
276
+ end
277
+ return unless spec
256
278
 
257
279
  if checksums
258
280
  checksums.split(",") do |lock_checksum|
@@ -1,10 +1,10 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-ADD" "1" "March 2026" ""
3
+ .TH "BUNDLE\-ADD" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install
6
6
  .SH "SYNOPSIS"
7
- \fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic]
7
+ \fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-cooldown=NUMBER] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic]
8
8
  .SH "DESCRIPTION"
9
9
  Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\.
10
10
  .SH "OPTIONS"
@@ -50,6 +50,9 @@ Adds optimistic declaration of version\.
50
50
  .TP
51
51
  \fB\-\-strict\fR
52
52
  Adds strict declaration of version\.
53
+ .TP
54
+ \fB\-\-cooldown=<number>\fR
55
+ Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\.
53
56
  .SH "EXAMPLES"
54
57
  .IP "1." 4
55
58
  You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\.
@@ -73,4 +76,4 @@ You can assign the gem to more than one group\.
73
76
  \fBbundle add rails \-\-group "development, test"\fR
74
77
  .IP "" 0
75
78
  .SH "SEE ALSO"
76
- Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR
79
+ Gemfile(5) \fIhttps://guides\.rubygems\.org/gemfile/\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR
@@ -5,7 +5,7 @@ bundle-add(1) -- Add gem to the Gemfile and run bundle install
5
5
 
6
6
  `bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE]
7
7
  [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF]
8
- [--quiet] [--skip-install] [--strict|--optimistic]
8
+ [--cooldown=NUMBER] [--quiet] [--skip-install] [--strict|--optimistic]
9
9
 
10
10
  ## DESCRIPTION
11
11
 
@@ -56,6 +56,11 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`.
56
56
  * `--strict`:
57
57
  Adds strict declaration of version.
58
58
 
59
+ * `--cooldown=<number>`:
60
+ Only consider gem versions published at least <number> days ago when
61
+ resolving. Pass `0` to disable cooldown for this run. See `cooldown`
62
+ in bundle-config(1) for precedence rules.
63
+
59
64
  ## EXAMPLES
60
65
 
61
66
  1. You can add the `rails` gem to the Gemfile without any version restriction.
@@ -83,5 +88,5 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`.
83
88
 
84
89
  ## SEE ALSO
85
90
 
86
- [Gemfile(5)](https://bundler.io/man/gemfile.5.html),
91
+ [Gemfile(5)](https://guides.rubygems.org/gemfile/),
87
92
  [bundle-remove(1)](bundle-remove.1.html)
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-BINSTUBS" "1" "March 2026" ""
3
+ .TH "BUNDLE\-BINSTUBS" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-binstubs\fR \- Install the binstubs of the listed gems
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-CACHE" "1" "March 2026" ""
3
+ .TH "BUNDLE\-CACHE" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application
6
6
  .SH "SYNOPSIS"
@@ -17,6 +17,9 @@ Include gems for all platforms present in the lockfile, not only the current one
17
17
  \fB\-\-cache\-path=CACHE\-PATH\fR
18
18
  Specify a different cache path than the default (vendor/cache)\.
19
19
  .TP
20
+ \fB\-\-cooldown=<number>\fR
21
+ Only consider gem versions published at least \fInumber\fR days ago when resolving before caching\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\.
22
+ .TP
20
23
  \fB\-\-gemfile=GEMFILE\fR
21
24
  Use the specified gemfile instead of Gemfile\.
22
25
  .TP
@@ -21,6 +21,12 @@ use the gems in the cache in preference to the ones on `rubygems.org`.
21
21
  * `--cache-path=CACHE-PATH`:
22
22
  Specify a different cache path than the default (vendor/cache).
23
23
 
24
+ * `--cooldown=<number>`:
25
+ Only consider gem versions published at least <number> days ago when
26
+ resolving before caching. Pass `0` to disable cooldown for this run,
27
+ overriding any per-source or global configuration. See `cooldown` in
28
+ bundle-config(1).
29
+
24
30
  * `--gemfile=GEMFILE`:
25
31
  Use the specified gemfile instead of Gemfile.
26
32