bundler 4.0.21 → 4.1.0.beta1

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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/{lib/bundler/vendor/connection_pool/LICENSE → MIT.txt} +8 -6
  3. data/bundler.gemspec +23 -6
  4. data/lib/bundler/build_metadata.rb +2 -2
  5. data/lib/bundler/checksum.rb +40 -25
  6. data/lib/bundler/ci_detector.rb +1 -1
  7. data/lib/bundler/cli/add.rb +3 -4
  8. data/lib/bundler/cli/cache.rb +4 -0
  9. data/lib/bundler/cli/clean.rb +5 -0
  10. data/lib/bundler/cli/common.rb +37 -1
  11. data/lib/bundler/cli/config.rb +4 -2
  12. data/lib/bundler/cli/gem.rb +0 -17
  13. data/lib/bundler/cli/install.rb +4 -5
  14. data/lib/bundler/cli/lock.rb +1 -2
  15. data/lib/bundler/cli/outdated.rb +8 -3
  16. data/lib/bundler/cli/pristine.rb +2 -1
  17. data/lib/bundler/cli/update.rb +21 -12
  18. data/lib/bundler/cli.rb +14 -11
  19. data/lib/bundler/definition.rb +61 -24
  20. data/lib/bundler/dependency.rb +4 -0
  21. data/lib/bundler/dsl.rb +77 -11
  22. data/lib/bundler/endpoint_specification.rb +51 -14
  23. data/lib/bundler/env.rb +7 -24
  24. data/lib/bundler/errors.rb +34 -5
  25. data/lib/bundler/fetcher/compact_index.rb +14 -5
  26. data/lib/bundler/fetcher/connection_pools.rb +150 -0
  27. data/lib/bundler/fetcher/downloader.rb +31 -16
  28. data/lib/bundler/fetcher.rb +8 -47
  29. data/lib/bundler/injector.rb +3 -3
  30. data/lib/bundler/inline.rb +44 -4
  31. data/lib/bundler/installer/parallel_installer.rb +25 -30
  32. data/lib/bundler/installer.rb +20 -6
  33. data/lib/bundler/lazy_specification.rb +44 -17
  34. data/lib/bundler/lockfile_generator.rb +24 -2
  35. data/lib/bundler/lockfile_parser.rb +43 -5
  36. data/lib/bundler/man/bundle-add.1 +7 -4
  37. data/lib/bundler/man/bundle-add.1.ronn +8 -4
  38. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  39. data/lib/bundler/man/bundle-cache.1 +2 -2
  40. data/lib/bundler/man/bundle-cache.1.ronn +2 -2
  41. data/lib/bundler/man/bundle-check.1 +1 -1
  42. data/lib/bundler/man/bundle-clean.1 +1 -1
  43. data/lib/bundler/man/bundle-config.1 +38 -6
  44. data/lib/bundler/man/bundle-config.1.ronn +143 -10
  45. data/lib/bundler/man/bundle-console.1 +1 -1
  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 +1 -1
  54. data/lib/bundler/man/bundle-install.1 +2 -2
  55. data/lib/bundler/man/bundle-install.1.ronn +4 -4
  56. data/lib/bundler/man/bundle-issue.1 +1 -1
  57. data/lib/bundler/man/bundle-licenses.1 +1 -1
  58. data/lib/bundler/man/bundle-list.1 +1 -1
  59. data/lib/bundler/man/bundle-lock.1 +3 -3
  60. data/lib/bundler/man/bundle-lock.1.ronn +5 -5
  61. data/lib/bundler/man/bundle-open.1 +1 -1
  62. data/lib/bundler/man/bundle-outdated.1 +1 -1
  63. data/lib/bundler/man/bundle-platform.1 +1 -1
  64. data/lib/bundler/man/bundle-plugin.1 +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 +3 -3
  69. data/lib/bundler/man/bundle-update.1.ronn +3 -5
  70. data/lib/bundler/man/bundle-version.1 +1 -1
  71. data/lib/bundler/man/bundle.1 +1 -1
  72. data/lib/bundler/man/gemfile.5 +45 -1
  73. data/lib/bundler/man/gemfile.5.ronn +53 -0
  74. data/lib/bundler/match_metadata.rb +20 -0
  75. data/lib/bundler/match_platform.rb +25 -2
  76. data/lib/bundler/match_remote_metadata.rb +21 -6
  77. data/lib/bundler/materialization.rb +2 -1
  78. data/lib/bundler/override.rb +69 -0
  79. data/lib/bundler/plugin/dsl.rb +3 -5
  80. data/lib/bundler/plugin/events.rb +55 -13
  81. data/lib/bundler/plugin/index.rb +87 -11
  82. data/lib/bundler/plugin/installer/path.rb +1 -1
  83. data/lib/bundler/plugin/installer.rb +13 -5
  84. data/lib/bundler/plugin.rb +58 -30
  85. data/lib/bundler/remote_specification.rb +6 -3
  86. data/lib/bundler/resolver/base.rb +2 -1
  87. data/lib/bundler/resolver/incompatibility.rb +1 -1
  88. data/lib/bundler/resolver.rb +73 -23
  89. data/lib/bundler/ruby_version.rb +0 -2
  90. data/lib/bundler/rubygems_ext.rb +71 -1
  91. data/lib/bundler/rubygems_gem_installer.rb +41 -2
  92. data/lib/bundler/rubygems_integration.rb +7 -2
  93. data/lib/bundler/runtime.rb +69 -2
  94. data/lib/bundler/self_manager.rb +9 -15
  95. data/lib/bundler/settings.rb +373 -30
  96. data/lib/bundler/shared_helpers.rb +2 -1
  97. data/lib/bundler/source/git/git_proxy.rb +2 -10
  98. data/lib/bundler/source/git.rb +15 -6
  99. data/lib/bundler/source/path/installer.rb +1 -1
  100. data/lib/bundler/source/rubygems/remote.rb +10 -5
  101. data/lib/bundler/source/rubygems.rb +48 -12
  102. data/lib/bundler/spec_set.rb +16 -4
  103. data/lib/bundler/stub_specification.rb +2 -1
  104. data/lib/bundler/templates/newgem/Gemfile.tt +4 -4
  105. data/lib/bundler/templates/newgem/newgem.gemspec.tt +3 -3
  106. data/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +1 -1
  107. data/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt +1 -1
  108. data/lib/bundler/ui/shell.rb +0 -4
  109. data/lib/bundler/ui/silent.rb +0 -4
  110. data/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +35 -2
  111. data/lib/bundler/vendor/thor/lib/thor/base.rb +2 -1
  112. data/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +14 -6
  113. data/lib/bundler/vendor/thor/lib/thor/shell/color.rb +2 -0
  114. data/lib/bundler/vendor/thor/lib/thor/version.rb +1 -1
  115. data/lib/bundler/vendor/thor/lib/thor.rb +34 -1
  116. data/lib/bundler/vendored_pub_grub.rb +5 -2
  117. data/lib/bundler/vendored_securerandom.rb +11 -8
  118. data/lib/bundler/vendored_uri.rb +14 -14
  119. data/lib/bundler/version.rb +1 -1
  120. data/lib/bundler/worker.rb +3 -2
  121. data/lib/bundler.rb +1 -1
  122. data/lib/rubygems/compact_index_client/cache.rb +122 -0
  123. data/lib/rubygems/compact_index_client/cache_file.rb +147 -0
  124. data/lib/rubygems/compact_index_client/http_fetcher.rb +88 -0
  125. data/lib/rubygems/compact_index_client/parser.rb +90 -0
  126. data/lib/rubygems/compact_index_client/updater.rb +113 -0
  127. data/lib/rubygems/compact_index_client.rb +111 -0
  128. data/lib/rubygems/cooldown_settings.rb +57 -0
  129. data/lib/rubygems/credential_store/native/linux.rb +97 -0
  130. data/lib/rubygems/credential_store/native/macos.rb +88 -0
  131. data/lib/rubygems/credential_store/native/windows.rb +130 -0
  132. data/lib/rubygems/credential_store.rb +332 -0
  133. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/assignment.rb +1 -1
  134. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +7 -7
  135. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/failure_writer.rb +1 -1
  136. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/incompatibility.rb +4 -4
  137. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/package.rb +1 -1
  138. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/partial_solution.rb +1 -1
  139. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/rubygems.rb +3 -3
  140. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/solve_failure.rb +1 -1
  141. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/static_package_source.rb +3 -3
  142. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/strategy.rb +2 -2
  143. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/term.rb +1 -1
  144. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version.rb +1 -1
  145. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_constraint.rb +3 -3
  146. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_range.rb +1 -1
  147. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_solver.rb +2 -2
  148. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_union.rb +1 -1
  149. data/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb +57 -0
  150. data/lib/{bundler → rubygems}/vendor/securerandom/lib/securerandom.rb +10 -6
  151. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/common.rb +119 -119
  152. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/file.rb +13 -13
  153. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ftp.rb +24 -24
  154. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/generic.rb +132 -132
  155. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/http.rb +18 -18
  156. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/https.rb +4 -4
  157. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldap.rb +14 -14
  158. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldaps.rb +4 -4
  159. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/mailto.rb +21 -21
  160. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc2396_parser.rb +39 -39
  161. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc3986_parser.rb +14 -14
  162. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/version.rb +1 -1
  163. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ws.rb +12 -12
  164. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/wss.rb +4 -4
  165. data/lib/{bundler → rubygems}/vendor/uri/lib/uri.rb +35 -31
  166. data/lib/rubygems/yaml_serializer.rb +880 -0
  167. metadata +77 -68
  168. data/CHANGELOG.md +0 -5784
  169. data/LICENSE.md +0 -22
  170. data/README.md +0 -58
  171. data/lib/bundler/compact_index_client/cache.rb +0 -107
  172. data/lib/bundler/compact_index_client/cache_file.rb +0 -148
  173. data/lib/bundler/compact_index_client/parser.rb +0 -87
  174. data/lib/bundler/compact_index_client/updater.rb +0 -105
  175. data/lib/bundler/compact_index_client.rb +0 -92
  176. data/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +0 -227
  177. data/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +0 -3
  178. data/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb +0 -56
  179. data/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +0 -230
  180. data/lib/bundler/vendor/net-http-persistent/README.rdoc +0 -82
  181. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb +0 -41
  182. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb +0 -65
  183. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb +0 -80
  184. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +0 -1153
  185. data/lib/bundler/vendor/pub_grub/lib/pub_grub.rb +0 -31
  186. data/lib/bundler/vendored_persistent.rb +0 -11
  187. data/lib/bundler/yaml_serializer.rb +0 -98
  188. /data/lib/{bundler → rubygems}/vendor/pub_grub/LICENSE.txt +0 -0
  189. /data/lib/{bundler → rubygems}/vendor/securerandom/COPYING +0 -0
  190. /data/lib/{bundler → rubygems}/vendor/uri/COPYING +0 -0
