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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +147 -0
- data/bundler.gemspec +2 -2
- data/lib/bundler/build_metadata.rb +1 -1
- data/lib/bundler/cli/add.rb +3 -0
- data/lib/bundler/cli/check.rb +1 -3
- data/lib/bundler/cli/common.rb +6 -0
- data/lib/bundler/cli/config.rb +8 -3
- data/lib/bundler/cli/gem.rb +2 -3
- data/lib/bundler/cli/install.rb +3 -0
- data/lib/bundler/cli/issue.rb +2 -2
- data/lib/bundler/cli/lock.rb +3 -0
- data/lib/bundler/cli/open.rb +12 -2
- data/lib/bundler/cli/outdated.rb +42 -2
- data/lib/bundler/cli/update.rb +2 -0
- data/lib/bundler/cli.rb +18 -12
- data/lib/bundler/compact_index_client/cache.rb +11 -0
- data/lib/bundler/compact_index_client/cache_file.rb +1 -1
- data/lib/bundler/compact_index_client/parser.rb +4 -1
- data/lib/bundler/definition.rb +51 -12
- data/lib/bundler/dsl.rb +10 -3
- data/lib/bundler/endpoint_specification.rb +17 -2
- data/lib/bundler/fetcher/base.rb +3 -0
- data/lib/bundler/fetcher/compact_index.rb +13 -0
- data/lib/bundler/fetcher/gem_remote_fetcher.rb +2 -0
- data/lib/bundler/fetcher.rb +12 -2
- data/lib/bundler/injector.rb +1 -2
- data/lib/bundler/installer/parallel_installer.rb +68 -23
- data/lib/bundler/installer.rb +12 -1
- data/lib/bundler/lockfile_generator.rb +39 -1
- data/lib/bundler/lockfile_parser.rb +23 -1
- data/lib/bundler/man/bundle-add.1 +6 -3
- data/lib/bundler/man/bundle-add.1.ronn +7 -2
- data/lib/bundler/man/bundle-binstubs.1 +1 -1
- data/lib/bundler/man/bundle-cache.1 +4 -1
- data/lib/bundler/man/bundle-cache.1.ronn +6 -0
- data/lib/bundler/man/bundle-check.1 +1 -1
- data/lib/bundler/man/bundle-clean.1 +1 -1
- data/lib/bundler/man/bundle-config.1 +121 -157
- data/lib/bundler/man/bundle-config.1.ronn +33 -1
- data/lib/bundler/man/bundle-console.1 +3 -3
- data/lib/bundler/man/bundle-console.1.ronn +2 -2
- data/lib/bundler/man/bundle-doctor.1 +1 -1
- data/lib/bundler/man/bundle-env.1 +1 -1
- data/lib/bundler/man/bundle-exec.1 +1 -1
- data/lib/bundler/man/bundle-fund.1 +1 -1
- data/lib/bundler/man/bundle-gem.1 +1 -1
- data/lib/bundler/man/bundle-help.1 +1 -1
- data/lib/bundler/man/bundle-info.1 +1 -1
- data/lib/bundler/man/bundle-init.1 +2 -2
- data/lib/bundler/man/bundle-init.1.ronn +1 -1
- data/lib/bundler/man/bundle-install.1 +5 -2
- data/lib/bundler/man/bundle-install.1.ronn +9 -1
- data/lib/bundler/man/bundle-issue.1 +1 -1
- data/lib/bundler/man/bundle-licenses.1 +1 -1
- data/lib/bundler/man/bundle-list.1 +1 -1
- data/lib/bundler/man/bundle-lock.1 +5 -2
- data/lib/bundler/man/bundle-lock.1.ronn +6 -0
- data/lib/bundler/man/bundle-open.1 +1 -1
- data/lib/bundler/man/bundle-outdated.1 +17 -14
- data/lib/bundler/man/bundle-outdated.1.ronn +19 -12
- data/lib/bundler/man/bundle-platform.1 +1 -1
- data/lib/bundler/man/bundle-plugin.1 +2 -2
- data/lib/bundler/man/bundle-plugin.1.ronn +1 -1
- data/lib/bundler/man/bundle-pristine.1 +1 -1
- data/lib/bundler/man/bundle-remove.1 +1 -1
- data/lib/bundler/man/bundle-show.1 +1 -1
- data/lib/bundler/man/bundle-update.1 +5 -2
- data/lib/bundler/man/bundle-update.1.ronn +8 -0
- data/lib/bundler/man/bundle-version.1 +1 -1
- data/lib/bundler/man/bundle.1 +23 -2
- data/lib/bundler/man/bundle.1.ronn +22 -1
- data/lib/bundler/man/gemfile.5 +13 -2
- data/lib/bundler/man/gemfile.5.ronn +20 -1
- data/lib/bundler/mirror.rb +7 -7
- data/lib/bundler/plugin/unloaded_source.rb +25 -0
- data/lib/bundler/plugin.rb +12 -6
- data/lib/bundler/remote_specification.rb +1 -1
- data/lib/bundler/resolver/base.rb +9 -0
- data/lib/bundler/resolver/strategy.rb +6 -3
- data/lib/bundler/resolver.rb +80 -1
- data/lib/bundler/rubygems_ext.rb +22 -0
- data/lib/bundler/rubygems_gem_installer.rb +35 -2
- data/lib/bundler/runtime.rb +11 -11
- data/lib/bundler/self_manager.rb +6 -1
- data/lib/bundler/settings/validator.rb +7 -0
- data/lib/bundler/settings.rb +2 -0
- data/lib/bundler/shared_helpers.rb +6 -1
- data/lib/bundler/source/git/git_proxy.rb +13 -2
- data/lib/bundler/source/git.rb +1 -1
- data/lib/bundler/source/metadata.rb +4 -0
- data/lib/bundler/source/path.rb +3 -2
- data/lib/bundler/source/rubygems/remote.rb +12 -2
- data/lib/bundler/source/rubygems.rb +81 -13
- data/lib/bundler/source_list.rb +44 -3
- data/lib/bundler/spec_set.rb +2 -2
- data/lib/bundler/stub_specification.rb +1 -0
- data/lib/bundler/templates/newgem/newgem.gemspec.tt +10 -1
- data/lib/bundler/vendored_tsort.rb +19 -2
- data/lib/bundler/version.rb +1 -1
- data/lib/bundler.rb +11 -1
- metadata +5 -6
- data/lib/bundler/vendor/tsort/LICENSE.txt +0 -22
- data/lib/bundler/vendor/tsort/lib/tsort.rb +0 -455
data/lib/bundler/resolver.rb
CHANGED
|
@@ -184,6 +184,9 @@ module Bundler
|
|
|
184
184
|
|
|
185
185
|
platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : ""
|
|
186
186
|
custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}"
|
|
187
|
+
if hint = cooldown_hint(specs_matching_other_platforms)
|
|
188
|
+
custom_explanation += " (#{hint})"
|
|
189
|
+
end
|
|
187
190
|
|
|
188
191
|
label = "#{name} (#{constraint_string})"
|
|
189
192
|
extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any?
|
|
@@ -353,9 +356,31 @@ module Bundler
|
|
|
353
356
|
message << "\n#{other_specs_matching_message(specs, matching_part)}"
|
|
354
357
|
end
|
|
355
358
|
|
|
359
|
+
if hint = cooldown_hint(specs_matching_requirement)
|
|
360
|
+
message << "\n\n#{hint}."
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
if specs_matching_requirement.any? && (hint = platform_mismatch_hint)
|
|
364
|
+
message << "\n\n#{hint}"
|
|
365
|
+
end
|
|
366
|
+
|
|
356
367
|
raise GemNotFound, message
|
|
357
368
|
end
|
|
358
369
|
|
|
370
|
+
def platform_mismatch_hint
|
|
371
|
+
locked_platforms = Bundler.locked_gems&.platforms
|
|
372
|
+
return unless locked_platforms
|
|
373
|
+
|
|
374
|
+
local_platform = Bundler.local_platform
|
|
375
|
+
return if locked_platforms.include?(local_platform)
|
|
376
|
+
return if locked_platforms.any? {|p| p == Gem::Platform::RUBY }
|
|
377
|
+
|
|
378
|
+
"Your current platform (#{local_platform}) is not included in the lockfile's platforms (#{locked_platforms.join(", ")}). " \
|
|
379
|
+
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
|
|
380
|
+
rescue GemfileNotFound
|
|
381
|
+
nil
|
|
382
|
+
end
|
|
383
|
+
|
|
359
384
|
def filtered_versions_for(package)
|
|
360
385
|
@gem_version_promoter.filter_versions(package, @all_versions[package])
|
|
361
386
|
end
|
|
@@ -378,7 +403,7 @@ module Bundler
|
|
|
378
403
|
end
|
|
379
404
|
|
|
380
405
|
def filter_specs(specs, package)
|
|
381
|
-
filter_remote_specs(filter_prereleases(specs, package), package)
|
|
406
|
+
filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package)
|
|
382
407
|
end
|
|
383
408
|
|
|
384
409
|
def filter_prereleases(specs, package)
|
|
@@ -387,6 +412,60 @@ module Bundler
|
|
|
387
412
|
specs.reject {|s| s.version.prerelease? }
|
|
388
413
|
end
|
|
389
414
|
|
|
415
|
+
def filter_cooldown(specs)
|
|
416
|
+
return specs if specs.empty?
|
|
417
|
+
excluded_versions = cooldown_excluded_versions(specs)
|
|
418
|
+
return specs if excluded_versions.empty?
|
|
419
|
+
specs.reject {|s| excluded_versions.include?([s.name, s.version]) }
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def cooldown_excluded_versions(specs)
|
|
423
|
+
excluded = {}
|
|
424
|
+
specs.each do |spec|
|
|
425
|
+
next unless cooldown_excluded?(spec)
|
|
426
|
+
excluded[[spec.name, spec.version]] = true
|
|
427
|
+
end
|
|
428
|
+
excluded
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def cooldown_hint(specs)
|
|
432
|
+
excluded_versions = cooldown_excluded_versions(specs)
|
|
433
|
+
return nil if excluded_versions.empty?
|
|
434
|
+
"#{excluded_versions.size} version#{"s" if excluded_versions.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass"
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def cooldown_excluded?(spec)
|
|
438
|
+
return false unless spec.respond_to?(:created_at) && spec.created_at
|
|
439
|
+
return false unless spec.respond_to?(:remote) && spec.remote
|
|
440
|
+
return false if locked_by_lockfile?(spec)
|
|
441
|
+
days = spec.remote.effective_cooldown
|
|
442
|
+
return false if days.nil? || days <= 0
|
|
443
|
+
(cooldown_now - spec.created_at) < (days * 86_400)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# A version already written to the lockfile has been adopted, and cooldown
|
|
447
|
+
# only governs the adoption of *new* versions, so it must never retract one
|
|
448
|
+
# the lockfile already pins. Keying this off the locked specs rather than the
|
|
449
|
+
# prevent-downgrade floor matters because that floor is absent on resolutions
|
|
450
|
+
# that re-pick a gem from scratch: the auxiliary full update run to compute
|
|
451
|
+
# `--update` targets, and the from-scratch retries after a conflict unlocks a
|
|
452
|
+
# gem. In those passes the locked version is the only candidate, so filtering
|
|
453
|
+
# it out makes an unrelated operation impossible whenever every published
|
|
454
|
+
# version matching the requirement sits inside the cooldown window.
|
|
455
|
+
#
|
|
456
|
+
# Gems named on a `bundle update GEM` command are the exception: the user
|
|
457
|
+
# asked to move them, so they stay subject to cooldown and a locked-but-fresh
|
|
458
|
+
# release is pushed back to an older one (or fails loudly when none exists).
|
|
459
|
+
def locked_by_lockfile?(spec)
|
|
460
|
+
return false unless defined?(@base) && @base
|
|
461
|
+
return false if @base.explicitly_unlocked?(spec.name)
|
|
462
|
+
@base.locked_specs[spec.name].any? {|locked| locked.version == spec.version }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def cooldown_now
|
|
466
|
+
@cooldown_now ||= Time.now
|
|
467
|
+
end
|
|
468
|
+
|
|
390
469
|
def filter_remote_specs(specs, package)
|
|
391
470
|
if package.prefer_local?
|
|
392
471
|
local_specs = specs.select {|s| s.is_a?(StubSpecification) }
|
data/lib/bundler/rubygems_ext.rb
CHANGED
|
@@ -465,6 +465,28 @@ module Gem
|
|
|
465
465
|
Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse)
|
|
466
466
|
end
|
|
467
467
|
|
|
468
|
+
# RubyGems before 4.0.13 split compact index dependency/requirement entries
|
|
469
|
+
# on every colon, which mangles metadata values that contain colons such as
|
|
470
|
+
# the `created_at` timestamps the cooldown feature relies on. Split only on
|
|
471
|
+
# the first colon so those values survive on older RubyGems.
|
|
472
|
+
#
|
|
473
|
+
# The module is defined unconditionally so it stays testable on any RubyGems,
|
|
474
|
+
# but only prepended when the host RubyGems still has the buggy behavior.
|
|
475
|
+
module SplitCompactIndexEntryOnFirstColon
|
|
476
|
+
private
|
|
477
|
+
|
|
478
|
+
def parse_dependency(string)
|
|
479
|
+
dependency = string.split(":", 2)
|
|
480
|
+
dependency[-1] = dependency[-1].split("&") if dependency.size > 1
|
|
481
|
+
dependency[0] = -dependency[0]
|
|
482
|
+
dependency
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
unless Gem.rubygems_version >= Gem::Version.new("4.0.13")
|
|
487
|
+
Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon)
|
|
488
|
+
end
|
|
489
|
+
|
|
468
490
|
if Gem.rubygems_version < Gem::Version.new("3.6.0")
|
|
469
491
|
class Package; end
|
|
470
492
|
require "rubygems/package/tar_reader"
|
|
@@ -4,6 +4,11 @@ require "rubygems/installer"
|
|
|
4
4
|
|
|
5
5
|
module Bundler
|
|
6
6
|
class RubyGemsGemInstaller < Gem::Installer
|
|
7
|
+
# Cap how many jobserver slots a single gem's `make` may grab so that one
|
|
8
|
+
# gem with many recipes doesn't starve the others sharing the pool. Beyond
|
|
9
|
+
# a handful of jobs the extra parallelism rarely pays off in practice.
|
|
10
|
+
MAX_JOBS_PER_GEM = 3
|
|
11
|
+
|
|
7
12
|
def check_executable_overwrite(filename)
|
|
8
13
|
# Bundler needs to install gems regardless of binstub overwriting
|
|
9
14
|
end
|
|
@@ -20,7 +25,7 @@ module Bundler
|
|
|
20
25
|
strict_rm_rf spec.extension_dir
|
|
21
26
|
|
|
22
27
|
SharedHelpers.filesystem_access(gem_dir, :create) do
|
|
23
|
-
FileUtils.mkdir_p gem_dir
|
|
28
|
+
FileUtils.mkdir_p gem_dir
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
SharedHelpers.filesystem_access(gem_dir, :write) do
|
|
@@ -101,10 +106,18 @@ module Bundler
|
|
|
101
106
|
end
|
|
102
107
|
|
|
103
108
|
def build_jobs
|
|
104
|
-
|
|
109
|
+
@jobserver_read_io&.read_nonblock(MAX_JOBS_PER_GEM, @jobserver_tokens)
|
|
110
|
+
acquired_jobs = @jobserver_tokens.empty? ? nil : @jobserver_tokens.size
|
|
111
|
+
|
|
112
|
+
acquired_jobs || Bundler.settings[:jobs] || super
|
|
113
|
+
rescue IO::WaitReadable, EOFError
|
|
114
|
+
1
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
def build_extensions
|
|
118
|
+
@jobserver_tokens = +""
|
|
119
|
+
@jobserver_read_io, @jobserver_write_io = connect_to_jobserver
|
|
120
|
+
|
|
108
121
|
extension_cache_path = options[:bundler_extension_cache_path]
|
|
109
122
|
extension_dir = spec.extension_dir
|
|
110
123
|
unless extension_cache_path && extension_dir
|
|
@@ -128,6 +141,11 @@ module Bundler
|
|
|
128
141
|
FileUtils.cp_r extension_dir, extension_cache_path
|
|
129
142
|
end
|
|
130
143
|
end
|
|
144
|
+
ensure
|
|
145
|
+
unless @jobserver_tokens.empty?
|
|
146
|
+
@jobserver_write_io.write(@jobserver_tokens)
|
|
147
|
+
@jobserver_write_io.flush
|
|
148
|
+
end
|
|
131
149
|
end
|
|
132
150
|
|
|
133
151
|
def spec
|
|
@@ -144,6 +162,21 @@ module Bundler
|
|
|
144
162
|
|
|
145
163
|
private
|
|
146
164
|
|
|
165
|
+
def connect_to_jobserver
|
|
166
|
+
return unless ENV["MAKEFLAGS"]
|
|
167
|
+
# We append our own --jobserver-auth, so read the last one. Otherwise a
|
|
168
|
+
# parent jobserver's descriptors (e.g. `bundle install` run under
|
|
169
|
+
# `make -j`) would be picked up instead of the pool ParallelInstaller created.
|
|
170
|
+
read_fd, write_fd = ENV["MAKEFLAGS"].scan(/--jobserver-auth=(\d+),(\d+)/).last
|
|
171
|
+
|
|
172
|
+
return unless read_fd && write_fd
|
|
173
|
+
|
|
174
|
+
# Pass explicit modes. On POSIX, IO.new detects the descriptor's access
|
|
175
|
+
# mode, but on Windows it can't, so the write end would default to read
|
|
176
|
+
# mode and raise "IOError: not opened for writing" when releasing slots.
|
|
177
|
+
[IO.new(read_fd.to_i, "r", autoclose: false), IO.new(write_fd.to_i, "w", autoclose: false)]
|
|
178
|
+
end
|
|
179
|
+
|
|
147
180
|
def prepare_extension_build(extension_dir)
|
|
148
181
|
SharedHelpers.filesystem_access(extension_dir, :create) do
|
|
149
182
|
FileUtils.mkdir_p extension_dir
|
data/lib/bundler/runtime.rb
CHANGED
|
@@ -141,7 +141,7 @@ module Bundler
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
Gem::Util.glob_files_in_dir("*/.git", cache_path.to_s).each do |git_dir|
|
|
145
145
|
FileUtils.rm_rf(git_dir)
|
|
146
146
|
FileUtils.touch(File.expand_path("../.bundlecache", git_dir))
|
|
147
147
|
end
|
|
@@ -159,13 +159,13 @@ module Bundler
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def clean(dry_run = false)
|
|
162
|
-
gem_bins =
|
|
163
|
-
git_dirs =
|
|
164
|
-
git_cache_dirs =
|
|
165
|
-
gem_dirs =
|
|
166
|
-
gem_files =
|
|
167
|
-
gemspec_files =
|
|
168
|
-
extension_dirs =
|
|
162
|
+
gem_bins = Gem::Util.glob_files_in_dir("bin/*", Gem.dir)
|
|
163
|
+
git_dirs = Gem::Util.glob_files_in_dir("bundler/gems/*", Gem.dir)
|
|
164
|
+
git_cache_dirs = Gem::Util.glob_files_in_dir("cache/bundler/git/*", Gem.dir)
|
|
165
|
+
gem_dirs = Gem::Util.glob_files_in_dir("gems/*", Gem.dir)
|
|
166
|
+
gem_files = Gem::Util.glob_files_in_dir("cache/*.gem", Gem.dir)
|
|
167
|
+
gemspec_files = Gem::Util.glob_files_in_dir("specifications/*.gemspec", Gem.dir)
|
|
168
|
+
extension_dirs = Gem::Util.glob_files_in_dir("extensions/*/*/*", Gem.dir) + Gem::Util.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir)
|
|
169
169
|
spec_gem_paths = []
|
|
170
170
|
# need to keep git sources around
|
|
171
171
|
spec_git_paths = @definition.spec_git_paths
|
|
@@ -232,7 +232,7 @@ module Bundler
|
|
|
232
232
|
private
|
|
233
233
|
|
|
234
234
|
def prune_gem_cache(resolve, cache_path)
|
|
235
|
-
cached =
|
|
235
|
+
cached = Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s)
|
|
236
236
|
|
|
237
237
|
cached = cached.delete_if do |path|
|
|
238
238
|
spec = Bundler.rubygems.spec_from_gem path
|
|
@@ -257,7 +257,7 @@ module Bundler
|
|
|
257
257
|
end
|
|
258
258
|
|
|
259
259
|
def prune_git_and_path_cache(resolve, cache_path)
|
|
260
|
-
cached =
|
|
260
|
+
cached = Gem::Util.glob_files_in_dir("*/.bundlecache", cache_path.to_s)
|
|
261
261
|
|
|
262
262
|
cached = cached.delete_if do |path|
|
|
263
263
|
name = File.basename(File.dirname(path))
|
|
@@ -283,7 +283,7 @@ module Bundler
|
|
|
283
283
|
# Add man/ subdirectories from activated bundles to MANPATH for man(1)
|
|
284
284
|
manuals = $LOAD_PATH.filter_map do |path|
|
|
285
285
|
man_subdir = path.sub(/lib$/, "man")
|
|
286
|
-
man_subdir unless Dir
|
|
286
|
+
man_subdir unless Dir.glob("man?/", base: man_subdir).empty?
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
return if manuals.empty?
|
data/lib/bundler/self_manager.rb
CHANGED
|
@@ -75,7 +75,12 @@ module Bundler
|
|
|
75
75
|
|
|
76
76
|
argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0
|
|
77
77
|
cmd = [argv0, *ARGV]
|
|
78
|
-
|
|
78
|
+
unless File.executable?(argv0)
|
|
79
|
+
# Gem.ruby is quoted if it contains whitespace, so split it into argv
|
|
80
|
+
# elements to keep the quotes out of the exec'd command.
|
|
81
|
+
require "shellwords"
|
|
82
|
+
cmd.unshift(*Shellwords.split(Gem.ruby))
|
|
83
|
+
end
|
|
79
84
|
|
|
80
85
|
Bundler.with_original_env do
|
|
81
86
|
Kernel.exec(
|
|
@@ -74,6 +74,13 @@ module Bundler
|
|
|
74
74
|
fail!(key, value, "`#{other_key}` is current set to #{other_setting.inspect}", "the `#{conflicting.join("`, `")}` groups conflict")
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
rule %w[default_cli_command], "default_cli_command must be either 'install' or 'cli_help'" do |key, value, _settings|
|
|
79
|
+
valid_values = %w[install cli_help]
|
|
80
|
+
if !value.nil? && !valid_values.include?(value.to_s)
|
|
81
|
+
fail!(key, value, "must be one of: #{valid_values.join(", ")}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
77
84
|
end
|
|
78
85
|
end
|
|
79
86
|
end
|
data/lib/bundler/settings.rb
CHANGED
|
@@ -346,7 +346,12 @@ module Bundler
|
|
|
346
346
|
|
|
347
347
|
def set_rubyopt
|
|
348
348
|
rubyopt = [ENV["RUBYOPT"]].compact
|
|
349
|
-
|
|
349
|
+
setup_path = File.expand_path("setup", __dir__)
|
|
350
|
+
# RUBYOPT is split on whitespace with no quoting mechanism, so an
|
|
351
|
+
# absolute path containing spaces would be torn apart. Fall back to
|
|
352
|
+
# requiring by feature name; set_rubylib puts our lib directory first
|
|
353
|
+
# on the child's load path.
|
|
354
|
+
setup_require = /\s/.match?(setup_path) ? "-rbundler/setup" : "-r#{setup_path}"
|
|
350
355
|
return if !rubyopt.empty? && rubyopt.first.include?(setup_require)
|
|
351
356
|
rubyopt.unshift setup_require
|
|
352
357
|
Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ")
|
|
@@ -144,6 +144,12 @@ module Bundler
|
|
|
144
144
|
FileUtils.rm_rf(p)
|
|
145
145
|
end
|
|
146
146
|
git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
|
|
147
|
+
# The copy is cloned from the local bare cache, which holds no Git LFS
|
|
148
|
+
# objects, so point origin back at the real remote and let git-lfs derive
|
|
149
|
+
# its endpoint from there when checking out. Use the credential-filtered
|
|
150
|
+
# URI to avoid persisting secrets in the copy's .git/config; auth is left
|
|
151
|
+
# to git's credential helper.
|
|
152
|
+
git "remote", "set-url", "origin", credential_filtered_uri, dir: destination
|
|
147
153
|
File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination)
|
|
148
154
|
rescue Errno::EEXIST => e
|
|
149
155
|
file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1]
|
|
@@ -432,9 +438,14 @@ module Bundler
|
|
|
432
438
|
end
|
|
433
439
|
|
|
434
440
|
def capture3_args_for(cmd, dir)
|
|
435
|
-
|
|
441
|
+
# Disable automatic maintenance so a background commit-graph write in
|
|
442
|
+
# the source repo can't race the hardlinking local clone and fail with
|
|
443
|
+
# "hardlink different from source".
|
|
444
|
+
opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
|
|
436
445
|
|
|
437
|
-
["git",
|
|
446
|
+
return ["git", *opts, *cmd] unless dir
|
|
447
|
+
|
|
448
|
+
["git", "-C", dir.to_s, *opts, *cmd]
|
|
438
449
|
end
|
|
439
450
|
|
|
440
451
|
def extra_clone_args
|
data/lib/bundler/source/git.rb
CHANGED
|
@@ -320,7 +320,7 @@ module Bundler
|
|
|
320
320
|
|
|
321
321
|
def serialize_gemspecs_in(destination)
|
|
322
322
|
destination = destination.expand_path(Bundler.root) if destination.relative?
|
|
323
|
-
|
|
323
|
+
Gem::Util.glob_files_in_dir(@glob, destination.to_s).each do |spec_path|
|
|
324
324
|
# Evaluate gemspecs and cache the result. Gemspecs
|
|
325
325
|
# in git might require git or other dependencies.
|
|
326
326
|
# The gemspecs we cache should already be evaluated.
|
data/lib/bundler/source/path.rb
CHANGED
|
@@ -220,10 +220,11 @@ module Bundler
|
|
|
220
220
|
# Some gem authors put absolute paths in their gemspec
|
|
221
221
|
# and we have to save them from themselves
|
|
222
222
|
spec.files = spec.files.filter_map do |path|
|
|
223
|
-
|
|
223
|
+
pathname = Pathname.new(path)
|
|
224
|
+
next path unless pathname.absolute?
|
|
224
225
|
next if File.directory?(path)
|
|
225
226
|
begin
|
|
226
|
-
|
|
227
|
+
pathname.relative_path_from(gem_dir).to_s
|
|
227
228
|
rescue ArgumentError
|
|
228
229
|
path
|
|
229
230
|
end
|
|
@@ -4,9 +4,9 @@ module Bundler
|
|
|
4
4
|
class Source
|
|
5
5
|
class Rubygems
|
|
6
6
|
class Remote
|
|
7
|
-
attr_reader :uri, :anonymized_uri, :original_uri
|
|
7
|
+
attr_reader :uri, :anonymized_uri, :original_uri, :cooldown
|
|
8
8
|
|
|
9
|
-
def initialize(uri)
|
|
9
|
+
def initialize(uri, cooldown: nil)
|
|
10
10
|
orig_uri = uri
|
|
11
11
|
uri = Bundler.settings.mirror_for(uri)
|
|
12
12
|
@original_uri = orig_uri if orig_uri != uri
|
|
@@ -14,6 +14,16 @@ module Bundler
|
|
|
14
14
|
|
|
15
15
|
@uri = apply_auth(uri, fallback_auth).freeze
|
|
16
16
|
@anonymized_uri = remove_auth(@uri).freeze
|
|
17
|
+
@cooldown = cooldown
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the cooldown days that apply to this remote, resolving the
|
|
21
|
+
# precedence CLI > config > Gemfile per-source. Returns nil if no
|
|
22
|
+
# cooldown applies.
|
|
23
|
+
def effective_cooldown
|
|
24
|
+
override = Bundler.settings[:cooldown]
|
|
25
|
+
return override if override
|
|
26
|
+
@cooldown
|
|
17
27
|
end
|
|
18
28
|
|
|
19
29
|
MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length
|
|
@@ -11,11 +11,12 @@ module Bundler
|
|
|
11
11
|
API_REQUEST_SIZE = 100
|
|
12
12
|
REQUIRE_MUTEX = Mutex.new
|
|
13
13
|
|
|
14
|
-
attr_accessor :remotes
|
|
14
|
+
attr_accessor :remotes, :remote_cooldowns
|
|
15
15
|
|
|
16
16
|
def initialize(options = {})
|
|
17
17
|
@options = options
|
|
18
18
|
@remotes = []
|
|
19
|
+
@remote_cooldowns = {}
|
|
19
20
|
@dependency_names = []
|
|
20
21
|
@allow_remote = false
|
|
21
22
|
@allow_cached = false
|
|
@@ -24,8 +25,10 @@ module Bundler
|
|
|
24
25
|
@checksum_store = Checksum::Store.new
|
|
25
26
|
@gem_installers = {}
|
|
26
27
|
@gem_installers_mutex = Mutex.new
|
|
28
|
+
@remote_specs_mutex = Mutex.new
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
cooldown = options["cooldown"]
|
|
31
|
+
Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
|
|
29
32
|
|
|
30
33
|
@lockfile_remotes = @remotes if options["from_lockfile"]
|
|
31
34
|
end
|
|
@@ -148,6 +151,13 @@ module Bundler
|
|
|
148
151
|
# sources, and large_idx.merge! small_idx is way faster than
|
|
149
152
|
# small_idx.merge! large_idx.
|
|
150
153
|
index = @allow_remote ? remote_specs.dup : Index.new
|
|
154
|
+
|
|
155
|
+
# Snapshot per-version `created_at` from the remote info before installed
|
|
156
|
+
# / cached specs overwrite the EndpointSpecification objects that carry
|
|
157
|
+
# it. The cooldown filter consults `created_at` on every candidate, so
|
|
158
|
+
# local stubs need the published date back-filled to participate.
|
|
159
|
+
remote_created_at = collect_remote_created_at(index)
|
|
160
|
+
|
|
151
161
|
index.merge!(cached_specs) if @allow_cached
|
|
152
162
|
index.merge!(installed_specs) if @allow_local
|
|
153
163
|
|
|
@@ -161,6 +171,8 @@ module Bundler
|
|
|
161
171
|
end
|
|
162
172
|
end
|
|
163
173
|
|
|
174
|
+
backfill_created_at(index, remote_created_at) unless remote_created_at.empty?
|
|
175
|
+
|
|
164
176
|
index
|
|
165
177
|
end
|
|
166
178
|
end
|
|
@@ -232,7 +244,7 @@ module Bundler
|
|
|
232
244
|
def cached_built_in_gem(spec, local: false)
|
|
233
245
|
cached_path = cached_gem(spec)
|
|
234
246
|
if cached_path.nil? && !local
|
|
235
|
-
remote_spec =
|
|
247
|
+
remote_spec = remote_spec_for(spec)
|
|
236
248
|
if remote_spec
|
|
237
249
|
cached_path = fetch_gem(remote_spec)
|
|
238
250
|
spec.remote = remote_spec.remote
|
|
@@ -243,9 +255,14 @@ module Bundler
|
|
|
243
255
|
cached_path
|
|
244
256
|
end
|
|
245
257
|
|
|
246
|
-
def add_remote(source)
|
|
258
|
+
def add_remote(source, cooldown: nil)
|
|
247
259
|
uri = normalize_uri(source)
|
|
248
260
|
@remotes.unshift(uri) unless @remotes.include?(uri)
|
|
261
|
+
@remote_cooldowns[uri] = cooldown if cooldown
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def cooldown_for(uri)
|
|
265
|
+
@remote_cooldowns[uri]
|
|
249
266
|
end
|
|
250
267
|
|
|
251
268
|
def spec_names
|
|
@@ -266,7 +283,7 @@ module Bundler
|
|
|
266
283
|
|
|
267
284
|
def remote_fetchers
|
|
268
285
|
@remote_fetchers ||= remotes.to_h do |uri|
|
|
269
|
-
remote = Source::Rubygems::Remote.new(uri)
|
|
286
|
+
remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri))
|
|
270
287
|
[remote, Bundler::Fetcher.new(remote)]
|
|
271
288
|
end.freeze
|
|
272
289
|
end
|
|
@@ -314,6 +331,19 @@ module Bundler
|
|
|
314
331
|
@allow_remote && api_fetchers.any?
|
|
315
332
|
end
|
|
316
333
|
|
|
334
|
+
def clear_cache
|
|
335
|
+
@specs = nil
|
|
336
|
+
@installed_specs = nil
|
|
337
|
+
@default_specs = nil
|
|
338
|
+
@cached_specs = nil
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def release_resolution_memory!
|
|
342
|
+
@specs = nil
|
|
343
|
+
@remote_specs_mutex.synchronize { @remote_specs = nil }
|
|
344
|
+
@fetchers&.each(&:release_resolution_memory!)
|
|
345
|
+
end
|
|
346
|
+
|
|
317
347
|
protected
|
|
318
348
|
|
|
319
349
|
def remote_names
|
|
@@ -376,7 +406,7 @@ module Bundler
|
|
|
376
406
|
@cached_specs ||= begin
|
|
377
407
|
idx = Index.new
|
|
378
408
|
|
|
379
|
-
|
|
409
|
+
Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s).each do |gemfile|
|
|
380
410
|
s ||= Bundler.rubygems.spec_from_gem(gemfile)
|
|
381
411
|
s.source = self
|
|
382
412
|
idx << s
|
|
@@ -391,17 +421,30 @@ module Bundler
|
|
|
391
421
|
end
|
|
392
422
|
|
|
393
423
|
def remote_specs
|
|
394
|
-
@remote_specs ||=
|
|
395
|
-
|
|
424
|
+
@remote_specs ||= @remote_specs_mutex.synchronize do
|
|
425
|
+
@remote_specs ||= Index.build do |idx|
|
|
426
|
+
index_fetchers = fetchers - api_fetchers
|
|
396
427
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
428
|
+
if index_fetchers.empty?
|
|
429
|
+
fetch_names(api_fetchers, dependency_names, idx)
|
|
430
|
+
else
|
|
431
|
+
fetch_names(fetchers, nil, idx)
|
|
432
|
+
end
|
|
401
433
|
end
|
|
402
434
|
end
|
|
403
435
|
end
|
|
404
436
|
|
|
437
|
+
# Looks up a single spec in the remote sources, fetching only its own
|
|
438
|
+
# name when the full remote index is not already materialized.
|
|
439
|
+
def remote_spec_for(spec)
|
|
440
|
+
return remote_specs.search(spec).first if @remote_specs || api_fetchers.empty?
|
|
441
|
+
|
|
442
|
+
index = Index.build do |idx|
|
|
443
|
+
fetch_names(api_fetchers, [spec.name], idx)
|
|
444
|
+
end
|
|
445
|
+
index.search(spec).first
|
|
446
|
+
end
|
|
447
|
+
|
|
405
448
|
def fetch_names(fetchers, dependency_names, index)
|
|
406
449
|
fetchers.each do |f|
|
|
407
450
|
if dependency_names
|
|
@@ -456,6 +499,31 @@ module Bundler
|
|
|
456
499
|
|
|
457
500
|
private
|
|
458
501
|
|
|
502
|
+
def collect_remote_created_at(index)
|
|
503
|
+
return {} unless @allow_remote
|
|
504
|
+
|
|
505
|
+
snapshot = {}
|
|
506
|
+
index.each do |spec|
|
|
507
|
+
next unless spec.respond_to?(:created_at) && spec.created_at
|
|
508
|
+
# Remember the remote that supplied the date too: when a source has
|
|
509
|
+
# several remotes with different per-URI cooldown settings we must
|
|
510
|
+
# restore the same one during backfill so `effective_cooldown` agrees.
|
|
511
|
+
snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote]
|
|
512
|
+
end
|
|
513
|
+
snapshot
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def backfill_created_at(index, snapshot)
|
|
517
|
+
index.each do |spec|
|
|
518
|
+
next unless spec.respond_to?(:created_at=)
|
|
519
|
+
next if spec.created_at
|
|
520
|
+
remote_created_at, remote = snapshot[[spec.name, spec.version]]
|
|
521
|
+
next unless remote_created_at
|
|
522
|
+
spec.created_at = remote_created_at
|
|
523
|
+
spec.remote ||= remote if remote && spec.respond_to?(:remote=)
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
459
527
|
def lockfile_remotes
|
|
460
528
|
@lockfile_remotes || credless_remotes
|
|
461
529
|
end
|
|
@@ -508,7 +576,7 @@ module Bundler
|
|
|
508
576
|
|
|
509
577
|
# We are using a mutex to reaed and write from/to the hash.
|
|
510
578
|
# The reason this double synchronization was added is for performance
|
|
511
|
-
# and lock the mutex for the shortest possible amount of time. Otherwise,
|
|
579
|
+
# and to lock the mutex for the shortest possible amount of time. Otherwise,
|
|
512
580
|
# all threads are fighting over this mutex and when it gets acquired it gets locked
|
|
513
581
|
# until a threads finishes downloading a gem, leaving the other threads waiting
|
|
514
582
|
# doing nothing.
|