gem-repair 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c060c1e8acac437d6edfaab550b3a43a7bb19bde988e3307900610ce71fe0db2
4
- data.tar.gz: 2c753428a55544e7486f0acda99ca68868880a9367aeabe0994cd27c893925c3
3
+ metadata.gz: eb2259f7f61d7d6f437104759221a3b35fffc953aaaf7c625d39d6b50b102e75
4
+ data.tar.gz: ba502c3f411d10830de34adfa89be29ecbdceac2fbb091bc0e41a393a5bb004d
5
5
  SHA512:
6
- metadata.gz: 635ff70eec357b7300bc368df9b9dedd2ec88db326b39c5748e2ac00726c97ce6b5c043d00526e34d3e5b8891380468c946dc30214d3ba484a0c118f6f3e9e08
7
- data.tar.gz: 748b0a6d4396cfb10018ff025f1be733dd3444babc2c5eb88337878b45c6a7f66fd70a836e83b6310121f4b2704bee2e5cd1478dcffd6eacbd06e668746ff539
6
+ metadata.gz: 92ad9f8df853acf84d8614dad654cfb1f4379b6f9eb4537b5d1e136f224909ff7342aed0cc811526fc609c67e810872813a72cd1bd640062cfec45df677bff96
7
+ data.tar.gz: 4e03a34df2a763f8d25d10b4490e1a29d49864a40deb4891e7a5c685299d7182e588a71c80aac2201a8b95d43bee816df6f2663aacfb26a99ba22c7c1985b779
data/README.md CHANGED
@@ -1,43 +1,103 @@
1
1
  # gem-repair
2
2
 
3
- A RubyGems plugin to repair gems with missing extensions.
3
+ A RubyGems plugin for the broken C extensions you get when several rubies share one `GEM_HOME`.
4
4
 
5
- ## Installation
5
+ ## The problem
6
6
 
7
- Build and install the gem locally:
7
+ Version managers such as mise and rbenv let every ruby point at the same `GEM_HOME`, which saves installing the same gems once per ruby. C extensions do not survive that, because `gem install` compiles them only for the ruby that runs it.
8
+
9
+ Install `bcrypt` under ruby 3.3 into a shared `GEM_HOME`:
10
+
11
+ ```console
12
+ $ GEM_HOME=/tmp/shared mise x ruby@3.3 -- gem install bcrypt
13
+ Building native extensions. This could take a while...
14
+ Successfully installed bcrypt-3.1.22
15
+ ```
16
+
17
+ RubyGems writes the compiled extension to two places:
8
18
 
9
- ```sh
10
- gem build gem-repair.gemspec
11
- gem install ./gem-repair-0.1.0.gem
12
19
  ```
20
+ /tmp/shared/extensions/arm64-darwin-27/3.3.0/bcrypt-3.1.22/bcrypt_ext.bundle
21
+ /tmp/shared/gems/bcrypt-3.1.22/lib/bcrypt_ext.bundle
22
+ ```
23
+
24
+ The first is per-ABI and belongs to ruby 3.3 alone. The second is a copy inside the gem's own `lib`, which every ruby shares. Each one breaks the other rubies in a different way.
25
+
26
+ ### The gem is ignored
13
27
 
14
- Or, if you are managing it within a Bundler context, add it to your Gemfile:
28
+ Ruby 3.4 looks for `extensions/arm64-darwin-27/3.4.0/`, does not find it, and drops the gem off the load path:
15
29
 
16
- ```ruby
17
- gem 'gem-repair', path: './labs/gem-repair' # Adjust path as necessary
30
+ ```console
31
+ $ GEM_HOME=/tmp/shared mise x ruby@3.4 -- ruby -e 'require "bcrypt"'
32
+ Ignoring bcrypt-3.1.22 because its extensions are not built. Try: gem pristine bcrypt --version 3.1.22
33
+ -e:1:in 'Kernel#require': cannot load such file -- bcrypt (LoadError)
18
34
  ```
19
35
 
20
- ## Usage
36
+ ### The copy in lib shadows the right one
37
+
38
+ Build the extension for 3.4 as well and the gem is no longer ignored. It still fails, because `lib` comes before `extensions/` on the load path, so 3.4 loads the object file linked against ruby 3.3:
39
+
40
+ ```console
41
+ $ GEM_HOME=/tmp/shared mise x ruby@3.4 -- ruby -e 'require "bcrypt"'
42
+ -e:1:in 'Kernel#require': linked to incompatible /opt/rubies/3.3/lib/libruby.3.3.dylib - /tmp/shared/gems/bcrypt-3.1.22/lib/bcrypt_ext.bundle (LoadError)
43
+ ```
21
44
 