@@ -4,6 +4,8 @@ module Bundler
4
4
  class Runtime
5
5
  include SharedHelpers
6
6
 
7
+ PRUNE_CATEGORIES = [:cache, :git].freeze
8
+
7
9
  def initialize(root, definition)
8
10
  @root = root
9
11
  @definition = definition
@@ -146,7 +148,7 @@ module Bundler
146
148
  FileUtils.touch(File.expand_path("../.bundlecache", git_dir))
147
149
  end
148
150
 
149
- prune_cache(cache_path) unless Bundler.settings[:no_prune]
151
+ prune_cache(cache_path) unless Bundler.settings[:keep_outdated_cache]
150
152
  end
151
153
 
152
154
  def prune_cache(cache_path)
@@ -164,7 +166,9 @@ module Bundler
164
166
  git_cache_dirs = SharedHelpers.glob_files_in_dir("cache/bundler/git/*", Gem.dir)
165
167
  gem_dirs = SharedHelpers.glob_files_in_dir("gems/*", Gem.dir)
166
168
  gem_files = SharedHelpers.glob_files_in_dir("cache/*.gem", Gem.dir)
167
- gemspec_files = SharedHelpers.glob_files_in_dir("specifications/*.gemspec", Gem.dir)
169
+ gemspec_files = Gem::SpecificationRecord.dirs_from([Gem.dir]).flat_map do |dir|
170
+ SharedHelpers.glob_files_in_dir("*.gemspec", dir)
171
+ end
168
172
  extension_dirs = SharedHelpers.glob_files_in_dir("extensions/*/*/*", Gem.dir) + SharedHelpers.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir)
