gemvault 0.1.1 → 0.1.3
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/.rubocop.yml +1 -0
- data/CHANGELOG.md +29 -0
- data/CLAUDE.md +24 -1
- data/README.md +21 -0
- data/Rakefile +8 -1
- data/issues.rec +160 -0
- data/lib/bundler/plugin/vault_source.rb +59 -51
- data/lib/gemvault/cli/command.rb +1 -0
- data/lib/gemvault/cli/commands/add.rb +1 -0
- data/lib/gemvault/cli/commands/doctor.rb +28 -0
- data/lib/gemvault/cli/commands/extract.rb +23 -12
- data/lib/gemvault/cli/commands/list.rb +1 -0
- data/lib/gemvault/cli/commands/new.rb +1 -0
- data/lib/gemvault/cli/commands/remove.rb +23 -9
- data/lib/gemvault/cli.rb +1 -0
- data/lib/gemvault/gem_entry.rb +1 -0
- data/lib/gemvault/gem_reference/any_version.rb +7 -0
- data/lib/gemvault/gem_reference/specific_version.rb +29 -0
- data/lib/gemvault/gem_reference.rb +54 -0
- data/lib/gemvault/vault.rb +68 -53
- data/lib/gemvault/version.rb +1 -1
- data/lib/rubygems/resolver/vault_set.rb +16 -15
- data/lib/rubygems/source/vault.rb +47 -34
- data/lib/rubygems_plugin.rb +22 -12
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e990085de84c0a510129de569ba8ed103bb12d78dc5cb2a6b666eb4a9e8303d
|
|
4
|
+
data.tar.gz: 7c37b0d7ada34a481ed270185a120509a90d296c1bee01a596803e0e33d0a149
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3e09ff83f50247af6f394a7e1cae36387ddeb5de859c7ef845c024d13bc9062cff800f9061202e3af8ca3bffc6dfbc2e9cc7029986f726bd6bfa1221ca9e80b
|
|
7
|
+
data.tar.gz: 7d864a45342f84b6b82f0e168c8f9c2e9378bae0fe5fe4ff46bf2c5a454dfdda8f3c90f8446b82fad61caecb8ab136f385a2e6fd85de66829cad3f144141a416
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.3] - 2026-06-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `gemvault doctor` command to recover from a broken `bundler-source-vault`
|
|
12
|
+
plugin index entry after its path-installed source directory has been renamed
|
|
13
|
+
(issue #1).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- `gem install --source vault://<absolute-path>` no longer resolves the vault
|
|
17
|
+
relative to the working directory; the `vault://` scheme is now stripped just
|
|
18
|
+
like `file://` (issue #5).
|
|
19
|
+
- `bundle install` logs now show the vault path as written in the Gemfile
|
|
20
|
+
(e.g. `vendor/vendored.gemv`) instead of only the basename (issue #3).
|
|
21
|
+
- Renaming a `.gemv` file and updating the Gemfile to match no longer crashes
|
|
22
|
+
`bundle install` on the stale lockfile entry; the vault existence check is
|
|
23
|
+
deferred until the source is actually queried (issue #2).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- CI runs the unit and container-backed integration suites as separate jobs, and
|
|
27
|
+
the codebase is RuboCop-clean.
|
|
28
|
+
|
|
29
|
+
[0.1.3]: https://github.com/gillisd/gemvault/releases/tag/v0.1.3
|
data/CLAUDE.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# CLAUDE.md — Gemvault
|
|
2
2
|
|
|
3
|
+
## Rubocop
|
|
4
|
+
|
|
5
|
+
Do NOT modify `.rubocop.yml` or use inline `# rubocop:disable` tags without explicit permission. Fix the code to satisfy the cop instead.
|
|
6
|
+
|
|
7
|
+
## General rules
|
|
8
|
+
|
|
9
|
+
1. Specs always come first. Every plan should start with the skeleton of the BDD specs being added, changed or removed. Skeleton means the empty RSpec language, without implementation.
|
|
10
|
+
2. Specs should never have comments. Any urge to put a comment in a spec means that comment should probably be its own spec
|
|
11
|
+
3. DO NOT edit .rubocop.yml or add inline rubocop exemptions without explicit permission
|
|
12
|
+
4. DO NOT run any git command that will rewrite history without explicit permission
|
|
13
|
+
5. PREFER method & class extraction over comments
|
|
14
|
+
6. Making new files, classes, modules, and methods IS NOT overengineering
|
|
15
|
+
7. BEFORE writing code, identify which domain concept owns the behavior. Each class and module should have a single responsibility. If the new behavior doesn't fit an existing class's responsibility, create a new one — don't expand the scope of what's already there.
|
|
16
|
+
8. DO NOT name classes with suffixes like "-er" or "-or" unless using a canonical pattern name (e.g., Parser, Router, Controller)
|
|
17
|
+
9. ALWAYS write specs first. The workflow is: identify the domain concept (rule 5), write specs describing its behavior, then implement. No implementation without a failing spec.
|
|
18
|
+
10. Integration specs are the first line of defense for CLI-tool bugs. For any bug reported from using the CLI tool (not the gemvault lib / Ruby API), the FIRST spec you write is an integration spec that reproduces the user's exact invocation — real subprocess, real vault, real exit code. Stub-heavy unit specs are complementary, not sufficient: they prove internal logic produces the expected value assuming surrounding wiring works, but a user's bug report is evidence the wiring didn't work.
|
|
19
|
+
11. If an integration spec is not catching a reported CLI-tool bug, one of two things is true, and the fix starts by diagnosing which: (a) existing integration specs are not specific enough — extend them to cover the exact scenario before touching production code; or (b) the scenario is not spec'd at all, which means the work is not a bug fix but a new feature — write integration specs for the contract first (per rule 1), then implement.
|
|
20
|
+
|
|
21
|
+
## Additional rules
|
|
22
|
+
|
|
23
|
+
1. NEVER use Ruby's `sleep` method
|
|
24
|
+
2. NEVER create any class ending in "er" or "or"
|
|
25
|
+
|
|
3
26
|
## Project Overview
|
|
4
27
|
|
|
5
28
|
Multi-gem portable archives backed by SQLite. A single `.gemv` file contains multiple `.gem` files.
|
|
@@ -57,7 +80,7 @@ gem install --source file:///path/to/myvault.gemv foo
|
|
|
57
80
|
## Testing
|
|
58
81
|
|
|
59
82
|
```bash
|
|
60
|
-
bundle exec rake test
|
|
83
|
+
bundle exec rake test
|
|
61
84
|
```
|
|
62
85
|
|
|
63
86
|
- `test/vault_test.rb` — 33 unit tests for Vault class
|
data/README.md
CHANGED
|
@@ -58,6 +58,27 @@ When Bundler sees `type: :vault` in your Gemfile, it auto-installs the `bundler-
|
|
|
58
58
|
|
|
59
59
|
The RubyGems plugin works similarly: `gem install --source vault.gemv` loads specs and extracts gems on demand.
|
|
60
60
|
|
|
61
|
+
## Recovering from a broken bundler plugin path: `gemvault doctor`
|
|
62
|
+
|
|
63
|
+
If you installed `bundler-source-vault` from a local path (e.g. `plugin "bundler-source-vault", path: "/path/to/gemvault"` in a Gemfile), bundler records that absolute path in its plugin index. Moving, renaming, or deleting the source directory afterwards invalidates the stored path, and the next `bundle install` prints:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
The following plugin paths don't exist: /path/to/gemvault/shim/.
|
|
67
|
+
Continuing without installing plugin bundler-source-vault.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Once the plugin skips loading, bundler crashes with `NoMethodError: undefined method 'new' for nil` on any Gemfile that uses `type: :vault`. This is a bundler limitation — the plugin index isn't revalidated against the filesystem, and there's no plugin-side hook that fires early enough to preempt it.
|
|
71
|
+
|
|
72
|
+
To recover, update the Gemfile to point at the new path and run:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
gemvault doctor
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`doctor` clears the broken entry from bundler's plugin index (`bundle plugin uninstall bundler-source-vault`) and then re-runs `bundle install`, which reinstalls the plugin against whatever the current Gemfile declares. Run it from your project directory.
|
|
79
|
+
|
|
80
|
+
The published `bundler-source-vault` gem installed from rubygems.org is immune to this: it lives in a bundler-managed directory that does not move.
|
|
81
|
+
|
|
61
82
|
## Development
|
|
62
83
|
|
|
63
84
|
```bash
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,9 @@ end
|
|
|
8
8
|
|
|
9
9
|
require "rspec/core/rake_task"
|
|
10
10
|
|
|
11
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
11
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
12
|
+
t.exclude_pattern = "spec/integration/**/*_spec.rb"
|
|
13
|
+
end
|
|
12
14
|
|
|
13
15
|
require "rubocop/rake_task"
|
|
14
16
|
RuboCop::RakeTask.new
|
|
@@ -27,6 +29,11 @@ namespace :spec do
|
|
|
27
29
|
"-f", "Dockerfile.test",
|
|
28
30
|
"."
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
desc "Run container-backed integration specs (requires Podman; run spec:build first)"
|
|
34
|
+
RSpec::Core::RakeTask.new(:integration) do |t|
|
|
35
|
+
t.pattern = "spec/integration/**/*_spec.rb"
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
namespace :shim do
|
data/issues.rec
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
%rec: Issue
|
|
2
|
+
%key: Id
|
|
3
|
+
%typedef: text_t regexp /^.*$/
|
|
4
|
+
%typedef: Status_t enum open in_progress closed
|
|
5
|
+
%type: Id int
|
|
6
|
+
%type: Name line
|
|
7
|
+
%type: Description text_t
|
|
8
|
+
%type: Status Status_t
|
|
9
|
+
%type: Updated date
|
|
10
|
+
%auto: Id Updated
|
|
11
|
+
%mandatory: Name Description Status
|
|
12
|
+
|
|
13
|
+
Id: 0
|
|
14
|
+
Updated: Wed, 22 Apr 2026 13:07:40 +0000
|
|
15
|
+
Name: removal of gems is not working
|
|
16
|
+
Description: Removal of gems when version is specified is broken. try the following:
|
|
17
|
+
+
|
|
18
|
+
+ ```bash
|
|
19
|
+
+ gemvault remove vault.gemv somegemwithversion-1.0.1
|
|
20
|
+
+ ```
|
|
21
|
+
+
|
|
22
|
+
+ it will raise a not found error
|
|
23
|
+
Status: closed
|
|
24
|
+
|
|
25
|
+
Id: 1
|
|
26
|
+
Updated: Tue, 21 Apr 2026 23:28:44 -0400
|
|
27
|
+
Name: Getting error when dir structure has slightly changed
|
|
28
|
+
Description: Installing bundler-source-vault 0.1.2
|
|
29
|
+
+ The following plugin paths don't exist: /workspace/gemvault/shim/..
|
|
30
|
+
+
|
|
31
|
+
+ This can happen if the plugin was installed with a different version of Ruby that has since been uninstalled.
|
|
32
|
+
+
|
|
33
|
+
+ If you would like to reinstall the plugin, run:
|
|
34
|
+
+
|
|
35
|
+
+ bundler plugin uninstall bundler-source-vault && bundler plugin install bundler-source-vault
|
|
36
|
+
+
|
|
37
|
+
+ Continuing without installing plugin bundler-source-vault.
|
|
38
|
+
+ /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/source_list.rb:59:in 'Bundler::SourceList#add_plugin_source': undefined method 'new' for nil (NoMethodError)
|
|
39
|
+
+
|
|
40
|
+
+ add_source_to_list Plugin.source(source).new(options), @plugin_sources
|
|
41
|
+
+ ^^^^
|
|
42
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:131:in 'Bundler::Dsl#source'
|
|
43
|
+
+ from /Users/davidgillis/repos/tries/2026-03-27-5misc/fooz/Gemfile:5:in 'block in Bundler::Dsl#eval_gemfile'
|
|
44
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:49:in 'BasicObject#instance_eval'
|
|
45
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:49:in 'block in Bundler::Dsl#eval_gemfile'
|
|
46
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:326:in 'Bundler::Dsl#with_gemfile'
|
|
47
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:47:in 'Bundler::Dsl#eval_gemfile'
|
|
48
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:13:in 'Bundler::Dsl.evaluate'
|
|
49
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/definition.rb:40:in 'Bundler::Definition.build'
|
|
50
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler.rb:234:in 'Bundler.definition'
|
|
51
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/env.rb:36:in 'Bundler::Env.report'
|
|
52
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/friendly_errors.rb:71:in 'Bundler::FriendlyErrors.request_issue_report_for'
|
|
53
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/friendly_errors.rb:50:in 'Bundler::FriendlyErrors.log_error'
|
|
54
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/friendly_errors.rb:124:in 'Bundler.with_friendly_errors'
|
|
55
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/exe/bundle:20:in '<top (required)>'
|
|
56
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/site_ruby/4.0.0/rubygems.rb:304:in 'Kernel#load'
|
|
57
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/site_ruby/4.0.0/rubygems.rb:304:in 'Gem.activate_and_load_bin_path'
|
|
58
|
+
+ from /Users/davidgillis/.rbenv/versions/4.0.1/bin/bundle:25:in '<main>'
|
|
59
|
+
+ /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/source_list.rb:59:in 'Bundler::SourceList#add_plugin_source': undefined method 'new' for nil (NoMethodError)
|
|
60
|
+
+
|
|
61
|
+
+ add_source_to_list Plugin.source(source).new(options), @plugin_sources
|
|
62
|
+
Status: open
|
|
63
|
+
|
|
64
|
+
Id: 2
|
|
65
|
+
Updated: Tue, 21 Apr 2026 23:31:42 -0400
|
|
66
|
+
Name: Renaming gemv file breaks bundler config
|
|
67
|
+
Description: Steps to replicate:
|
|
68
|
+
+ 1. add vault source with some gems
|
|
69
|
+
+ 2. move gemv file to another directory in project, rename, and update Gemfile to reflect
|
|
70
|
+
+ 3. Run bundle install
|
|
71
|
+
+ 4. An error is raised that looks something like
|
|
72
|
+
+ ```bash
|
|
73
|
+
+ [dev@5misc2]/workspace/fooz% bundle install
|
|
74
|
+
+ Fetching gem metadata from https://rubygems.org/.......
|
|
75
|
+
+ Resolving dependencies...
|
|
76
|
+
+ Installing command_kit 0.6.0
|
|
77
|
+
+ Installing sqlite3 2.9.3 (aarch64-linux-gnu)
|
|
78
|
+
+ Installing gemvault 0.1.2
|
|
79
|
+
+ Installing bundler-source-vault 0.1.2
|
|
80
|
+
+ Could not find vault 'vendored.gemv' referenced in Gemfile (relative to /workspace/fooz)
|
|
81
|
+
+ ```
|
|
82
|
+
+
|
|
83
|
+
+ Where vendored.gemv is the name of the old file, and there is still an entry in Gemfile.lock
|
|
84
|
+
Status: open
|
|
85
|
+
|
|
86
|
+
Id: 3
|
|
87
|
+
Updated: Tue, 21 Apr 2026 23:42:59 -0400
|
|
88
|
+
Name: Name of vault in logs should show relative path
|
|
89
|
+
Description: This vault is in vendor/vendored_gems.gmv, relative to the Gemfile
|
|
90
|
+
+ in the logs of bundle install I see:
|
|
91
|
+
+
|
|
92
|
+
+ ```
|
|
93
|
+
+ Installing acme 1.0.1 from vault vendored_gems.gemv
|
|
94
|
+
+ ```
|
|
95
|
+
Status: open
|
|
96
|
+
|
|
97
|
+
Id: 4
|
|
98
|
+
Updated: Wed, 22 Apr 2026 12:16:44 -0400
|
|
99
|
+
Name: Running bundle install reinstalls gemvault deps every single run
|
|
100
|
+
Description: Anytime I run bundle install, I always see:
|
|
101
|
+
+
|
|
102
|
+
+ ```
|
|
103
|
+
+ Fetching gem metadata from https://rubygems.org/.......
|
|
104
|
+
+ Resolving dependencies...
|
|
105
|
+
+ Installing command_kit 0.6.0
|
|
106
|
+
+ Installing sqlite3 2.9.3 (aarch64-linux-gnu)
|
|
107
|
+
+ Installing gemvault 0.1.2
|
|
108
|
+
+ Installing bundler-source-vault 0.1.2
|
|
109
|
+
+ ```
|
|
110
|
+
+
|
|
111
|
+
+ This is unacceptable. these should only need to be installed once
|
|
112
|
+
Status: open
|
|
113
|
+
|
|
114
|
+
Id: 5
|
|
115
|
+
Updated: Wed, 22 Apr 2026 18:24:09 -0400
|
|
116
|
+
Name: Installing gem via rubygems fails to resolve absolute path
|
|
117
|
+
Description: Consider the following:
|
|
118
|
+
+ ```
|
|
119
|
+
+ ng command gem install --source vault:///Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv acme
|
|
120
|
+
+ ERROR: While executing gem ... (Gemvault::Vault::NotFoundError)
|
|
121
|
+
+ Vault not found: /private/tmp/local/ddog/vault:/Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv
|
|
122
|
+
+ ```
|
|
123
|
+
+
|
|
124
|
+
+ Notice an absolute path was passed to --source, yet it tried to resolve locally
|
|
125
|
+
Status: open
|
|
126
|
+
|
|
127
|
+
Id: 6
|
|
128
|
+
Updated: Thu, 23 Apr 2026 11:04:39 -0400
|
|
129
|
+
Name: Cannot add gem with non-numerical suffix
|
|
130
|
+
Description: When adding a gem like 'ronin-db-0.2.1.patch1.gem', gemvault successfully adds the gem, but the gem install command fails:
|
|
131
|
+
+ ```
|
|
132
|
+
+ gemvault add myvault.gemv ronin-db-0.2.1.patch1.gem # success
|
|
133
|
+
+ gem install --debug --verbose --clear-sources --source file://myvault.gemv ronin-db # failure
|
|
134
|
+
+ ```
|
|
135
|
+
Status: open
|
|
136
|
+
|
|
137
|
+
Id: 7
|
|
138
|
+
Updated: Fri, 19 Jun 2026 10:42:46 -0400
|
|
139
|
+
Name: Gemvault pollutes project dir with plugin files in .bundle
|
|
140
|
+
Description: Why is it not using the plugin that is already installed globally?
|
|
141
|
+
Status: open
|
|
142
|
+
|
|
143
|
+
Id: 8
|
|
144
|
+
Updated: Wed, 22 Apr 2026 18:24:09 -0400
|
|
145
|
+
Name: Installing gem via rubygems fails to resolve absolute path
|
|
146
|
+
Description: Consider the following:
|
|
147
|
+
+ ```
|
|
148
|
+
+ ng command gem install --source vault:///Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv acme
|
|
149
|
+
+ ERROR: While executing gem ... (Gemvault::Vault::NotFoundError)
|
|
150
|
+
+ Vault not found: /private/tmp/local/ddog/vault:/Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv
|
|
151
|
+
+ ```
|
|
152
|
+
+
|
|
153
|
+
+ Notice an absolute path was passed to --source, yet it tried to resolve locally
|
|
154
|
+
Status: open
|
|
155
|
+
|
|
156
|
+
Id: 9
|
|
157
|
+
Updated: Sun, 28 Jun 2026 00:17:27 -0400
|
|
158
|
+
Name: Gemvault commands should also be able to accept a uri
|
|
159
|
+
Description: e.g. gemvault list vault:///Users/davidgillis/repos/vault/rubylib/myvault.gemv
|
|
160
|
+
Status: open
|
|
@@ -3,72 +3,27 @@ require_relative "../../gemvault/vault"
|
|
|
3
3
|
|
|
4
4
|
module Bundler
|
|
5
5
|
module Plugin
|
|
6
|
+
# Bundler plugin source that installs gems from a .gemv vault file.
|
|
6
7
|
class VaultSource
|
|
7
8
|
def initialize(opts)
|
|
8
9
|
super
|
|
9
10
|
@vault_path = resolve_vault_path(@uri)
|
|
10
|
-
validate_vault_exists!
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def fetch_gemspec_files
|
|
14
|
-
|
|
14
|
+
validate_vault_exists!
|
|
15
15
|
|
|
16
16
|
Gemvault::Vault.open(@vault_path) do |vault|
|
|
17
|
-
vault.gem_entries.
|
|
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
|
|
17
|
+
vault.gem_entries.map { |entry| gemspec_file_for(vault, entry) }
|
|
33
18
|
end
|
|
34
|
-
|
|
35
|
-
gemspec_files
|
|
36
19
|
end
|
|
37
20
|
|
|
38
21
|
def install(spec, opts = {})
|
|
39
22
|
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
|
|
23
|
+
return use_installed_gem(spec, gem_dir) if File.directory?(gem_dir) && !opts[:force]
|
|
71
24
|
|
|
25
|
+
Bundler.ui.confirm "Installing #{version_message(spec)} from vault #{@uri}"
|
|
26
|
+
install_into_bundle(spec, opts)
|
|
72
27
|
spec.post_install_message
|
|
73
28
|
end
|
|
74
29
|
|
|
@@ -76,12 +31,65 @@ module Bundler
|
|
|
76
31
|
{}
|
|
77
32
|
end
|
|
78
33
|
|
|
34
|
+
# No source-level install_path to copy: VaultSource#install installs
|
|
35
|
+
# each gem into Bundler.bundle_path via RubyGemsGemInstaller, so the
|
|
36
|
+
# default Source#cache would dereference a non-existent directory.
|
|
37
|
+
def cache(spec, custom_path = nil); end
|
|
38
|
+
|
|
79
39
|
def to_s
|
|
80
40
|
"vault at #{@uri}"
|
|
81
41
|
end
|
|
82
42
|
|
|
83
43
|
private
|
|
84
44
|
|
|
45
|
+
def gemspec_file_for(vault, entry)
|
|
46
|
+
spec = vault.spec_from_blob(entry.name, entry.version, entry.platform)
|
|
47
|
+
gem_dir = gem_dir_for(spec.full_name)
|
|
48
|
+
return anchor_gemspec(gem_dir, spec.full_name, spec.to_ruby) if File.directory?(gem_dir)
|
|
49
|
+
|
|
50
|
+
write_tmp_gemspec(spec.full_name, spec.to_ruby)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def write_tmp_gemspec(full_name, spec_ruby)
|
|
54
|
+
gemspec_dir = File.join(Bundler.tmp("vault_source"), "specifications")
|
|
55
|
+
FileUtils.mkdir_p(gemspec_dir)
|
|
56
|
+
gemspec_path = File.join(gemspec_dir, "#{full_name}.gemspec")
|
|
57
|
+
File.write(gemspec_path, spec_ruby)
|
|
58
|
+
gemspec_path
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def use_installed_gem(spec, gem_dir)
|
|
62
|
+
Bundler.ui.debug "Using #{version_message(spec)} from vault #{@uri}"
|
|
63
|
+
spec.full_gem_path = gem_dir
|
|
64
|
+
spec.loaded_from = File.join(gem_dir, "#{spec.full_name}.gemspec")
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def install_into_bundle(spec, opts)
|
|
69
|
+
Gemvault::Vault.open(@vault_path) do |vault|
|
|
70
|
+
vault.with_gem_file(spec.name, spec.version.to_s, platform: spec.platform.to_s) do |gem_path|
|
|
71
|
+
installed_spec = build_installer(gem_path, opts).install
|
|
72
|
+
gem_dir = installed_spec.full_gem_path
|
|
73
|
+
spec.full_gem_path = gem_dir
|
|
74
|
+
spec.loaded_from = anchor_gemspec(gem_dir, spec.full_name, installed_spec.to_ruby)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_installer(gem_path, opts)
|
|
80
|
+
require "bundler/rubygems_gem_installer"
|
|
81
|
+
|
|
82
|
+
Bundler::RubyGemsGemInstaller.at(
|
|
83
|
+
gem_path,
|
|
84
|
+
install_dir: Bundler.bundle_path.to_s,
|
|
85
|
+
bin_dir: Bundler.system_bindir.to_s,
|
|
86
|
+
ignore_dependencies: true,
|
|
87
|
+
wrappers: true,
|
|
88
|
+
env_shebang: true,
|
|
89
|
+
build_args: opts[:build_args] || [],
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
85
93
|
def version_message(spec)
|
|
86
94
|
message = "#{spec.name} #{spec.version}"
|
|
87
95
|
message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil?
|
data/lib/gemvault/cli/command.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "../command"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Recovers from a bundler plugin index entry whose stored path no
|
|
7
|
+
# longer exists. Bundler records absolute paths in .bundle/plugin/index
|
|
8
|
+
# for path-installed plugins. Moving or renaming the source directory
|
|
9
|
+
# leaves an invalid path behind -- Bundler::Plugin.load_plugin warns
|
|
10
|
+
# "The following plugin paths don't exist: ..." and silently returns,
|
|
11
|
+
# leaving @sources[<type>] nil. The next `source X, type: :vault`
|
|
12
|
+
# crashes inside Bundler::SourceList#add_plugin_source with
|
|
13
|
+
# NoMethodError on nil.
|
|
14
|
+
#
|
|
15
|
+
# Uninstalling clears the broken entry; re-running bundle install
|
|
16
|
+
# triggers Bundler to reinstall the plugin against whatever the
|
|
17
|
+
# current Gemfile declares. Run this from the project directory.
|
|
18
|
+
class Doctor < Command
|
|
19
|
+
description "Clear a broken bundler-source-vault plugin index entry and reinstall it"
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
system("bundle", "plugin", "uninstall", "bundler-source-vault", exception: true)
|
|
23
|
+
exec("bundle", "install")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -4,6 +4,7 @@ require_relative "../command"
|
|
|
4
4
|
module Gemvault
|
|
5
5
|
class CLI
|
|
6
6
|
module Commands
|
|
7
|
+
# Extracts gem file(s) from a vault back onto the filesystem.
|
|
7
8
|
class Extract < Command
|
|
8
9
|
description "Extract gem file(s) from a vault"
|
|
9
10
|
|
|
@@ -28,19 +29,29 @@ module Gemvault
|
|
|
28
29
|
|
|
29
30
|
with_vault(vault) do |v|
|
|
30
31
|
::FileUtils.mkdir_p(output_dir)
|
|
32
|
+
entries = matching_entries(v, name, version)
|
|
33
|
+
abort_missing_gem(name) if entries.empty?
|
|
34
|
+
extract_entries(v, entries, output_dir)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def matching_entries(vault, name, version)
|
|
41
|
+
entries = vault.gem_entries.select { |e| e.name == name }
|
|
42
|
+
version ? entries.select { |e| e.version == version } : entries
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def abort_missing_gem(name)
|
|
46
|
+
print_error("No gem named '#{name}' in vault")
|
|
47
|
+
exit(1)
|
|
48
|
+
end
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
50
|
+
def extract_entries(vault, entries, output_dir)
|
|
51
|
+
entries.each do |entry|
|
|
52
|
+
data = vault.gem_data(entry.name, entry.version, platform: entry.platform)
|
|
53
|
+
File.binwrite(File.join(output_dir, entry.filename), data)
|
|
54
|
+
puts "Extracted #{entry.filename}"
|
|
44
55
|
end
|
|
45
56
|
end
|
|
46
57
|
end
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require_relative "../command"
|
|
2
|
+
require_relative "../../gem_reference"
|
|
2
3
|
|
|
3
4
|
module Gemvault
|
|
4
5
|
class CLI
|
|
5
6
|
module Commands
|
|
7
|
+
# `gemvault remove` subcommand. Parses argv into a GemReference and
|
|
8
|
+
# asks the vault to remove matching gems.
|
|
6
9
|
class Remove < Command
|
|
7
10
|
description "Remove gem(s) from a vault"
|
|
8
11
|
|
|
@@ -12,21 +15,32 @@ module Gemvault
|
|
|
12
15
|
|
|
13
16
|
argument :name, required: true,
|
|
14
17
|
usage: "NAME",
|
|
15
|
-
desc: "Gem name"
|
|
18
|
+
desc: "Gem name, or NAME-VERSION"
|
|
16
19
|
|
|
17
20
|
argument :version, required: false,
|
|
18
21
|
usage: "VERSION",
|
|
19
22
|
desc: "Gem version (omit to remove all versions)"
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
option :version, short: "-v",
|
|
25
|
+
value: { type: String, usage: "VERSION" },
|
|
26
|
+
desc: "Gem version (overrides positional and NAME-VERSION forms)"
|
|
27
|
+
|
|
28
|
+
def run(vault, name, positional_version = nil)
|
|
29
|
+
ref = Gemvault::GemReference.parse(name, version: options[:version] || positional_version)
|
|
30
|
+
with_vault(vault) { |v| report_removal(v.remove(ref)) }
|
|
31
|
+
rescue Gemvault::GemReference::NonExactVersionError => e
|
|
32
|
+
print_error(e.message)
|
|
33
|
+
exit(1)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def report_removal(count)
|
|
39
|
+
if count.zero?
|
|
40
|
+
print_error("No matching gem found")
|
|
41
|
+
exit(1)
|
|
29
42
|
end
|
|
43
|
+
puts "Removed #{count} gem(s)"
|
|
30
44
|
end
|
|
31
45
|
end
|
|
32
46
|
end
|
data/lib/gemvault/cli.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "command_kit/options/version"
|
|
|
4
4
|
require_relative "version"
|
|
5
5
|
|
|
6
6
|
module Gemvault
|
|
7
|
+
# Command-line entry point that auto-loads and dispatches gemvault subcommands.
|
|
7
8
|
class CLI < CommandKit::Command
|
|
8
9
|
include CommandKit::Commands::AutoLoad.new(
|
|
9
10
|
dir: "#{__dir__}/cli/commands",
|
data/lib/gemvault/gem_entry.rb
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Gemvault
|
|
2
|
+
class GemReference
|
|
3
|
+
# Matches one exact version of a named gem. `version:` is always a
|
|
4
|
+
# Gem::Version (never nil, never a String) `.parse` validates and
|
|
5
|
+
# constructs it before calling `.new`.
|
|
6
|
+
class SpecificVersion < GemReference
|
|
7
|
+
attr_reader :version
|
|
8
|
+
|
|
9
|
+
def initialize(name:, version:)
|
|
10
|
+
super(name: name)
|
|
11
|
+
@version = version
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ==(other)
|
|
15
|
+
super && version == other.version
|
|
16
|
+
end
|
|
17
|
+
alias eql? ==
|
|
18
|
+
|
|
19
|
+
def hash
|
|
20
|
+
[self.class, name, version].hash
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def deconstruct_keys(keys)
|
|
24
|
+
data = { name: name, version: version }
|
|
25
|
+
keys.nil? ? data : data.slice(*keys)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "rubygems/version"
|
|
2
|
+
require_relative "gem_reference/any_version"
|
|
3
|
+
require_relative "gem_reference/specific_version"
|
|
4
|
+
|
|
5
|
+
module Gemvault
|
|
6
|
+
# A reference to a gem in a vault. Abstract base class for two concrete
|
|
7
|
+
# kinds: AnyVersion (no version constraint) and SpecificVersion (an exact
|
|
8
|
+
# Gem::Version). Not instantiable directly. `.parse` is the factory that
|
|
9
|
+
# turns raw CLI input into one of the two subclasses.
|
|
10
|
+
class GemReference
|
|
11
|
+
class NonExactVersionError < StandardError; end
|
|
12
|
+
|
|
13
|
+
def self.parse(input, version: nil)
|
|
14
|
+
base_name, embedded = split_name_version(input)
|
|
15
|
+
version_string = version || embedded
|
|
16
|
+
return AnyVersion.new(name: base_name) if version_string.nil?
|
|
17
|
+
unless Gem::Version.correct?(version_string)
|
|
18
|
+
raise NonExactVersionError, "Version must be an exact version (got: #{version_string.inspect})"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
SpecificVersion.new(name: base_name, version: Gem::Version.new(version_string))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.split_name_version(input)
|
|
25
|
+
idx = input.rindex("-")
|
|
26
|
+
return [input, nil] unless idx && Gem::Version.correct?(input[(idx + 1)..])
|
|
27
|
+
|
|
28
|
+
[input[0...idx], input[(idx + 1)..]]
|
|
29
|
+
end
|
|
30
|
+
private_class_method :split_name_version
|
|
31
|
+
|
|
32
|
+
attr_reader :name
|
|
33
|
+
|
|
34
|
+
def initialize(name:)
|
|
35
|
+
raise NotImplementedError, "abstract base use AnyVersion or SpecificVersion" if instance_of?(GemReference)
|
|
36
|
+
|
|
37
|
+
@name = name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
self.class == other.class && name == other.name
|
|
42
|
+
end
|
|
43
|
+
alias eql? ==
|
|
44
|
+
|
|
45
|
+
def hash
|
|
46
|
+
[self.class, name].hash
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def deconstruct_keys(keys)
|
|
50
|
+
data = { name: name }
|
|
51
|
+
keys.nil? ? data : data.slice(*keys)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/gemvault/vault.rb
CHANGED
|
@@ -3,8 +3,10 @@ require "rubygems/package"
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "tempfile"
|
|
5
5
|
require_relative "gem_entry"
|
|
6
|
+
require_relative "gem_reference"
|
|
6
7
|
|
|
7
8
|
module Gemvault
|
|
9
|
+
# SQLite-backed archive of .gem blobs; supports add/remove/list/extract.
|
|
8
10
|
class Vault
|
|
9
11
|
class Error < StandardError; end
|
|
10
12
|
class NotFoundError < Error; end
|
|
@@ -12,6 +14,7 @@ module Gemvault
|
|
|
12
14
|
class InvalidGemError < Error; end
|
|
13
15
|
|
|
14
16
|
SCHEMA_VERSION = "1".freeze
|
|
17
|
+
SQLITE_MAGIC = "SQLite format 3#{0.chr}".freeze
|
|
15
18
|
|
|
16
19
|
attr_reader :path
|
|
17
20
|
|
|
@@ -28,63 +31,26 @@ module Gemvault
|
|
|
28
31
|
|
|
29
32
|
def initialize(path, create: false)
|
|
30
33
|
@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
|
|
34
|
+
create ? create_vault! : open_vault!
|
|
45
35
|
end
|
|
46
36
|
|
|
47
37
|
def add(gem_path)
|
|
48
38
|
gem_path = File.expand_path(gem_path)
|
|
49
39
|
raise NotFoundError, "Gem file not found: #{gem_path}" unless File.file?(gem_path)
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
)
|
|
41
|
+
spec = load_gem_spec(gem_path)
|
|
42
|
+
raise_if_duplicate(spec)
|
|
43
|
+
insert_gem(gem_path, spec)
|
|
76
44
|
end
|
|
77
45
|
|
|
78
|
-
def remove(
|
|
79
|
-
|
|
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:]
|
|
80
51
|
@db.execute(
|
|
81
52
|
"DELETE FROM gems WHERE name = ? AND version = ?",
|
|
82
|
-
[name, version],
|
|
83
|
-
)
|
|
84
|
-
else
|
|
85
|
-
@db.execute(
|
|
86
|
-
"DELETE FROM gems WHERE name = ?",
|
|
87
|
-
[name],
|
|
53
|
+
[name, version.to_s],
|
|
88
54
|
)
|
|
89
55
|
end
|
|
90
56
|
@db.changes
|
|
@@ -121,11 +87,8 @@ module Gemvault
|
|
|
121
87
|
|
|
122
88
|
def with_gem_file(name, version, platform: "ruby")
|
|
123
89
|
data = gem_data(name, version, platform: platform)
|
|
124
|
-
tmpfile =
|
|
90
|
+
tmpfile = write_tempfile(data)
|
|
125
91
|
begin
|
|
126
|
-
tmpfile.binmode
|
|
127
|
-
tmpfile.write(data)
|
|
128
|
-
tmpfile.close
|
|
129
92
|
yield tmpfile.path
|
|
130
93
|
ensure
|
|
131
94
|
tmpfile.close unless tmpfile.closed?
|
|
@@ -141,6 +104,59 @@ module Gemvault
|
|
|
141
104
|
|
|
142
105
|
private
|
|
143
106
|
|
|
107
|
+
def create_vault!
|
|
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})"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def insert_gem(gem_path, spec)
|
|
145
|
+
data = File.binread(gem_path)
|
|
146
|
+
@db.execute(
|
|
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
|
+
|
|
144
160
|
def create_schema
|
|
145
161
|
@db.execute_batch(<<~SQL)
|
|
146
162
|
CREATE TABLE metadata (
|
|
@@ -169,8 +185,7 @@ module Gemvault
|
|
|
169
185
|
end
|
|
170
186
|
|
|
171
187
|
def validate_sqlite!
|
|
172
|
-
|
|
173
|
-
return if magic == "SQLite format 3\x00"
|
|
188
|
+
return if File.binread(@path, SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
|
|
174
189
|
|
|
175
190
|
raise Error, "Not a valid vault file (not SQLite): #{@path}"
|
|
176
191
|
end
|
data/lib/gemvault/version.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Returns standard Gem::Resolver::IndexSpecification objects so the
|
|
5
5
|
# resolver's install pipeline (download -> Gem::Installer) works unchanged.
|
|
6
|
-
|
|
7
6
|
class Gem::Resolver::VaultSet < Gem::Resolver::Set
|
|
8
7
|
def initialize(source)
|
|
9
8
|
super()
|
|
@@ -12,28 +11,30 @@ class Gem::Resolver::VaultSet < Gem::Resolver::Set
|
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def find_all(req)
|
|
15
|
-
@specs.
|
|
16
|
-
Gem::Resolver::IndexSpecification.new(
|
|
17
|
-
self,
|
|
18
|
-
tuple.name,
|
|
19
|
-
tuple.version,
|
|
20
|
-
@source,
|
|
21
|
-
tuple.platform,
|
|
22
|
-
)
|
|
23
|
-
end
|
|
14
|
+
@specs.filter_map { |tuple| index_spec_for(req, tuple) }
|
|
24
15
|
end
|
|
25
16
|
|
|
26
17
|
def prefetch(reqs); end
|
|
27
18
|
|
|
28
|
-
def pretty_print(
|
|
29
|
-
|
|
19
|
+
def pretty_print(pp)
|
|
20
|
+
pp.group 2, "[VaultSet", "]" do
|
|
30
21
|
next if @specs.empty?
|
|
31
22
|
|
|
32
|
-
|
|
23
|
+
pp.breakable
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
pp.seplist @specs do |tuple|
|
|
26
|
+
pp.text tuple.full_name
|
|
36
27
|
end
|
|
37
28
|
end
|
|
38
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def index_spec_for(req, tuple)
|
|
34
|
+
return unless req.match?(tuple)
|
|
35
|
+
|
|
36
|
+
Gem::Resolver::IndexSpecification.new(
|
|
37
|
+
self, tuple.name, tuple.version, @source, tuple.platform
|
|
38
|
+
)
|
|
39
|
+
end
|
|
39
40
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "gemvault/vault"
|
|
2
|
+
require "uri"
|
|
2
3
|
|
|
3
4
|
##
|
|
4
5
|
# A source backed by a .gemv vault file (SQLite archive of .gem blobs).
|
|
@@ -6,37 +7,24 @@ require "gemvault/vault"
|
|
|
6
7
|
# Used by the gemvault RubyGems plugin to support:
|
|
7
8
|
#
|
|
8
9
|
# gem install --source myvault.gemv activesupport
|
|
9
|
-
|
|
10
10
|
class Gem::Source::Vault < Gem::Source
|
|
11
11
|
include Gem::UserInteraction
|
|
12
12
|
|
|
13
|
+
VAULT_URI_SCHEMES = %w[file vault].freeze
|
|
14
|
+
|
|
13
15
|
attr_reader :path
|
|
14
16
|
|
|
15
17
|
def initialize(path)
|
|
16
|
-
path = path
|
|
17
|
-
|
|
18
|
-
@
|
|
19
|
-
@uri = @path
|
|
18
|
+
@path = File.expand_path(filesystem_path(path))
|
|
19
|
+
super(@path)
|
|
20
|
+
@uri = @path
|
|
20
21
|
@specs = nil
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def load_specs(type)
|
|
24
25
|
verbose "Loading #{type} specs from vault at #{@path}"
|
|
25
26
|
ensure_specs_loaded
|
|
26
|
-
|
|
27
|
-
case type
|
|
28
|
-
when :released
|
|
29
|
-
@specs.keys.reject { |t| t.version.prerelease? }
|
|
30
|
-
when :prerelease
|
|
31
|
-
@specs.keys.select { |t| t.version.prerelease? }
|
|
32
|
-
when :latest
|
|
33
|
-
@specs.keys
|
|
34
|
-
.group_by { |tuple| [tuple.name, tuple.platform] }
|
|
35
|
-
.values
|
|
36
|
-
.map { |tuples| tuples.max_by(&:version) }
|
|
37
|
-
else
|
|
38
|
-
@specs.keys
|
|
39
|
-
end
|
|
27
|
+
select_tuples(type)
|
|
40
28
|
end
|
|
41
29
|
|
|
42
30
|
def fetch_spec(name_tuple)
|
|
@@ -63,7 +51,7 @@ class Gem::Source::Vault < Gem::Source
|
|
|
63
51
|
dest
|
|
64
52
|
end
|
|
65
53
|
|
|
66
|
-
def dependency_resolver_set(prerelease =
|
|
54
|
+
def dependency_resolver_set(prerelease = nil)
|
|
67
55
|
require_relative "../resolver/vault_set"
|
|
68
56
|
set = Gem::Resolver::VaultSet.new(self)
|
|
69
57
|
set.prerelease = prerelease
|
|
@@ -72,15 +60,9 @@ class Gem::Source::Vault < Gem::Source
|
|
|
72
60
|
|
|
73
61
|
def <=>(other)
|
|
74
62
|
case other
|
|
75
|
-
when Gem::Source::Installed,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
when Gem::Source::Vault
|
|
79
|
-
0
|
|
80
|
-
when Gem::Source::Local
|
|
81
|
-
-1
|
|
82
|
-
when Gem::Source
|
|
83
|
-
1
|
|
63
|
+
when Gem::Source::Installed, Gem::Source::Lock, Gem::Source::Local then -1
|
|
64
|
+
when Gem::Source::Vault then 0
|
|
65
|
+
when Gem::Source then 1
|
|
84
66
|
end
|
|
85
67
|
end
|
|
86
68
|
|
|
@@ -98,17 +80,48 @@ class Gem::Source::Vault < Gem::Source
|
|
|
98
80
|
"vault at #{@path}"
|
|
99
81
|
end
|
|
100
82
|
|
|
101
|
-
def pretty_print(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
83
|
+
def pretty_print(pp)
|
|
84
|
+
pp.object_group(self) do
|
|
85
|
+
pp.group 2, "[Vault:", "]" do
|
|
86
|
+
pp.breakable
|
|
87
|
+
pp.text @path
|
|
106
88
|
end
|
|
107
89
|
end
|
|
108
90
|
end
|
|
109
91
|
|
|
110
92
|
private
|
|
111
93
|
|
|
94
|
+
def filesystem_path(path)
|
|
95
|
+
uri = URI.parse(path.to_s)
|
|
96
|
+
VAULT_URI_SCHEMES.include?(uri.scheme) ? uri.path : path.to_s
|
|
97
|
+
rescue URI::InvalidURIError
|
|
98
|
+
path.to_s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def select_tuples(type)
|
|
102
|
+
case type
|
|
103
|
+
when :released then released_tuples
|
|
104
|
+
when :prerelease then prerelease_tuples
|
|
105
|
+
when :latest then latest_tuples
|
|
106
|
+
else @specs.keys
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def released_tuples
|
|
111
|
+
@specs.keys.reject { |tuple| tuple.version.prerelease? }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def prerelease_tuples
|
|
115
|
+
@specs.keys.select { |tuple| tuple.version.prerelease? }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def latest_tuples
|
|
119
|
+
@specs.keys
|
|
120
|
+
.group_by { |tuple| [tuple.name, tuple.platform] }
|
|
121
|
+
.values
|
|
122
|
+
.map { |tuples| tuples.max_by(&:version) }
|
|
123
|
+
end
|
|
124
|
+
|
|
112
125
|
def ensure_specs_loaded
|
|
113
126
|
return if @specs
|
|
114
127
|
|
data/lib/rubygems_plugin.rb
CHANGED
|
@@ -14,28 +14,37 @@ require "rubygems/source_list"
|
|
|
14
14
|
# 3. SourceList#<< -- route .gemv strings to Gem::Source::Vault
|
|
15
15
|
|
|
16
16
|
module Gemvault
|
|
17
|
+
# Lets .gemv paths bypass RubyGems' URI scheme validation.
|
|
17
18
|
module AcceptVaultURI
|
|
19
|
+
VALID_URI_SCHEMES = ["http", "https", "file", "s3"].freeze
|
|
20
|
+
|
|
18
21
|
def accept_uri_http
|
|
19
22
|
Gem::OptionParser.accept Gem::URI::HTTP do |value|
|
|
20
23
|
next value if value.to_s.end_with?(".gemv")
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
rescue Gem::URI::InvalidURIError
|
|
25
|
-
raise Gem::OptionParser::InvalidArgument, value
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
valid_uri_schemes = ["http", "https", "file", "s3"]
|
|
29
|
-
unless valid_uri_schemes.include?(uri.scheme)
|
|
30
|
-
msg = "Invalid uri scheme for #{value}\nPreface URLs with one of #{valid_uri_schemes.map { |s| "#{s}://" }}"
|
|
31
|
-
raise ArgumentError, msg
|
|
32
|
-
end
|
|
33
|
-
|
|
25
|
+
uri = parse_uri(value)
|
|
26
|
+
validate_scheme!(uri, value)
|
|
34
27
|
value
|
|
35
28
|
end
|
|
36
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_uri(value)
|
|
34
|
+
Gem::URI.parse value
|
|
35
|
+
rescue Gem::URI::InvalidURIError
|
|
36
|
+
raise Gem::OptionParser::InvalidArgument, value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_scheme!(uri, value)
|
|
40
|
+
return if VALID_URI_SCHEMES.include?(uri.scheme)
|
|
41
|
+
|
|
42
|
+
schemes = VALID_URI_SCHEMES.map { |scheme| "#{scheme}://" }
|
|
43
|
+
raise ArgumentError, "Invalid uri scheme for #{value}\nPreface URLs with one of #{schemes}"
|
|
44
|
+
end
|
|
37
45
|
end
|
|
38
46
|
|
|
47
|
+
# Skips the trailing-slash append for .gemv source URLs.
|
|
39
48
|
module AddVaultSourceOption
|
|
40
49
|
def add_source_option
|
|
41
50
|
accept_uri_http
|
|
@@ -53,6 +62,7 @@ module Gemvault
|
|
|
53
62
|
end
|
|
54
63
|
end
|
|
55
64
|
|
|
65
|
+
# Routes .gemv source strings to Gem::Source::Vault.
|
|
56
66
|
module VaultSourceList
|
|
57
67
|
def <<(obj)
|
|
58
68
|
if obj.is_a?(String) && obj.end_with?(".gemv")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemvault
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- ".rubocop.yml"
|
|
65
65
|
- ".ruby-version"
|
|
66
66
|
- ASSESSMENT.md
|
|
67
|
+
- CHANGELOG.md
|
|
67
68
|
- CLAUDE.md
|
|
68
69
|
- Dockerfile.test
|
|
69
70
|
- LICENSE
|
|
@@ -73,16 +74,21 @@ files:
|
|
|
73
74
|
- Rakefile
|
|
74
75
|
- docs/superpowers/plans/2026-04-16-container-integration-tests.md
|
|
75
76
|
- exe/gemvault
|
|
77
|
+
- issues.rec
|
|
76
78
|
- lib/bundler/plugin/vault_source.rb
|
|
77
79
|
- lib/gemvault.rb
|
|
78
80
|
- lib/gemvault/cli.rb
|
|
79
81
|
- lib/gemvault/cli/command.rb
|
|
80
82
|
- lib/gemvault/cli/commands/add.rb
|
|
83
|
+
- lib/gemvault/cli/commands/doctor.rb
|
|
81
84
|
- lib/gemvault/cli/commands/extract.rb
|
|
82
85
|
- lib/gemvault/cli/commands/list.rb
|
|
83
86
|
- lib/gemvault/cli/commands/new.rb
|
|
84
87
|
- lib/gemvault/cli/commands/remove.rb
|
|
85
88
|
- lib/gemvault/gem_entry.rb
|
|
89
|
+
- lib/gemvault/gem_reference.rb
|
|
90
|
+
- lib/gemvault/gem_reference/any_version.rb
|
|
91
|
+
- lib/gemvault/gem_reference/specific_version.rb
|
|
86
92
|
- lib/gemvault/vault.rb
|
|
87
93
|
- lib/gemvault/version.rb
|
|
88
94
|
- lib/rubygems/resolver/vault_set.rb
|
|
@@ -110,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
110
116
|
- !ruby/object:Gem::Version
|
|
111
117
|
version: '0'
|
|
112
118
|
requirements: []
|
|
113
|
-
rubygems_version: 4.0.
|
|
119
|
+
rubygems_version: 4.0.15
|
|
114
120
|
specification_version: 4
|
|
115
121
|
summary: Multi-gem portable archives backed by SQLite
|
|
116
122
|
test_files: []
|