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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c643dd2d86d0a918dbecb5a8f20026746a0e60670eaa5590009fe35e6f9b9b0
|
|
4
|
+
data.tar.gz: fa0f695ae5d5d9ab9686caa479cc89c2c6954fc8c53bfdd02841cf12578622bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d16d9d84c05c143a2372eaee7bd55b162aca32019a4709227f689ce47a4c725bd4d2d8a64b9b788f32d107ec37ae8c61b73178d547e60613a221bd55d48aff53
|
|
7
|
+
data.tar.gz: 21b8fbd66feeea5146546632f5636a194c698c2ab6aa1d51ae0d9544b021dc15f32d4c1171a98e92c67fe62b56cfe2f57dab5138fef2c8dc6a4bb5d194b1952d
|
data/.containerignore
ADDED
data/.rubocop.yml
CHANGED
|
@@ -17,7 +17,7 @@ plugins:
|
|
|
17
17
|
|
|
18
18
|
AllCops:
|
|
19
19
|
NewCops: enable
|
|
20
|
-
TargetRubyVersion: 4.
|
|
20
|
+
TargetRubyVersion: 3.4.8
|
|
21
21
|
Exclude:
|
|
22
22
|
- bin/*
|
|
23
23
|
- vendor/**/*
|
|
@@ -137,6 +137,13 @@ Style/ArgumentsForwarding:
|
|
|
137
137
|
Style/RedundantBegin:
|
|
138
138
|
Enabled: false
|
|
139
139
|
|
|
140
|
+
# Data value objects that carry constants, nested value types, or any class
|
|
141
|
+
# body read best as `class X < Data.define(...)`. The assignment form the cop
|
|
142
|
+
# prefers leaks block-level constants to the enclosing scope and forces a
|
|
143
|
+
# `.freeze` (Style/MutableConstant) on the class. Permitted by David.
|
|
144
|
+
Style/DataInheritance:
|
|
145
|
+
Enabled: false
|
|
146
|
+
|
|
140
147
|
# Compact class names are fine for small files and tests.
|
|
141
148
|
Style/ClassAndModuleChildren:
|
|
142
149
|
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,62 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Tarball vault format ("Tarvault"): new vaults are portable tarballs with a
|
|
12
|
+
`manifest.json` index and per-gem SHA256 integrity, with no sqlite3 dependency
|
|
13
|
+
on the read/write path (works on JRuby). The original SQLite format
|
|
14
|
+
("Dbvault") is still read transparently.
|
|
15
|
+
- Vaults carry an explicit on-disk **format version**, decoupled from the gem
|
|
16
|
+
version and validated on open; gemvault refuses a vault written by a newer
|
|
17
|
+
gemvault instead of misreading it.
|
|
18
|
+
- `gemvault upgrade` migrates a vault to the current format (e.g. SQLite → tar),
|
|
19
|
+
preserving every gem and timestamp, writing a `.bak` backup by default, with
|
|
20
|
+
`--dry-run` and `--no-backup` flags. It is a no-op on an already-current vault.
|
|
21
|
+
|
|
22
|
+
- All CLI commands accept `vault://` and `file://` locators wherever they take
|
|
23
|
+
a vault path, e.g. `gemvault list vault:///path/to/myvault.gemv`; resolution
|
|
24
|
+
is shared with the RubyGems source via `Gemvault::VaultPath` (issue #9).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `bundle plugin install bundler-source-vault` no longer dies with
|
|
28
|
+
`LoadError: cannot load such file -- bundler/plugin/vault_source`. The shim's
|
|
29
|
+
`plugins.rb` now derives the gem root from its own installed location (local
|
|
30
|
+
plugin root, global plugin root, or plain GEM_HOME), registers any spec dirs
|
|
31
|
+
RubyGems is not searching, and resets RubyGems' spec stub cache when it
|
|
32
|
+
predates the just-installed gems (issue #10).
|
|
33
|
+
- `bundle exec` no longer fails Gemfile parsing with `Could not find 'bundler'
|
|
34
|
+
(>= 2.0)` under `path: vendor` on rubies that ship bundler as a regular gem:
|
|
35
|
+
gemvault no longer declares `bundler` as a runtime dependency (issue #12).
|
|
36
|
+
- Vault gems whose versions carry a non-numeric suffix (e.g. `0.2.1.patch1`)
|
|
37
|
+
install from a vault with `gem install --pre`, matching RubyGems' prerelease
|
|
38
|
+
semantics (issue #6).
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- `sqlite3` is no longer a runtime dependency. Gemvault runs dependency-free on
|
|
42
|
+
the tarball path (including JRuby); `sqlite3` is loaded lazily only to read a
|
|
43
|
+
legacy SQLite vault, with a clear error if it is not installed.
|
|
44
|
+
- `bundler` is no longer a runtime dependency; the Bundler plugin always runs
|
|
45
|
+
inside an existing Bundler process, and the declared dependency broke gem
|
|
46
|
+
activation in restricted GEM_PATH contexts.
|
|
47
|
+
- CI is unblocked (issue #11): RuboCop's TargetRubyVersion now matches the
|
|
48
|
+
gemspec's `required_ruby_version` floor (3.4.8), the previously-missing
|
|
49
|
+
`spec:integration` rake task exists alongside a non-integration `spec:core`, the
|
|
50
|
+
unit job no longer needs podman, and the integration job installs podman
|
|
51
|
+
when the runner image lacks it.
|
|
52
|
+
- Reading a vault's `manifest.json` is now strict: an entry missing any required
|
|
53
|
+
field (`name`, `version`, `platform`, `created_at`, `sha256`, `encrypted`)
|
|
54
|
+
raises instead of silently loading `nil` fields, so a truncated or hand-edited
|
|
55
|
+
manifest fails fast rather than yielding a subtly broken vault.
|
|
56
|
+
|
|
57
|
+
### Deprecated
|
|
58
|
+
- The SQLite vault format is deprecated and now **read-only**: existing SQLite
|
|
59
|
+
vaults can be read and migrated but no longer written (`add`/`remove` raise and
|
|
60
|
+
point at `gemvault upgrade`). Opening one prints a one-time deprecation notice
|
|
61
|
+
(silenceable with `GEMVAULT_SILENCE_DEPRECATIONS=1`). SQLite support will be
|
|
62
|
+
removed in a future release (0.3-0.5); migrate with `gemvault upgrade`.
|
|
63
|
+
|
|
8
64
|
## [0.1.3] - 2026-06-22
|
|
9
65
|
|
|
10
66
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -17,6 +17,7 @@ Do NOT modify `.rubocop.yml` or use inline `# rubocop:disable` tags without expl
|
|
|
17
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
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
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
|
+
12. NEVER write to /tmp. Use /workspace/tmp
|
|
20
21
|
|
|
21
22
|
## Additional rules
|
|
22
23
|
|
|
@@ -25,7 +26,7 @@ Do NOT modify `.rubocop.yml` or use inline `# rubocop:disable` tags without expl
|
|
|
25
26
|
|
|
26
27
|
## Project Overview
|
|
27
28
|
|
|
28
|
-
Multi-gem portable archives
|
|
29
|
+
Multi-gem portable archives. A single `.gemv` file is a tarball holding multiple `.gem` files plus a `manifest.json` index; legacy SQLite vaults are read-only.
|
|
29
30
|
|
|
30
31
|
Two gems, one repo:
|
|
31
32
|
|
|
@@ -56,7 +57,7 @@ gem install --source file:///path/to/myvault.gemv foo
|
|
|
56
57
|
## Architecture
|
|
57
58
|
|
|
58
59
|
- `gemvault.gemspec` — main gem spec (name: `gemvault`)
|
|
59
|
-
- `lib/gemvault/vault.rb` —
|
|
60
|
+
- `lib/gemvault/vault.rb` — Vault facade choosing a backend by file format (Tarvault current, legacy Dbvault read-only)
|
|
60
61
|
- `lib/gemvault/cli.rb` — CLI dispatcher (new/add/list/remove/extract)
|
|
61
62
|
- `lib/bundler/plugin/vault_source.rb` — Bundler `Plugin::API::Source` implementation
|
|
62
63
|
- `lib/rubygems_plugin.rb` — RubyGems plugin: monkey-patches for `--source myvault.gemv` support
|
|
@@ -69,7 +70,7 @@ gem install --source file:///path/to/myvault.gemv foo
|
|
|
69
70
|
|
|
70
71
|
## Key Design Decisions
|
|
71
72
|
|
|
72
|
-
-
|
|
73
|
+
- Tar storage — portable, dependency-free, single file, inspectable with `tar`; legacy SQLite vaults readable via lazily-loaded sqlite3
|
|
73
74
|
- Specs extracted from gem blobs at runtime (no separate spec storage)
|
|
74
75
|
- Vault opened/closed per operation in the source plugin (no persistent connection)
|
|
75
76
|
- `fetch_gemspec_files` checks installed state — Bundler computes `full_gem_path` as `dirname(loaded_from)`, so `loaded_from` must point inside the gem directory
|
|
@@ -93,7 +94,7 @@ Integration tests use a manually-written Bundler plugin index to avoid rubygems.
|
|
|
93
94
|
|
|
94
95
|
## Dependencies
|
|
95
96
|
|
|
96
|
-
- `
|
|
97
|
-
- `bundler` (>= 2.0) — runtime
|
|
97
|
+
- `bundler` — NOT a dependency; the plugin always runs inside an existing Bundler process, and declaring it breaks gem activation under `bundle exec`'s restricted GEM_PATH
|
|
98
98
|
- `command_kit` (~> 0.6) — runtime (CLI)
|
|
99
|
+
- `sqlite3` (~> 2.0) — NOT a runtime dependency; loaded lazily only to read a legacy SQLite (Dbvault) vault. Declared in the Gemfile for development/test.
|
|
99
100
|
- `minitest`, `rake` — development
|
data/Dockerfile.test
CHANGED
|
@@ -9,3 +9,17 @@ RUN cd /tmp/build \
|
|
|
9
9
|
&& gem build bundler-source-vault.gemspec \
|
|
10
10
|
&& gem install --no-document bundler-source-vault-*.gem \
|
|
11
11
|
&& rm -rf /tmp/build
|
|
12
|
+
|
|
13
|
+
# sqlite3 is not a gemvault runtime dependency; install it here so the upgrade
|
|
14
|
+
# integration test can read the committed legacy SQLite (Dbvault) fixture it
|
|
15
|
+
# migrates.
|
|
16
|
+
RUN gem install --no-document sqlite3
|
|
17
|
+
|
|
18
|
+
# Gem files for integration specs that install the plugin from a local gem
|
|
19
|
+
# index at spec runtime, kept for offline use: command_kit is gemvault's
|
|
20
|
+
# runtime dependency; the bundler gem lets specs simulate distro rubies where
|
|
21
|
+
# bundler is a regular (non-default) gem.
|
|
22
|
+
RUN mkdir -p /opt/gems \
|
|
23
|
+
&& cd /opt/gems \
|
|
24
|
+
&& gem fetch command_kit -v 0.6.0 \
|
|
25
|
+
&& gem fetch bundler -v "$(ruby -rbundler -e 'print Bundler::VERSION')"
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Gemvault
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A file that behaves like a private gem server.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You can commit it to your repo, drop it on S3, email it, put it on a USB drive, or whatever you feel like doing. Bundler and RubyGems read from it directly.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -27,7 +27,6 @@ end
|
|
|
27
27
|
bundle install
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Bundler auto-discovers the `bundler-source-vault` plugin, installs it, and resolves gems from the vault alongside rubygems.org. No extra configuration.
|
|
31
30
|
|
|
32
31
|
### RubyGems CLI
|
|
33
32
|
|
|
@@ -44,16 +43,29 @@ gemvault add myvault.gemv foo.gem bar.gem # add .gem files
|
|
|
44
43
|
gemvault list myvault.gemv # list contents
|
|
45
44
|
gemvault remove myvault.gemv foo 1.0.0 # remove a gem
|
|
46
45
|
gemvault extract myvault.gemv foo -o vendor/ # extract .gem file to disk
|
|
46
|
+
gemvault upgrade myvault.gemv # migrate to the current format
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Vault format versioning
|
|
50
|
+
|
|
51
|
+
Every vault records an on-disk **format version** — `1` for the original SQLite format, `2` for the current tarball format. This version lives inside the file and is **independent of the gemvault gem version**: it changes only when the storage layout changes, so upgrading the gem never invalidates your vaults.
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
gemvault reads any format up to the one it understands and **refuses a vault written by a newer gemvault** with a clear message (rather than silently misreading it).
|
|
54
|
+
|
|
55
|
+
The SQLite format (`1`) is **deprecated and read-only**: you can still read and migrate an existing SQLite vault, but `add`/`remove` are refused and print `gemvault upgrade`. Opening one shows a one-time deprecation notice (silence it with `GEMVAULT_SILENCE_DEPRECATIONS=1`). SQLite support will be removed in a future release (0.3–0.5). To migrate a vault to the current format:
|
|
52
56
|
|
|
53
57
|
```bash
|
|
54
|
-
|
|
58
|
+
gemvault upgrade myvault.gemv # e.g. SQLite (v1) -> tarball (v2)
|
|
59
|
+
gemvault upgrade myvault.gemv --dry-run # show the plan, change nothing
|
|
60
|
+
gemvault upgrade myvault.gemv --no-backup # skip the default myvault.gemv.bak copy
|
|
55
61
|
```
|
|
56
62
|
|
|
63
|
+
`upgrade` preserves every gem and its timestamp, writes `myvault.gemv.bak` by default, and is a no-op on an already-current vault.
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
|
|
67
|
+
The gemv file is a tarball containing your .gem files and a json manifest. It has no dependencies other than the tar utilities that rubygems provides.
|
|
68
|
+
|
|
57
69
|
When Bundler sees `type: :vault` in your Gemfile, it auto-installs the `bundler-source-vault` plugin from rubygems.org. The plugin implements the `Bundler::Plugin::API::Source` interface — it reads gemspecs from the vault, participates in dependency resolution, then extracts and installs gems from the vault's blob storage.
|
|
58
70
|
|
|
59
71
|
The RubyGems plugin works similarly: `gem install --source vault.gemv` loads specs and extracts gems on demand.
|
data/Rakefile
CHANGED
|
@@ -10,6 +10,14 @@ require "rspec/core/rake_task"
|
|
|
10
10
|
|
|
11
11
|
RSpec::Core::RakeTask.new(:spec)
|
|
12
12
|
|
|
13
|
+
RSpec::Core::RakeTask.new("spec:core") do |t|
|
|
14
|
+
t.rspec_opts = "--tag ~integration"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
RSpec::Core::RakeTask.new("spec:integration") do |t|
|
|
18
|
+
t.rspec_opts = "--tag integration"
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
require "rubocop/rake_task"
|
|
14
22
|
RuboCop::RakeTask.new
|
|
15
23
|
|
|
@@ -43,12 +51,23 @@ namespace :spec do
|
|
|
43
51
|
task(:teardown) { destroy_cached_image if cached_image_exists? }
|
|
44
52
|
end
|
|
45
53
|
|
|
54
|
+
directory "pkg" do
|
|
55
|
+
mkdir "pkg"
|
|
56
|
+
end
|
|
57
|
+
|
|
46
58
|
namespace :shim do
|
|
47
59
|
Bundler::GemHelper.install_tasks dir: "shim", name: "bundler-source-vault"
|
|
48
60
|
CLOBBER.include "shim/pkg"
|
|
61
|
+
|
|
62
|
+
Rake::Task[:build].enhance ["pkg"] do
|
|
63
|
+
FileList["shim/pkg/*.gem"].each do |g|
|
|
64
|
+
mv g, "pkg", verbose: false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
49
67
|
end
|
|
50
68
|
|
|
51
69
|
Rake::Task[:spec].enhance ["spec:setup"]
|
|
70
|
+
Rake::Task["spec:integration"].enhance ["spec:setup"]
|
|
52
71
|
Rake::Task[:build].enhance ["shim:build"]
|
|
53
72
|
Rake::Task[:release].enhance ["shim:release"]
|
|
54
73
|
Rake::Task[:clobber].enhance ["spec:teardown"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Tarvault Spike — Findings
|
|
2
|
+
|
|
3
|
+
Spike for `docs/tarvault.md`: replace/augment the SQLite storage of a `.gemv`
|
|
4
|
+
with a **tarball** ("Tarvault") whose first entry is `manifest.json` and whose
|
|
5
|
+
remaining entries are `.gem` files. Motivation: drop the hard `sqlite3`
|
|
6
|
+
dependency (unportable, no JRuby) using only tooling that already ships with
|
|
7
|
+
rubygems.
|
|
8
|
+
|
|
9
|
+
## Result
|
|
10
|
+
|
|
11
|
+
Tarvault is implemented and wired **behind the existing `Gemvault::Vault`
|
|
12
|
+
facade**, so the CLI, the Bundler `type: :vault` source, and the RubyGems
|
|
13
|
+
`--source` path use it transparently:
|
|
14
|
+
|
|
15
|
+
- `Gemvault::Vault` is now a `Forwardable` delegator that selects a backend by
|
|
16
|
+
file format — existing SQLite files open as `Gemvault::Dbvault` (the old code,
|
|
17
|
+
extracted verbatim); new or tar files as `Gemvault::Tarvault`. New vaults are
|
|
18
|
+
Tarvaults.
|
|
19
|
+
- Backends are **lazily required**, so a process that only touches Tarvaults
|
|
20
|
+
never loads `sqlite3` — the JRuby-portability win.
|
|
21
|
+
- `Gemvault::Manifest` owns `manifest.json`; `Gemvault::TarArchive` owns the
|
|
22
|
+
low-level tar read + atomic rewrite; `Gemvault::GemExtraction` /
|
|
23
|
+
`Gemvault::VaultSession` are shared mixins.
|
|
24
|
+
|
|
25
|
+
**Drop-in proof:** the entire pre-existing suite passes with *no change to any
|
|
26
|
+
`spec/` behavior or integration file* — only new unit specs were added, plus two
|
|
27
|
+
Minitest assertions that had probed a SQLite closed-handle artifact.
|
|
28
|
+
|
|
29
|
+
- Minitest: `111 tests, 0 failures` (the Vault-contract, Bundler-source, and
|
|
30
|
+
RubyGems-plugin tests now run on the **tar** backend via the facade).
|
|
31
|
+
- RSpec incl. podman integration: `88 examples, 0 failures` — real
|
|
32
|
+
`bundle install` (`type: :vault`), `gem install --source myvault.gemv`,
|
|
33
|
+
bundler-inline, and CLI, all against tar-backed vaults. *(Note: the container
|
|
34
|
+
image must be rebuilt (`rake spec:build`) after code changes — a stale image
|
|
35
|
+
silently tests old code and still passes on SQLite.)*
|
|
36
|
+
- New unit specs (`spec/gemvault/**`): `42 examples, 0 failures`.
|
|
37
|
+
|
|
38
|
+
A Tarvault on disk:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
$ file myvault.gemv # POSIX tar archive
|
|
42
|
+
$ tar -tf myvault.gemv
|
|
43
|
+
manifest.json
|
|
44
|
+
addressable-2.9.0.gem
|
|
45
|
+
rstore-0.3.9.gem
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
// manifest.json
|
|
50
|
+
{
|
|
51
|
+
"vault_version": "2",
|
|
52
|
+
"format": "tarvault",
|
|
53
|
+
"created_at": "2026-07-11 20:32:25",
|
|
54
|
+
"gems": [
|
|
55
|
+
{ "name": "addressable", "version": "2.9.0", "platform": "ruby",
|
|
56
|
+
"created_at": "…", "sha256": "7fdf6ac3…", "encrypted": false }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Doc Q1 — Integrity: are checksums used?
|
|
62
|
+
|
|
63
|
+
Yes. Three layers exist; the manifest adds the one that matters for the container:
|
|
64
|
+
|
|
65
|
+
1. **Tar header checksum** — every 512-byte tar header carries an octal
|
|
66
|
+
*byte-sum*. It protects the header only and is not cryptographic.
|
|
67
|
+
2. **Inside each `.gem`** — a `.gem` already contains `checksums.yaml.gz`
|
|
68
|
+
(SHA256/SHA512 of its own members). This protects a gem's internals, not its
|
|
69
|
+
identity within the vault.
|
|
70
|
+
3. **Manifest per-gem digest (added)** — each `manifest.json` record stores a
|
|
71
|
+
cryptographic **SHA256 of the whole `.gem` blob**
|
|
72
|
+
(`OpenSSL::Digest.new("SHA256")`, matching rubygems' own `Gem::Security`
|
|
73
|
+
convention). `Tarvault#gem_data` recomputes and compares on read, raising
|
|
74
|
+
`Vault::Error` on mismatch (verified by a test that flips a byte inside the
|
|
75
|
+
stored blob).
|
|
76
|
+
|
|
77
|
+
**Boundary:** `gem_entries`/`specs` read the manifest and do **not** re-hash
|
|
78
|
+
blobs; only `gem_data` (the byte-serving path used by install/extract) verifies.
|
|
79
|
+
This is a deliberate scope for the spike — list operations stay O(manifest). If
|
|
80
|
+
list-time verification is ever wanted, the same `Manifest.digest` is the hook.
|
|
81
|
+
|
|
82
|
+
## Doc feasibility — the one-liner
|
|
83
|
+
|
|
84
|
+
`docs/tarvault.md`'s one-liner works, but for a subtle reason worth recording:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Enumerator.new { |y|
|
|
88
|
+
Gem::Package::TarReader.new(File.open(path)) { |r| r.each_entry { |e| y << Gem::Package.new(e) } }
|
|
89
|
+
}.map(&:spec)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`TarReader#each` **closes each entry after yielding** (verified: reading a
|
|
93
|
+
stashed entry raises `IOError: closed Entry`). The one-liner still returns
|
|
94
|
+
correct specs *only* because `Enumerator.new{}.map(&:spec)` interleaves
|
|
95
|
+
consumption — `.spec` runs while the entry is still open. Materialize the
|
|
96
|
+
packages first (`.to_a` then `map(&:spec)`, or stash the entries) and every
|
|
97
|
+
`.spec` raises `IOError`.
|
|
98
|
+
|
|
99
|
+
**Rule:** read specs eagerly, inside the iteration. Tarvault sidesteps the trap
|
|
100
|
+
entirely — it lists from the manifest and reads any single gem via
|
|
101
|
+
`TarReader#seek` into a private `StringIO`
|
|
102
|
+
(`Gem::Package.new(StringIO.new(entry.read)).spec`), never depending on
|
|
103
|
+
post-close entry behavior.
|
|
104
|
+
|
|
105
|
+
## Doc Q2 — Encryption: is it possible?
|
|
106
|
+
|
|
107
|
+
Yes (feasibility only — not implemented). `OpenSSL::Cipher` AES-256-GCM
|
|
108
|
+
round-trips raw `.gem` bytes cleanly (verified, incl. auth-tag failure on
|
|
109
|
+
tamper). The design already reserves the extension point: `Manifest::Record`
|
|
110
|
+
carries an `encrypted` flag (currently always `false`). Encryption would:
|
|
111
|
+
|
|
112
|
+
- encrypt the raw `.gem` bytes, store the ciphertext as the tar entry, and set
|
|
113
|
+
`encrypted: true` plus cipher/iv/auth-tag on the record;
|
|
114
|
+
- `gem_data` decrypts (key from env/keyring) before returning, composing with
|
|
115
|
+
the checksum by hashing the plaintext.
|
|
116
|
+
|
|
117
|
+
Because gemvault mediates every read and the manifest signals encryption, this
|
|
118
|
+
is a localized change in `gem_data`. `OpenSSL` ships on JRuby, so it does not
|
|
119
|
+
reintroduce a portability problem.
|
|
120
|
+
|
|
121
|
+
## Portability (the motivation)
|
|
122
|
+
|
|
123
|
+
The tar path uses only pure-Ruby rubygems tooling (`Gem::Package::TarReader`/
|
|
124
|
+
`TarWriter`), `OpenSSL`, and `JSON` — all present on JRuby. The facade lazily
|
|
125
|
+
`require`s `dbvault` (hence `sqlite3`) **only** when opening an existing SQLite
|
|
126
|
+
file, so a JRuby process that only touches Tarvaults never loads `sqlite3`.
|
|
127
|
+
|
|
128
|
+
**Done:** `sqlite3` is no longer a runtime dependency. It is removed from the
|
|
129
|
+
gemspec and loaded lazily only to read a legacy SQLite vault; if it is absent,
|
|
130
|
+
`Vault.build_dbvault` raises a clear "install sqlite3 or upgrade the vault"
|
|
131
|
+
error. A process that only touches Tarvaults never loads it (verified by a
|
|
132
|
+
subprocess spec asserting `defined?(SQLite3)` is `nil`).
|
|
133
|
+
|
|
134
|
+
## Atomicity & locking
|
|
135
|
+
|
|
136
|
+
Tar has no index, and the leading `manifest.json` changes size on every
|
|
137
|
+
mutation, so **every add/remove is a full rewrite** — to a sibling tempfile in
|
|
138
|
+
the target directory (same filesystem → atomic `File.rename`), with `fsync`
|
|
139
|
+
before rename. `flock` (the doc's suggestion) would guard concurrent *gemvault*
|
|
140
|
+
processes only; external tar edits remain unsupported by design and are out of
|
|
141
|
+
spike scope.
|
|
142
|
+
|
|
143
|
+
## Follow-on / productionization notes
|
|
144
|
+
|
|
145
|
+
- Streaming rewrite: the current model reads all gem blobs into memory during a
|
|
146
|
+
rewrite — fine for a spike; large vaults would want a copy-through stream.
|
|
147
|
+
- Encryption (`encrypted` flag → real cipher params).
|
|
148
|
+
- List-time integrity verification, if wanted.
|
|
149
|
+
- Unify the two backends' error taxonomy and temp-file logic once Tarvault is
|
|
150
|
+
confirmed as "the way."
|
|
151
|
+
|
|
152
|
+
There is deliberately **no** format-selection flag on `gemvault new`. Tarvault is
|
|
153
|
+
the way forward; new vaults are always Tarvaults. A user who needs a SQLite vault
|
|
154
|
+
uses an older gemvault.
|
|
155
|
+
|
|
156
|
+
## Update — format versioning & `gemvault upgrade` (implemented)
|
|
157
|
+
|
|
158
|
+
A follow-up shipped explicit format versioning and a migration command:
|
|
159
|
+
|
|
160
|
+
- Each vault records an on-disk **format version** (`1` = Dbvault, `2` = Tarvault)
|
|
161
|
+
read and validated on open. It is **decoupled from the gemvault gem version** —
|
|
162
|
+
it bumps only when the byte layout changes. `Vault::UnsupportedVersionError` is
|
|
163
|
+
raised for a vault newer than `Vault::CURRENT_FORMAT` instead of misreading it,
|
|
164
|
+
closing the "reads fail open" hole.
|
|
165
|
+
- `Vault.backend_for` now positively identifies the container (`:sqlite`/`:tar`/
|
|
166
|
+
`:unknown`) ahead of parsing and refuses unrecognized envelopes.
|
|
167
|
+
- `Gemvault::VaultUpgrade` (+ `gemvault upgrade`) reads a vault through its
|
|
168
|
+
existing backend and rewrites it in the current format with an atomic swap,
|
|
169
|
+
a default `.bak` backup, `--dry-run`, idempotency, and `created_at` preservation
|
|
170
|
+
(the write path gained `add(created_at:)`).
|
|
171
|
+
|
|
172
|
+
`sqlite3` is now optional (removed from the gemspec, loaded lazily, clear error
|
|
173
|
+
when absent). There is no `gemvault new --format` flag by design — Tarvault is
|
|
174
|
+
the only write format.
|
|
175
|
+
|
|
176
|
+
The Dbvault deprecation lifecycle is decided and phase 1 is shipped:
|
|
177
|
+
|
|
178
|
+
- **Now:** SQLite vaults are **read-only** (`Dbvault#add`/`#remove` raise
|
|
179
|
+
`Vault::ReadOnlyError` → `gemvault upgrade`), reads still work, and opening one
|
|
180
|
+
emits a once-per-process notice via `Gemvault::Deprecation` (stderr, silenceable
|
|
181
|
+
with `GEMVAULT_SILENCE_DEPRECATIONS=1`, suppressed during `gemvault upgrade`).
|
|
182
|
+
"Writable" tracks *created-this-session* so migration/legacy tooling that
|
|
183
|
+
constructs a Dbvault directly still works.
|
|
184
|
+
- **0.3–0.5:** Dbvault removed entirely.
|
|
185
|
+
|
|
186
|
+
Still open: streaming rewrite and encryption.
|
data/docs/tarvault.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# GEMVAULT TAR SPIKE
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Gemvault has been working thus far, but the sqlite3 dependency makes the gem less portable then it should be:
|
|
6
|
+
|
|
7
|
+
1. The whole idea and basis of gemvault is that it _is_ a portable gem server in a file, and you shouldn't need dependencies to use it.
|
|
8
|
+
2. sqlite3 is not supported by jruby
|
|
9
|
+
|
|
10
|
+
## Solution
|
|
11
|
+
|
|
12
|
+
Since gemvault is just reading blobs from sqlite3, and doesn't really need any of its other stuff, I propose using a simpler storage model - a tarball. *.gem files, otherwise known as Packages, already are stored in this format, and all of the necessary equipment for reading them is already in rubygems, so it's a natural fit. If we are really worried about integrity that sqlite3 provides, we can use flock or similar to do locking. It is not intended or recommended to manipulate the tar version of the gemv file without going through gemvault, so anything beyond that is not supported. We shall refer to the tar version of a gemv file as a Tarvault and the sqlite3 version as a Dbvault to ensure communication is clear.
|
|
13
|
+
|
|
14
|
+
One can read all the gemspecs from a Tarvault using this chained oneliner:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gemspecs = Enumerator.new { |y| Gem::Package::TarReader.new(File.open("tarvault.gemv")) { |reader| reader.each_entry { y << Gem::Package.new(it) } } }.map(&:spec)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
All of the dependencies are already provided.
|
|
23
|
+
|
|
24
|
+
## Things to think about
|
|
25
|
+
|
|
26
|
+
1. A Dbvault makes it trivial to store additional information about a given Package. I think we can meet any needs for metadata via a json manifest or metadata file to keep as the very first entry in a Tarvault. So structure would resemble something like:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
$ tar -tf tarvault.gemv
|
|
30
|
+
|
|
31
|
+
manifest.json
|
|
32
|
+
myprivategem-0.1.0.gem
|
|
33
|
+
rails-8.0.0.gem
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
1. For files like this, e.g. files within a file, what is the usual way to maintain integrity? Are checksums used?
|
|
38
|
+
2. Encryption can be supported by encrypting a Package directly, and appending the raw bytes directly to the Tarvault. The manifest can indicate that they are encrypted. OpenSSL can be used all the way through (Encryption is not to be done on this spike, but whether it is possible is a key factor)
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
1. gemvault is below 1.0.0 at the moment and therefore does not guarantee any backwards compatibility with sqlite3 version or anything for that matter. Keeping this compatitble with the Dbvault is not something you should keep in mind. Move forward. Users can use an old version of gemvault if we decide that Tarvault is _the way_
|