gemvault 0.1.4 → 0.2.0
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/.containerignore +10 -0
- data/.rubocop.yml +8 -1
- data/CHANGELOG.md +56 -0
- data/CLAUDE.md +6 -5
- data/Dockerfile.test +14 -0
- data/README.md +18 -6
- data/Rakefile +19 -0
- data/docs/tarvault-findings.md +186 -0
- data/docs/tarvault.md +42 -0
- data/issues.rec +540 -17
- data/lib/bundler/plugin/vault_source.rb +61 -73
- data/lib/bundler/plugin/vaulted_gem.rb +66 -0
- data/lib/gemvault/archive_entry.rb +6 -0
- data/lib/gemvault/cli/command.rb +9 -5
- data/lib/gemvault/cli/commands/extract.rb +11 -11
- data/lib/gemvault/cli/commands/new.rb +3 -2
- data/lib/gemvault/cli/commands/remove.rb +15 -5
- data/lib/gemvault/cli/commands/upgrade.rb +72 -0
- data/lib/gemvault/dbvault.rb +101 -0
- data/lib/gemvault/deprecation.rb +48 -0
- data/lib/gemvault/gem_entry.rb +20 -24
- data/lib/gemvault/gem_extraction.rb +41 -0
- data/lib/gemvault/gem_reference/any_version.rb +6 -2
- data/lib/gemvault/gem_reference/parser.rb +44 -0
- data/lib/gemvault/gem_reference/specific_version.rb +9 -25
- data/lib/gemvault/gem_reference.rb +13 -45
- data/lib/gemvault/manifest.rb +78 -0
- data/lib/gemvault/tarball.rb +53 -0
- data/lib/gemvault/tarvault.rb +139 -0
- data/lib/gemvault/vault.rb +55 -157
- data/lib/gemvault/vault_path.rb +33 -0
- data/lib/gemvault/vault_session.rb +16 -0
- data/lib/gemvault/vault_upgrade.rb +94 -0
- data/lib/gemvault/version.rb +1 -1
- data/lib/gemvault.rb +2 -0
- data/lib/rubygems/resolver/vault_set.rb +5 -7
- data/lib/rubygems/source/vault.rb +28 -32
- data/lib/rubygems_plugin.rb +17 -6
- metadata +21 -33
data/lib/gemvault/vault.rb
CHANGED
|
@@ -1,193 +1,91 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "
|
|
3
|
-
|
|
4
|
-
require "tempfile"
|
|
5
|
-
require_relative "gem_entry"
|
|
6
|
-
require_relative "gem_reference"
|
|
1
|
+
require "forwardable"
|
|
2
|
+
require "pathname"
|
|
3
|
+
require_relative "vault_session"
|
|
7
4
|
|
|
8
5
|
module Gemvault
|
|
9
|
-
#
|
|
6
|
+
# The public vault interface. Delegates storage to a backend chosen by file
|
|
7
|
+
# format: a Dbvault (SQLite) for existing SQLite files, a Tarvault (tarball)
|
|
8
|
+
# otherwise. New vaults are Tarvaults. Only the selected backend is loaded,
|
|
9
|
+
# so the tar path never requires sqlite3.
|
|
10
10
|
class Vault
|
|
11
|
+
extend VaultSession
|
|
12
|
+
extend Forwardable
|
|
13
|
+
|
|
11
14
|
class Error < StandardError; end
|
|
12
15
|
class NotFoundError < Error; end
|
|
13
16
|
class DuplicateGemError < Error; end
|
|
14
17
|
class InvalidGemError < Error; end
|
|
18
|
+
class UnsupportedVersionError < Error; end
|
|
19
|
+
class ReadOnlyError < Error; end
|
|
15
20
|
|
|
16
|
-
SCHEMA_VERSION = "1".freeze
|
|
17
21
|
SQLITE_MAGIC = "SQLite format 3#{0.chr}".freeze
|
|
22
|
+
TAR_MAGIC = "ustar".freeze
|
|
23
|
+
TAR_MAGIC_OFFSET = 257
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
CURRENT_FORMAT = 2
|
|
26
|
+
MIN_READABLE_FORMAT = 1
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
def_delegators :@backend,
|
|
29
|
+
:add, :remove, :gem_data, :gem_entries, :specs,
|
|
30
|
+
:spec_from_blob, :with_gem_file, :size, :close, :closed?,
|
|
31
|
+
:path, :format_version
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
yield vault
|
|
27
|
-
ensure
|
|
28
|
-
vault.close
|
|
29
|
-
end
|
|
30
|
-
end
|
|
33
|
+
def self.assert_readable!(version:, path:)
|
|
34
|
+
return if version.between?(MIN_READABLE_FORMAT, CURRENT_FORMAT)
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
create ? create_vault! : open_vault!
|
|
36
|
+
raise UnsupportedVersionError,
|
|
37
|
+
"Vault #{path} is format #{version}; this gemvault reads up to #{CURRENT_FORMAT}. Upgrade gemvault."
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
raise NotFoundError, "
|
|
40
|
+
def self.backend_for(path, create:)
|
|
41
|
+
return build_tarvault(path, create: true) if create
|
|
42
|
+
raise NotFoundError, "Vault not found: #{path}" unless path.exist?
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def remove(reference)
|
|
47
|
-
case reference
|
|
48
|
-
in GemReference::AnyVersion[name:]
|
|
49
|
-
@db.execute("DELETE FROM gems WHERE name = ?", [name])
|
|
50
|
-
in GemReference::SpecificVersion[name:, version:]
|
|
51
|
-
@db.execute(
|
|
52
|
-
"DELETE FROM gems WHERE name = ? AND version = ?",
|
|
53
|
-
[name, version.to_s],
|
|
54
|
-
)
|
|
44
|
+
case container_kind(path)
|
|
45
|
+
when :sqlite then build_dbvault(path)
|
|
46
|
+
when :tar then build_tarvault(path, create: false)
|
|
47
|
+
else
|
|
48
|
+
raise Error, "Unrecognized vault format: #{path} (not a Dbvault or Tarvault; it may require a newer gemvault)"
|
|
55
49
|
end
|
|
56
|
-
@db.changes
|
|
57
50
|
end
|
|
58
51
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[name, version, platform],
|
|
63
|
-
).first
|
|
52
|
+
def self.container_kind(path)
|
|
53
|
+
return :sqlite if sqlite?(path)
|
|
54
|
+
return :tar if tar?(path)
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
row["data"]
|
|
56
|
+
:unknown
|
|
68
57
|
end
|
|
69
58
|
|
|
70
|
-
def
|
|
71
|
-
|
|
59
|
+
def self.sqlite?(path)
|
|
60
|
+
path.exist? && path.binread(SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
|
|
72
61
|
end
|
|
73
62
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
"SELECT name, version, platform, created_at FROM gems ORDER BY name, version",
|
|
77
|
-
).map { |row| GemEntry.new(**row.transform_keys(&:to_sym)) }
|
|
78
|
-
end
|
|
63
|
+
def self.tar?(path)
|
|
64
|
+
return false unless path.exist?
|
|
79
65
|
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
header = path.binread(TAR_MAGIC_OFFSET + TAR_MAGIC.bytesize)
|
|
67
|
+
header.to_s[TAR_MAGIC_OFFSET, TAR_MAGIC.bytesize] == TAR_MAGIC
|
|
82
68
|
end
|
|
83
69
|
|
|
84
|
-
def
|
|
85
|
-
@db.close if @db && !@db.closed?
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def with_gem_file(name, version, platform: "ruby")
|
|
89
|
-
data = gem_data(name, version, platform: platform)
|
|
90
|
-
tmpfile = write_tempfile(data)
|
|
70
|
+
def self.build_dbvault(path)
|
|
91
71
|
begin
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def spec_from_blob(name, version, platform = "ruby")
|
|
100
|
-
with_gem_file(name, version, platform: platform) do |path|
|
|
101
|
-
Gem::Package.new(path).spec
|
|
72
|
+
require_relative "dbvault"
|
|
73
|
+
Dbvault.new(path)
|
|
74
|
+
rescue LoadError => e
|
|
75
|
+
raise Error,
|
|
76
|
+
"#{path} is a legacy SQLite vault; it needs the sqlite3 gem (#{e.message}). " \
|
|
77
|
+
"Install sqlite3, or upgrade the vault with a gemvault that includes it."
|
|
102
78
|
end
|
|
103
79
|
end
|
|
104
80
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
raise Error, "Vault already exists: #{@path}" if File.exist?(@path)
|
|
109
|
-
|
|
110
|
-
@db = new_database
|
|
111
|
-
create_schema
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def open_vault!
|
|
115
|
-
raise NotFoundError, "Vault not found: #{@path}" unless File.exist?(@path)
|
|
116
|
-
|
|
117
|
-
validate_sqlite!
|
|
118
|
-
@db = new_database
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def new_database
|
|
122
|
-
db = SQLite3::Database.new(@path)
|
|
123
|
-
db.results_as_hash = true
|
|
124
|
-
db
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def load_gem_spec(gem_path)
|
|
128
|
-
Gem::Package.new(gem_path).spec
|
|
129
|
-
rescue StandardError => e
|
|
130
|
-
raise InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def raise_if_duplicate(spec)
|
|
134
|
-
existing = @db.execute(
|
|
135
|
-
"SELECT 1 FROM gems WHERE name = ? AND version = ? AND platform = ?",
|
|
136
|
-
[spec.name, spec.version.to_s, spec.platform.to_s],
|
|
137
|
-
)
|
|
138
|
-
return if existing.empty?
|
|
139
|
-
|
|
140
|
-
raise DuplicateGemError,
|
|
141
|
-
"Gem already in vault: #{spec.name}-#{spec.version} (#{spec.platform})"
|
|
81
|
+
def self.build_tarvault(path, create:)
|
|
82
|
+
require_relative "tarvault"
|
|
83
|
+
Tarvault.new(path, create:)
|
|
142
84
|
end
|
|
143
85
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
@
|
|
147
|
-
"INSERT INTO gems (name, version, platform, data) VALUES (?, ?, ?, ?)",
|
|
148
|
-
[spec.name, spec.version.to_s, spec.platform.to_s, SQLite3::Blob.new(data)],
|
|
149
|
-
)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def write_tempfile(data)
|
|
153
|
-
tmpfile = Tempfile.new(["vault_gem", ".gem"])
|
|
154
|
-
tmpfile.binmode
|
|
155
|
-
tmpfile.write(data)
|
|
156
|
-
tmpfile.close
|
|
157
|
-
tmpfile
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def create_schema
|
|
161
|
-
@db.execute_batch(<<~SQL)
|
|
162
|
-
CREATE TABLE metadata (
|
|
163
|
-
key TEXT PRIMARY KEY,
|
|
164
|
-
value TEXT NOT NULL
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
CREATE TABLE gems (
|
|
168
|
-
name TEXT NOT NULL,
|
|
169
|
-
version TEXT NOT NULL,
|
|
170
|
-
platform TEXT NOT NULL DEFAULT 'ruby',
|
|
171
|
-
data BLOB NOT NULL,
|
|
172
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
173
|
-
PRIMARY KEY (name, version, platform)
|
|
174
|
-
);
|
|
175
|
-
SQL
|
|
176
|
-
|
|
177
|
-
@db.execute(
|
|
178
|
-
"INSERT INTO metadata (key, value) VALUES (?, ?)",
|
|
179
|
-
["vault_version", SCHEMA_VERSION],
|
|
180
|
-
)
|
|
181
|
-
@db.execute(
|
|
182
|
-
"INSERT INTO metadata (key, value) VALUES (?, ?)",
|
|
183
|
-
["created_at", Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")],
|
|
184
|
-
)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def validate_sqlite!
|
|
188
|
-
return if File.binread(@path, SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
|
|
189
|
-
|
|
190
|
-
raise Error, "Not a valid vault file (not SQLite): #{@path}"
|
|
86
|
+
def initialize(path, create: false)
|
|
87
|
+
absolute_path = Pathname(path).expand_path
|
|
88
|
+
@backend = self.class.backend_for(absolute_path, create:)
|
|
191
89
|
end
|
|
192
90
|
end
|
|
193
91
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require "pathname"
|
|
3
|
+
|
|
4
|
+
module Gemvault
|
|
5
|
+
##
|
|
6
|
+
# Translates vault locators into filesystem paths.
|
|
7
|
+
#
|
|
8
|
+
# A locator is whatever a user hands the CLI or a package manager as "the
|
|
9
|
+
# vault": a plain filesystem path, a <tt>file://</tt> URI, or a
|
|
10
|
+
# <tt>vault://</tt> URI. Anything else passes through unchanged.
|
|
11
|
+
module VaultPath
|
|
12
|
+
SCHEMES = ["file", "vault"].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# :call-seq:
|
|
17
|
+
# resolve(locator) -> Pathname
|
|
18
|
+
#
|
|
19
|
+
# Resolves +locator+ to a Pathname. Two-slash relative forms such as
|
|
20
|
+
# <tt>file://vault.gemv</tt> parse their first segment as a URI host, so
|
|
21
|
+
# host and path are rejoined.
|
|
22
|
+
def resolve(locator)
|
|
23
|
+
begin
|
|
24
|
+
uri = URI.parse(locator.to_s)
|
|
25
|
+
return Pathname(locator.to_s) unless SCHEMES.include?(uri.scheme)
|
|
26
|
+
|
|
27
|
+
Pathname([uri.host, uri.path].compact.join)
|
|
28
|
+
rescue URI::InvalidURIError
|
|
29
|
+
Pathname(locator.to_s)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Gemvault
|
|
2
|
+
# Class-method mixin giving vault types a block-scoped `open` that news up
|
|
3
|
+
# an instance, yields it, and guarantees `close`.
|
|
4
|
+
module VaultSession
|
|
5
|
+
def open(path, **opts)
|
|
6
|
+
raise ArgumentError, "#{name}.open requires a block" unless block_given?
|
|
7
|
+
|
|
8
|
+
vault = new(path, **opts)
|
|
9
|
+
begin
|
|
10
|
+
yield vault
|
|
11
|
+
ensure
|
|
12
|
+
vault.close
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require_relative "vault"
|
|
4
|
+
require_relative "deprecation"
|
|
5
|
+
|
|
6
|
+
module Gemvault
|
|
7
|
+
# Migrates a vault to the current storage format by reading it through its
|
|
8
|
+
# existing backend and rewriting it through the current-format writer, then
|
|
9
|
+
# atomically swapping the file into place. Preserves each gem's created_at.
|
|
10
|
+
class VaultUpgrade
|
|
11
|
+
include FileUtils
|
|
12
|
+
|
|
13
|
+
# The from/to/gem-count summary of an upgrade; also detects an
|
|
14
|
+
# already-current (no-op) vault.
|
|
15
|
+
class Plan < Data.define(:from_version, :to_version, :gem_count)
|
|
16
|
+
def no_op? = from_version == to_version
|
|
17
|
+
|
|
18
|
+
def to_s = "format #{from_version} -> #{to_version}"
|
|
19
|
+
|
|
20
|
+
def deconstruct_keys(_keys)
|
|
21
|
+
{ from_version:, to_version:, gem_count:, no_op: no_op? }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Copies gems from a source vault into a target vault, preserving each
|
|
26
|
+
# gem's stored timestamp. Holds the two endpoints so the per-gem call
|
|
27
|
+
# takes only the entry.
|
|
28
|
+
class GemCopy < Data.define(:source, :target)
|
|
29
|
+
def call(entry)
|
|
30
|
+
source.with_gem_file(entry) do |gem_path|
|
|
31
|
+
target.add(gem_path, created_at: entry.created_at)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(path, backup: true)
|
|
37
|
+
@path = Pathname(path).expand_path
|
|
38
|
+
@backup = backup
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def plan
|
|
42
|
+
Deprecation.silence do
|
|
43
|
+
Vault.open(@path) do |vault|
|
|
44
|
+
Plan.new(from_version: vault.format_version, to_version: Vault::CURRENT_FORMAT, gem_count: vault.size)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call
|
|
50
|
+
summary = plan
|
|
51
|
+
return summary if summary.no_op?
|
|
52
|
+
|
|
53
|
+
backup! if @backup
|
|
54
|
+
rebuild(summary.gem_count)
|
|
55
|
+
summary
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def backup!
|
|
61
|
+
backup_path = Pathname("#{@path}.bak")
|
|
62
|
+
raise Vault::Error, "Backup already exists: #{backup_path} (remove it or pass --no-backup)" if backup_path.exist?
|
|
63
|
+
|
|
64
|
+
cp(@path, backup_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def rebuild(expected_count)
|
|
68
|
+
tmp = Pathname("#{@path}.upgrading")
|
|
69
|
+
rm_f(tmp)
|
|
70
|
+
copy_current_format(tmp)
|
|
71
|
+
verify_count!(tmp:, expected_count:)
|
|
72
|
+
tmp.rename(@path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def copy_current_format(tmp)
|
|
76
|
+
Deprecation.silence do
|
|
77
|
+
Vault.open(@path) do |old|
|
|
78
|
+
target = Vault.new(tmp, create: true)
|
|
79
|
+
copy = GemCopy.new(source: old, target:)
|
|
80
|
+
old.gem_entries.each { |entry| copy.call(entry) }
|
|
81
|
+
target.close
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def verify_count!(tmp:, expected_count:)
|
|
87
|
+
actual = Vault.open(tmp, &:size)
|
|
88
|
+
return if actual == expected_count
|
|
89
|
+
|
|
90
|
+
rm_f(tmp)
|
|
91
|
+
raise Vault::Error, "Upgrade verification failed: expected #{expected_count} gems, got #{actual}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/gemvault/version.rb
CHANGED
data/lib/gemvault.rb
CHANGED
|
@@ -11,7 +11,7 @@ class Gem::Resolver::VaultSet < Gem::Resolver::Set
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def find_all(req)
|
|
14
|
-
@specs.filter_map { |
|
|
14
|
+
@specs.filter_map { |candidate| index_spec_for(candidate) if req.match?(candidate) }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def prefetch(reqs); end
|
|
@@ -22,19 +22,17 @@ class Gem::Resolver::VaultSet < Gem::Resolver::Set
|
|
|
22
22
|
|
|
23
23
|
pp.breakable
|
|
24
24
|
|
|
25
|
-
pp.seplist @specs do |
|
|
26
|
-
pp.text
|
|
25
|
+
pp.seplist @specs do |candidate|
|
|
26
|
+
pp.text candidate.full_name
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
-
def index_spec_for(
|
|
34
|
-
return unless req.match?(tuple)
|
|
35
|
-
|
|
33
|
+
def index_spec_for(candidate)
|
|
36
34
|
Gem::Resolver::IndexSpecification.new(
|
|
37
|
-
self,
|
|
35
|
+
self, candidate.name, candidate.version, @source, candidate.platform
|
|
38
36
|
)
|
|
39
37
|
end
|
|
40
38
|
end
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require "gemvault/vault"
|
|
2
|
-
require "
|
|
2
|
+
require "gemvault/vault_path"
|
|
3
|
+
require "gemvault/gem_entry"
|
|
4
|
+
require "pathname"
|
|
3
5
|
|
|
4
6
|
##
|
|
5
|
-
# A source backed by a .gemv vault file
|
|
7
|
+
# A source backed by a .gemv vault file: a tar archive of .gem files indexed
|
|
8
|
+
# by a manifest. Legacy SQLite vaults are read transparently.
|
|
6
9
|
#
|
|
7
10
|
# Used by the gemvault RubyGems plugin to support:
|
|
8
11
|
#
|
|
@@ -10,12 +13,11 @@ require "uri"
|
|
|
10
13
|
class Gem::Source::Vault < Gem::Source
|
|
11
14
|
include Gem::UserInteraction
|
|
12
15
|
|
|
13
|
-
VAULT_URI_SCHEMES = %w[file vault].freeze
|
|
14
|
-
|
|
15
16
|
attr_reader :path
|
|
16
17
|
|
|
17
18
|
def initialize(path)
|
|
18
|
-
|
|
19
|
+
resolved = Gemvault::VaultPath.resolve(path)
|
|
20
|
+
@path = Pathname(resolved).expand_path.to_s
|
|
19
21
|
super(@path)
|
|
20
22
|
@uri = @path
|
|
21
23
|
@specs = nil
|
|
@@ -24,7 +26,7 @@ class Gem::Source::Vault < Gem::Source
|
|
|
24
26
|
def load_specs(type)
|
|
25
27
|
verbose "Loading #{type} specs from vault at #{@path}"
|
|
26
28
|
ensure_specs_loaded
|
|
27
|
-
|
|
29
|
+
select_candidates(type)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def fetch_spec(name_tuple)
|
|
@@ -38,17 +40,15 @@ class Gem::Source::Vault < Gem::Source
|
|
|
38
40
|
|
|
39
41
|
def download(spec, dir = Dir.pwd)
|
|
40
42
|
verbose "Extracting #{spec.file_name} from vault at #{@path}"
|
|
41
|
-
|
|
42
|
-
FileUtils.mkdir_p(cache_dir)
|
|
43
|
-
|
|
44
|
-
dest = File.join(cache_dir, spec.file_name)
|
|
43
|
+
dest = Pathname(dir).join("cache").tap(&:mkpath).join(spec.file_name)
|
|
45
44
|
|
|
46
45
|
Gemvault::Vault.open(@path) do |vault|
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
entry = entry_for(spec)
|
|
47
|
+
bytes = vault.gem_data(entry)
|
|
48
|
+
dest.binwrite(bytes)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
dest
|
|
51
|
+
dest.to_s
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def dependency_resolver_set(prerelease = nil)
|
|
@@ -91,35 +91,32 @@ class Gem::Source::Vault < Gem::Source
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
VAULT_URI_SCHEMES.include?(uri.scheme) ? uri.path : path.to_s
|
|
97
|
-
rescue URI::InvalidURIError
|
|
98
|
-
path.to_s
|
|
94
|
+
def entry_for(spec)
|
|
95
|
+
Gemvault::GemEntry.from_spec(spec)
|
|
99
96
|
end
|
|
100
97
|
|
|
101
|
-
def
|
|
98
|
+
def select_candidates(type)
|
|
102
99
|
case type
|
|
103
|
-
when :released then
|
|
104
|
-
when :prerelease then
|
|
105
|
-
when :latest then
|
|
100
|
+
when :released then released_candidates
|
|
101
|
+
when :prerelease then prerelease_candidates
|
|
102
|
+
when :latest then latest_candidates
|
|
106
103
|
else @specs.keys
|
|
107
104
|
end
|
|
108
105
|
end
|
|
109
106
|
|
|
110
|
-
def
|
|
111
|
-
@specs.keys.reject { |
|
|
107
|
+
def released_candidates
|
|
108
|
+
@specs.keys.reject { |candidate| candidate.version.prerelease? }
|
|
112
109
|
end
|
|
113
110
|
|
|
114
|
-
def
|
|
115
|
-
@specs.keys.select { |
|
|
111
|
+
def prerelease_candidates
|
|
112
|
+
@specs.keys.select { |candidate| candidate.version.prerelease? }
|
|
116
113
|
end
|
|
117
114
|
|
|
118
|
-
def
|
|
115
|
+
def latest_candidates
|
|
119
116
|
@specs.keys
|
|
120
|
-
.group_by { |
|
|
117
|
+
.group_by { |candidate| [candidate.name, candidate.platform] }
|
|
121
118
|
.values
|
|
122
|
-
.map { |
|
|
119
|
+
.map { |versions| versions.max_by(&:version) }
|
|
123
120
|
end
|
|
124
121
|
|
|
125
122
|
def ensure_specs_loaded
|
|
@@ -128,9 +125,8 @@ class Gem::Source::Vault < Gem::Source
|
|
|
128
125
|
@specs = {}
|
|
129
126
|
Gemvault::Vault.open(@path) do |vault|
|
|
130
127
|
vault.gem_entries.each do |entry|
|
|
131
|
-
spec = vault.spec_from_blob(entry
|
|
132
|
-
|
|
133
|
-
@specs[tuple] = spec
|
|
128
|
+
spec = vault.spec_from_blob(entry)
|
|
129
|
+
@specs[spec.name_tuple] = spec
|
|
134
130
|
end
|
|
135
131
|
end
|
|
136
132
|
end
|
data/lib/rubygems_plugin.rb
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# RubyGems can evaluate this plugin twice under Bundler: once through the
|
|
2
|
+
# install-generated stub (via require) and again through Gem.load_env_plugins
|
|
3
|
+
# (via load, which ignores $LOADED_FEATURES) once the gem's lib/ is on
|
|
4
|
+
# $LOAD_PATH. Skip the redundant second evaluation so the constants and
|
|
5
|
+
# monkey-patches below are installed exactly once.
|
|
6
|
+
return if defined?(Gemvault::PLUGIN_LOADED)
|
|
7
|
+
|
|
1
8
|
require_relative "rubygems/source/vault"
|
|
2
9
|
require "rubygems/source_list"
|
|
3
10
|
|
|
@@ -14,6 +21,8 @@ require "rubygems/source_list"
|
|
|
14
21
|
# 3. SourceList#<< -- route .gemv strings to Gem::Source::Vault
|
|
15
22
|
|
|
16
23
|
module Gemvault
|
|
24
|
+
PLUGIN_LOADED = true
|
|
25
|
+
|
|
17
26
|
# Lets .gemv paths bypass RubyGems' URI scheme validation.
|
|
18
27
|
module AcceptVaultURI
|
|
19
28
|
VALID_URI_SCHEMES = ["http", "https", "file", "s3"].freeze
|
|
@@ -22,8 +31,7 @@ module Gemvault
|
|
|
22
31
|
Gem::OptionParser.accept Gem::URI::HTTP do |value|
|
|
23
32
|
next value if value.to_s.end_with?(".gemv")
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
validate_scheme!(uri, value)
|
|
34
|
+
validate_uri!(value)
|
|
27
35
|
value
|
|
28
36
|
end
|
|
29
37
|
end
|
|
@@ -31,12 +39,15 @@ module Gemvault
|
|
|
31
39
|
private
|
|
32
40
|
|
|
33
41
|
def parse_uri(value)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
begin
|
|
43
|
+
Gem::URI.parse value
|
|
44
|
+
rescue Gem::URI::InvalidURIError
|
|
45
|
+
raise Gem::OptionParser::InvalidArgument, value
|
|
46
|
+
end
|
|
37
47
|
end
|
|
38
48
|
|
|
39
|
-
def
|
|
49
|
+
def validate_uri!(value)
|
|
50
|
+
uri = parse_uri(value)
|
|
40
51
|
return if VALID_URI_SCHEMES.include?(uri.scheme)
|
|
41
52
|
|
|
42
53
|
schemes = VALID_URI_SCHEMES.map { |scheme| "#{scheme}://" }
|