22
- After installation, the `gem repair` command will be available:
45
+ Running `gem pristine --all` under each ruby in turn does not settle this. The last ruby to run it wins the copy in `lib` and breaks the others, and the next `gem install` starts the cycle again.
46
+
47
+ ## What this plugin does
48
+
49
+ It covers the problem from three directions.
50
+
51
+ 1. After every install it deletes the extension copy from the gem's `lib`, so no ruby can load another ruby's object file.
52
+ 2. After every install it builds the extensions of the gem just installed for the other rubies sharing the `GEM_HOME`, so switching rubies does not leave the gem ignored.
53
+ 3. `gem repair` does both across a `GEM_HOME` that is already broken.
54
+
55
+ With the plugin installed, one `gem install` under 3.3 leaves 3.4 working:
56
+
57
+ ```console
58
+ $ GEM_HOME=/tmp/shared GEM_REPAIR_RUBIES=3.4 mise x ruby@3.3 -- gem install bcrypt
59
+ Building native extensions. This could take a while...
60
+ Removed /tmp/shared/gems/bcrypt-3.1.22/lib/bcrypt_ext.bundle
61
+ Built bcrypt-3.1.22 extensions for 3.4
62
+ Successfully installed bcrypt-3.1.22
63
+
64
+ $ GEM_HOME=/tmp/shared mise x ruby@3.4 -- ruby -e 'require "bcrypt"; puts BCrypt::Password.create("x")'
65
+ $2a$12$ko9GRH9PfrJSkF4yD4xXA.BZILRlBpCyOVTnB2XvkYYo5r/cT5FOi
66
+ ```
67
+
68
+ ## Installation
23
69
 
24
70
  ```sh
25
- gem repair
71
+ gem install gem-repair
26
72
  ```
27
73
 
28
- You can also specify the number of parallel threads to use:
74
+ Install it into the shared `GEM_HOME`, not into a per-ruby one. RubyGems loads plugins from `GEM_PATH`, so one copy there is what makes every ruby run the hook.
75
+
76
+ Rubies below 2.7 load the plugin too. They get the `gem repair` command but not the install hook.
77
+
78
+ ## Repairing a GEM_HOME you already broke
29
79
 
30
80
  ```sh
31
- gem repair -j 8 # Use 8 threads
32
- gem repair --jobs 2 # Use 2 threads
81
+ gem repair
33
82
  ```
34
83
 
35
- This command will:
36
- 1. Remove the copies of built extensions left in the `lib` directory of every gem with extensions (see below).
37
- 2. Find all installed gems that are missing their compiled C extensions.
38
- 3. Attempt to reinstall these gems using multiple threads.
84
+ It sweeps the extension copies out of `lib`, finds every gem whose extensions are missing for the running ruby, and reinstalls them. Reinstalling runs the install hooks, so the fan-out to the other rubies happens here as well.
85
+
86
+ ```console
87
+ $ gem repair -j 2
88
+ Searching for gems with missing extensions...
89
+ Removed /tmp/shared/gems/bcrypt-3.1.22/lib/bcrypt_ext.bundle
90
+ Removed /tmp/shared/gems/msgpack-1.8.4/lib/msgpack/msgpack.bundle
91
+ Found 2 gem(s) to repair: msgpack-1.8.4, bcrypt-3.1.22
92
+ Repairing gems using 2 parallel threads...
93
+ Repairing msgpack-1.8.4...
94
+ Repairing bcrypt-3.1.22...
95
+ Successfully repaired bcrypt-3.1.22 to /tmp/shared
96
+ Successfully repaired msgpack-1.8.4 to /tmp/shared
97
+ Gem repair process complete.
98
+ ```
39
99
 
40
- Options:
100
+ Start with `-n` if you want to see the plan first.
41
101
 
42
102
  | Option | Effect |
43
103
  | --- | --- |
@@ -46,23 +106,13 @@ Options:
46
106
  | `--aggressive-sweep` | Also remove the `test`, `spec`, `features` and `tmp` directories of every gem |
47
107
  | `-n`, `--dry-run` | Show what would be removed, repaired or uninstalled without changing anything |
48
108
 
49
- ## Removing extension copies from lib
50
-
51
- RubyGems copies each built extension into the gem's `lib` directory as well as the per-ruby `extensions/<platform>/<abi>/` directory. RubyGems 3.6 added the `install_extension_in_lib` gemrc setting to skip that copy, but it stays on by default. `lib` comes first on the load path, so every other ruby sharing the `GEM_HOME` loads the copy built for the installing ruby and fails with a `LoadError`. The plugin removes those copies right after each install and on every `gem repair`. Copies under versioned directories such as `lib/3.3/`, which precompiled gems use to ship one extension per ABI, are left alone.
52
-
53
- ### Migrating from gem-sweep
54
-
55
- This replaces the `gem sweep` command and the install hook of the gem-sweep plugin. Uninstall it so the two hooks do not run side by side:
56
-
57
- ```sh
58
- gem uninstall gem-sweep
59
- ```
109
+ ## How the sweep picks its targets
60
110
 
