bundler 4.0.10 → 4.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +174 -0
  3. data/bundler.gemspec +2 -2
  4. data/lib/bundler/build_metadata.rb +2 -2
  5. data/lib/bundler/cli/add.rb +3 -0
  6. data/lib/bundler/cli/check.rb +1 -3
  7. data/lib/bundler/cli/common.rb +17 -0
  8. data/lib/bundler/cli/config.rb +8 -3
  9. data/lib/bundler/cli/doctor/diagnose.rb +1 -1
  10. data/lib/bundler/cli/exec.rb +15 -1
  11. data/lib/bundler/cli/gem.rb +2 -3
  12. data/lib/bundler/cli/init.rb +4 -0
  13. data/lib/bundler/cli/install.rb +4 -0
  14. data/lib/bundler/cli/issue.rb +2 -2
  15. data/lib/bundler/cli/lock.rb +10 -2
  16. data/lib/bundler/cli/open.rb +12 -2
  17. data/lib/bundler/cli/outdated.rb +76 -6
  18. data/lib/bundler/cli/update.rb +3 -0
  19. data/lib/bundler/cli.rb +21 -15
  20. data/lib/bundler/compact_index_client/cache.rb +11 -0
  21. data/lib/bundler/compact_index_client/cache_file.rb +1 -1
  22. data/lib/bundler/compact_index_client/parser.rb +4 -1
  23. data/lib/bundler/definition.rb +64 -15
  24. data/lib/bundler/dsl.rb +11 -4
  25. data/lib/bundler/endpoint_specification.rb +30 -2
  26. data/lib/bundler/fetcher/base.rb +3 -0
  27. data/lib/bundler/fetcher/compact_index.rb +13 -0
  28. data/lib/bundler/fetcher/gem_remote_fetcher.rb +2 -0
  29. data/lib/bundler/fetcher.rb +12 -2
  30. data/lib/bundler/gem_helper.rb +2 -2
  31. data/lib/bundler/installer/parallel_installer.rb +68 -23
  32. data/lib/bundler/installer.rb +18 -1
  33. data/lib/bundler/lockfile_generator.rb +55 -1
  34. data/lib/bundler/lockfile_parser.rb +23 -1
  35. data/lib/bundler/man/bundle-add.1 +6 -3
  36. data/lib/bundler/man/bundle-add.1.ronn +7 -2
  37. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  38. data/lib/bundler/man/bundle-cache.1 +4 -1
  39. data/lib/bundler/man/bundle-cache.1.ronn +6 -0
  40. data/lib/bundler/man/bundle-check.1 +1 -1
  41. data/lib/bundler/man/bundle-clean.1 +1 -1
  42. data/lib/bundler/man/bundle-config.1 +121 -157
  43. data/lib/bundler/man/bundle-config.1.ronn +38 -6
  44. data/lib/bundler/man/bundle-console.1 +3 -3
  45. data/lib/bundler/man/bundle-console.1.ronn +2 -2
  46. data/lib/bundler/man/bundle-doctor.1 +1 -1
  47. data/lib/bundler/man/bundle-env.1 +1 -1
  48. data/lib/bundler/man/bundle-exec.1 +1 -1
  49. data/lib/bundler/man/bundle-fund.1 +1 -1
  50. data/lib/bundler/man/bundle-gem.1 +1 -1
  51. data/lib/bundler/man/bundle-help.1 +1 -1
  52. data/lib/bundler/man/bundle-info.1 +1 -1
  53. data/lib/bundler/man/bundle-init.1 +2 -2
  54. data/lib/bundler/man/bundle-init.1.ronn +1 -1
  55. data/lib/bundler/man/bundle-install.1 +5 -2
  56. data/lib/bundler/man/bundle-install.1.ronn +9 -1
  57. data/lib/bundler/man/bundle-issue.1 +1 -1
  58. data/lib/bundler/man/bundle-licenses.1 +1 -1
  59. data/lib/bundler/man/bundle-list.1 +1 -1
  60. data/lib/bundler/man/bundle-lock.1 +5 -2
  61. data/lib/bundler/man/bundle-lock.1.ronn +6 -0
  62. data/lib/bundler/man/bundle-open.1 +1 -1
  63. data/lib/bundler/man/bundle-outdated.1 +17 -14
  64. data/lib/bundler/man/bundle-outdated.1.ronn +25 -12
  65. data/lib/bundler/man/bundle-platform.1 +5 -1
  66. data/lib/bundler/man/bundle-platform.1.ronn +4 -0
  67. data/lib/bundler/man/bundle-plugin.1 +2 -2
  68. data/lib/bundler/man/bundle-plugin.1.ronn +1 -1
  69. data/lib/bundler/man/bundle-pristine.1 +1 -1
  70. data/lib/bundler/man/bundle-remove.1 +1 -1
  71. data/lib/bundler/man/bundle-show.1 +1 -1
  72. data/lib/bundler/man/bundle-update.1 +5 -2
  73. data/lib/bundler/man/bundle-update.1.ronn +8 -0
  74. data/lib/bundler/man/bundle-version.1 +1 -1
  75. data/lib/bundler/man/bundle.1 +23 -2
  76. data/lib/bundler/man/bundle.1.ronn +22 -1
  77. data/lib/bundler/man/gemfile.5 +14 -3
  78. data/lib/bundler/man/gemfile.5.ronn +23 -2
  79. data/lib/bundler/mirror.rb +7 -7
  80. data/lib/bundler/plugin/unloaded_source.rb +25 -0
  81. data/lib/bundler/plugin.rb +12 -6
  82. data/lib/bundler/remote_specification.rb +1 -1
  83. data/lib/bundler/resolver/base.rb +9 -0
  84. data/lib/bundler/resolver.rb +116 -2
  85. data/lib/bundler/rubygems_ext.rb +22 -0
  86. data/lib/bundler/rubygems_gem_installer.rb +35 -2
  87. data/lib/bundler/runtime.rb +11 -11
  88. data/lib/bundler/self_manager.rb +6 -1
  89. data/lib/bundler/settings.rb +1 -0
  90. data/lib/bundler/shared_helpers.rb +21 -1
  91. data/lib/bundler/source/git/git_proxy.rb +83 -10
  92. data/lib/bundler/source/git.rb +4 -4
  93. data/lib/bundler/source/metadata.rb +4 -0
  94. data/lib/bundler/source/path.rb +4 -3
  95. data/lib/bundler/source/rubygems/remote.rb +12 -2
  96. data/lib/bundler/source/rubygems.rb +81 -13
  97. data/lib/bundler/source_list.rb +44 -3
  98. data/lib/bundler/spec_set.rb +2 -2
  99. data/lib/bundler/stub_specification.rb +3 -1
  100. data/lib/bundler/templates/newgem/newgem.gemspec.tt +8 -2
  101. data/lib/bundler/vendored_tsort.rb +19 -2
  102. data/lib/bundler/version.rb +1 -1
  103. data/lib/bundler.rb +26 -6
  104. metadata +5 -6
  105. data/lib/bundler/vendor/tsort/LICENSE.txt +0 -22
  106. data/lib/bundler/vendor/tsort/lib/tsort.rb +0 -455
