Repository: ruby/rubygems (Ruby)

Issue:
`gem uninstall --user-install GEM_NAME` throws InstallError, even if the gem is installed
<!--

Thank you for contributing to the
[rubygems](https://github.com/rubygems/rubygems) repository, and specifically to
[RubyGems](https://guides.rubygems.org/).

-->

### Describe the problem as clearly as you can

<!--

Replace this with an explanation of the problem you are having. Be as much clear and precise as you can.

-->

If you run `gem install --user-install SOME_GEM`, then run `gem uninstall --user-install SOME_GEM`, RubyGems throws an error because the gem is not in `GEM_HOME` and tells you to use `--install-dir` instead.

There is a test which seems relevant, and passes. It does not catch this problem, however.

https://github.com/rubygems/rubygems/blob/625682996020c0d192cfacc0799fce2a86f005b0/test/rubygems/test_gem_uninstaller.rb#L371-L394

I suspect we need something closer to an integration test, possibly in [test/rubygems/test_gem_commands_uninstall_command.rb](https://github.com/rubygems/rubygems/blob/master/test/rubygems/test_gem_commands_uninstall_command.rb).

### Did you try upgrading RubyGems?

<!--

Make sure you're using the latest version of both RubyGems by running `gem
update --system`.

It's likely that your issue has been fixed in recent versions, so just upgrading
might do the trick, and will also save us some time :)

-->

Yes. This occurs both on RubyGems 3.4.8 and master (as of https://github.com/rubygems/rubygems/commit/625682996020c0d192cfacc0799fce2a86f005b0)

### Post steps to reproduce the problem

<!--

Fill this with a list of steps maintainers can follow to reproduce your issue.
Note that while you are seeing this issue in your computer, maintainers might
not see the same thing on theirs. There is a number of things that could
influence this:

* How your ruby is setup (OS package, from source, using a version manager).
* How RubyGems is configured.
* The version of each involved piece of software that you are using.
* ...

The more complete the steps to simulate your particular environment are, the
easier it will be for maintainers to reproduce your issue on their machines.

Ideally, we recommend you to set up the list of steps as a
[Dockerfile](https://docs.docker.com/get-started/). A Dockerfile provides a
neutral environment that should give the same results, no matter where it's run.

-->

Using Ruby 3.0.5p211, via the FreeBSD `lang/ruby30` package.

```
$ gem --version
3.4.8
$ gem install --user-install boop
Fetching boop-0.5.0.gem
WARNING:  You don't have /home/puppy/.gem/ruby/3.0/bin in your PATH,
          gem executables will not run.
Successfully installed boop-0.5.0
1 gem installed
$ gem uninstall --user-install boop
ERROR:  While executing gem ... (Gem::InstallError)
    boop is not installed in GEM_HOME, try:
        gem uninstall -i /usr/home/puppy/.gem/ruby/3.0 boop
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/uninstaller.rb:124:in `uninstall'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/commands/uninstall_command.rb:195:in `uninstall'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/commands/uninstall_command.rb:181:in `uninstall_gem'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/commands/uninstall_command.rb:175:in `block in uninstall_specific'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/commands/uninstall_command.rb:167:in `each'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/commands/uninstall_command.rb:167:in `uninstall_specific'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/commands/uninstall_command.rb:133:in `execute'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/command.rb:327:in `invoke_with_build_args'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/command_manager.rb:252:in `invoke_command'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/command_manager.rb:192:in `process_args'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/command_manager.rb:150:in `run'
        /usr/local/lib/ruby/site_ruby/3.0/rubygems/gem_runner.rb:51:in `run'
        /usr/local/bin/gem:10:in `<main>'
puppy@orthrus.fox:~/test$
```


### Which command did you run?

<!-- Replace this with the specific command that is causing trouble. -->

`gem uninstall --user-install boop` (exact gem does not matter).

### What were you expecting to happen?

<!-- Replace this with the results you expected before running the command. -->

I was expecting it to uninstall the specified gem.

### What actually happened?

<!-- Replace this with the actual result you got. Paste the output of your command here. -->

It said it is not installed in GEM_HOME, and recommends using `-i` (`--install-dir`) with the user-install directory (the value of `Gem.user_dir`).

### Run `gem env` and paste the output below

<!-- Replace this with the result of `gem env`. Don't forget to anonymize any private data! -->

```
RubyGems Environment:
  - RUBYGEMS VERSION: 3.4.8
  - RUBY VERSION: 3.0.5 (2022-11-24 patchlevel 211) [amd64-freebsd13]
  - INSTALLATION DIRECTORY: /usr/local/lib/ruby/gems/3.0
  - USER INSTALLATION DIRECTORY: /home/puppy/.gem/ruby/3.0
  - RUBY EXECUTABLE: /usr/local/bin/ruby30
  - GIT EXECUTABLE: /usr/local/bin/git
  - EXECUTABLE DIRECTORY: /usr/local/bin
  - SPEC CACHE DIRECTORY: /home/puppy/.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /usr/local/etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - amd64-freebsd-13
  - GEM PATHS:
     - /usr/local/lib/ruby/gems/3.0
     - /usr/home/puppy/.gem/ruby/3.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => true
     - :bulk_threshold => 1000
     - "gem" => "--suggestions --no-document"
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /usr/home/puppy/.rubies/ruby-3.2.1/lib/ruby/gems/3.2.0/bin
     - /home/puppy/.cargo/bin
     - /home/puppy/bin
     - /home/puppy/.local/bin
     - /home/puppy/.gem/ruby/bin
     - /home/puppy/.npm-global/bin
     - /home/puppy/go/bin
     - /bin
     - /usr/bin
     - /sbin
     - /usr/sbin
     - /usr/local/bin
     - /usr/local/sbin
```


Discussion:
This issue blocks [#5327 (If GEM_HOME is not writable, use --user-install)](https://github.com/rubygems/rubygems/pull/5327).

The error is obviously deliberate, but I suspect it was not intended to affect `--user-install`:

https://github.com/rubygems/rubygems/blob/625682996020c0d192cfacc0799fce2a86f005b0/lib/rubygems/commands/uninstall_command.rb#L180-L192
Just FTR, the `gem uninstall --user-install boop` command is questionable IMHO. Should the `--user-install` influence anything? 

* RubyGems already knows where the gem is
* if there are more possibilities, it allows to pick
* If the location is RW, then is should be able to remove such gem.
Yeah, I'm not entirely sure what the appropriate behavior is. I just know that "throw an error" feels like it is _definitely_ wrong.
> Should the --user-install influence anything?

That is a good question, but as far as I know, if you use gem --user-install, it will install into the home dir, right? And there may be situations where you have /usr/lib/ as target (say a campus university computer system) and you need to override some things via custom gems so --user-install is used. You are probably correct that this may not make a difference because of gem's default behaviour targeting the user's home dir first - that may be. But I can understand why users can come up with using --user-install at gem uninstall too, even though that seems a misnomer. :dagger: 

> if there are more possibilities, it allows to pick

Well, while I agree with you in general, IF the user passes a flag that says "ruby, target user-installed gems first", then there should not be any picking. It should default to uninstall the first gem it can find AT the user-install directory.
I guess an additional source of confusion may be for users to wonder what GEM_HOME is used for and how it relates to --user-install, if at all.
Interesting. This works as documented for me:

```
$ gem --version
3.4.8

$ gem install --user-install boop  
WARNING:  You don't have /Users/deivid/.gem/ruby/3.2.0/bin in your PATH,
	  gem executables will not run.
Successfully installed boop-0.5.0
1 gem installed

$ gem uninstall --user-install boop
Remove executables:
	boop

in addition to the gem? [Yn]  Y
Removing boop
Successfully uninstalled boop-0.5.0
```

So this is probably specific to @duckinator's setup. Have you investigated anything yet, @duckinator?

For what it's worth, the option is documented like this:

https://github.com/rubygems/rubygems/blob/ac7c3176fce04fd7bdacc79d4594d92e4b93f24c/lib/rubygems/commands/uninstall_command.rb#L54-L58

I haven't dug into this yet. Planning to get to it this week. 
This is reproducible on a fresh FreeBSD 13.1 VM.

Installed FreeBSD 13.1, added a non-root user, and installed the `ruby` and `ruby30-gems` packages.

<details>
<summary>`gem env` after installing `ruby` and `ruby30-gems` packages</summary>

```
RubyGems Environment:
  - RUBYGEMS VERSION: 3.3.26
  - RUBY VERSION: 3.0.5 (2022-11-24 patchlevel 211) [amd64-freebsd13]
  - INSTALLATION DIRECTORY: /usr/local/lib/ruby/gems/3.0
  - USER INSTALLATION DIRECTORY: /home/freebsd/.local/share/gem/ruby/3.0
  - RUBY EXECUTABLE: /usr/local/bin/ruby30
  - GIT EXECUTABLE: 
  - EXECUTABLE DIRECTORY: /usr/local/bin
  - SPEC CACHE DIRECTORY: /home/freebsd/.local/share/gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /usr/local/etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - amd64-freebsd-13
  - GEM PATHS:
     - /usr/local/lib/ruby/gems/3.0
     - /usr/home/freebsd/.local/share/gem/ruby/3.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => true
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /sbin
     - /bin
     - /usr/sbin
     - /usr/bin
     - /usr/local/sbin
     - /usr/local/bin
     - /home/freebsd/bin
```

</details>

This also happens with the latest RubyGems (installed via `gem update --system` as root).

<details>
<summary>`gem env` after `gem update --system`</summary>

```
RubyGems Environment:
  - RUBYGEMS VERSION: 3.4.10
  - RUBY VERSION: 3.0.5 (2022-11-24 patchlevel 211) [amd64-freebsd13]
  - INSTALLATION DIRECTORY: /usr/local/lib/ruby/gems/3.0
  - USER INSTALLATION DIRECTORY: /home/freebsd/.local/share/gem/ruby/3.0
  - RUBY EXECUTABLE: /usr/local/bin/ruby30
  - GIT EXECUTABLE: 
  - EXECUTABLE DIRECTORY: /usr/local/bin
  - SPEC CACHE DIRECTORY: /home/freebsd/.local/share/gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /usr/local/etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - amd64-freebsd-13
  - GEM PATHS:
     - /usr/local/lib/ruby/gems/3.0
     - /usr/home/freebsd/.local/share/gem/ruby/3.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => true
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /sbin
     - /bin
     - /usr/sbin
     - /usr/bin
     - /usr/local/sbin
     - /usr/local/bin
     - /home/freebsd/bin
```

</details>

I could not reproduce it on Ubuntu 22.04. I'm attempting to narrow it down farther.
It took a little more effort (I had to [manually install RubyGems from a TGZ file](https://rubygems.org/pages/download)), but almost-latest Ruby (3.2.0) and latest RubyGems (3.4.10) still had the problem.

<details>
<summary>`gem env` on fresh FreeBSD 13.1 VM, after installing `ruby32` and installing RubyGems manually</summary>

```
  - RUBYGEMS VERSION: 3.4.10
  - RUBY VERSION: 3.2.0 (2022-12-25 patchlevel 0) [amd64-freebsd13]
  - INSTALLATION DIRECTORY: /usr/local/lib/ruby/gems/3.2
  - USER INSTALLATION DIRECTORY: /home/freebsd/.local/share/gem/ruby/3.2
  - RUBY EXECUTABLE: /usr/local/bin/ruby32
  - GIT EXECUTABLE: 
  - EXECUTABLE DIRECTORY: /usr/local/bin
  - SPEC CACHE DIRECTORY: /home/freebsd/.local/share/gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /usr/local/etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - amd64-freebsd-13
  - GEM PATHS:
     - /usr/local/lib/ruby/gems/3.2
     - /usr/home/freebsd/.local/share/gem/ruby/3.2
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => true
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /sbin
     - /bin
     - /usr/sbin
     - /usr/bin
     - /usr/local/sbin
     - /usr/local/bin
     - /home/freebsd/bin
```

</details>
Some info from the working system, for comparison.

<details>
<summary>`gem env` on Ubuntu 22.04 LTS</summary>

```
RubyGems Environment:
  - RUBYGEMS VERSION: 3.3.5
  - RUBY VERSION: 3.0.2 (2021-07-07 patchlevel 107) [x86_64-linux-gnu]
  - INSTALLATION DIRECTORY: /var/lib/gems/3.0.0
  - USER INSTALLATION DIRECTORY: /home/ubuntu/.local/share/gem/ruby/3.0.0
  - RUBY EXECUTABLE: /usr/bin/ruby3.0
  - GIT EXECUTABLE: /usr/bin/git
  - EXECUTABLE DIRECTORY: /usr/local/bin
  - SPEC CACHE DIRECTORY: /home/ubuntu/.local/share/gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - x86_64-linux
  - GEM PATHS:
     - /var/lib/gems/3.0.0
     - /home/ubuntu/.local/share/gem/ruby/3.0.0
     - /usr/local/lib/ruby/gems/3.0.0
     - /usr/lib/ruby/gems/3.0.0
     - /usr/lib/x86_64-linux-gnu/ruby/gems/3.0.0
     - /usr/share/rubygems-integration/3.0.0
     - /usr/share/rubygems-integration/all
     - /usr/lib/x86_64-linux-gnu/rubygems-integration/3.0.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => false
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /usr/local/sbin
     - /usr/local/bin
     - /usr/sbin
     - /usr/bin
     - /sbin
     - /bin
     - /usr/games
     - /usr/local/games
     - /snap/bin
```

</details>
Hmm. Off the top of my head, that sounds like an issue with the FreeBSD ruby package, rather than an issue with Ruby itself.  If you install ruby on FreeBSD using a version manager like rvm or ruby-install, does it still have the same problem?
I used Ruby 3.0.5 installed via `ruby-install` and switched to it with `chruby` (this is the closest version I had without waiting ages for Ruby to compile). I deleted `~/.gem/` before each attempt, in case something else I was doing was causing problems.

By default I get a _different_ failure: it just doesn't think the gem is installed.

```
puppy@orthrus.fox:~$ chruby 3.0
puppy@orthrus.fox:~$ gem install --user-install boop
Fetching boop-0.5.0.gem
WARNING:  You don't have /home/puppy/.gem/ruby/3.0.0/bin in your PATH,
          gem executables will not run.
Successfully installed boop-0.5.0
1 gem installed
puppy@orthrus.fox:~$ gem uninstall --user-install boop
Gem 'boop' is not installed
puppy@orthrus.fox:~$ 
```

After deleting `~/.gem` to reset, I unset the `GEM_*` variables that `chruby` sets by default and tried again. This time I got the behavior with it telling me to use `gem uninstall -i [...] boop`:

<details>
<summary>attempting with `GEM_*` unset</summary>

```
puppy@orthrus.fox:~$ chruby 3.0
puppy@orthrus.fox:~$ env | grep GEM_
GEM_ROOT=/usr/home/puppy/.rubies/ruby-3.0.5/lib/ruby/gems/3.0.0
GEM_PATH=/home/puppy/.gem/ruby/3.0.5:/usr/home/puppy/.rubies/ruby-3.0.5/lib/ruby/gems/3.0.0
GEM_HOME=/home/puppy/.gem/ruby/3.0.5
puppy@orthrus.fox:~$ unset GEM_ROOT GEM_PATH GEM_HOME
puppy@orthrus.fox:~$ rm -rf ~/.local/share/gem/ ~/.gem/
puppy@orthrus.fox:~$ gem install --user-install boop
Fetching boop-0.5.0.gem
WARNING:  You don't have /home/puppy/.local/share/gem/ruby/3.0.0/bin in your PATH,
          gem executables will not run.
Successfully installed boop-0.5.0
1 gem installed
puppy@orthrus.fox:~$ gem uninstall --user-install boop
ERROR:  While executing gem ... (Gem::InstallError)
    boop is not installed in GEM_HOME, try:
        gem uninstall -i /usr/home/puppy/.local/share/gem/ruby/3.0.0 boop
puppy@orthrus.fox:~$ 
```

</details>


I also realized that the code I linked to before was _not_ the area the error happens. This is the correct one: https://github.com/rubygems/rubygems/blob/63a6a4b1e6ca64577cd8bee9c21c64e253a29b82/lib/rubygems/uninstaller.rb#L113-L123

It looks like that error has been there for 12 years: <https://github.com/rubygems/rubygems/commit/7096384625bf2685fe4487a413ecb5b6b78b96c9>.

I'm not sure what's happening that's making `install` and `uninstall` behave so differently here.
Is `ENV['HOME']` a symbolic link?

It seems to compare `ENV['HOME']/.gem/ruby/x.y.z` with `File.realpath(ENV['HOME']/.gem/ruby/x.y.z)` and cannot find the installation location correctly.

@fd00 `ENV['HOME']` is `/home/puppy`, and `/home` is a symlink to `/usr/home`. (This is the default setup on FreeBSD.)
I am using a cygwin environment where `ENV['HOME']` is a symbolic link. 
I had the same problem.

After checking the contents of the variable, this comparison failed.

https://github.com/rubygems/rubygems/blob/a67b2657a0fc0cc124c3412ef7526eb74a4e43fe/lib/rubygems/uninstaller.rb#L107-L110

- `Gem.user_dir -> /home/puppy/.gem/ruby/x.y.z`
- `spec.base_dir -> /usr/home/puppy/.gem/ruby/x.y.z`

`spec.base_dir` is expanded with `Gem::PathSupport`.

The proper fix is still unknown to me.

@duckinator I'm having a pass over related RubyGems issues and I found a repro for your problem and a potential fix at #7645. Ruby core specs are failing, I should look into that, but other than that seems fine.



Patch:
diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb
index 2a77ec72cf10..283bc96ce3e8 100644
--- a/lib/rubygems/commands/uninstall_command.rb
+++ b/lib/rubygems/commands/uninstall_command.rb
@@ -184,7 +184,7 @@ def uninstall_gem(gem_name)
   rescue Gem::GemNotInHomeException => e
     spec = e.spec
     alert("In order to remove #{spec.name}, please execute:\n" \
-          "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}")
+          "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}")
   rescue Gem::UninstallError => e
     spec = e.spec
     alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \
diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb
index e1f82e6a21bd..4d72f6fd0a2c 100644
--- a/lib/rubygems/uninstaller.rb
+++ b/lib/rubygems/uninstaller.rb
@@ -51,6 +51,7 @@ def initialize(gem, options = {})
     @version            = options[:version] || Gem::Requirement.default
     @install_dir        = options[:install_dir]
     @gem_home           = File.realpath(@install_dir || Gem.dir)
+    @user_dir           = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir
     @force_executables  = options[:executables]
     @force_all          = options[:all]
     @force_ignore       = options[:ignore]
@@ -105,7 +106,7 @@ def uninstall
 
     list, other_repo_specs = list.partition do |spec|
       @gem_home == spec.base_dir ||
-        (@user_install && spec.base_dir == Gem.user_dir)
+        (@user_install && spec.base_dir == @user_dir)
     end
 
     list.sort!
@@ -239,7 +240,7 @@ def remove_all(list)
 
   def remove(spec)
     unless path_ok?(@gem_home, spec) ||
-           (@user_install && path_ok?(Gem.user_dir, spec))
+           (@user_install && path_ok?(@user_dir, spec))
       e = Gem::GemNotInHomeException.new \
         "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}"
       e.spec = spec


Test patch:
diff --git a/test/rubygems/test_gem_uninstaller.rb b/test/rubygems/test_gem_uninstaller.rb
index eb25b505e6cb..aa5ab0ed6768 100644
--- a/test/rubygems/test_gem_uninstaller.rb
+++ b/test/rubygems/test_gem_uninstaller.rb
@@ -429,7 +429,7 @@ def test_uninstall_not_ok
   end
 
   def test_uninstall_user_install
-    @user_spec = Gem::Specification.find_by_name "b"
+    Gem::Specification.dirs = [Gem.user_dir]
 
     uninstaller = Gem::Uninstaller.new(@user_spec.name,
                                        executables: true,
@@ -453,6 +453,32 @@ def test_uninstall_user_install
     assert_same uninstaller, @post_uninstall_hook_arg
   end
 
+  def test_uninstall_user_install_with_symlinked_home
+    pend "Symlinks not supported or not enabled" unless symlink_supported?
+
+    Gem::Specification.dirs = [Gem.user_dir]
+
+    symlinked_home = File.join(@tempdir, "new-home")
+    FileUtils.ln_s(Gem.user_home, symlinked_home)
+
+    ENV["HOME"] = symlinked_home
+    Gem.instance_variable_set(:@user_home, nil)
+    Gem.instance_variable_set(:@data_home, nil)
+
+    uninstaller = Gem::Uninstaller.new(@user_spec.name,
+                                       executables: true,
+                                       user_install: true,
+                                       force: true)
+
+    gem_dir = File.join @user_spec.gem_dir
+
+    assert_path_exist gem_dir
+
+    uninstaller.uninstall
+
+    assert_path_not_exist gem_dir
+  end
+
   def test_uninstall_wrong_repo
     Dir.mkdir "#{@gemhome}2"
     Gem.use_paths "#{@gemhome}2", [@gemhome]