169
173
  spec_gem_paths = []
170
174
  # need to keep git sources around
@@ -229,8 +233,71 @@ module Bundler
229
233
  output
230
234
  end
231
235
 
236
+ # Removes the artifacts Bundler keeps for its own bookkeeping and can rebuild
237
+ # from the lockfile. Gem contents are never touched.
238
+ def prune(categories)
239
+ categories = expand_prune_categories(categories)
240
+ return if categories.empty?
241
+
242
+ # Without a bundle path the cache is shared with RubyGems, so it holds gem
243
+ # files Bundler never put there.
244
+ if Bundler.use_system_gems?
245
+ Bundler.ui.warn "The `prune` setting was ignored because this bundle installs into the system gem " \
246
+ "directory, which Bundler shares with RubyGems. Run " \
247
+ "`bundle config set --local path vendor/bundle` to prune.", wrap: true
248
+ return
249
+ end
250
+
251
+ # Git metadata first, because resolving a checkout's install path can need
252
+ # the mirror that pruning the cache removes.
253
+ prune_git_metadata if categories.include?(:git)
254
+ prune_download_cache if categories.include?(:cache)
255
+ end
256
+
232
257
  private
233
258
 
259
+ # Anything that is not a category name is read as a boolean with Bundler's
260
+ # usual vocabulary, so `BUNDLE_PRUNE=1` selects every category and keeps
261
+ # doing so as categories are added. That way a tool can set the flag without
262
+ # tracking this list.
263
+ def expand_prune_categories(categories)
264
+ Array(categories).flat_map do |category|
265
+ name = category.to_s
266
+ next name.to_sym if PRUNE_CATEGORIES.include?(name.to_sym)
267
+
268
+ Settings.to_bool(name) ? PRUNE_CATEGORIES : []
269
+ end.uniq
270
+ end
271
+
272
+ def prune_download_cache
273
+ cache_path = File.join(Bundler.bundle_path, "cache")
274
+ return unless File.exist?(cache_path)
275
+
276
+ Bundler.ui.info "Removing the download cache at #{cache_path}"
277
+ SharedHelpers.filesystem_access(cache_path) do |p|
278
+ FileUtils.rm_rf(p)
279
+ end
280
+ end
281
+
282
+ def prune_git_metadata
283
+ owned = "#{Bundler.install_path}#{File::SEPARATOR}"
284
+ git_dirs = @definition.sources.git_sources.reject(&:local?).filter_map do |source|
285
+ install_path = source.install_path.to_s
286
+ next unless install_path.start_with?(owned)
287
+
288
+ git_dir = File.join(install_path, ".git")
289
+ git_dir if File.exist?(git_dir)
290
+ end
291
+ return if git_dirs.empty?
292
+
293
+ Bundler.ui.info "Removing git metadata from checked out git gems"
294
+ git_dirs.each do |git_dir|
295
+ SharedHelpers.filesystem_access(git_dir) do |p|
296
+ FileUtils.rm_rf(p)
297
+ end
298
+ end
299
+ end
300
+
234
301
  def prune_gem_cache(resolve, cache_path)