data/lib/bundler/cli.rb CHANGED
@@ -61,18 +61,18 @@ module Bundler
61
61
 
62
62
  current_cmd = args.last[:current_command].name
63
63
 
64
- custom_gemfile = options[:gemfile] || Bundler.settings[:gemfile]
65
- if custom_gemfile && !custom_gemfile.empty?
66
- Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", File.expand_path(custom_gemfile)
67
- reset_settings = true
68
- end
69
-
70
- # lock --lockfile works differently than install --lockfile
71
- unless current_cmd == "lock"
72
- custom_lockfile = options[:lockfile] || ENV["BUNDLE_LOCKFILE"] || Bundler.settings[:lockfile]
73
- if custom_lockfile && !custom_lockfile.empty?
74
- Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", File.expand_path(custom_lockfile)
75
- reset_settings = true
64
+ # `bundle config` manages stored settings, so avoid promoting settings
65
+ # like `gemfile` or `lockfile` to environment variables before it runs.
66
+ unless current_cmd == "config"
67
+ Bundler.configure_custom_gemfile(options[:gemfile])
68
+
69
+ # lock --lockfile works differently than install --lockfile
70
+ unless current_cmd == "lock"
71
+ custom_lockfile = options[:lockfile] || ENV["BUNDLE_LOCKFILE"] || Bundler.settings[:lockfile]
72
+ if custom_lockfile && !custom_lockfile.empty?
73
+ Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", File.expand_path(custom_lockfile)
74
+ reset_settings = true
75
+ end
76
76
  end