61
- `gem sweep --missing-extensions` becomes `gem repair --prune`, `gem sweep --aggressive` becomes `gem repair --aggressive-sweep`, and `gem sweep -n` becomes `gem repair -n`.
111
+ RubyGems 3.6 added the `install_extension_in_lib` gemrc setting to skip the copy into `lib`, but it is still on by default, and gems installed by an older RubyGems already have the copy. So the sweep cannot be retired yet.
62
112
 
63
- ## Building for other rubies on install
113
+ Only files that also exist under `extensions/` are removed, which is how a copy is told apart from an object file the gem shipped itself. Copies under versioned directories such as `lib/3.3/`, which precompiled gems use to carry one extension per ABI, are left alone.
64
114
 
65
- Several rubies can share one `GEM_HOME`, but `gem install` only compiles extensions for the ruby that runs it. The plugin hooks `Gem.post_install` and builds the extensions of the gem just installed for every other ruby it can find, so switching rubies does not leave the gem ignored with "extensions are not built".
115
+ ## How the fan-out finds the other rubies
66
116
 
67
117
  Rubies are looked up under the install roots of mise and rbenv:
68
118
 
@@ -71,7 +121,19 @@ Rubies are looked up under the install roots of mise and rbenv:
71
121
  | `MISE_DATA_DIR` | `$XDG_DATA_HOME/mise` | `installs/ruby/*` |
72
122
  | `RBENV_ROOT` | `~/.rbenv` | `versions/*` |
73
123
 
74
- `GEM_REPAIR_RUBIES` narrows the targets to a comma-separated list of version names, for example `GEM_REPAIR_RUBIES=3.3-dev,3.4-dev`. Set it to an empty string to turn the fan-out off. JRuby, TruffleRuby, mruby and Rubinius installs are always skipped, as is a ruby whose version the gem does not support. The fan-out does not run inside `bundle install`, though the sweep still does. It only runs for a `GEM_HOME` outside the per-ABI directories the running ruby keeps to itself, `Gem.default_dir` and `Gem.user_dir`. The hook also fires when `gem repair` or `gem pristine` reinstalls a gem.
124
+ `GEM_REPAIR_RUBIES` narrows the targets to a comma-separated list of version names, for example `GEM_REPAIR_RUBIES=3.3-dev,3.4-dev`. Set it to an empty string to turn the fan-out off. That is worth doing if you keep dozens of rubies installed but only use a few.
125
+
126
+ JRuby, TruffleRuby, mruby and Rubinius installs are always skipped, as is a ruby whose version the gem does not support. The fan-out does not run inside `bundle install`, though the sweep still does. It only runs for a `GEM_HOME` outside the per-ABI directories the running ruby keeps to itself, `Gem.default_dir` and `Gem.user_dir`, since nothing else reads those.
127
+
128
+ ## Migrating from gem-sweep
129
+
130
+ This replaces the `gem sweep` command and the install hook of the gem-sweep plugin. Uninstall it so the two hooks do not run side by side:
131
+
132
+ ```sh
133
+ gem uninstall gem-sweep
134
+ ```
135
+
136
+ `gem sweep --missing-extensions` becomes `gem repair --prune`, `gem sweep --aggressive` becomes `gem repair --aggressive-sweep`, and `gem sweep -n` becomes `gem repair -n`.
75
137
 
76
138
  ## Development
77
139
 
@@ -81,7 +143,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
81
143
 
82
144
  ## Contributing
83
145
 
84
- Bug reports and pull requests are welcome on GitHub at https://github.com/hsbt/gem-repare.
146
+ Bug reports and pull requests are welcome on GitHub at https://github.com/hsbt/gem-repair.
85
147
 
86
148
  ## License
87
149
 
@@ -1,6 +1,7 @@
1
1
  require 'zlib'
2
2
  require 'rubygems/command'
3
3
  require 'rubygems/installer'
4
+ require 'rubygems/uninstaller'
4
5
  # see rubygems_plugin.rb
5
6
  require_relative '../../gem_repair/sweep' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7")
6
7
 
@@ -167,8 +168,8 @@ Use -n to only show what would be done.
167
168
  end
168
169
 
169
170
  def prune_gem(spec)
170
- require 'rubygems/uninstaller'
171
- Gem::Uninstaller.new(spec.name, version: spec.version, install_dir: spec.base_dir, executables: true).uninstall_gem(spec)
171
+ # force skips the dependent-gem prompt, which RubyGems answers no to off a tty.
172
+ Gem::Uninstaller.new(spec.name, version: spec.version, install_dir: spec.base_dir, executables: true, force: true).uninstall_gem(spec)
172
173
  rescue Gem::Exception => e
173
174
  alert_error "Failed to uninstall #{spec.full_name}: #{e.message}"
174
175
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem-repair
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroshi SHIBATA