235
302
  cached = SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s)
236
303
 
@@ -30,8 +30,8 @@ module Bundler
30
30
  install_and_restart_with(restart_version)
31
31
  end
32
32
 
33
- def update_bundler_and_restart_with_it_if_needed(target, pre: false)
34
- spec = resolve_update_version_from(target, pre: pre)
33
+ def update_bundler_and_restart_with_it_if_needed(target)
34
+ spec = resolve_update_version_from(target)
35
35
  return unless spec
36
36
 
37
37
  version = spec.version
@@ -108,9 +108,9 @@ module Bundler
108
108
  lockfile_version
109
109
  end
110
110
 
111
- def resolve_update_version_from(target, pre: false)
111
+ def resolve_update_version_from(target)
112
112
  requirement = Gem::Requirement.new(target)
113
- update_candidate = find_latest_matching_spec(requirement, pre: pre)
113
+ update_candidate = find_latest_matching_spec(requirement)
114
114
 
115
115
  if update_candidate.nil?
116
116
  raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist"
@@ -137,24 +137,18 @@ module Bundler
137
137
  end
138
138
  end
139
139
 
140
- def find_latest_matching_spec(requirement, pre: false)
140
+ def find_latest_matching_spec(requirement)
141
141
  Bundler.configure
142
- # A bare `bundle update --bundler` must stay on releases, like `gem update
143
- # --system`, so only `--pre` or a prerelease requirement opts into one.
144
- allow_prerelease = pre || requirement.prerelease?
145
-
146
- local_result = find_latest_matching_spec_from_collection(local_specs, requirement, allow_prerelease)
142
+ local_result = find_latest_matching_spec_from_collection(local_specs, requirement)
147
143
  return local_result if local_result && requirement.specific?
148
144
 
149
- remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement, allow_prerelease)
145
+ remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement)
150
146
  return remote_result if local_result.nil?
151
147
 
152
- [local_result, remote_result].compact.max
148
+ [local_result, remote_result].max
153
149
  end
154
150
 
155
- def find_latest_matching_spec_from_collection(specs, requirement, allow_prerelease)
156
- specs = specs.reject {|spec| spec.version.prerelease? } unless allow_prerelease
157
-
151
+ def find_latest_matching_spec_from_collection(specs, requirement)
158
152
  specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) }
159
153
  end
160
154
 
@@ -29,8 +29,11 @@ module Bundler
29
29
  ignore_messages
30
30
  init_gems_rb
31
31
  inline
32
+ keep_outdated_cache
32
33
  lockfile_checksums
34
+ no_build_extension
33
35
  no_install
36
+ no_install_plugin
34
37
  no_prune
35
38
  path.system
36
39
  plugins
@@ -52,6 +55,7 @@ module Bundler
52
55
 
53
56
  ARRAY_KEYS = %w[
54
57
  only
58
+ prune
55
59
  with
56
60
  without
57
61
  ].freeze
@@ -60,6 +64,7 @@ module Bundler
60
64
  bin
61
65
  cache_path
62
66
  console
67
+ credential_store
63
68
  default_cli_command
64
69
  gem.ci
65
70
  gem.github_username
@@ -91,6 +96,14 @@ module Bundler
91
96
  "BUNDLE_UPDATE_REQUIRES_ALL_FLAG" => false,
92
97
  }.freeze
93
98
 
99
+ ##
100
+ # Settings renamed in Bundler 4, mapping the current name to the one it
101
+ # replaced. The old name is still read, and goes away in Bundler 5.
102
+
103
+ RENAMED_KEYS = {
104
+ "keep_outdated_cache" => "no_prune",
105
+ }.freeze
106
+
94
107
  def initialize(root = nil)
95
108
  @root = root
96
109
  @local_config = load_config(local_config_file)
@@ -107,16 +120,7 @@ module Bundler
107
120
  end
108
121
 
109
122
  def [](name)
110
- key = key_for(name)
111
-
112
- value = nil
113
- configs.each do |_, config|
114
- value = config[key]
115
- next if value.nil?
116
- break
117
- end
118
-
119
- converted_value(value, name)
123
+ converted_value(configured_value(name), name)
120
124
  end
121
125
 
122
126
  def set_command_option(key, value)
@@ -165,6 +169,29 @@ module Bundler
165
169
  keys
166
170
  end
167
171
 