77
77
  end
78
78
 
@@ -274,6 +274,7 @@ module Bundler
274
274
  method_option "target-rbconfig", type: :string, banner: "Path to rbconfig.rb for the deployment target platform"
275
275
  method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group (removed)."
276
276
  method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)."
277
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
277
278
  def install
278
279
  %w[clean deployment frozen no-prune path shebang without with].each do |option|
279
280
  remembered_flag_deprecation(option)
@@ -324,6 +325,7 @@ module Bundler
324
325
  method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major"
325
326
  method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated."
326
327
  method_option "all", type: :boolean, banner: "Update everything."
328
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
327
329
  def update(*gems)
328
330
  require_relative "cli/update"
329
331
  Bundler.settings.temporary(no_install: false) do
@@ -405,6 +407,7 @@ module Bundler
405
407
  method_option "skip-install", type: :boolean, banner: "Adds gem to the Gemfile but does not install it"
406
408
  method_option "optimistic", type: :boolean, banner: "Adds optimistic declaration of version to gem"
407
409
  method_option "strict", type: :boolean, banner: "Adds strict declaration of version to gem"
410
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
408
411
  def add(*gems)
409
412
  require_relative "cli/add"
410
413
  Add.new(options.dup, gems).run
@@ -435,6 +438,7 @@ module Bundler
435
438
  method_option "filter-patch", type: :boolean, banner: "Only list patch newer versions"
436
439
  method_option "parseable", aliases: "--porcelain", type: :boolean, banner: "Use minimal formatting for more parseable output"
437
440
  method_option "only-explicit", type: :boolean, banner: "Only list gems specified in your Gemfile, not their dependencies"
441
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
438
442
  def outdated(*gems)
439
443
  require_relative "cli/outdated"
440
444
  Outdated.new(options, gems).run
@@ -457,6 +461,7 @@ module Bundler
457
461
  method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)."
458
462
  method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
459
463
  method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this bundle cache operation's install (removed)"
464
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
460
465
  long_desc <<-D
461
466
  The cache command will copy the .gem files for every gem in the bundle into the
462
467
  directory ./vendor/cache. If you then check that directory into your source
@@ -621,15 +626,15 @@ module Bundler
621
626
  end
622
627
 
623
628
  desc "lock", "Creates a lockfile without installing"
624
- method_option "update", type: :array, lazy_default: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems"
629
+ method_option "update", type: :array, lazy_default: true, repeatable: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems"
625
630
  method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only"
626
631
  method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system"
627
632
  method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
628
633
  method_option "lockfile", type: :string, default: nil, banner: "the path the lockfile should be written to"
629
634
  method_option "full-index", type: :boolean, default: false, banner: "Fall back to using the single-file index of all gems"
630
635
  method_option "add-checksums", type: :boolean, default: false, banner: "Adds checksums to the lockfile"
631
- method_option "add-platform", type: :array, default: [], banner: "Add a new platform to the lockfile"
632
- method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile"
636
+ method_option "add-platform", type: :array, default: [], repeatable: true, banner: "Add a new platform to the lockfile"
637
+ method_option "remove-platform", type: :array, default: [], repeatable: true, banner: "Remove a platform from the lockfile"
633
638
  method_option "normalize-platforms", type: :boolean, default: false, banner: "Normalize lockfile platforms"
