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.
- checksums.yaml +4 -4
- data/{lib/bundler/vendor/connection_pool/LICENSE → MIT.txt} +8 -6
- data/bundler.gemspec +23 -6
- data/lib/bundler/build_metadata.rb +2 -2
- data/lib/bundler/checksum.rb +40 -25
- data/lib/bundler/ci_detector.rb +1 -1
- data/lib/bundler/cli/add.rb +3 -4
- data/lib/bundler/cli/cache.rb +4 -0
- data/lib/bundler/cli/clean.rb +5 -0
- data/lib/bundler/cli/common.rb +37 -1
- data/lib/bundler/cli/config.rb +4 -2
- data/lib/bundler/cli/gem.rb +0 -17
- data/lib/bundler/cli/install.rb +4 -5
- data/lib/bundler/cli/lock.rb +1 -2
- data/lib/bundler/cli/outdated.rb +8 -3
- data/lib/bundler/cli/pristine.rb +2 -1
- data/lib/bundler/cli/update.rb +21 -12
- data/lib/bundler/cli.rb +14 -11
- data/lib/bundler/definition.rb +61 -24
- data/lib/bundler/dependency.rb +4 -0
- data/lib/bundler/dsl.rb +77 -11
- data/lib/bundler/endpoint_specification.rb +51 -14
- data/lib/bundler/env.rb +7 -24
- data/lib/bundler/errors.rb +34 -5
- data/lib/bundler/fetcher/compact_index.rb +14 -5
- data/lib/bundler/fetcher/connection_pools.rb +150 -0
- data/lib/bundler/fetcher/downloader.rb +31 -16
- data/lib/bundler/fetcher.rb +8 -47
- data/lib/bundler/injector.rb +3 -3
- data/lib/bundler/inline.rb +44 -4
- data/lib/bundler/installer/parallel_installer.rb +25 -30
- data/lib/bundler/installer.rb +20 -6
- data/lib/bundler/lazy_specification.rb +44 -17
- data/lib/bundler/lockfile_generator.rb +24 -2
- data/lib/bundler/lockfile_parser.rb +43 -5
- data/lib/bundler/man/bundle-add.1 +7 -4
- data/lib/bundler/man/bundle-add.1.ronn +8 -4
- data/lib/bundler/man/bundle-binstubs.1 +1 -1
- data/lib/bundler/man/bundle-cache.1 +2 -2
- data/lib/bundler/man/bundle-cache.1.ronn +2 -2
- 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 +38 -6
- data/lib/bundler/man/bundle-config.1.ronn +143 -10
- data/lib/bundler/man/bundle-console.1 +1 -1
- 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 +1 -1
- data/lib/bundler/man/bundle-install.1 +2 -2
- data/lib/bundler/man/bundle-install.1.ronn +4 -4
- 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 +3 -3
- data/lib/bundler/man/bundle-lock.1.ronn +5 -5
- data/lib/bundler/man/bundle-open.1 +1 -1
- data/lib/bundler/man/bundle-outdated.1 +1 -1
- data/lib/bundler/man/bundle-platform.1 +1 -1
- data/lib/bundler/man/bundle-plugin.1 +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 +3 -3
- data/lib/bundler/man/bundle-update.1.ronn +3 -5
- data/lib/bundler/man/bundle-version.1 +1 -1
- data/lib/bundler/man/bundle.1 +1 -1
- data/lib/bundler/man/gemfile.5 +45 -1
- data/lib/bundler/man/gemfile.5.ronn +53 -0
- data/lib/bundler/match_metadata.rb +20 -0
- data/lib/bundler/match_platform.rb +25 -2
- data/lib/bundler/match_remote_metadata.rb +21 -6
- data/lib/bundler/materialization.rb +2 -1
- data/lib/bundler/override.rb +69 -0
- data/lib/bundler/plugin/dsl.rb +3 -5
- data/lib/bundler/plugin/events.rb +55 -13
- data/lib/bundler/plugin/index.rb +87 -11
- data/lib/bundler/plugin/installer/path.rb +1 -1
- data/lib/bundler/plugin/installer.rb +13 -5
- data/lib/bundler/plugin.rb +58 -30
- data/lib/bundler/remote_specification.rb +6 -3
- data/lib/bundler/resolver/base.rb +2 -1
- data/lib/bundler/resolver/incompatibility.rb +1 -1
- data/lib/bundler/resolver.rb +73 -23
- data/lib/bundler/ruby_version.rb +0 -2
- data/lib/bundler/rubygems_ext.rb +71 -1
- data/lib/bundler/rubygems_gem_installer.rb +41 -2
- data/lib/bundler/rubygems_integration.rb +7 -2
- data/lib/bundler/runtime.rb +69 -2
- data/lib/bundler/self_manager.rb +9 -15
- data/lib/bundler/settings.rb +373 -30
- data/lib/bundler/shared_helpers.rb +2 -1
- data/lib/bundler/source/git/git_proxy.rb +2 -10
- data/lib/bundler/source/git.rb +15 -6
- data/lib/bundler/source/path/installer.rb +1 -1
- data/lib/bundler/source/rubygems/remote.rb +10 -5
- data/lib/bundler/source/rubygems.rb +48 -12
- data/lib/bundler/spec_set.rb +16 -4
- data/lib/bundler/stub_specification.rb +2 -1
- data/lib/bundler/templates/newgem/Gemfile.tt +4 -4
- data/lib/bundler/templates/newgem/newgem.gemspec.tt +3 -3
- data/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +1 -1
- data/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt +1 -1
- data/lib/bundler/ui/shell.rb +0 -4
- data/lib/bundler/ui/silent.rb +0 -4
- data/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +35 -2
- data/lib/bundler/vendor/thor/lib/thor/base.rb +2 -1
- data/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +14 -6
- data/lib/bundler/vendor/thor/lib/thor/shell/color.rb +2 -0
- data/lib/bundler/vendor/thor/lib/thor/version.rb +1 -1
- data/lib/bundler/vendor/thor/lib/thor.rb +34 -1
- data/lib/bundler/vendored_pub_grub.rb +5 -2
- data/lib/bundler/vendored_securerandom.rb +11 -8
- data/lib/bundler/vendored_uri.rb +14 -14
- data/lib/bundler/version.rb +1 -1
- data/lib/bundler/worker.rb +3 -2
- data/lib/bundler.rb +1 -1
- data/lib/rubygems/compact_index_client/cache.rb +122 -0
- data/lib/rubygems/compact_index_client/cache_file.rb +147 -0
- data/lib/rubygems/compact_index_client/http_fetcher.rb +88 -0
- data/lib/rubygems/compact_index_client/parser.rb +90 -0
- data/lib/rubygems/compact_index_client/updater.rb +113 -0
- data/lib/rubygems/compact_index_client.rb +111 -0
- data/lib/rubygems/cooldown_settings.rb +57 -0
- data/lib/rubygems/credential_store/native/linux.rb +97 -0
- data/lib/rubygems/credential_store/native/macos.rb +88 -0
- data/lib/rubygems/credential_store/native/windows.rb +130 -0
- data/lib/rubygems/credential_store.rb +332 -0
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/assignment.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +7 -7
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/failure_writer.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/incompatibility.rb +4 -4
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/package.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/partial_solution.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/rubygems.rb +3 -3
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/solve_failure.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/static_package_source.rb +3 -3
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/strategy.rb +2 -2
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/term.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_constraint.rb +3 -3
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_range.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_solver.rb +2 -2
- data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_union.rb +1 -1
- data/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb +57 -0
- data/lib/{bundler → rubygems}/vendor/securerandom/lib/securerandom.rb +10 -6
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/common.rb +119 -119
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/file.rb +13 -13
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ftp.rb +24 -24
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/generic.rb +132 -132
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/http.rb +18 -18
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/https.rb +4 -4
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldap.rb +14 -14
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldaps.rb +4 -4
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/mailto.rb +21 -21
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc2396_parser.rb +39 -39
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc3986_parser.rb +14 -14
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/version.rb +1 -1
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ws.rb +12 -12
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri/wss.rb +4 -4
- data/lib/{bundler → rubygems}/vendor/uri/lib/uri.rb +35 -31
- data/lib/rubygems/yaml_serializer.rb +880 -0
- metadata +77 -68
- data/CHANGELOG.md +0 -5784
- data/LICENSE.md +0 -22
- data/README.md +0 -58
- data/lib/bundler/compact_index_client/cache.rb +0 -107
- data/lib/bundler/compact_index_client/cache_file.rb +0 -148
- data/lib/bundler/compact_index_client/parser.rb +0 -87
- data/lib/bundler/compact_index_client/updater.rb +0 -105
- data/lib/bundler/compact_index_client.rb +0 -92
- data/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +0 -227
- data/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +0 -3
- data/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb +0 -56
- data/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +0 -230
- data/lib/bundler/vendor/net-http-persistent/README.rdoc +0 -82
- data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb +0 -41
- data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb +0 -65
- data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb +0 -80
- data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +0 -1153
- data/lib/bundler/vendor/pub_grub/lib/pub_grub.rb +0 -31
- data/lib/bundler/vendored_persistent.rb +0 -11
- data/lib/bundler/yaml_serializer.rb +0 -98
- /data/lib/{bundler → rubygems}/vendor/pub_grub/LICENSE.txt +0 -0
- /data/lib/{bundler → rubygems}/vendor/securerandom/COPYING +0 -0
- /data/lib/{bundler → rubygems}/vendor/uri/COPYING +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Skip reloading when an identical copy (e.g. the one shipped inside the Bundler
|
|
4
|
+
# gem) was already required from a different path, to avoid redefinition warnings.
|
|
5
|
+
return if defined?(Gem::CompactIndexClient::INFO_REQS)
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# The CompactIndexClient fetches and parses the compact index files
|
|
9
|
+
# (names, versions and info/[gem]) served by a gem server, keeping a
|
|
10
|
+
# local cache so subsequent fetches only transfer what changed.
|
|
11
|
+
#
|
|
12
|
+
# This is the single shared client: Bundler ships a copy of it and uses
|
|
13
|
+
# it underneath its own fetcher orchestration, injecting its fetcher and
|
|
14
|
+
# filesystem access hook.
|
|
15
|
+
|
|
16
|
+
class Gem::CompactIndexClient
|
|
17
|
+
SUPPORTED_DIGESTS = { "sha-256" => :SHA256 }.freeze
|
|
18
|
+
DEBUG_MUTEX = Thread::Mutex.new
|
|
19
|
+
|
|
20
|
+
# info returns an Array of INFO Arrays. Each INFO Array has the following indices:
|
|
21
|
+
INFO_NAME = 0
|
|
22
|
+
INFO_VERSION = 1
|
|
23
|
+
INFO_SUFFIX = 2
|
|
24
|
+
INFO_DEPS = 3
|
|
25
|
+
INFO_REQS = 4
|
|
26
|
+
|
|
27
|
+
INFO_PLATFORM = INFO_SUFFIX
|
|
28
|
+
deprecate_constant :INFO_PLATFORM
|
|
29
|
+
|
|
30
|
+
def self.debug
|
|
31
|
+
return unless ENV["DEBUG_COMPACT_INDEX"]
|
|
32
|
+
DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Error < StandardError; end
|
|
36
|
+
|
|
37
|
+
# Filesystem reads and writes funnel through this hook so that a host
|
|
38
|
+
# (e.g. Bundler) can translate low-level Errno exceptions into its own
|
|
39
|
+
# friendlier errors. The default just yields the path.
|
|
40
|
+
def self.filesystem_access(path, action = :write, &block)
|
|
41
|
+
if @filesystem_access
|
|
42
|
+
@filesystem_access.call(path, action, &block)
|
|
43
|
+
else
|
|
44
|
+
yield path
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.filesystem_access=(hook)
|
|
49
|
+
@filesystem_access = hook
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
require_relative "compact_index_client/cache"
|
|
53
|
+
require_relative "compact_index_client/cache_file"
|
|
54
|
+
require_relative "compact_index_client/http_fetcher"
|
|
55
|
+
require_relative "compact_index_client/parser"
|
|
56
|
+
require_relative "compact_index_client/updater"
|
|
57
|
+
|
|
58
|
+
# The client is instantiated with:
|
|
59
|
+
# - `directory`: the root directory where the cache files are stored.
|
|
60
|
+
# - `fetcher`: (optional) an object that responds to #call(uri_path, headers)
|
|
61
|
+
# and returns a Gem::Net::HTTP response. When the fetcher is not provided,
|
|
62
|
+
# the client only reads cached files from disk.
|
|
63
|
+
def initialize(directory, fetcher = nil)
|
|
64
|
+
@cache = Cache.new(directory, fetcher)
|
|
65
|
+
@parser = Parser.new(@cache)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def names
|
|
69
|
+
Gem::CompactIndexClient.debug { "names" }
|
|
70
|
+
@parser.names
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def versions
|
|
74
|
+
Gem::CompactIndexClient.debug { "versions" }
|
|
75
|
+
@parser.versions
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dependencies(names)
|
|
79
|
+
Gem::CompactIndexClient.debug { "dependencies(#{names})" }
|
|
80
|
+
names.map {|name| info(name) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def info(name)
|
|
84
|
+
Gem::CompactIndexClient.debug { "info(#{name})" }
|
|
85
|
+
@parser.info(name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Fetches a single gem's info without consulting the versions index,
|
|
89
|
+
# using a conditional request to refresh the cached file. Useful when
|
|
90
|
+
# only a few gems are needed and the versions index download would
|
|
91
|
+
# dominate, as in gem install.
|
|
92
|
+
def fetch_info(name)
|
|
93
|
+
Gem::CompactIndexClient.debug { "fetch_info(#{name})" }
|
|
94
|
+
@parser.parse_info(@cache.fetch_info(name), name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def latest_version(name)
|
|
98
|
+
Gem::CompactIndexClient.debug { "latest_version(#{name})" }
|
|
99
|
+
@parser.info(name).map {|d| Gem::Version.new(d[INFO_VERSION]) }.max
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def available?
|
|
103
|
+
Gem::CompactIndexClient.debug { "available?" }
|
|
104
|
+
@parser.available?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def reset!
|
|
108
|
+
Gem::CompactIndexClient.debug { "reset!" }
|
|
109
|
+
@cache.reset!
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Skip reloading when an identical copy (e.g. the one shipped inside the Bundler
|
|
4
|
+
# gem) was already required from a different path, to avoid redefinition warnings.
|
|
5
|
+
return if defined?(Gem::CooldownSettings)
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# The rules RubyGems and Bundler share for reading a configured cooldown
|
|
9
|
+
# period. Bundler ships a copy of this file, so it deliberately has no
|
|
10
|
+
# requires: none of the rest of RubyGems is guaranteed to be around it.
|
|
11
|
+
|
|
12
|
+
module Gem::CooldownSettings
|
|
13
|
+
##
|
|
14
|
+
# +value+ read as a number of days, or nil when it is absent or cannot be
|
|
15
|
+
# read as a non-negative integer. A gemrc entry and a Bundler config entry
|
|
16
|
+
# are both arbitrary YAML, so either can be any type at all.
|
|
17
|
+
|
|
18
|
+
def self.days(value)
|
|
19
|
+
return if value.nil?
|
|
20
|
+
|
|
21
|
+
# Base 10 explicitly: a leading zero is how a user writes a small number
|
|
22
|
+
# of days, not a request for octal.
|
|
23
|
+
days = Integer(value.to_s, 10, exception: false)
|
|
24
|
+
|
|
25
|
+
days if days && !days.negative?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# True when +value+ is configured but cannot be read as a number of days.
|
|
30
|
+
|
|
31
|
+
def self.invalid?(value)
|
|
32
|
+
!value.nil? && days(value).nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# The cooldown that applies when RubyGems and Bundler are configured
|
|
37
|
+
# separately: the longest of +values+, so a cooldown configured for only
|
|
38
|
+
# one of the two tools protects both. Returns nil when none of them is
|
|
39
|
+
# usable.
|
|
40
|
+
#
|
|
41
|
+
# A configured 0 takes part like any other value rather than switching the
|
|
42
|
+
# cooldown off, which leaves <tt>--cooldown 0</tt> as the way to bypass a
|
|
43
|
+
# cooldown that the other tool configures.
|
|
44
|
+
|
|
45
|
+
def self.combine(*values)
|
|
46
|
+
values.filter_map {|value| days(value) }.max
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# +source+ names where the value came from, since RubyGems and Bundler now
|
|
50
|
+
# each read the other's setting and the same complaint from either of them
|
|
51
|
+
# is otherwise impossible to trace back to a file.
|
|
52
|
+
|
|
53
|
+
def self.invalid_message(value, source) # :nodoc:
|
|
54
|
+
"Invalid cooldown value #{value.inspect} in #{source}, so it is ignored. " \
|
|
55
|
+
"Expected a non-negative integer number of days."
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
class Gem::CredentialStore; end unless defined?(Gem::CredentialStore)
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# Stores credentials in the Secret Service API (GNOME Keyring, KWallet,
|
|
9
|
+
# ...) via the +secret-tool+ command line tool from libsecret.
|
|
10
|
+
|
|
11
|
+
class Gem::CredentialStore::LinuxBackend
|
|
12
|
+
# secret-tool prints an item's attributes to stderr, one per line.
|
|
13
|
+
ACCOUNT_ATTRIBUTE = /^attribute\.account = (.*)$/
|
|
14
|
+
def self.available?
|
|
15
|
+
return @available if defined?(@available)
|
|
16
|
+
|
|
17
|
+
@available = ENV["PATH"].to_s.split(File::PATH_SEPARATOR).any? do |dir|
|
|
18
|
+
File.executable?(File.join(dir, "secret-tool"))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Clears the memoized #available? result. Intended for tests only.
|
|
24
|
+
|
|
25
|
+
def self.reset!
|
|
26
|
+
remove_instance_variable(:@available) if defined?(@available)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.get(service, account)
|
|
30
|
+
out, err, status = Open3.capture3(
|
|
31
|
+
"secret-tool", "lookup", "service", service, "account", account
|
|
32
|
+
)
|
|
33
|
+
# secret-tool exits 1 with nothing on stderr when the entry is simply
|
|
34
|
+
# absent. Anything else is a real failure.
|
|
35
|
+
unless status.success?
|
|
36
|
+
return nil if status.exitstatus == 1 && err.to_s.strip.empty?
|
|
37
|
+
|
|
38
|
+
raise "secret-tool exited with #{status.exitstatus}: #{err.strip}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
secret = out.chomp
|
|
42
|
+
secret.empty? ? nil : secret
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.set(service, account, secret)
|
|
46
|
+
_out, status = Open3.capture2(
|
|
47
|
+
"secret-tool", "store", "--label=RubyGems", "service", service, "account", account,
|
|
48
|
+
stdin_data: secret
|
|
49
|
+
)
|
|
50
|
+
status.success?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# secret-tool writes attributes to stderr and secrets to stdout, so accounts
|
|
54
|
+
# are read from stderr. Discarding stdout also keeps a secret containing a
|
|
55
|
+
# newline from being mistaken for an attribute line.
|
|
56
|
+
def self.list(service)
|
|
57
|
+
_out, err, status = Open3.capture3(
|
|
58
|
+
"secret-tool", "search", "--all", "service", service
|
|
59
|
+
)
|
|
60
|
+
return [] unless status.success?
|
|
61
|
+
|
|
62
|
+
err.scan(ACCOUNT_ATTRIBUTE).flatten.uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.delete(service, account)
|
|
66
|
+
_out, err, status = Open3.capture3(
|
|
67
|
+
"secret-tool", "clear", "service", service, "account", account
|
|
68
|
+
)
|
|
69
|
+
return cleared?(service, account) if status.success?
|
|
70
|
+
|
|
71
|
+
# secret-tool clear exits 1 with no stderr when nothing matched.
|
|
72
|
+
status.exitstatus == 1 && err.to_s.strip.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.delete_all(service)
|
|
76
|
+
_out, err, status = Open3.capture3(
|
|
77
|
+
"secret-tool", "clear", "service", service
|
|
78
|
+
)
|
|
79
|
+
return cleared?(service) if status.success?
|
|
80
|
+
|
|
81
|
+
status.exitstatus == 1 && err.to_s.strip.empty?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# libsecret clears only unlocked items and reports no error for the ones it
|
|
85
|
+
# skipped, so a locked keyring answers a clear with success while keeping
|
|
86
|
+
# every secret. search exits zero either way, so its output is the answer.
|
|
87
|
+
def self.cleared?(service, account = nil)
|
|
88
|
+
_out, err, status = Open3.capture3(
|
|
89
|
+
"secret-tool", "search", "--all", "service", service
|
|
90
|
+
)
|
|
91
|
+
return false unless status.success?
|
|
92
|
+
|
|
93
|
+
remaining = err.scan(ACCOUNT_ATTRIBUTE).flatten
|
|
94
|
+
account ? !remaining.include?(account) : remaining.empty?
|
|
95
|
+
end
|
|
96
|
+
private_class_method :cleared?
|
|
97
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
class Gem::CredentialStore; end unless defined?(Gem::CredentialStore)
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# Stores credentials in the macOS Keychain via the +security+ command line
|
|
9
|
+
# tool. +security+ has no way to read a password from stdin as raw bytes
|
|
10
|
+
# for +add-generic-password+, so #set uses +security -i+ (batch/interactive
|
|
11
|
+
# mode, one tokenized command per stdin line) to keep the secret off argv
|
|
12
|
+
# and out of +ps+ output.
|
|
13
|
+
#
|
|
14
|
+
# A newline would start a second command in the +security -i+ batch, and
|
|
15
|
+
# +security find-generic-password -w+ prints any non-printable byte back as a
|
|
16
|
+
# hex string rather than the original value, so a non-ASCII secret would
|
|
17
|
+
# round-trip corrupted. Those are the limits Gem::CredentialStore#set enforces
|
|
18
|
+
# for every backend, so the caller falls back to file storage rather than
|
|
19
|
+
# storing something that cannot be read back.
|
|
20
|
+
|
|
21
|
+
class Gem::CredentialStore::MacOSBackend
|
|
22
|
+
NOT_FOUND_STATUS = 44
|
|
23
|
+
|
|
24
|
+
def self.get(service, account)
|
|
25
|
+
out, err, status = Open3.capture3(
|
|
26
|
+
"security", "find-generic-password", "-a", account, "-s", service, "-w"
|
|
27
|
+
)
|
|
28
|
+
# A locked keychain and an absent entry both yield no secret, but only the
|
|
29
|
+
# second one is ordinary.
|
|
30
|
+
unless status.success?
|
|
31
|
+
return nil if status.exitstatus == NOT_FOUND_STATUS
|
|
32
|
+
|
|
33
|
+
raise "security exited with #{status.exitstatus}: #{err.strip}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
secret = out.chomp
|
|
37
|
+
secret.empty? ? nil : secret
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.set(service, account, secret)
|
|
41
|
+
command = "add-generic-password -U -a #{quote(account)} -s #{quote(service)} -w #{quote(secret)}\n"
|
|
42
|
+
_out, err, status = Open3.capture3("security", "-i", stdin_data: command)
|
|
43
|
+
return true if status.success?
|
|
44
|
+
|
|
45
|
+
# Raise rather than return false so the reason reaches the user. The
|
|
46
|
+
# wrapper turns it back into false after reporting it.
|
|
47
|
+
raise "security exited with #{status.exitstatus}: #{err.strip}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# security has no "list by service" subcommand, so this reads the dump, which
|
|
51
|
+
# never reports the secrets.
|
|
52
|
+
def self.list(service)
|
|
53
|
+
out, status = Open3.capture2("security", "dump-keychain", err: File::NULL)
|
|
54
|
+
return [] unless status.success?
|
|
55
|
+
|
|
56
|
+
out.split(/^keychain: /).filter_map do |entry|
|
|
57
|
+
next unless entry[/^\s*"svce"<blob>="(.*)"$/, 1] == service
|
|
58
|
+
|
|
59
|
+
entry[/^\s*"acct"<blob>="(.*)"$/, 1]
|
|
60
|
+
end.uniq
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.delete(service, account)
|
|
64
|
+
_out, status = Open3.capture2(
|
|
65
|
+
"security", "delete-generic-password", "-a", account, "-s", service,
|
|
66
|
+
err: File::NULL
|
|
67
|
+
)
|
|
68
|
+
status.success? || status.exitstatus == NOT_FOUND_STATUS
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# security deletes one entry per call, so keep going until it reports there
|
|
72
|
+
# is nothing left (exit 44). Other services are untouched.
|
|
73
|
+
def self.delete_all(service)
|
|
74
|
+
loop do
|
|
75
|
+
_out, status = Open3.capture2(
|
|
76
|
+
"security", "delete-generic-password", "-s", service,
|
|
77
|
+
err: File::NULL
|
|
78
|
+
)
|
|
79
|
+
return true if status.exitstatus == NOT_FOUND_STATUS
|
|
80
|
+
return false unless status.success?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.quote(value)
|
|
85
|
+
%("#{value.gsub("\\", "\\\\\\\\").gsub('"', '\\"')}")
|
|
86
|
+
end
|
|
87
|
+
private_class_method :quote
|
|
88
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
class Gem::CredentialStore; end unless defined?(Gem::CredentialStore)
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# Stores credentials in the Windows Credential Manager via the
|
|
9
|
+
# +Windows.Security.Credentials.PasswordVault+ WinRT API, driven from
|
|
10
|
+
# PowerShell. Account/service/secret values are passed as environment
|
|
11
|
+
# variables rather than interpolated into the script text, so no quoting
|
|
12
|
+
# scheme is needed and values cannot break out of the script.
|
|
13
|
+
#
|
|
14
|
+
# Windows PowerShell is used rather than PowerShell 7 (+pwsh+) because the
|
|
15
|
+
# WinRT projection used here is not reliably available under pwsh. It is
|
|
16
|
+
# spawned as +powershell+ rather than +powershell.exe+, the way this codebase
|
|
17
|
+
# spawns +git+, so PATHEXT resolves it. That also lets the tests put a shim
|
|
18
|
+
# ahead of it on Windows, where a file with a shebang is not executable.
|
|
19
|
+
|
|
20
|
+
class Gem::CredentialStore::WindowsBackend
|
|
21
|
+
LOAD_VAULT_TYPE = <<~POWERSHELL
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
|
|
24
|
+
POWERSHELL
|
|
25
|
+
private_constant :LOAD_VAULT_TYPE
|
|
26
|
+
|
|
27
|
+
def self.get(service, account)
|
|
28
|
+
script = <<~POWERSHELL
|
|
29
|
+
#{LOAD_VAULT_TYPE}
|
|
30
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
31
|
+
$credential = $vault.Retrieve($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT)
|
|
32
|
+
# Emitting into the pipeline would send the string through PowerShell's
|
|
33
|
+
# formatter, which wraps at the host width and would corrupt a secret.
|
|
34
|
+
[Console]::Out.Write($credential.Password)
|
|
35
|
+
POWERSHELL
|
|
36
|
+
|
|
37
|
+
out, err, status = run(script, service, account)
|
|
38
|
+
# An absent credential is ordinary. Any other failure is not, so raise and
|
|
39
|
+
# let the wrapper report why rather than authenticating without one.
|
|
40
|
+
unless status.success?
|
|
41
|
+
return nil if missing_credential?(err)
|
|
42
|
+
|
|
43
|
+
raise "powershell exited with #{status.exitstatus}: #{err.strip}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
secret = out.chomp
|
|
47
|
+
secret.empty? ? nil : secret
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.set(service, account, secret)
|
|
51
|
+
script = <<~POWERSHELL
|
|
52
|
+
#{LOAD_VAULT_TYPE}
|
|
53
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
54
|
+
try {
|
|
55
|
+
$existing = $vault.Retrieve($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT)
|
|
56
|
+
$vault.Remove($existing)
|
|
57
|
+
} catch {}
|
|
58
|
+
$credential = New-Object Windows.Security.Credentials.PasswordCredential($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT, $env:RUBYGEMS_CRED_SECRET)
|
|
59
|
+
$vault.Add($credential)
|
|
60
|
+
POWERSHELL
|
|
61
|
+
|
|
62
|
+
_out, err, status = run(script, service, account, secret)
|
|
63
|
+
return true if status.success?
|
|
64
|
+
|
|
65
|
+
# Raise so the reason reaches the user; see MacOSBackend.set.
|
|
66
|
+
raise "powershell exited with #{status.exitstatus}: #{err.strip}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.delete(service, account)
|
|
70
|
+
script = <<~POWERSHELL
|
|
71
|
+
#{LOAD_VAULT_TYPE}
|
|
72
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
73
|
+
$credential = $vault.Retrieve($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT)
|
|
74
|
+
$vault.Remove($credential)
|
|
75
|
+
POWERSHELL
|
|
76
|
+
|
|
77
|
+
_out, err, status = run(script, service, account)
|
|
78
|
+
status.success? || missing_credential?(err)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# FindAllByResource raises when the resource has no entries, which is an
|
|
82
|
+
# empty list rather than an error.
|
|
83
|
+
def self.list(service)
|
|
84
|
+
script = <<~POWERSHELL
|
|
85
|
+
#{LOAD_VAULT_TYPE}
|
|
86
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
87
|
+
try {
|
|
88
|
+
$vault.FindAllByResource($env:RUBYGEMS_CRED_SERVICE) | ForEach-Object {
|
|
89
|
+
[Console]::Out.WriteLine($_.UserName)
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
if (-not ($_.Exception.Message -match 'not found|0x80070490')) { throw }
|
|
93
|
+
}
|
|
94
|
+
POWERSHELL
|
|
95
|
+
|
|
96
|
+
out, _err, status = run(script, service, nil)
|
|
97
|
+
return [] unless status.success?
|
|
98
|
+
|
|
99
|
+
out.split("\n").map(&:chomp).reject(&:empty?).uniq
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.delete_all(service)
|
|
103
|
+
script = <<~POWERSHELL
|
|
104
|
+
#{LOAD_VAULT_TYPE}
|
|
105
|
+
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
106
|
+
try {
|
|
107
|
+
$vault.FindAllByResource($env:RUBYGEMS_CRED_SERVICE) | ForEach-Object { $vault.Remove($_) }
|
|
108
|
+
} catch {
|
|
109
|
+
if (-not ($_.Exception.Message -match 'not found|0x80070490')) { throw }
|
|
110
|
+
}
|
|
111
|
+
POWERSHELL
|
|
112
|
+
|
|
113
|
+
_out, err, status = run(script, service, nil)
|
|
114
|
+
status.success? || missing_credential?(err)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.run(script, service, account, secret = nil)
|
|
118
|
+
env = { "RUBYGEMS_CRED_SERVICE" => service, "RUBYGEMS_CRED_ACCOUNT" => account }
|
|
119
|
+
env["RUBYGEMS_CRED_SECRET"] = secret if secret
|
|
120
|
+
|
|
121
|
+
Open3.capture3(env, "powershell", "-NoProfile", "-NonInteractive", "-Command", "-", stdin_data: script)
|
|
122
|
+
end
|
|
123
|
+
private_class_method :run
|
|
124
|
+
|
|
125
|
+
def self.missing_credential?(message)
|
|
126
|
+
text = message.to_s.downcase
|
|
127
|
+
text.include?("element not found") || text.include?("0x80070490") || text.include?("could not be found")
|
|
128
|
+
end
|
|
129
|
+
private_class_method :missing_credential?
|
|
130
|
+
end
|