172
+ ##
173
+ # #all plus the keys whose credential lives in the credential store. Kept
174
+ # apart from #all because that one is on the hot path (it is read per gem
175
+ # source and per download, and its keys are advertised in the User-Agent),
176
+ # while this one is for the commands that display settings.
177
+
178
+ def all_including_stored_credentials
179
+ keys = stored_credential_keys.map do |key|
180
+ key = key.delete_prefix("BUNDLE_")
181
+ key.gsub!("___", "-")
182
+ key.gsub!("__", ".")
183
+ key.downcase!
184
+ key
185
+ end
186
+
187
+ # The listing comes from the globally selected store, but a host can name
188
+ # its own, so keep only the keys the per-host lookup agrees are set.
189
+ keys.select! {|key| credential_stored?(key) }
190
+ keys << "cooldown" if gemrc_cooldown_days
191
+
192
+ all.union(keys).sort
193
+ end
194
+
168
195
  def local_overrides
169
196
  repos = {}
170
197
  all.each do |k|
@@ -183,6 +210,9 @@ module Bundler
183
210
  end
184
211
 
185
212
  def credentials_for(uri)
213
+ stored = credentials_from_store(uri)
214
+ return credentials_from_env(uri) || stored if stored
215
+
186
216
  self[uri.to_s] || self[uri.host]
187
217
  end
188
218
 
@@ -219,14 +249,62 @@ module Bundler
219
249
  locations << "Set via #{key}: #{printable_value(value, exposed_key).inspect}"
220
250
  end
221
251
 
252
+ if credential_stored?(exposed_key)
253
+ # The heading calls this a priority order, but a stored credential sits
254
+ # outside it and is used ahead of every config file.
255
+ locations << "Set in the credential store, which is used ahead of the config files"
256
+ end
257
+
222
258
  if value = @global_config[key]
223
259
  locations << "Set for the current user (#{global_config_file}): #{printable_value(value, exposed_key).inspect}"
224
260
  end
225
261
 
262
+ # The gemrc cooldown sits outside the priority order too. It is not one
263
+ # of the layers, it raises whatever they resolve to. See #cooldown_for.
264
+ if key == key_for(:cooldown) && (days = gemrc_cooldown_days)
265
+ line = "Set in the RubyGems configuration as `:cooldown:`: #{days}"
266
+ line += ". The longer of that and the top value applies" unless locations.empty?
267
+ locations << line
268
+ end
269
+
226
270
  return ["You have not configured a value for `#{exposed_key}`"] if locations.empty?
227
271
  locations
228
272
  end
229
273
 
274
+ ##
275
+ # True when +name+ has a configured value Settings#[] cannot see. A
276
+ # credential in the store is one, and so is the RubyGems `:cooldown:`
277
+ # setting that #cooldown_for raises the config layers to.
278
+
279
+ def stored_outside_config_files?(name)
280
+ credential_stored?(name) || (key_for(name) == key_for(:cooldown) && !gemrc_cooldown_days.nil?)
281
+ end
282
+
283
+ ##
284
+ # True when +name+'s credential lives in the credential store. The secret
285
+ # itself is never returned: callers only need to know the setting exists,
286
+ # since Settings#[] cannot see past the config files.
287
+
288
+ def credential_stored?(name)
289
+ raw_key = self.class.key_to_s(name)
290
+ return false unless credential_store_key?(raw_key)
291
+ return false unless store = active_credential_store(credential_host(raw_key))
292
+
293
+ !store.get(credential_account(raw_key)).nil?
294
+ end
295
+
296
+ ##
297
+ # The keys credentials are stored under, in the same encoding the config
298
+ # hashes use, so #all can fold them in. Empty when no store is enabled or
299
+ # when the backend cannot enumerate its entries, which is why
300
+ # bundle-config(1) warns that a third-party backend may not list.
301
+
302
+ def stored_credential_keys
303
+ return [] unless store = active_credential_store
304
+
305
+ Array(store.list)
306
+ end
307
+
230
308
  def processor_count
231
309
  require "etc"
232
310
  Etc.nprocessors
@@ -309,6 +387,50 @@ module Bundler
309
387
  self[:jobs] || processor_count
310
388
  end
311
389
 
390
+ ##
391
+ # The cooldown that applies to a source whose Gemfile declaration asks for
392
+ # +source_cooldown+ days.
393
+ #
394
+ # `--cooldown` is set as a command line option, and it wins outright so
395
+ # that `--cooldown 0` bypasses the cooldown however the two tools are
396
+ # configured. Otherwise this setting, or the per-source value when this
397
+ # setting is unset, is raised to RubyGems' own `:cooldown:` setting, so a
398
+ # cooldown configured for only one of the two tools covers both.
399
+
400
+ def cooldown_for(source_cooldown = nil)
401
+ command_line = @temporary[key_for(:cooldown)]
402
+ return converted_value(command_line, :cooldown) unless command_line.nil?
403
+
404
+ # Read raw rather than through #[], whose `to_i` would turn a value that
405
+ # is not a number into a 0 that suppresses `source_cooldown`.
406
+ configured = cooldown_settings.days(configured_value(:cooldown))
407
+
408
+ cooldown_settings.combine(configured || source_cooldown, rubygems_cooldown)
409
+ end
410
+
411
+ ##
412
+ # RubyGems' `:cooldown:` gemrc setting, read from the loaded gemrc rather
413
+ # than from `Gem.configuration.cooldown`, which only exists on RubyGems
414
+ # versions that know the setting. A value assigned to that accessor from
415
+ # Ruby after startup is therefore not seen here.
416
+ #
417
+ # Reading it builds Gem::ConfigFile, which costs a command that never
418
+ # resolves against a remote around 30ms it has no use for, so the setting
419
+ # is validated here, at the point of use, rather than when the command
420
+ # starts.
421
+
422
+ def rubygems_cooldown
423
+ return @rubygems_cooldown if defined?(@rubygems_cooldown)
424
+
425
+ @rubygems_cooldown = gemrc_cooldown
426
+
427
+ if cooldown_settings.invalid?(@rubygems_cooldown)
428
+ Bundler.ui.warn cooldown_settings.invalid_message(@rubygems_cooldown, "the gemrc file")
429
+ end
430
+
431
+ @rubygems_cooldown
432
+ end
433
+
312
434
  def validate!