634
639
  method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version"
635
640
  method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version"
@@ -638,6 +643,7 @@ module Bundler
638
643
  method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major"
639
644
  method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated"
640
645
  method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
646
+ method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
641
647
  def lock
642
648
  require_relative "cli/lock"
643
649
  Lock.new(options).run
@@ -50,6 +50,7 @@ module Bundler
50
50
 
51
51
  def info_path(name)
52
52
  name = name.to_s
53
+ validate_name!(name)
53
54
  # TODO: converge this into the info_root by hashing all filenames like info_etag_path
54
55
  if /[^a-z0-9_-]/.match?(name)
55
56
  name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}"
@@ -61,9 +62,19 @@ module Bundler
61
62
 
62
63
  def info_etag_path(name)
63
64
  name = name.to_s
65
+ validate_name!(name)
64
66
  @info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}")
65
67
  end
66
68
 
69
+ # Gem names come from the remote index and are not otherwise
70
+ # validated, so refuse anything that would escape the cache
71
+ # directory when used as a path component.
72
+ def validate_name!(name)
73
+ return if File.basename(name) == name
74
+
75
+ raise SecurityError, "malformed gem name: #{name.inspect}"
76
+ end
77
+
67
78
  def mkdir(name)
68
79
  directory.join(name).tap do |dir|
69
80
  SharedHelpers.filesystem_access(dir) do
@@ -103,7 +103,7 @@ module Bundler
103
103
  # Returns false without appending when no digests since appending is too error prone to do without digests.
104
104
  def append(data)
105
105
  return false unless digests?
106
- open("a") {|f| f.write data }
106
+ open("ab") {|f| f.write data }
107
107
  verify && commit
108
108
  end
109
109
 
@@ -71,7 +71,10 @@ module Bundler
71
71
  # This method gets called at least once for every gem when parsing versions.
72
72
  def parse_version_checksum(line, checksums)
73
73
  return unless (name_end = line.index(" ")) # Artifactory bug causes blank lines in artifactor index files
74
- return unless (checksum_start = line.index(" ", name_end + 1) + 1)
74
+ checksum_start = line.index(" ", name_end + 1)
75
+ return unless checksum_start
76
+ checksum_start += 1
77
+
75
78
  checksum_end = line.size - checksum_start
76
79
 
77
80
  line.freeze # allows slicing into the string to not allocate a copy of the line
@@ -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
@@ -356,6 +366,12 @@ module Bundler
356
366
  sources.git_sources.filter_map {|s| File.realpath(s.path) if File.exist?(s.path) }
357
367
  end
358
368
 
369
+ # Versions excluded by cooldown during the last resolution, one entry per
370
+ # gem with the newest skipped version. Empty when no resolution ran.
371
+ def cooldown_skipped
372
+ @cooldown_skipped || []
373
+ end
374
+
359
375
  def groups
360
376
  dependencies.flat_map(&:groups).uniq
361
377
  end
@@ -387,10 +403,6 @@ module Bundler
387
403
 
388
404
  contents = to_lock
389
405
 
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
406
  if @locked_bundler_version
395
407
  locked_major = @locked_bundler_version.segments.first
396
408
  current_major = bundler_version_to_lock.segments.first
@@ -398,7 +410,7 @@ module Bundler
398
410
  updating_major = locked_major < current_major
399
411
  end
400
412
 
401
- preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler))
413
+ preserve_unknown_sections ||= Bundler.frozen_bundle? || (!updating_major && !(unlocking? || @unlocking_bundler))
402
414
 
403
415
  if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections)
404
416
  return if Bundler.frozen_bundle?
@@ -407,8 +419,18 @@ module Bundler
407
419
  end
408
420
 
409
421
  if Bundler.frozen_bundle?
