gemvault 0.1.1
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +181 -0
- data/.ruby-version +1 -0
- data/ASSESSMENT.md +88 -0
- data/CLAUDE.md +76 -0
- data/Dockerfile.test +11 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/MACROPLAN.md +63 -0
- data/README.md +74 -0
- data/Rakefile +39 -0
- data/docs/superpowers/plans/2026-04-16-container-integration-tests.md +605 -0
- data/exe/gemvault +5 -0
- data/lib/bundler/plugin/vault_source.rb +117 -0
- data/lib/gemvault/cli/command.rb +17 -0
- data/lib/gemvault/cli/commands/add.rb +30 -0
- data/lib/gemvault/cli/commands/extract.rb +49 -0
- data/lib/gemvault/cli/commands/list.rb +26 -0
- data/lib/gemvault/cli/commands/new.rb +28 -0
- data/lib/gemvault/cli/commands/remove.rb +34 -0
- data/lib/gemvault/cli.rb +45 -0
- data/lib/gemvault/gem_entry.rb +37 -0
- data/lib/gemvault/vault.rb +178 -0
- data/lib/gemvault/version.rb +3 -0
- data/lib/gemvault.rb +10 -0
- data/lib/rubygems/resolver/vault_set.rb +39 -0
- data/lib/rubygems/source/vault.rb +124 -0
- data/lib/rubygems_plugin.rb +72 -0
- metadata +116 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "../../gemvault/vault"
|
|
3
|
+
|
|
4
|
+
module Bundler
|
|
5
|
+
module Plugin
|
|
6
|
+
class VaultSource
|
|
7
|
+
def initialize(opts)
|
|
8
|
+
super
|
|
9
|
+
@vault_path = resolve_vault_path(@uri)
|
|
10
|
+
validate_vault_exists!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fetch_gemspec_files
|
|
14
|
+
gemspec_files = []
|
|
15
|
+
|
|
16
|
+
Gemvault::Vault.open(@vault_path) do |vault|
|
|
17
|
+
vault.gem_entries.each do |entry|
|
|
18
|
+
spec = vault.spec_from_blob(entry.name, entry.version, entry.platform)
|
|
19
|
+
full_name = spec.full_name
|
|
20
|
+
spec_ruby = spec.to_ruby
|
|
21
|
+
|
|
22
|
+
gem_dir = gem_dir_for(full_name)
|
|
23
|
+
if File.directory?(gem_dir)
|
|
24
|
+
gemspec_files << anchor_gemspec(gem_dir, full_name, spec_ruby)
|
|
25
|
+
else
|
|
26
|
+
gemspec_dir = File.join(Bundler.tmp("vault_source"), "specifications")
|
|
27
|
+
FileUtils.mkdir_p(gemspec_dir)
|
|
28
|
+
gemspec_path = File.join(gemspec_dir, "#{full_name}.gemspec")
|
|
29
|
+
File.write(gemspec_path, spec_ruby)
|
|
30
|
+
gemspec_files << gemspec_path
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
gemspec_files
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def install(spec, opts = {})
|
|
39
|
+
gem_dir = gem_dir_for(spec.full_name)
|
|
40
|
+
if File.directory?(gem_dir) && !opts[:force]
|
|
41
|
+
Bundler.ui.debug "Using #{version_message(spec)} from vault #{File.basename(@vault_path)}"
|
|
42
|
+
gemspec_in_gem = File.join(gem_dir, "#{spec.full_name}.gemspec")
|
|
43
|
+
spec.full_gem_path = gem_dir
|
|
44
|
+
spec.loaded_from = gemspec_in_gem
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Bundler.ui.confirm "Installing #{version_message(spec)} from vault #{File.basename(@vault_path)}"
|
|
49
|
+
|
|
50
|
+
Gemvault::Vault.open(@vault_path) do |vault|
|
|
51
|
+
vault.with_gem_file(spec.name, spec.version.to_s, platform: spec.platform.to_s) do |gem_path|
|
|
52
|
+
require "bundler/rubygems_gem_installer"
|
|
53
|
+
|
|
54
|
+
installer = Bundler::RubyGemsGemInstaller.at(
|
|
55
|
+
gem_path,
|
|
56
|
+
install_dir: Bundler.bundle_path.to_s,
|
|
57
|
+
bin_dir: Bundler.system_bindir.to_s,
|
|
58
|
+
ignore_dependencies: true,
|
|
59
|
+
wrappers: true,
|
|
60
|
+
env_shebang: true,
|
|
61
|
+
build_args: opts[:build_args] || [],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
installed_spec = installer.install
|
|
65
|
+
|
|
66
|
+
gem_dir = installed_spec.full_gem_path
|
|
67
|
+
spec.full_gem_path = gem_dir
|
|
68
|
+
spec.loaded_from = anchor_gemspec(gem_dir, spec.full_name, installed_spec.to_ruby)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
spec.post_install_message
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def options_to_lock
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_s
|
|
80
|
+
"vault at #{@uri}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def version_message(spec)
|
|
86
|
+
message = "#{spec.name} #{spec.version}"
|
|
87
|
+
message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil?
|
|
88
|
+
message
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_vault_path(uri)
|
|
92
|
+
File.expand_path(uri, Bundler.root.to_s)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Bundler computes full_gem_path as dirname(loaded_from) for plugin
|
|
96
|
+
# sources, so the gemspec must live inside the gem directory -- not
|
|
97
|
+
# in specifications/ -- for load paths to resolve correctly.
|
|
98
|
+
def anchor_gemspec(gem_dir, full_name, spec_ruby)
|
|
99
|
+
gemspec_path = File.join(gem_dir, "#{full_name}.gemspec")
|
|
100
|
+
File.write(gemspec_path, spec_ruby) unless File.exist?(gemspec_path)
|
|
101
|
+
gemspec_path
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def gem_dir_for(full_name)
|
|
105
|
+
File.join(Bundler.bundle_path, "gems", full_name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_vault_exists!
|
|
109
|
+
return if File.file?(@vault_path)
|
|
110
|
+
|
|
111
|
+
raise Bundler::PathError,
|
|
112
|
+
"Could not find vault '#{@uri}' referenced in Gemfile " \
|
|
113
|
+
"(relative to #{Bundler.root})"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "command_kit/command"
|
|
2
|
+
require_relative "../vault"
|
|
3
|
+
|
|
4
|
+
module Gemvault
|
|
5
|
+
class CLI
|
|
6
|
+
class Command < CommandKit::Command
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def with_vault(path, create: false, &block)
|
|
10
|
+
Gemvault::Vault.open(path, create: create, &block)
|
|
11
|
+
rescue Gemvault::Vault::Error => e
|
|
12
|
+
print_error(e.message)
|
|
13
|
+
exit(1)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative "../command"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Add < Command
|
|
7
|
+
description "Add gem files to a vault"
|
|
8
|
+
|
|
9
|
+
argument :vault, required: true,
|
|
10
|
+
usage: "VAULT",
|
|
11
|
+
desc: "Vault file"
|
|
12
|
+
|
|
13
|
+
argument :gems, required: true,
|
|
14
|
+
repeats: true,
|
|
15
|
+
usage: "GEM",
|
|
16
|
+
desc: "Gem files to add"
|
|
17
|
+
|
|
18
|
+
def run(vault, *gems)
|
|
19
|
+
with_vault(vault) do |v|
|
|
20
|
+
gems.each do |gem_path|
|
|
21
|
+
v.add(gem_path)
|
|
22
|
+
spec = Gem::Package.new(gem_path).spec
|
|
23
|
+
puts "Added #{spec.name}-#{spec.version}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "../command"
|
|
3
|
+
|
|
4
|
+
module Gemvault
|
|
5
|
+
class CLI
|
|
6
|
+
module Commands
|
|
7
|
+
class Extract < Command
|
|
8
|
+
description "Extract gem file(s) from a vault"
|
|
9
|
+
|
|
10
|
+
argument :vault, required: true,
|
|
11
|
+
usage: "VAULT",
|
|
12
|
+
desc: "Vault file"
|
|
13
|
+
|
|
14
|
+
argument :name, required: true,
|
|
15
|
+
usage: "NAME",
|
|
16
|
+
desc: "Gem name"
|
|
17
|
+
|
|
18
|
+
argument :version, required: false,
|
|
19
|
+
usage: "VERSION",
|
|
20
|
+
desc: "Gem version (omit to extract all versions)"
|
|
21
|
+
|
|
22
|
+
option :output, short: "-o",
|
|
23
|
+
value: { type: String, default: "." },
|
|
24
|
+
desc: "Output directory"
|
|
25
|
+
|
|
26
|
+
def run(vault, name, version = nil)
|
|
27
|
+
output_dir = options[:output]
|
|
28
|
+
|
|
29
|
+
with_vault(vault) do |v|
|
|
30
|
+
::FileUtils.mkdir_p(output_dir)
|
|
31
|
+
|
|
32
|
+
entries = v.gem_entries.select { |e| e.name == name }
|
|
33
|
+
entries = entries.select { |e| e.version == version } if version
|
|
34
|
+
if entries.empty?
|
|
35
|
+
print_error("No gem named '#{name}' in vault")
|
|
36
|
+
exit(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
entries.each do |entry|
|
|
40
|
+
data = v.gem_data(entry.name, entry.version, platform: entry.platform)
|
|
41
|
+
File.binwrite(File.join(output_dir, entry.filename), data)
|
|
42
|
+
puts "Extracted #{entry.filename}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative "../command"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class List < Command
|
|
7
|
+
description "List gems in a vault"
|
|
8
|
+
|
|
9
|
+
argument :vault, required: true,
|
|
10
|
+
usage: "VAULT",
|
|
11
|
+
desc: "Vault file"
|
|
12
|
+
|
|
13
|
+
def run(vault)
|
|
14
|
+
with_vault(vault) do |v|
|
|
15
|
+
entries = v.gem_entries
|
|
16
|
+
if entries.empty?
|
|
17
|
+
puts "Vault is empty"
|
|
18
|
+
else
|
|
19
|
+
entries.each { |entry| puts entry }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "../command"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class New < Command
|
|
7
|
+
description "Create a new vault"
|
|
8
|
+
|
|
9
|
+
argument :name, required: true,
|
|
10
|
+
usage: "NAME",
|
|
11
|
+
desc: "Vault name (auto-appends .gemv)"
|
|
12
|
+
|
|
13
|
+
def run(name)
|
|
14
|
+
path = name.end_with?(".gemv") ? name : "#{name}.gemv"
|
|
15
|
+
|
|
16
|
+
if File.exist?(path)
|
|
17
|
+
print_error("#{path} already exists")
|
|
18
|
+
exit(1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
vault = Vault.new(path, create: true)
|
|
22
|
+
vault.close
|
|
23
|
+
puts "Created #{path}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative "../command"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Remove < Command
|
|
7
|
+
description "Remove gem(s) from a vault"
|
|
8
|
+
|
|
9
|
+
argument :vault, required: true,
|
|
10
|
+
usage: "VAULT",
|
|
11
|
+
desc: "Vault file"
|
|
12
|
+
|
|
13
|
+
argument :name, required: true,
|
|
14
|
+
usage: "NAME",
|
|
15
|
+
desc: "Gem name"
|
|
16
|
+
|
|
17
|
+
argument :version, required: false,
|
|
18
|
+
usage: "VERSION",
|
|
19
|
+
desc: "Gem version (omit to remove all versions)"
|
|
20
|
+
|
|
21
|
+
def run(vault, name, version = nil)
|
|
22
|
+
with_vault(vault) do |v|
|
|
23
|
+
count = v.remove(name, version)
|
|
24
|
+
if count.zero?
|
|
25
|
+
print_error("No matching gem found")
|
|
26
|
+
exit(1)
|
|
27
|
+
end
|
|
28
|
+
puts "Removed #{count} gem(s)"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/gemvault/cli.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "command_kit/command"
|
|
2
|
+
require "command_kit/commands/auto_load"
|
|
3
|
+
require "command_kit/options/version"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
|
|
6
|
+
module Gemvault
|
|
7
|
+
class CLI < CommandKit::Command
|
|
8
|
+
include CommandKit::Commands::AutoLoad.new(
|
|
9
|
+
dir: "#{__dir__}/cli/commands",
|
|
10
|
+
namespace: "#{name}::Commands",
|
|
11
|
+
)
|
|
12
|
+
include CommandKit::Options::Version
|
|
13
|
+
|
|
14
|
+
version Gemvault::VERSION
|
|
15
|
+
command_name "gemvault"
|
|
16
|
+
description "Manage gem vault archives"
|
|
17
|
+
|
|
18
|
+
examples [
|
|
19
|
+
"new myvault",
|
|
20
|
+
"add myvault.gemv foo-1.0.0.gem bar-2.0.0.gem",
|
|
21
|
+
"list myvault.gemv",
|
|
22
|
+
"extract myvault.gemv foo -o vendor/",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def run(command = nil, *argv)
|
|
26
|
+
if command
|
|
27
|
+
super
|
|
28
|
+
else
|
|
29
|
+
help
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def help
|
|
34
|
+
super
|
|
35
|
+
puts
|
|
36
|
+
puts "Gemfile usage:"
|
|
37
|
+
puts " # REQUIRED until bundler-source-vault is published to rubygems.org:"
|
|
38
|
+
puts ' plugin "bundler-source-vault", path: "/path/to/gemvault"'
|
|
39
|
+
puts
|
|
40
|
+
puts ' source "myvault.gemv", type: :vault do'
|
|
41
|
+
puts ' gem "foo"'
|
|
42
|
+
puts " end"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Gemvault
|
|
2
|
+
class GemEntry
|
|
3
|
+
attr_reader :name, :version, :platform, :created_at
|
|
4
|
+
|
|
5
|
+
def initialize(name:, version:, platform: "ruby", created_at: nil)
|
|
6
|
+
@name = name
|
|
7
|
+
@version = version
|
|
8
|
+
@platform = platform
|
|
9
|
+
@created_at = created_at
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def full_name
|
|
13
|
+
platform == "ruby" ? "#{name}-#{version}" : "#{name}-#{version}-#{platform}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def filename
|
|
17
|
+
"#{full_name}.gem"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
platform == "ruby" ? "#{name}-#{version}" : "#{name}-#{version} (#{platform})"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ==(other)
|
|
25
|
+
other.is_a?(self.class) &&
|
|
26
|
+
name == other.name &&
|
|
27
|
+
version == other.version &&
|
|
28
|
+
platform == other.platform
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash
|
|
34
|
+
[name, version, platform].hash
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require "sqlite3"
|
|
2
|
+
require "rubygems/package"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "gem_entry"
|
|
6
|
+
|
|
7
|
+
module Gemvault
|
|
8
|
+
class Vault
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
class NotFoundError < Error; end
|
|
11
|
+
class DuplicateGemError < Error; end
|
|
12
|
+
class InvalidGemError < Error; end
|
|
13
|
+
|
|
14
|
+
SCHEMA_VERSION = "1".freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :path
|
|
17
|
+
|
|
18
|
+
def self.open(path, **opts, &block)
|
|
19
|
+
raise ArgumentError, "#{name}.open requires a block" unless block
|
|
20
|
+
|
|
21
|
+
vault = new(path, **opts)
|
|
22
|
+
begin
|
|
23
|
+
yield vault
|
|
24
|
+
ensure
|
|
25
|
+
vault.close
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(path, create: false)
|
|
30
|
+
@path = File.expand_path(path)
|
|
31
|
+
|
|
32
|
+
if create
|
|
33
|
+
raise Error, "Vault already exists: #{@path}" if File.exist?(@path)
|
|
34
|
+
|
|
35
|
+
@db = SQLite3::Database.new(@path)
|
|
36
|
+
@db.results_as_hash = true
|
|
37
|
+
create_schema
|
|
38
|
+
else
|
|
39
|
+
raise NotFoundError, "Vault not found: #{@path}" unless File.exist?(@path)
|
|
40
|
+
|
|
41
|
+
validate_sqlite!
|
|
42
|
+
@db = SQLite3::Database.new(@path)
|
|
43
|
+
@db.results_as_hash = true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add(gem_path)
|
|
48
|
+
gem_path = File.expand_path(gem_path)
|
|
49
|
+
raise NotFoundError, "Gem file not found: #{gem_path}" unless File.file?(gem_path)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
pkg = Gem::Package.new(gem_path)
|
|
53
|
+
spec = pkg.spec
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
name = spec.name
|
|
59
|
+
version = spec.version.to_s
|
|
60
|
+
platform = spec.platform.to_s
|
|
61
|
+
|
|
62
|
+
existing = @db.execute(
|
|
63
|
+
"SELECT 1 FROM gems WHERE name = ? AND version = ? AND platform = ?",
|
|
64
|
+
[name, version, platform],
|
|
65
|
+
)
|
|
66
|
+
unless existing.empty?
|
|
67
|
+
raise DuplicateGemError,
|
|
68
|
+
"Gem already in vault: #{name}-#{version} (#{platform})"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
data = File.binread(gem_path)
|
|
72
|
+
@db.execute(
|
|
73
|
+
"INSERT INTO gems (name, version, platform, data) VALUES (?, ?, ?, ?)",
|
|
74
|
+
[name, version, platform, SQLite3::Blob.new(data)],
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def remove(name, version = nil)
|
|
79
|
+
if version
|
|
80
|
+
@db.execute(
|
|
81
|
+
"DELETE FROM gems WHERE name = ? AND version = ?",
|
|
82
|
+
[name, version],
|
|
83
|
+
)
|
|
84
|
+
else
|
|
85
|
+
@db.execute(
|
|
86
|
+
"DELETE FROM gems WHERE name = ?",
|
|
87
|
+
[name],
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
@db.changes
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def gem_data(name, version, platform: "ruby")
|
|
94
|
+
row = @db.execute(
|
|
95
|
+
"SELECT data FROM gems WHERE name = ? AND version = ? AND platform = ?",
|
|
96
|
+
[name, version, platform],
|
|
97
|
+
).first
|
|
98
|
+
|
|
99
|
+
raise NotFoundError, "Gem not found: #{name}-#{version} (#{platform})" unless row
|
|
100
|
+
|
|
101
|
+
row["data"]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def specs
|
|
105
|
+
gem_entries.map { |entry| spec_from_blob(entry.name, entry.version, entry.platform) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def gem_entries
|
|
109
|
+
@db.execute(
|
|
110
|
+
"SELECT name, version, platform, created_at FROM gems ORDER BY name, version",
|
|
111
|
+
).map { |row| GemEntry.new(**row.transform_keys(&:to_sym)) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def size
|
|
115
|
+
@db.execute("SELECT COUNT(*) AS count FROM gems").first["count"]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def close
|
|
119
|
+
@db.close if @db && !@db.closed?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def with_gem_file(name, version, platform: "ruby")
|
|
123
|
+
data = gem_data(name, version, platform: platform)
|
|
124
|
+
tmpfile = Tempfile.new(["vault_gem", ".gem"])
|
|
125
|
+
begin
|
|
126
|
+
tmpfile.binmode
|
|
127
|
+
tmpfile.write(data)
|
|
128
|
+
tmpfile.close
|
|
129
|
+
yield tmpfile.path
|
|
130
|
+
ensure
|
|
131
|
+
tmpfile.close unless tmpfile.closed?
|
|
132
|
+
tmpfile.unlink
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def spec_from_blob(name, version, platform = "ruby")
|
|
137
|
+
with_gem_file(name, version, platform: platform) do |path|
|
|
138
|
+
Gem::Package.new(path).spec
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def create_schema
|
|
145
|
+
@db.execute_batch(<<~SQL)
|
|
146
|
+
CREATE TABLE metadata (
|
|
147
|
+
key TEXT PRIMARY KEY,
|
|
148
|
+
value TEXT NOT NULL
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE gems (
|
|
152
|
+
name TEXT NOT NULL,
|
|
153
|
+
version TEXT NOT NULL,
|
|
154
|
+
platform TEXT NOT NULL DEFAULT 'ruby',
|
|
155
|
+
data BLOB NOT NULL,
|
|
156
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
157
|
+
PRIMARY KEY (name, version, platform)
|
|
158
|
+
);
|
|
159
|
+
SQL
|
|
160
|
+
|
|
161
|
+
@db.execute(
|
|
162
|
+
"INSERT INTO metadata (key, value) VALUES (?, ?)",
|
|
163
|
+
["vault_version", SCHEMA_VERSION],
|
|
164
|
+
)
|
|
165
|
+
@db.execute(
|
|
166
|
+
"INSERT INTO metadata (key, value) VALUES (?, ?)",
|
|
167
|
+
["created_at", Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")],
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def validate_sqlite!
|
|
172
|
+
magic = File.binread(@path, 16)
|
|
173
|
+
return if magic == "SQLite format 3\x00"
|
|
174
|
+
|
|
175
|
+
raise Error, "Not a valid vault file (not SQLite): #{@path}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/gemvault.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
##
|
|
2
|
+
# A VaultSet looks up specifications from a .gemv vault source.
|
|
3
|
+
#
|
|
4
|
+
# Returns standard Gem::Resolver::IndexSpecification objects so the
|
|
5
|
+
# resolver's install pipeline (download -> Gem::Installer) works unchanged.
|
|
6
|
+
|
|
7
|
+
class Gem::Resolver::VaultSet < Gem::Resolver::Set
|
|
8
|
+
def initialize(source)
|
|
9
|
+
super()
|
|
10
|
+
@source = source
|
|
11
|
+
@specs = source.load_specs(:complete)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def find_all(req)
|
|
15
|
+
@specs.select { |tuple| req.match?(tuple) }.map do |tuple|
|
|
16
|
+
Gem::Resolver::IndexSpecification.new(
|
|
17
|
+
self,
|
|
18
|
+
tuple.name,
|
|
19
|
+
tuple.version,
|
|
20
|
+
@source,
|
|
21
|
+
tuple.platform,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prefetch(reqs); end
|
|
27
|
+
|
|
28
|
+
def pretty_print(q)
|
|
29
|
+
q.group 2, "[VaultSet", "]" do
|
|
30
|
+
next if @specs.empty?
|
|
31
|
+
|
|
32
|
+
q.breakable
|
|
33
|
+
|
|
34
|
+
q.seplist @specs do |tuple|
|
|
35
|
+
q.text tuple.full_name
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|