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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Gemvault
2
+ VERSION = "0.1.1".freeze
3
+ end
data/lib/gemvault.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative "gemvault/version"
2
+ require_relative "gemvault/vault"
3
+
4
+ ##
5
+ # Top-level namespace for the gemvault gem.
6
+ module Gemvault
7
+ ##
8
+ # Base error class for gemvault.
9
+ class Error < StandardError; end
10
+ end
@@ -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