410
- Bundler.ui.error "Cannot write a changed lockfile while frozen."
411
- return
422
+ msg = lockfile_changes_summary("frozen mode is set") ||
423
+ "Your lockfile needs to be updated, but it can't be because frozen mode is set.\n\n" \
424
+ "Run `bundle install` elsewhere and add the updated #{SharedHelpers.relative_lockfile_path} to version control."
425
+ raise ProductionError, msg
426
+ end
427
+
428
+ # Convert to \r\n if the existing lock has them, i.e., Windows with
429
+ # `git config core.autocrlf=true`. Detect from the bytes on disk because
430
+ # reading in text mode strips carriage returns on Windows, which would
431
+ # otherwise defeat this check and rewrite a `\r\n` lockfile with `\n`.
432
+ if File.exist?(file) && SharedHelpers.filesystem_access(file, :read) {|p| File.binread(p).include?("\r\n") }
433
+ contents.gsub!(/\n/, "\r\n")
412
434
  end
413
435
 
414
436
  begin
@@ -688,9 +710,10 @@ module Bundler
688
710
  "available locally before rerunning Bundler."
689
711
  else
690
712
  "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."
713
+ "no longer be found in that source. That means either the author of #{locked_gem} has removed it, " \
714
+ "or you no longer have access to that source. You'll need to update your bundle to a version other " \
715
+ "than #{locked_gem} that hasn't been removed, or check your credentials and access rights for " \
716
+ "#{locked_gem.source}, in order to install."
694
717
  end
695
718
 
696
719
  raise GemNotFound, message
@@ -755,6 +778,8 @@ module Bundler
755
778
 
756
779
  result = SpecSet.new(resolver.start)
757
780
 
781
+ @cooldown_skipped = resolver.cooldown_skipped
782
+
758
783
  @resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version
759
784
 
760
785
  @new_platforms.each do |platform|
@@ -783,7 +808,25 @@ module Bundler
783
808
  end
784
809
 
785
810
  def precompute_source_requirements_for_indirect_dependencies?
786
- sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
811
+ if sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
812
+ true
813
+ else
814
+ non_dependency_api_warning
815
+ false
816
+ end
817
+ end
818
+
819
+ def non_dependency_api_warning
820
+ non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
821
+ non_api_source_names = non_api_sources.map {|d| " * #{d}" }.join("\n")
822
+
823
+ msg = String.new
824
+ msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
825
+ msg << non_api_source_names
826
+ msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
827
+ "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
828
+ "such as gemstash or geminabox gem servers."
829
+ Bundler.ui.warn msg
787
830
  end
788
831
 
789
832
  def current_platform_locked?
@@ -988,6 +1031,8 @@ module Bundler
988
1031
  end
989
1032
  end
990
1033
 
1034
+ sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems
1035
+
991
1036
  changes
992
1037
  end
993
1038
 
@@ -1157,16 +1202,20 @@ module Bundler
1157
1202
  def find_source_requirements
1158
1203
  preload_git_sources
1159
1204
 
1205
+ # Only safe to exclude when locked_requirements (merged below) backfills the gap.
1206
+ nothing_changed = nothing_changed?
1207
+ excluded = nothing_changed ? excluded_git_sources : []
1208
+
1160
1209
  # Record the specs available in each gem's source, so that those
1161
1210
  # specs will be available later when the resolver knows where to
1162
1211
  # look for that gemspec (or its dependencies)
1163
1212
  source_requirements = if precompute_source_requirements_for_indirect_dependencies?
1164
- all_requirements = source_map.all_requirements(excluded_git_sources)
1213
+ all_requirements = source_map.all_requirements(excluded)
1165
1214
  { default: default_source }.merge(all_requirements)
1166
1215
  else
1167
- { default: Source::RubygemsAggregate.new(sources, source_map, excluded_git_sources) }.merge(source_map.direct_requirements)
1216
+ { default: Source::RubygemsAggregate.new(sources, source_map, excluded) }.merge(source_map.direct_requirements)
1168
1217
  end
1169
- source_requirements.merge!(source_map.locked_requirements) if nothing_changed?
1218
+ source_requirements.merge!(source_map.locked_requirements) if nothing_changed
1170
1219
  metadata_dependencies.each do |dep|
