gemvault 0.2.4 → 0.2.6
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/CHANGELOG.md +32 -0
- data/CLAUDE.md +4 -3
- data/README.md +18 -3
- data/issues.rec +209 -3
- data/lib/gemvault/bundler_gemfile.rb +63 -0
- data/lib/gemvault/bundler_plugin_index.rb +58 -0
- data/lib/gemvault/bundler_plugin_root.rb +59 -0
- data/lib/gemvault/cli/commands/doctor.rb +127 -10
- data/lib/gemvault/ghost_specification.rb +87 -0
- data/lib/gemvault/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c8bf335258cabfbfbe016fe547e1302da87c37060d1523cae394e941bd493dd
|
|
4
|
+
data.tar.gz: f4a027c561cb8edf619f777989654d657bae70d741106ba8730b540b38de7f2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f3691b960faaa2d6ab493d18363091691ce3ac106a23d8859a052e946fd6aaa78178282da8269823e07c21cf52cb5ba5e9e262c51767aea19d805b620ee6287
|
|
7
|
+
data.tar.gz: 99feeb31a5af138c4da80b61b3fe3853cd545044954b895e202d57a5f7c2817b3cb59701f752140efbce75c839120f4b6bcf0a5871a806a94cc80cc6d12670ed
|
data/CHANGELOG.md
CHANGED
|
@@ -24,6 +24,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
24
24
|
is shared with the RubyGems source via `Gemvault::VaultPath` (issue #9).
|
|
25
25
|
|
|
26
26
|
### Fixed
|
|
27
|
+
- `gemvault doctor` now repairs ghost installations: a
|
|
28
|
+
`specifications/<gem>-<version>.gemspec` left behind after its gem directory
|
|
29
|
+
was removed (interrupted uninstall, hand-cleaned gem home), even when the
|
|
30
|
+
record itself is truncated or unreadable. Such a ghost of
|
|
31
|
+
`bundler-source-vault` makes every `bundle install` fail with
|
|
32
|
+
`Bundler::Plugin::MalformattedPlugin (plugins.rb was not found in the
|
|
33
|
+
plugin.)` — Bundler reinstalls the gem correctly but validates the ghost's
|
|
34
|
+
dead path — and nothing project-local can recover, because the wreck lives
|
|
35
|
+
in the ambient gem home (issue #23).
|
|
36
|
+
- `gemvault doctor` now works in a project whose Gemfile is inline
|
|
37
|
+
(`bundler/inline`). Such a script keeps its plugin index in
|
|
38
|
+
`<project>/.bundle/plugin`, a root bundler only consults mid-script, so
|
|
39
|
+
doctor's uninstall silently repaired nothing and the `bundle install` that
|
|
40
|
+
followed dumped bundler's entire usage screen. Doctor now reaches the
|
|
41
|
+
project's index the way `bundler/inline` does and, with no Gemfile to
|
|
42
|
+
reinstall from, says so and exits 0 — the inline gemfile reinstalls the
|
|
43
|
+
plugin the next time the script runs (issue #14). Run outside any project,
|
|
44
|
+
doctor no longer claims to have cleared a project index it never touched.
|
|
45
|
+
- `gemvault doctor` no longer strands a machine without the plugin. Its
|
|
46
|
+
closing `bundle install` does more than reinstall the plugin, and when that
|
|
47
|
+
extra work failed (a Ruby version pin, an unresolvable gem) the already-run
|
|
48
|
+
uninstall had cleared the plugin index for nothing — strictly worse than
|
|
49
|
+
the wreck doctor was asked to repair. The index is now snapshotted before
|
|
50
|
+
the uninstall and restored when the reinstall never happened, `bundle
|
|
51
|
+
install` runs as a child rather than replacing the process, and every
|
|
52
|
+
failure is one line on stderr and exit 1 instead of a backtrace
|
|
53
|
+
(issues #27, #24).
|
|
54
|
+
- The shim no longer spams `already initialized constant` warnings when
|
|
55
|
+
bundler evaluates two installed copies of it in one process — an upgrade
|
|
56
|
+
resolving a newer bundler-source-vault while the plugin index still names
|
|
57
|
+
the old one. The first copy wins, matching the guard plugins.rb already
|
|
58
|
+
applies to the vault source class (issue #26).
|
|
27
59
|
- `bundle plugin install bundler-source-vault` no longer dies with
|
|
28
60
|
`LoadError: cannot load such file -- bundler/plugin/vault_source`. The shim's
|
|
29
61
|
`plugins.rb` now derives the gem root from its own installed location (local
|
data/CLAUDE.md
CHANGED
|
@@ -60,6 +60,7 @@ gem install --source file:///path/to/myvault.gemv foo
|
|
|
60
60
|
- `gemvault.gemspec` — main gem spec (name: `gemvault`)
|
|
61
61
|
- `lib/gemvault/vault.rb` — Vault facade choosing a backend by file format (Tarvault current, legacy Dbvault read-only)
|
|
62
62
|
- `lib/gemvault/cli.rb` — CLI dispatcher (new/add/list/remove/extract)
|
|
63
|
+
- `lib/gemvault/ghost_specification.rb` — installation records whose gem directory is gone; swept by `gemvault doctor` (issue #23)
|
|
63
64
|
- `lib/bundler/plugin/vault_source.rb` — Bundler `Plugin::API::Source` implementation
|
|
64
65
|
- `lib/rubygems_plugin.rb` — RubyGems plugin: monkey-patches for `--source myvault.gemv` support
|
|
65
66
|
- `lib/rubygems/source/vault.rb` — `Gem::Source::Vault` class (spec loading, download, `file://` URI handling, verbose logging)
|
|
@@ -115,9 +116,9 @@ bundle exec rake spec:teardown # remove it
|
|
|
115
116
|
Integration specs serve the tree's own gems from a local gem index (`GemIndex`)
|
|
116
117
|
to avoid rubygems.org resolution during testing.
|
|
117
118
|
|
|
118
|
-
Rubycritic scores `lib/`, `test/` and `shim/` but not
|
|
119
|
-
block-nesting level, so idiomatic
|
|
120
|
-
rubocop-rspec owns spec style.
|
|
119
|
+
Rubycritic scores `lib/`, `spec/support/`, `test/` and `shim/` but not spec
|
|
120
|
+
example files: flog taxes each block-nesting level, so idiomatic
|
|
121
|
+
describe/context nesting reads as complexity. rubocop-rspec owns spec style.
|
|
121
122
|
|
|
122
123
|
### Container fidelity — do not undo these
|
|
123
124
|
|
data/README.md
CHANGED
|
@@ -70,9 +70,20 @@ When Bundler sees `type: :vault` in your Gemfile, it auto-installs the `bundler-
|
|
|
70
70
|
|
|
71
71
|
The RubyGems plugin works similarly: `gem install --source vault.gemv` loads specs and extracts gems on demand.
|
|
72
72
|
|
|
73
|
-
## Recovering from
|
|
73
|
+
## Recovering from broken bundler plugin state: `gemvault doctor`
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
Two kinds of wreckage make every `bundle install` fail, and `gemvault doctor` repairs both.
|
|
76
|
+
|
|
77
|
+
**A ghost installation.** If a gem home holds a `specifications/bundler-source-vault-<version>.gemspec` whose `gems/` directory is gone (an interrupted `gem uninstall`, a hand-cleaned gem home), bundler trusts the leftover record and every install dies with:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Installing bundler-source-vault 0.2.4
|
|
81
|
+
Failed to install plugin `bundler-source-vault`, due to Bundler::Plugin::MalformattedPlugin (plugins.rb was not found in the plugin.)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Deleting `.bundle` or the lockfile cannot help — the wreck lives in the machine's gem home, not the project. `doctor` finds such records across your gem roots, removes them (reporting each), and reinstalls the plugin.
|
|
85
|
+
|
|
86
|
+
**A broken plugin path.** 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:
|
|
76
87
|
|
|
77
88
|
```
|
|
78
89
|
The following plugin paths don't exist: /path/to/gemvault/shim/.
|
|
@@ -87,7 +98,11 @@ To recover, update the Gemfile to point at the new path and run:
|
|
|
87
98
|
gemvault doctor
|
|
88
99
|
```
|
|
89
100
|
|
|
90
|
-
`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.
|
|
101
|
+
`doctor` removes any ghost installation records, clears the broken entry from bundler's plugin index (`bundle plugin uninstall bundler-source-vault`) and then, when a Gemfile exists, re-runs `bundle install`, which reinstalls the plugin against whatever the current Gemfile declares. Run it from your project directory. If a ghost record sits in a root-owned gem home, doctor says so on one line — re-run it with permissions for that gem home (e.g. `sudo gemvault doctor`).
|
|
102
|
+
|
|
103
|
+
A project whose Gemfile is inline (`require "bundler/inline"`) works too. Such a script keeps its plugin index in `<project>/.bundle/plugin`, a root bundler only consults while the script runs; doctor reaches it the same way the script did. With no Gemfile on disk there is nothing to reinstall from, so doctor clears the entry, says so, and exits 0 — the inline gemfile reinstalls the plugin the next time the script runs. Run outside any project, doctor works against bundler's global index and points you back at the project directory for the reinstall.
|
|
104
|
+
|
|
105
|
+
The repair is transactional. `bundle install` does more than reinstall the plugin, and that extra work can fail on its own — a Ruby version pin, an unresolvable gem. When it does, doctor says so in one line and exits 1; and if the failure struck before the plugin was reinstalled, doctor puts the plugin index back the way it found it. A failed run never leaves the machine with less than it started with — fix the install error and re-run.
|
|
91
106
|
|
|
92
107
|
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.
|
|
93
108
|
|
data/issues.rec
CHANGED
|
@@ -750,7 +750,42 @@ Id: 14
|
|
|
750
750
|
Updated: Wed, 29 Jul 2026 16:41:11 -0400
|
|
751
751
|
Name: gemvault doctor fails when bundler/inline is used
|
|
752
752
|
Description: Loudly fails with 2 errors, one of which is "can't find gemfile"
|
|
753
|
-
|
|
753
|
+
+
|
|
754
|
+
+ RESOLUTION: both errors reproduced in podman, and doctor turned out to be
|
|
755
|
+
+ broken twice over rather than once.
|
|
756
|
+
+
|
|
757
|
+
+ (1) "Plugin bundler-source-vault is not installed". bundler/inline sets
|
|
758
|
+
+ BUNDLE_GEMFILE to a bare "Gemfile" for the life of the script (bundler
|
|
759
|
+
+ inline.rb:53), so Bundler.root is the script's working directory and the plugin
|
|
760
|
+
+ index is written to <project>/.bundle/plugin. A later `gemvault doctor` sets no
|
|
761
|
+
+ such variable and finds no Gemfile on disk, so Bundler::Plugin.root --
|
|
762
|
+
+ local_root only when SharedHelpers.in_bundle?, global_root otherwise
|
|
763
|
+
+ (plugin.rb:162) -- falls back to ~/.bundle/plugin. The uninstall therefore
|
|
764
|
+
+ looked in the wrong root and repaired nothing, silently.
|
|
765
|
+
+
|
|
766
|
+
+ (2) "Could not locate Gemfile". doctor then exec'd `bundle install`, which with
|
|
767
|
+
+ no Gemfile prints its entire usage screen and exits 10 -- reading as gemvault
|
|
768
|
+
+ itself failing, after the repair it was asked for had already not happened.
|
|
769
|
+
+
|
|
770
|
+
+ doctor now runs the uninstall with BUNDLE_GEMFILE="Gemfile" when the project
|
|
771
|
+
+ owns a plugin root Bundler would ignore, which is bundler/inline's own trick and
|
|
772
|
+
+ needs no file to exist; verified against the real thing (plain uninstall says
|
|
773
|
+
+ "not installed", the same command with BUNDLE_GEMFILE set says "Uninstalled
|
|
774
|
+
+ plugin bundler-source-vault" and empties the index). When no Gemfile exists it
|
|
775
|
+
+ skips the reinstall and says so, exiting 0, because clearing the entry is the
|
|
776
|
+
+ repair and only the reinstall is unavailable -- an inline gemfile reinstalls the
|
|
777
|
+
+ plugin the next time the script runs.
|
|
778
|
+
+
|
|
779
|
+
+ Gemvault::BundlerGemfile mirrors SharedHelpers#find_gemfile and
|
|
780
|
+
+ Gemvault::BundlerPluginRoot mirrors Plugin.root, both reimplemented rather than
|
|
781
|
+
+ delegated because bundler is deliberately not a dependency and doctor runs
|
|
782
|
+
+ outside any Bundler process. Replicated first by
|
|
783
|
+
+ spec/integration/cli/commands/doctor_inline_spec.rb -- all 8 examples failed
|
|
784
|
+
+ against the old code -- with unit cover in spec/gemvault/bundler_gemfile_spec.rb,
|
|
785
|
+
+ spec/gemvault/bundler_plugin_root_spec.rb and the extended doctor_spec.rb. The
|
|
786
|
+
+ inline support now runs its script from the project directory, as a real user
|
|
787
|
+
+ does; that is what puts the plugin root where the report describes.
|
|
788
|
+
Status: closed
|
|
754
789
|
|
|
755
790
|
Id: 15
|
|
756
791
|
Updated: Wed, 29 Jul 2026 17:39:23 -0400
|
|
@@ -783,7 +818,11 @@ Id: 16
|
|
|
783
818
|
Updated: Wed, 29 Jul 2026 18:25:48 -0400
|
|
784
819
|
Name: Add jruby-ci workflow
|
|
785
820
|
Description: It should be already set up on the jruby-support branch. It just needs to be picked from it. DO NOT use the other code in that - it is very outdated
|
|
786
|
-
|
|
821
|
+
+
|
|
822
|
+
+ RESOLUTION: shipped in commit 635ba41 ("ci: add JRuby test job") -- the jruby
|
|
823
|
+
+ job in .github/workflows/ci.yml runs the minitest suite on jruby-10.1.0.0 and
|
|
824
|
+
+ is green on CI.
|
|
825
|
+
Status: closed
|
|
787
826
|
|
|
788
827
|
Id: 17
|
|
789
828
|
Updated: Thu, 30 Jul 2026 00:07:42 +0000
|
|
@@ -972,7 +1011,15 @@ Description: [dev@rstore]/workspace% bundle install
|
|
|
972
1011
|
+ from /usr/local/bin/bundle:25:in '<main>'
|
|
973
1012
|
+
|
|
974
1013
|
+ Though I will add that gemvault doctor followed by bundle install fixed the issue
|
|
975
|
-
|
|
1014
|
+
+
|
|
1015
|
+
+ RESOLUTION: the broken-plugin-path wreck, recovered exactly as reported --
|
|
1016
|
+
+ gemvault doctor then bundle install. The plugin index entry pointed into a
|
|
1017
|
+
+ reinstalled rbenv ruby's gem home; bundler does not revalidate stored plugin
|
|
1018
|
+
+ paths, skips loading the plugin, and crashes in SourceList#add_plugin_source,
|
|
1019
|
+
+ upstream behavior README documents under "A broken plugin path". doctor is
|
|
1020
|
+
+ the repair. Hinting at doctor from the failure site was considered and
|
|
1021
|
+
+ declined: the crash happens in bundler frames before any gemvault code loads.
|
|
1022
|
+
Status: closed
|
|
976
1023
|
|
|
977
1024
|
Id: 21
|
|
978
1025
|
Updated: Fri, 31 Jul 2026 13:54:09 -0400
|
|
@@ -1011,9 +1058,168 @@ Description: bundler-cache: true on the ruby/setup-ruby action will not work cor
|
|
|
1011
1058
|
+ run: | # confine gems to vendor/bundle so the cache never shadows vendor/vendored.gemv
|
|
1012
1059
|
+ bundle config set --local path vendor/bundle
|
|
1013
1060
|
+ bundle install --jobs 4 --retry 3
|
|
1061
|
+
Status: open
|
|
1062
|
+
|
|
1063
|
+
Id: 26
|
|
1014
1064
|
Updated: Mon, 10 Aug 2026 10:14:51 -0400
|
|
1015
1065
|
Name: Gemvault spams warnings on bundle install
|
|
1016
1066
|
Description: /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:31: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::GEM
|
|
1017
1067
|
+ /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:32: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::SHIM
|
|
1018
1068
|
+ /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:33: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::VAULT_SOURCE
|
|
1069
|
+
+
|
|
1070
|
+
+ RESOLUTION: bundler can evaluate two installed copies of the shim in one
|
|
1071
|
+
+ process -- an upgrade resolving a newer bundler-source-vault while the index
|
|
1072
|
+
+ still names the old one, the dual-version plugin root the failure report
|
|
1073
|
+
+ also recorded as issue #29 -- and require_relative cannot deduplicate
|
|
1074
|
+
+ across paths, so the second copy reopened GemvaultLoadPath and warned on
|
|
1075
|
+
+ every constant. gemvault_load_path.rb now returns before redefining
|
|
1076
|
+
+ anything when the module is already loaded, first copy winning as
|
|
1077
|
+
+ plugins.rb already arranges for Bundler::Plugin::VaultSource. Replicated by
|
|
1078
|
+
+ spec/shim_reload_spec.rb (two copies loaded in one process, red against the
|
|
1079
|
+
+ old shim), with the plain-install path guarded in
|
|
1080
|
+
+ spec/integration/bundle_install_spec.rb.
|
|
1081
|
+
Status: closed
|
|
1082
|
+
|
|
1083
|
+
Id: 23
|
|
1084
|
+
Updated: Mon, 10 Aug 2026 17:40:28 -0400
|
|
1085
|
+
Name: URGENT bundler plugin completely broken
|
|
1086
|
+
Description: ❯
|
|
1087
|
+
+ ❯ bundle install
|
|
1088
|
+
+ Fetching gem metadata from https://rubygems.org/.......
|
|
1089
|
+
+ Resolving dependencies...
|
|
1090
|
+
+ Installing bundler-source-vault 0.2.4
|
|
1091
|
+
+ Failed to install plugin `bundler-source-vault`, due to Bundler::Plugin::MalformattedPlugin (plugins.rb was not found in the plugin.)
|
|
1092
|
+
+
|
|
1093
|
+
+ RESOLUTION: the machine, not the gem. bundler-source-vault-0.2.4 as published
|
|
1094
|
+
+ contains plugins.rb (verified against rubygems.org; identical to 0.2.2 but for
|
|
1095
|
+
+ versions). The trigger is a ghost installation in an ambient gem root:
|
|
1096
|
+
+ specifications/bundler-source-vault-0.2.4.gemspec still present after
|
|
1097
|
+
+ gems/bundler-source-vault-0.2.4/ was removed (interrupted uninstall or
|
|
1098
|
+
+ hand-cleaned gem home). Bundler's Gemfile-driven plugin install then walks
|
|
1099
|
+
+ into an upstream bug: resolution materializes the plugin as the installed
|
|
1100
|
+
+ stub; rubygems.org's compact index carries created_at, and the cooldown
|
|
1101
|
+
+ backfill (Source::Rubygems#backfill_created_at) attaches the remote to that
|
|
1102
|
+
+ stub; download() therefore swaps the freshly fetched package spec into the
|
|
1103
|
+
+ stub, and the installed? check inside install() memoizes that spec's
|
|
1104
|
+
+ full_gem_path while its loaded_from is still nil -- the Gem.dir fallback,
|
|
1105
|
+
+ i.e. the ghost's ambient path. Gem::Installer then extracts the gem
|
|
1106
|
+
+ correctly into .bundle/plugin, but the poisoned memo survives, install()
|
|
1107
|
+
+ copies it back onto the stub, and validate_plugin! checks plugins.rb against
|
|
1108
|
+
+ the dead ambient directory instead of the copy it just installed. Hence the
|
|
1109
|
+
+ report's shape -- "Installing bundler-source-vault 0.2.4" every run, then
|
|
1110
|
+
+ MalformattedPlugin -- and hence its permanence: rm -rf .bundle, deleting the
|
|
1111
|
+
+ lockfile and gemvault doctor all rebuilt project state while the wreck sat
|
|
1112
|
+
+ in the ambient gem home. Present in bundler 4.0.17, 4.0.18 and master;
|
|
1113
|
+
+ worth filing at rubygems/rubygems.
|
|
1114
|
+
+
|
|
1115
|
+
+ gemvault doctor now repairs the wreck: Gemvault::GhostSpecification finds
|
|
1116
|
+
+ specifications whose gem directory is gone across Gem.path for the two gems
|
|
1117
|
+
+ this project ships, and doctor removes them (reporting each) before its
|
|
1118
|
+
+ existing uninstall-and-reinstall; a removal it cannot perform is one line on
|
|
1119
|
+
+ stderr and exit 1. Replicated first by spec/integration/ghost_plugin_spec.rb
|
|
1120
|
+
+ (real container, real ghost, real exit code): bundle install fails with
|
|
1121
|
+
+ exactly the reported message, and the doctor context recovers to a complete
|
|
1122
|
+
+ bundle. Unit cover in spec/gemvault/ghost_specification_spec.rb and
|
|
1123
|
+
+ spec/gemvault/cli/commands/doctor_spec.rb.
|
|
1124
|
+
+
|
|
1125
|
+
+ The integration suite could not have caught this: GemIndex served only the
|
|
1126
|
+
+ legacy marshal index, and only a compact index that carries created_at sends
|
|
1127
|
+
+ Bundler down the stub-with-remote path. GemIndex now serves the compact
|
|
1128
|
+
+ index too (versions and info/NAME with checksum and created_at, rendered by
|
|
1129
|
+
+ GemIndexFiles), matching what rubygems.org sends, with the marshal files
|
|
1130
|
+
+ kept for `gem install --source`.
|
|
1131
|
+
+
|
|
1132
|
+
+ Review hardening: detection evaluates no Ruby. The gem directory's name is
|
|
1133
|
+
+ the record's file name minus .gemspec by RubyGems' install convention, so a
|
|
1134
|
+
+ missing payload is recognized from the filesystem alone; scoping to the
|
|
1135
|
+
+ asked gem reads the record's "# stub:" header when readable and falls back
|
|
1136
|
+
+ to the filename. That sweeps truncated or unreadable records -- wreckage in
|
|
1137
|
+
+ their own right, and Bundler's stub path needs only the header line, so a
|
|
1138
|
+
+ truncated record can keep triggering this bug -- instead of silently
|
|
1139
|
+
+ skipping them, and an unreadable neighboring record no longer aborts the
|
|
1140
|
+
+ scan. Deliberately out of scope: a gem directory that exists but lost part
|
|
1141
|
+
+ of its payload (same MalformattedPlugin symptom via a shorter path).
|
|
1142
|
+
+ Telling that wreck apart from a healthy install would mean trusting the
|
|
1143
|
+
+ record's file list, and a false positive there deletes a working
|
|
1144
|
+
+ installation's record; a report of that wreck reopens this.
|
|
1145
|
+
Status: closed
|
|
1146
|
+
|
|
1147
|
+
Id: 24
|
|
1148
|
+
Updated: Mon, 10 Aug 2026 23:16:35 +0000
|
|
1149
|
+
Name: doctor prints a backtrace when the plugin uninstall step fails
|
|
1150
|
+
Description: gemvault doctor's `bundle plugin uninstall` step runs with exception: true; a nonzero exit raises RuntimeError and a missing bundle raises Errno::ENOENT, and both escape Command#run to command_kit's ExceptionHandler, which prints error.full_message -- a full backtrace -- violating the one-line-and-exit-1 convention issue #15 established. Adjacent to issue #14 (doctor failing under bundler/inline). The ghost-specification sweep added for issue #23 reports its own failures as one line; the uninstall step should match.
|
|
1151
|
+
+
|
|
1152
|
+
+ RESOLUTION: fixed alongside issue #27's transactional rework. The uninstall
|
|
1153
|
+
+ step no longer runs with exception: true; a failing or missing bundle is one
|
|
1154
|
+
+ line on stderr ("doctor: bundle plugin uninstall bundler-source-vault
|
|
1155
|
+
+ failed") and exit 1, matching the convention issues #15 and #23 established,
|
|
1156
|
+
+ and the snapshotted index is left untouched.
|
|
1157
|
+
Status: closed
|
|
1158
|
+
|
|
1159
|
+
Id: 25
|
|
1160
|
+
Updated: Tue, 11 Aug 2026 20:05:27 -0400
|
|
1161
|
+
Name: Project with Gemfile + locked json version + gemvault throws error
|
|
1162
|
+
Description: Project with Gemfile + locked json version + gemvault throws "bundler: failed to load command: rails (/private/tmp/local/flipmine/flipmine/vendor/ruby/4.0.0/bin/rails)
|
|
1163
|
+
+ /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.18/lib/bundler/runtime.rb:328:in 'Bundler::Runtime#check_for_activated_spec!': You have already activated json 2.21.2, but your Gemfile requires json 2.19.7. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)"
|
|
1164
|
+
+
|
|
1165
|
+
+ I believe this error to be coming from gemvault's dependency on JSON. It is clear that dependencies are cursed in this project, whether its the added load time for reinstalling them on every `bundle install`, or errors like this one. Rather than rely on JSON, I think it would be better to just marshal our own manifest, or use some other serialization tool if any that rubygems already provides.
|
|
1166
|
+
Status: open
|
|
1167
|
+
|
|
1168
|
+
Id: 27
|
|
1169
|
+
Updated: Wed, 12 Aug 2026 16:05:55 +0000
|
|
1170
|
+
Name: gemvault doctor can leave the machine with no plugin at all
|
|
1171
|
+
Description: Reported from a container where bundle install could not succeed
|
|
1172
|
+
+ (ruby version pin mismatch): doctor removed ghost records, uninstalled the
|
|
1173
|
+
+ plugin, emptied .bundle/plugin/index, then exec'd bundle install -- which
|
|
1174
|
+
+ failed, leaving no plugin installed. Strictly worse than the broken-path
|
|
1175
|
+
+ state doctor was invoked to repair, and recoverable only by knowing to run
|
|
1176
|
+
+ `bundle plugin install bundler-source-vault` by hand. doctor repairs by
|
|
1177
|
+
+ mutation with no dry-run, no report of findings before acting, and no
|
|
1178
|
+
+ rollback when the reinstall it hands off to fails. Suggested in the report:
|
|
1179
|
+
+ report-then-fix (--dry-run, or report by default and repair under --fix);
|
|
1180
|
+
+ reinstall the plugin directly with `bundle plugin install` and verify the
|
|
1181
|
+
+ index rather than exec'ing into `bundle install`, so a failed dependency
|
|
1182
|
+
+ install cannot take the plugin down with it; or restore the previous index
|
|
1183
|
+
+ when the step after the uninstall fails.
|
|
1184
|
+
+
|
|
1185
|
+
+ RESOLUTION: the uninstall-reinstall pair is now transactional. doctor
|
|
1186
|
+
+ snapshots the plugin index bundler will consult (Gemvault::BundlerPluginIndex
|
|
1187
|
+
+ over BundlerPluginRoot#consulted) before clearing it, runs `bundle install`
|
|
1188
|
+
+ as a child instead of exec'ing into it, and on failure checks whether the
|
|
1189
|
+
+ plugin came back: reinstalled means the repair stands and the one-line error
|
|
1190
|
+
+ says only the bundle is unfinished; not reinstalled means the snapshot is
|
|
1191
|
+
+ restored, so the machine is left as found and re-running doctor once the
|
|
1192
|
+
+ install error is fixed recovers fully. Replicated first by
|
|
1193
|
+
+ spec/integration/cli/commands/doctor_reinstall_failure_spec.rb -- doctor run
|
|
1194
|
+
+ against a Gemfile still naming a renamed plugin path, an un-installable
|
|
1195
|
+
+ bundle in the reporter's shape -- which fails against the old code and
|
|
1196
|
+
+ proves the restored index heals on the next run. Unit cover in
|
|
1197
|
+
+ spec/gemvault/bundler_plugin_index_spec.rb and the reworked doctor_spec.rb.
|
|
1198
|
+
Status: closed
|
|
1199
|
+
|
|
1200
|
+
Id: 28
|
|
1201
|
+
Updated: Wed, 12 Aug 2026 16:05:55 +0000
|
|
1202
|
+
Name: plugins.rb dies with a bare LoadError when gemvault is installed nowhere
|
|
1203
|
+
Description: A plugin root holding only the shim -- the normal state wherever
|
|
1204
|
+
+ `gem install gemvault` has run, since Bundler skips installing a plugin
|
|
1205
|
+
+ dependency already present -- stops loading when the tree lands on a machine
|
|
1206
|
+
+ with no gemvault in any gem root: GemvaultLoadPath.entries returns [] and
|
|
1207
|
+
+ `require "bundler/plugin/vault_source"` raises a bare LoadError with no hint.
|
|
1208
|
+
+ Observed after a macOS working tree was copied into a Linux container (the
|
|
1209
|
+
+ shim alone in .bundle/plugin, gemvault absent from every gem root on the
|
|
1210
|
+
+ box); also reachable by `gem uninstall gemvault` on a single machine. The
|
|
1211
|
+
+ ambient-root search cannot cover gemvault being truly absent. Suggested:
|
|
1212
|
+
+ fail with an explicit "gemvault not found in any gem root" message naming
|
|
1213
|
+
+ the remedy (gem install gemvault, or bundle plugin uninstall + install to
|
|
1214
|
+
+ get a self-contained root), or vendor gemvault into the shim.
|
|
1215
|
+
Status: open
|
|
1216
|
+
|
|
1217
|
+
Id: 29
|
|
1218
|
+
Updated: Wed, 12 Aug 2026 16:05:55 +0000
|
|
1219
|
+
Name: Stale plugin versions are never reaped from the plugin root
|
|
1220
|
+
Description: bundler-source-vault-0.2.4 survived in the plugin root's gems/,
|
|
1221
|
+
+ specifications/ and cache/ across the 0.2.5 upgrade and an explicit `bundle
|
|
1222
|
+
+ plugin uninstall`; only 0.2.5 was ever referenced by the index. Possibly
|
|
1223
|
+
+ Bundler's doing rather than the plugin's, but doctor is well placed to sweep
|
|
1224
|
+
+ plugin-root versions the index no longer references.
|
|
1019
1225
|
Status: open
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
##
|
|
5
|
+
# The Gemfile Bundler would load from a directory.
|
|
6
|
+
#
|
|
7
|
+
# Mirrors Bundler::SharedHelpers#find_gemfile: <tt>BUNDLE_GEMFILE</tt> wins
|
|
8
|
+
# when set and non-empty, otherwise the search walks up looking for
|
|
9
|
+
# <tt>gems.rb</tt> then <tt>Gemfile</tt> in each directory. Reimplemented
|
|
10
|
+
# rather than delegated because bundler is deliberately not a gemvault
|
|
11
|
+
# dependency (see gemvault.gemspec) and +gemvault doctor+ runs as a plain
|
|
12
|
+
# CLI, outside any Bundler process.
|
|
13
|
+
#
|
|
14
|
+
# One difference is deliberate. Bundler returns <tt>BUNDLE_GEMFILE</tt>
|
|
15
|
+
# whatever it points at, existing or not -- bundler/inline relies on that,
|
|
16
|
+
# setting it to a bare "Gemfile" purely to make Bundler treat the working
|
|
17
|
+
# directory as the app root. The question here is whether +bundle install+
|
|
18
|
+
# could work, so a value that does not name a file is ignored and the
|
|
19
|
+
# walk-up search decides instead.
|
|
20
|
+
class BundlerGemfile
|
|
21
|
+
# Checked in this order within each directory, as Bundler checks them.
|
|
22
|
+
NAMES = ["gems.rb", "Gemfile"].freeze
|
|
23
|
+
|
|
24
|
+
def initialize(dir: Dir.pwd, env: ENV)
|
|
25
|
+
@dir = Pathname(dir)
|
|
26
|
+
@env = env
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# :call-seq:
|
|
30
|
+
# path -> Pathname or nil
|
|
31
|
+
#
|
|
32
|
+
# The Gemfile Bundler would load, or +nil+ when there is none.
|
|
33
|
+
def path
|
|
34
|
+
return @path if defined?(@path)
|
|
35
|
+
|
|
36
|
+
@path = from_env || search_up
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether Bundler would find a Gemfile at all.
|
|
40
|
+
def exist?
|
|
41
|
+
!path.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def from_env
|
|
47
|
+
given = @env["BUNDLE_GEMFILE"].to_s
|
|
48
|
+
return nil if given.empty?
|
|
49
|
+
|
|
50
|
+
candidate = Pathname(given).expand_path(@dir)
|
|
51
|
+
candidate.file? ? candidate : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def search_up
|
|
55
|
+
@dir.expand_path.ascend do |dir|
|
|
56
|
+
found = NAMES.map { |name| dir / name }.find(&:file?)
|
|
57
|
+
return found if found
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
##
|
|
5
|
+
# Bundler's plugin index file inside a plugin root.
|
|
6
|
+
#
|
|
7
|
+
# The uninstall step of +gemvault doctor+ clears entries from this file, and
|
|
8
|
+
# the reinstall that follows can fail for reasons unrelated to the plugin.
|
|
9
|
+
# Snapshot and restore make that pair transactional: a repair that cannot
|
|
10
|
+
# finish puts the index back instead of leaving the machine with no plugin
|
|
11
|
+
# registered at all (issue #27).
|
|
12
|
+
#
|
|
13
|
+
# The plugin_paths section is scanned textually rather than parsed as YAML:
|
|
14
|
+
# loading a YAML library into doctor's process invites the same
|
|
15
|
+
# stdlib-activation conflicts as issue #25, and bundler's own emitter writes
|
|
16
|
+
# one two-space-indented "name: path" line per plugin.
|
|
17
|
+
class BundlerPluginIndex
|
|
18
|
+
def initialize(root)
|
|
19
|
+
@root = Pathname(root)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def file
|
|
23
|
+
@root / "index"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether plugin_paths currently lists +plugin+.
|
|
27
|
+
def registered?(plugin)
|
|
28
|
+
plugin_paths.any? { |line| line.match?(/\A\s{2}#{Regexp.escape(plugin)}:/) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# :call-seq:
|
|
32
|
+
# snapshot -> String or nil
|
|
33
|
+
#
|
|
34
|
+
# The index content as it stands, +nil+ when no index exists.
|
|
35
|
+
def snapshot
|
|
36
|
+
file.file? ? file.read : nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Puts the index back the way +snapshot+ recorded it; restoring +nil+
|
|
40
|
+
# removes an index that did not exist at snapshot time.
|
|
41
|
+
def restore(snapshot)
|
|
42
|
+
return file.write(snapshot) if snapshot
|
|
43
|
+
|
|
44
|
+
file.delete if file.exist?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def plugin_paths
|
|
50
|
+
return [] unless file.file?
|
|
51
|
+
|
|
52
|
+
file.readlines
|
|
53
|
+
.drop_while { |line| !line.start_with?("plugin_paths:") }
|
|
54
|
+
.drop(1)
|
|
55
|
+
.take_while { |line| line.start_with?(" ") }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require_relative "bundler_gemfile"
|
|
3
|
+
|
|
4
|
+
module Gemvault
|
|
5
|
+
##
|
|
6
|
+
# Where Bundler keeps plugin data for a directory.
|
|
7
|
+
#
|
|
8
|
+
# Bundler::Plugin.root answers the project-local <tt>.bundle/plugin</tt> when
|
|
9
|
+
# <tt>SharedHelpers.in_bundle?</tt> holds, and the global
|
|
10
|
+
# <tt>~/.bundle/plugin</tt> when it does not. Since +in_bundle?+ is just
|
|
11
|
+
# "was a Gemfile found", a project can own a plugin root that Bundler
|
|
12
|
+
# declines to look in.
|
|
13
|
+
class BundlerPluginRoot
|
|
14
|
+
LOCAL_DIR = ".bundle/plugin".freeze
|
|
15
|
+
|
|
16
|
+
def initialize(dir: Dir.pwd, gemfile: BundlerGemfile.new(dir: dir), env: ENV)
|
|
17
|
+
@dir = Pathname(dir)
|
|
18
|
+
@gemfile = gemfile
|
|
19
|
+
@env = env
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The project's own plugin root, whether or not Bundler would consult it.
|
|
23
|
+
def local
|
|
24
|
+
@dir.expand_path / LOCAL_DIR
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Where Bundler keeps plugins for a user outside any project, mirroring
|
|
28
|
+
# Bundler's user_bundle_path lookup for plugins: <tt>BUNDLE_USER_PLUGIN</tt>
|
|
29
|
+
# names the root directly, <tt>BUNDLE_USER_HOME</tt> relocates
|
|
30
|
+
# <tt>.bundle</tt>, and the home directory is the default.
|
|
31
|
+
def global
|
|
32
|
+
named = @env["BUNDLE_USER_PLUGIN"]
|
|
33
|
+
return Pathname(named) if named
|
|
34
|
+
|
|
35
|
+
Pathname(@env["BUNDLE_USER_HOME"] || File.join(Dir.home, ".bundle")) / "plugin"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The plugin root Bundler will consult here: beside the Gemfile when one
|
|
39
|
+
# was found, the project's own root when doctor points Bundler at it (see
|
|
40
|
+
# #unreachable?), the global root otherwise.
|
|
41
|
+
def consulted
|
|
42
|
+
return @gemfile.path.dirname / LOCAL_DIR if @gemfile.exist?
|
|
43
|
+
return local if unreachable?
|
|
44
|
+
|
|
45
|
+
global
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether this project has a plugin root that Bundler currently ignores.
|
|
49
|
+
#
|
|
50
|
+
# bundler/inline installs plugins into <tt><cwd>/.bundle/plugin</tt>, having
|
|
51
|
+
# set <tt>BUNDLE_GEMFILE</tt> to a bare "Gemfile" for the duration of the
|
|
52
|
+
# script. A later command in that same directory sets no such variable and
|
|
53
|
+
# finds no Gemfile on disk, so Bundler falls back to the global root and
|
|
54
|
+
# reports the plugin as not installed -- while the entry sits here.
|
|
55
|
+
def unreachable?
|
|
56
|
+
!@gemfile.exist? && local.directory?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -1,26 +1,143 @@
|
|
|
1
1
|
require_relative "../command"
|
|
2
|
+
require_relative "../../bundler_gemfile"
|
|
3
|
+
require_relative "../../bundler_plugin_index"
|
|
4
|
+
require_relative "../../bundler_plugin_root"
|
|
5
|
+
require_relative "../../ghost_specification"
|
|
2
6
|
|
|
3
7
|
module Gemvault
|
|
4
8
|
class CLI
|
|
5
9
|
module Commands
|
|
6
|
-
# Recovers from
|
|
7
|
-
#
|
|
8
|
-
#
|
|
10
|
+
# Recovers from wrecked bundler plugin state. Two wrecks are repaired:
|
|
11
|
+
#
|
|
12
|
+
# A bundler plugin index entry whose stored path no longer exists.
|
|
13
|
+
# Bundler records absolute paths in .bundle/plugin/index for
|
|
14
|
+
# path-installed plugins. Moving or renaming the source directory
|
|
9
15
|
# leaves an invalid path behind -- Bundler::Plugin.load_plugin warns
|
|
10
16
|
# "The following plugin paths don't exist: ..." and silently returns,
|
|
11
17
|
# leaving @sources[<type>] nil. The next `source X, type: :vault`
|
|
12
18
|
# crashes inside Bundler::SourceList#add_plugin_source with
|
|
13
|
-
# NoMethodError on nil.
|
|
19
|
+
# NoMethodError on nil. Uninstalling clears the broken entry.
|
|
20
|
+
#
|
|
21
|
+
# A ghost specification: an installation record whose gem directory is
|
|
22
|
+
# gone (see Gemvault::GhostSpecification). Bundler's plugin installer
|
|
23
|
+
# trusts the record and validates plugins.rb against the missing
|
|
24
|
+
# directory instead of the copy it just installed, so every
|
|
25
|
+
# `bundle install` fails with MalformattedPlugin (issue #23). Removing
|
|
26
|
+
# the record lets the reinstall see the machine as it is.
|
|
27
|
+
#
|
|
28
|
+
# Re-running bundle install afterwards triggers Bundler to reinstall
|
|
29
|
+
# the plugin against whatever the current Gemfile declares. Run this
|
|
30
|
+
# from the project directory.
|
|
14
31
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
32
|
+
# A project whose Gemfile is inline has no Gemfile to reinstall from, and
|
|
33
|
+
# keeps its plugins in a root Bundler will not consult unless told to.
|
|
34
|
+
# Both are handled below.
|
|
35
|
+
#
|
|
36
|
+
# The repair is transactional. `bundle install` does more than reinstall
|
|
37
|
+
# the plugin, and that extra work can fail on its own; the index the
|
|
38
|
+
# uninstall clears is snapshotted first and put back when the reinstall
|
|
39
|
+
# never happened, so a failed run leaves the machine as found rather
|
|
40
|
+
# than with no plugin at all (issue #27). Every failure is one line on
|
|
41
|
+
# stderr and exit 1 (issue #24).
|
|
18
42
|
class Doctor < Command
|
|
19
|
-
description "
|
|
43
|
+
description "Repair broken bundler-source-vault plugin state and reinstall the plugin"
|
|
44
|
+
|
|
45
|
+
PLUGIN = "bundler-source-vault".freeze
|
|
46
|
+
|
|
47
|
+
OWNED_GEMS = %w[gemvault bundler-source-vault].freeze
|
|
48
|
+
|
|
49
|
+
PERMISSION_HINT = "(re-run with permissions for that gem home, e.g. sudo gemvault doctor)".freeze
|
|
20
50
|
|
|
21
51
|
def run
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
begin
|
|
53
|
+
remove_ghost_specifications
|
|
54
|
+
rescue SystemCallError => e
|
|
55
|
+
print_error("#{e.message} #{PERMISSION_HINT}")
|
|
56
|
+
exit(1)
|
|
57
|
+
end
|
|
58
|
+
saved = index.snapshot
|
|
59
|
+
uninstall_plugin
|
|
60
|
+
reinstall_or_explain(saved)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def remove_ghost_specifications
|
|
66
|
+
OWNED_GEMS.flat_map { |name| GhostSpecification.of(name) }.each do |ghost|
|
|
67
|
+
ghost.delete
|
|
68
|
+
puts "Removed ghost specification #{ghost}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def gemfile
|
|
73
|
+
@gemfile ||= BundlerGemfile.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def plugin_root
|
|
77
|
+
@plugin_root ||= BundlerPluginRoot.new(gemfile: gemfile)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def index
|
|
81
|
+
@index ||= BundlerPluginIndex.new(plugin_root.consulted)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# bundler/inline's own trick: a non-empty BUNDLE_GEMFILE is the whole of
|
|
85
|
+
# what Bundler::Plugin.root consults to prefer a project's plugin
|
|
86
|
+
# directory over the global one, so setting it reaches the entry that
|
|
87
|
+
# needs clearing. The file it names never has to exist.
|
|
88
|
+
def uninstall_env
|
|
89
|
+
return {} unless plugin_root.unreachable?
|
|
90
|
+
|
|
91
|
+
{ "BUNDLE_GEMFILE" => "Gemfile" }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def uninstall_plugin
|
|
95
|
+
return if system(uninstall_env, "bundle", "plugin", "uninstall", PLUGIN)
|
|
96
|
+
|
|
97
|
+
print_error("bundle plugin uninstall #{PLUGIN} failed")
|
|
98
|
+
exit(1)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `bundle install` with no Gemfile prints its entire usage screen and
|
|
102
|
+
# exits 10, which reads as gemvault itself failing. By this point the
|
|
103
|
+
# repair has already happened; only the reinstall is unavailable.
|
|
104
|
+
def reinstall_or_explain(saved)
|
|
105
|
+
return reinstall(saved) if gemfile.exist?
|
|
106
|
+
return explain_inline_repair if plugin_root.unreachable?
|
|
107
|
+
|
|
108
|
+
puts "No Gemfile here to reinstall from -- re-run gemvault doctor from the"
|
|
109
|
+
puts "project directory to reinstall the plugin."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def reinstall(saved)
|
|
113
|
+
return if system("bundle", "install")
|
|
114
|
+
return exit_partially_repaired if index.registered?(PLUGIN)
|
|
115
|
+
|
|
116
|
+
exit_restoring(saved)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The plugin came back before the install died, so the repair stands;
|
|
120
|
+
# only the rest of the bundle is unfinished.
|
|
121
|
+
def exit_partially_repaired
|
|
122
|
+
print_error("bundle install failed; the #{PLUGIN} plugin itself was reinstalled " \
|
|
123
|
+
"-- fix the install error and re-run bundle install")
|
|
124
|
+
exit(1)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def exit_restoring(saved)
|
|
128
|
+
index.restore(saved)
|
|
129
|
+
print_error("bundle install failed before reinstalling #{PLUGIN}; restored the " \
|
|
130
|
+
"previous plugin index -- fix the install error and re-run gemvault doctor")
|
|
131
|
+
exit(1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The local path is named only when it was the uninstall's target --
|
|
135
|
+
# otherwise bundler worked against its global root, and naming this
|
|
136
|
+
# project's path would misreport what happened.
|
|
137
|
+
def explain_inline_repair
|
|
138
|
+
puts "Cleared the #{PLUGIN} plugin index at #{plugin_root.local}."
|
|
139
|
+
puts "No Gemfile here to reinstall from -- an inline gemfile installs the plugin"
|
|
140
|
+
puts "again the next time the script runs."
|
|
24
141
|
end
|
|
25
142
|
end
|
|
26
143
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
##
|
|
5
|
+
# A gem installation record -- <tt>specifications/<full name>.gemspec</tt> --
|
|
6
|
+
# whose <tt>gems/<full name></tt> directory is gone: the wreckage of an
|
|
7
|
+
# interrupted uninstall or a hand-cleaned gem home.
|
|
8
|
+
#
|
|
9
|
+
# RubyGems still lists such a gem as installed, and Bundler's Gemfile-driven
|
|
10
|
+
# plugin installer trusts that record: it materializes the plugin against the
|
|
11
|
+
# ambient gem roots, reinstalls the missing gem into the plugin root, but
|
|
12
|
+
# then validates <tt>plugins.rb</tt> against the ghost's dead directory
|
|
13
|
+
# instead of the copy it just extracted. Every <tt>bundle install</tt> after
|
|
14
|
+
# that dies with <tt>MalformattedPlugin (plugins.rb was not found in the
|
|
15
|
+
# plugin.)</tt> -- issue #23. Removing the ghost record lets the next install
|
|
16
|
+
# see the machine as it really is.
|
|
17
|
+
#
|
|
18
|
+
# Detection never evaluates the record. The gem directory's name is the
|
|
19
|
+
# record's file name minus <tt>.gemspec</tt> by RubyGems' own install
|
|
20
|
+
# convention, so a missing payload is recognized from the filesystem alone --
|
|
21
|
+
# which lets a truncated or unreadable record, wreckage in its own right, be
|
|
22
|
+
# swept rather than silently skipped. Scoping to the asked gem reads the
|
|
23
|
+
# record's "# stub:" header when it is readable and falls back to the
|
|
24
|
+
# filename when it is not.
|
|
25
|
+
#
|
|
26
|
+
# Deliberately out of scope: a gem directory that exists but has lost part of
|
|
27
|
+
# its payload. Telling that wreck apart from a healthy install would mean
|
|
28
|
+
# trusting the record's file list, and a false positive here deletes a
|
|
29
|
+
# working installation's record.
|
|
30
|
+
class GhostSpecification
|
|
31
|
+
# All ghost specifications of +name+ across +roots+ (gem homes as listed
|
|
32
|
+
# on Gem.path, the same view Bundler's plugin installer resolves against).
|
|
33
|
+
def self.of(name, roots: Gem.path)
|
|
34
|
+
roots.flat_map { |root| in_root(Pathname(root), name) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.in_root(root, name)
|
|
38
|
+
root.join("specifications").glob("#{name}-*.gemspec")
|
|
39
|
+
.select { |file| of_gem?(file, name) }
|
|
40
|
+
.reject { |file| root.join("gems", file.basename(".gemspec").to_s).directory? }
|
|
41
|
+
.map { |file| new(file) }
|
|
42
|
+
end
|
|
43
|
+
private_class_method :in_root
|
|
44
|
+
|
|
45
|
+
def self.of_gem?(file, name)
|
|
46
|
+
stub_name = stub_header_name(file)
|
|
47
|
+
return stub_name == name unless stub_name.nil?
|
|
48
|
+
|
|
49
|
+
version_shaped?(file.basename(".gemspec").to_s.delete_prefix("#{name}-"))
|
|
50
|
+
end
|
|
51
|
+
private_class_method :of_gem?
|
|
52
|
+
|
|
53
|
+
# Installed records open with an optional encoding magic comment followed
|
|
54
|
+
# by "# stub: <name> <version> <platform> <require paths>".
|
|
55
|
+
def self.stub_header_name(file)
|
|
56
|
+
file.open("r") do |io|
|
|
57
|
+
2.times do
|
|
58
|
+
line = io.gets or break
|
|
59
|
+
name = line[/\A# stub: (\S+) /, 1] and return name
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
nil
|
|
63
|
+
rescue SystemCallError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
private_class_method :stub_header_name
|
|
67
|
+
|
|
68
|
+
# A version starts with a digit and a gem-name segment rarely does; with
|
|
69
|
+
# no stub header left to say so, that is the best remaining evidence, and
|
|
70
|
+
# it is only ever weighed for a record whose payload is already missing.
|
|
71
|
+
def self.version_shaped?(remainder) = remainder.match?(/\A\d/)
|
|
72
|
+
private_class_method :version_shaped?
|
|
73
|
+
|
|
74
|
+
attr_reader :file
|
|
75
|
+
|
|
76
|
+
def initialize(file)
|
|
77
|
+
@file = file
|
|
78
|
+
end
|
|
79
|
+
private_class_method :new
|
|
80
|
+
|
|
81
|
+
def to_s = file.to_s
|
|
82
|
+
|
|
83
|
+
def delete
|
|
84
|
+
File.delete(file)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/gemvault/version.rb
CHANGED
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.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -56,6 +56,9 @@ files:
|
|
|
56
56
|
- lib/bundler/plugin/vaulted_gem.rb
|
|
57
57
|
- lib/gemvault.rb
|
|
58
58
|
- lib/gemvault/archive_entry.rb
|
|
59
|
+
- lib/gemvault/bundler_gemfile.rb
|
|
60
|
+
- lib/gemvault/bundler_plugin_index.rb
|
|
61
|
+
- lib/gemvault/bundler_plugin_root.rb
|
|
59
62
|
- lib/gemvault/cli.rb
|
|
60
63
|
- lib/gemvault/cli/command.rb
|
|
61
64
|
- lib/gemvault/cli/commands/add.rb
|
|
@@ -74,6 +77,7 @@ files:
|
|
|
74
77
|
- lib/gemvault/gem_reference/any_version.rb
|
|
75
78
|
- lib/gemvault/gem_reference/parser.rb
|
|
76
79
|
- lib/gemvault/gem_reference/specific_version.rb
|
|
80
|
+
- lib/gemvault/ghost_specification.rb
|
|
77
81
|
- lib/gemvault/manifest.rb
|
|
78
82
|
- lib/gemvault/tarball.rb
|
|
79
83
|
- lib/gemvault/tarvault.rb
|