313
435
  all.each do |raw_key|
314
436
  [@local_config, @env_config, @global_config].each do |settings|
@@ -324,6 +446,27 @@ module Bundler
324
446
 
325
447
  private
326
448
 
449
+ def cooldown_settings
450
+ require "rubygems/cooldown_settings"
451
+ Gem::CooldownSettings
452
+ end
453
+
454
+ # Scanned rather than looked up, because ConfigFile#[] stringifies the key
455
+ # on RubyGems 3.4 and a `:cooldown:` gemrc entry is stored under a Symbol.
456
+
457
+ def gemrc_cooldown
458
+ Gem.configuration.each {|key, value| return value if key.to_s == "cooldown" }
459
+ nil
460
+ end
461
+
462
+ # The gemrc cooldown as a usable number of days, or nil. A value that is
463
+ # not one takes no part in the resolution, so nothing reports it as
464
+ # configured either.
465
+
466
+ def gemrc_cooldown_days
467
+ cooldown_settings.days(rubygems_cooldown)
468
+ end
469
+
327
470
  def configs
328
471
  @configs ||= {
329
472
  temporary: @temporary,
@@ -338,12 +481,37 @@ module Bundler
338
481
  converted_value(config[key_for(name)], name)
339
482
  end
340
483
 
341
- def parent_setting_for(name)
342
- split_specific_setting_for(name)[0]
484
+ ##
485
+ # A renamed setting is resolved one level at a time rather than by looking
486
+ # for the current name everywhere first, so that the old name keeps the
487
+ # documented priority order: an old name set locally still beats a current
488
+ # name set globally.
489
+
490
+ def configured_value(name)
491
+ key = key_for(name)
492
+ old_name = RENAMED_KEYS[self.class.key_to_s(name)]
493
+ old_key = key_for(old_name) if old_name
494
+
495
+ configs.each do |_, config|
496
+ value = config[key]
497
+ return value unless value.nil?
498
+
499
+ next if old_key.nil?
500
+
501
+ value = config[old_key]
502
+ next if value.nil?
503
+
504
+ SharedHelpers.feature_deprecated! "The `#{old_name}` setting has been renamed to `#{name}` and will be " \
505
+ "removed in Bundler 5. Use `#{name}` instead."
506
+
507
+ return value
508
+ end
509
+
510
+ nil
343
511
  end
344
512
 
345
- def specific_gem_for(name)
346
- split_specific_setting_for(name)[1]
513
+ def parent_setting_for(name)
514
+ split_specific_setting_for(name)[0]
347
515
  end
348
516
 
349
517
  def split_specific_setting_for(name)
@@ -357,18 +525,11 @@ module Bundler
357
525
 
358
526
  def is_string(name)
359
527
  name = self.class.key_to_s(name)
360
- STRING_KEYS.include?(name) || name.start_with?("local.") || name.start_with?("mirror.") || name.start_with?("build.")
528
+ STRING_KEYS.include?(name) || name.start_with?("local.") || name.start_with?("mirror.") || name.start_with?("build.") || name.start_with?("credential_store.")
361
529
  end
362
530
 
363
531
  def to_bool(value)
364
- case value
365
- when String
366
- value.match?(/\A(false|f|no|n|0|)\z/i) ? false : true
367
- when nil, false
368
- false
369
- else
370
- true
371
- end
532
+ self.class.to_bool(value)
372
533
  end
373
534
 
374
535
  def is_num(key)
@@ -387,6 +548,145 @@ module Bundler
387
548
  value.include?(":")
388
549
  end
389
550
 
551
+ # Kept separate from RubyGems so gem signout does not remove Bundler's
552
+ # host credentials.
553
+ CREDENTIAL_STORE_SERVICE = "bundler"
554
+
555
+ ##
556
+ # The Gem::CredentialStore for the spec #credential_store_spec returns,
557
+ # or nil when the setting is off or this RubyGems has no credential store.
558
+ # Guarded by a cheap lookup so reading and writing settings costs nothing
559
+ # extra when the setting is disabled.
560
+
561
+ def active_credential_store(host = nil)
562
+ spec = credential_store_spec(host)
563
+ return nil unless spec
564
+
565
+ store_class = credential_store_class
566
+ return nil unless store_class
567
+
568
+ store_class.for(spec, service: CREDENTIAL_STORE_SERVICE)
569
+ end
570
+
571
+ # A `credential_store.<host>` setting overrides the global one for that
572
+ # host only. There is no chain between backends.
573
+ def credential_store_spec(host = nil)
574
+ value = self["credential_store.#{host}"] if host
575
+ value = self[:credential_store] if value.nil?
576
+
577
+ # An environment variable can carry bytes String#downcase would reject.
578
+ normalized = value.to_s.b.downcase
579
+
580
+ # Tri-state, unlike a BOOL_KEYS setting, so #to_bool is only consulted
581
+ # for the false half.
582
+ return nil unless to_bool(normalized)
583
+
584
+ case normalized
585
+ when "true", "1", "yes", "on", "t", "y" then true
586
+ else value.to_s
587
+ end
588
+ end
589
+
590
+ def credential_store_class
591
+ return @credential_store_class if defined?(@credential_store_class)
592
+
593
+ @credential_store_class =
594
+ begin
595
+ require "rubygems/credential_store"
596
+ Gem::CredentialStore if Gem::CredentialStore.respond_to?(:for)
597
+ rescue LoadError
598
+ nil
599
+ end
600
+
601
+ if @credential_store_class.nil?
602
+ Bundler.ui.warn "The `credential_store` setting is set but this RubyGems does not provide a credential store. Falling back to the Bundler config file."
603
+ elsif @credential_store_class.respond_to?(:warn_handler=)
604
+ # Bundler replaces Gem.ui with a Gem::SilentUI subclass, which drops
605
+ # alert_warning, so every store warning would be lost.
606
+ @credential_store_class.warn_handler = ->(message) { Bundler.ui.warn(message) }
607
+ end
608
+
609
+ @credential_store_class
610
+ end
611
+
612
+ CREDENTIAL_URL_KEY = %r{\Ahttps?://}i
613
+ CREDENTIAL_HOST_KEY = /\A[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?\z/i
614
+
615
+ ##
616
+ # True for keys that name a host and can therefore hold a credential,
617
+ # like the ones set via `bundle config set gems.example.com user:pass`.
618
+ # Deliberately a positive test: a key this version does not recognize
619
+ # stays in the config file, where Settings#[] can read it back. Matching
620
+ # everything not on the known-settings lists would send values such as
621
+ # `ssl_client_cert` to the credential store, and they would then read
622
+ # back as nil because only #credentials_for consults the store.
623
+
624
+ def credential_store_key?(raw_key)
625
+ return false if is_bool(raw_key) || is_num(raw_key) || is_array(raw_key) || is_string(raw_key) || is_credential(raw_key)
626
+
627
+ CREDENTIAL_URL_KEY.match?(raw_key) || CREDENTIAL_HOST_KEY.match?(raw_key)
628
+ end
629
+
630
+ def remove_from_store(store, key)
631
+ unless store.available?
632
+ Bundler.ui.warn "The credential store is enabled but unavailable, so any credential it holds was left in place."
633
+ return true
634
+ end
635
+
636
+ store.delete(key)
637
+ end
638
+
639
+ # A write clears the plaintext only from the config file it targets, so a
640
+ # copy in the other scope comes back into use once the setting is off.
641
+ def warn_plaintext_in_other_scope(raw_key, key, hash)
642
+ other, other_file =
643
+ if hash.equal?(@local_config)
644
+ [@global_config, global_config_file]
645
+ else
646
+ [@local_config, @local_root.join("config")]
647
+ end
648
+
649
+ return unless other.key?(key)
650
+
651
+ # Deliberately not `bundle config unset`, which would clear the store as
652
+ # well and throw away the credential this write moved into it.
653
+ safe_key = self.class.remove_userinfo(raw_key)
654
+ Bundler.ui.warn "The credential for #{safe_key} moved into the credential store, but a plain text copy" \
655
+ " remains in #{other_file}. Delete the #{key_for(safe_key)} entry from that file to finish the move."
656
+ end
657
+
658
+ def warn_unremoved_credential(raw_key)
659
+ Bundler.ui.warn "Could not remove the credential for #{self.class.remove_userinfo(raw_key)} from the credential store." \
660
+ " It is still there. Remove it with your platform's credential manager."
661
+ end
662
+
663
+ # See Gem::ConfigFile.credential_store_account for why userinfo is dropped.
664
+ def credential_account(raw_key)
665
+ key_for(self.class.remove_userinfo(raw_key))
666
+ end
667
+
668
+ def credential_host(raw_key)
669
+ return raw_key unless CREDENTIAL_URL_KEY.match?(raw_key)
670
+
671
+ require_relative "vendored_uri"
672
+ Gem::URI(raw_key).host || raw_key
673
+ rescue Gem::URI::Error
674
+ raw_key
675
+ end
676
+
677
+ # The store stands in for the config file, so it must not override the
678
+ # environment, which already overrides that file. Consulted only when the
679
+ # store answered, so the layer order without a store is unchanged.
680
+ def credentials_from_env(uri)
681
+ @env_config[key_for(uri.to_s)] || @env_config[key_for(uri.host)]
682
+ end
683
+
684
+ def credentials_from_store(uri)
685
+ return nil unless store = active_credential_store(uri.host)
686
+
687
+ store.get(credential_account(uri.to_s)) || store.get(credential_account(uri.host))
688
+ end
689
+
390
690
  def to_array(value)
391
691
  return [] unless value
392
692
  value.tr(" ", ":").split(":").map(&:to_sym)
@@ -400,9 +700,29 @@ module Bundler
400
700
 
401
701
  def set_key(raw_key, value, hash, file)
402
702
  raw_key = self.class.key_to_s(raw_key)
403
- value = array_to_s(value) if is_array(raw_key)
404
-
405
703
  key = key_for(raw_key)
704
+ account = credential_account(raw_key)
705
+
706
+ # #temporary passes a nil file, and storing its value would outlive the
707
+ # block while its restore pass deleted the real entry.
708
+ if file && credential_store_key?(raw_key) && (store = active_credential_store(credential_host(raw_key)))
709
+ if value.nil?
710
+ warn_unremoved_credential(raw_key) unless remove_from_store(store, account)
711
+ elsif value.is_a?(String) && is_userinfo(value)
712
+ if store.set(account, value)
713
+ value = nil
714
+ warn_plaintext_in_other_scope(raw_key, key, hash)
715
+ else
716
+ warn_unremoved_credential(raw_key) if store.available? && !store.delete(account)
717
+ Bundler.ui.warn "Could not write the credential for #{self.class.remove_userinfo(raw_key)} to the credential store," \
718
+ " so it was written to #{file} in plain text."
719
+ end
720
+ else
721
+ warn_unremoved_credential(raw_key) unless remove_from_store(store, account)
722
+ end
723
+ end
724
+
725
+ value = array_to_s(value) if is_array(raw_key)
406
726
 
407
727
  return if hash[key] == value
408
728
 
@@ -482,7 +802,7 @@ module Bundler
482
802
  SharedHelpers.filesystem_access(config_file, :read) do |file|
483
803
  valid_file = file.exist? && !file.size.zero?
484
804
  return {} unless valid_file
485
- serializer_class.load(file.read).inject({}) do |config, (k, v)|
805
+ (serializer_class.load(file.read) || {}).inject({}) do |config, (k, v)|
486
806
  k = k.dup
487
807
  k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}")
488
808
  k.gsub!(".", "__")
@@ -506,12 +826,10 @@ module Bundler
506
826
  end
507
827
 
508
828
  def serializer_class
829
+ # The Bundler gem ships its own copy of Gem::YAMLSerializer, so this
830
+ # resolves even on RubyGems versions that predate it.
509
831
  require "rubygems/yaml_serializer"
510
832
  Gem::YAMLSerializer
511
- rescue LoadError
512
- # TODO: Remove this when RubyGems 3.4 is EOL
513
- require_relative "yaml_serializer"
514
- YAMLSerializer
515
833
  end
516
834
 
517
835
  FALLBACK_TIMEOUT_URI_OPTION = "fallback_timeout"
@@ -525,6 +843,17 @@ module Bundler
525
843
  \z
526
844
  /ix
527
845
 
846
+ def self.to_bool(value)
847
+ case value
848
+ when String
849
+ value.match?(/\A(false|f|no|n|0|)\z/i) ? false : true
850
+ when nil, false
851
+ false
852
+ else
853
+ true
854
+ end
855
+ end
856
+
528
857
  def self.key_for(key)
529
858
  key = key_to_s(key)
530
859
  key = normalize_uri(key) if key.start_with?("http", "mirror.http")
@@ -535,6 +864,20 @@ module Bundler
535
864
  key.gsub(/\A([ #]*)/, '\1BUNDLE_')
536
865
  end
537
866
 
867
+ def self.remove_userinfo(key)
868
+ return key unless CREDENTIAL_URL_KEY.match?(key)
869
+
870
+ require_relative "vendored_uri"
871
+ uri = Gem::URI(key)
872
+ return key unless uri.userinfo
873
+
874
+ uri = uri.dup
875
+ uri.user = uri.password = nil
876
+ uri.to_s
877
+ rescue Gem::URI::Error
878
+ key
879
+ end
880
+
538
881
  # TODO: duplicates Rubygems#normalize_uri
539
882
  # TODO: is this the correct place to validate mirror URIs?
540
883
  def self.normalize_uri(uri)
@@ -105,7 +105,8 @@ module Bundler
105
105
  def filesystem_access(path, action = :write, &block)
106
106
  yield(path.dup)
107
107
  rescue Errno::EACCES => e
108
- raise unless e.message.include?(path.to_s) || action == :create
108
+ path_basename = File.basename(path.to_s)
109
+ raise unless e.message.include?(path_basename) || action == :create
109
110
 
110
111
  raise PermissionError.new(path, action)
111
112
  rescue Errno::EAGAIN