1171
1220
  source_requirements[dep.name] = sources.metadata_source
1172
1221
  end
@@ -1259,7 +1308,7 @@ module Bundler
1259
1308
 
1260
1309
  def new_resolution_base(last_resolve:, unlock:)
1261
1310
  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)
1311
+ 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
1312
  end
1264
1313
 
1265
1314
  def new_resolver(base)
data/lib/bundler/dsl.rb CHANGED
@@ -68,7 +68,7 @@ module Bundler
68
68
  development_group = opts[:development_group] || :development
69
69
  expanded_path = gemfile_root.join(path)
70
70
 
71
- gemspecs = Gem::Util.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) }
71
+ gemspecs = SharedHelpers.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) }
72
72
  gemspecs.reject! {|s| s.name != name } if name
73
73
  specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] }
74
74
 
@@ -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
 
@@ -50,6 +50,19 @@ module Bundler
50
50
  end
51
51
  end
52
52
 
53
+ # `require_paths` is overridden above, but `full_require_paths` (and so
54
+ # `load_paths`) is computed from `raw_require_paths`, which would otherwise
55
+ # report the default `lib` for every gem
56
+ def raw_require_paths
57
+ if @remote_specification
58
+ @remote_specification.raw_require_paths
59
+ elsif _local_specification
60
+ _local_specification.raw_require_paths
61
+ else
62
+ super
63
+ end
64
+ end
65
+
53
66
  # needed for inline
54
67
  def load_paths
55
68
  # remote specs aren't installed, and can't have load_paths
@@ -145,6 +158,7 @@ module Bundler
145
158
  unless data
146
159
  @required_ruby_version = nil
147
160
  @required_rubygems_version = nil
161
+ @created_at = nil
148
162
  return
149
163
  end
150
164
 
@@ -152,8 +166,13 @@ module Bundler
152
166
  next unless v
153
167
  case k.to_s
154
168
  when "checksum"
169
+ # Some registries send empty checksum values, treat them as if the
170
+ # checksum was not included at all
171
+ checksum = v.last
172
+ next unless checksum
173
+
155
174
  begin
156
- @checksum = Checksum.from_api(v.last, @spec_fetcher.uri)
175
+ @checksum = Checksum.from_api(checksum, @spec_fetcher.uri)
157
176
  rescue ArgumentError => e
158
177
  raise ArgumentError, "Invalid checksum for #{full_name}: #{e.message}"
159
178
  end
@@ -161,6 +180,15 @@ module Bundler
161
180
  @required_rubygems_version = Gem::Requirement.new(v)
162
181
  when "ruby"
163
182
  @required_ruby_version = Gem::Requirement.new(v)
183
+ when "created_at"
184
+ value = v.is_a?(Array) ? v.last : v
185
+ if value.is_a?(String)
186
+ @created_at = begin
187
+ Time.new(value)
188
+ rescue ArgumentError
189
+ nil
190
+ end
191
+ end
164
192
  end
165
193
  end
166
194
  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
@@ -32,7 +32,7 @@ module Bundler
32
32
 
33
33
  def initialize(base = nil, name = nil)
34
34
  @base = File.expand_path(base || SharedHelpers.pwd)
35
- gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : Gem::Util.glob_files_in_dir("{,*}.gemspec", @base)
35
+ gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : SharedHelpers.glob_files_in_dir("{,*}.gemspec", @base)
36
36
  raise "Unable to determine name from existing gemspec. Use :name => 'gemname' in #install_tasks to manually set it." unless gemspecs.size == 1
37
37
  @spec_path = gemspecs.first
38
38
  @gemspec = Bundler.load_gemspec(@spec_path)
@@ -124,7 +124,7 @@ module Bundler
124
124
  end
125
125
 
126
126
  def built_gem_path
127
- Gem::Util.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last
127
+ SharedHelpers.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last
128
128
  end
129
129
 
130
130
  def git_push(remote = nil)
@@ -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