[PATCH 0/2] Finally fix gem/bundle user installs once and for all

Currently there is no single place for packagers (or users) to configure both
gem and bundle to do user installs (e.g. ~/.local/share/gem/ruby/3.0.0). Setting
GEM_HOME to ~/.gem is one option, but then you would be mixing ruby versions,
and it's not the same location --user-install uses. Another option is
GEM_HOME=$(ruby -e 'puts Gem.user_dir'), but that is hardly user-friendly.

My last attempt to fix this was in pull request #4028, but that was closed
without any explanation as to what was wrong with the approach, or a way to move
forward.

This patch series expands on that previous attempt by introducing a
`Gem.user_install` method that distributions can override to enable this
feature. It's off by default.

If enabled all it does is switch the default installation path from
`Gem.default_dir` to `Gem.user_dir`.

This solution fixes all the problems.

Distributions can override this method and even do something fancy like
`Process.uid != 0`, which would enable user_install for users, but disable it
for root (thus tapering the need for --[no-]user-install). This could eventually
become the default for everyone.

If a user wants to install to `Gem.default_dir` all she has to do is
--no-user-install (I made sure that works).

In addition there's an environment variable GEM_USER_INSTALL that when set does
the same thing as `Gem.user_install`, but with it users don't need to depend on
their distribution package and can enable the feature themselves without doing
any convoluted GEM_HOME tricks.

Moreover, I found some issues with the interaction of --user-install and `gem
uninstall`, which I've fixed.

You can find the branch here:

And here is the diffstat:

  lib/rubygems/commands/environment_command.rb | 5 +++++
  lib/rubygems/commands/uninstall_command.rb | 2 +-
  lib/rubygems/defaults.rb | 8 ++++++++
  lib/rubygems/installer.rb | 20 ++++++++++++--------
  lib/rubygems/installer_test_case.rb | 12 ++++--------
  lib/rubygems/path_support.rb | 4 +++-
  lib/rubygems/test_case.rb | 2 +-
  lib/rubygems/uninstaller.rb | 17 ++++++++++++-----
  test/rubygems/test_gem.rb | 10 ++++++++++
  test/rubygems/test_gem_commands_uninstall_command.rb | 4 ++--
  test/rubygems/test_gem_installer.rb | 18 +++++++++---------
  test/rubygems/test_gem_uninstaller.rb | 21 ++++++++++++++++++++-
  12 files changed, 87 insertions(+), 36 deletions(-)

I am sending here only a simplified version with the crucial stuff:
`Gem.user_install`.

Why am I sending this here? Because the maintainers of rubygems would rather
harm their users than hear my voice, so they blocked me from their GitHub
project.

I tried to make a pull request, and GitHub did not let me.

Any feedback is welcome.

Cheers.

[1] Add GEM_USER_INSTALL environment variable by felipec · Pull Request #4028 · rubygems/rubygems · GitHub

Felipe Contreras (2):
  Add Gem.user_install
  install: fix --[no-]user-install

lib/rubygems/defaults.rb | 8 ++++++++
lib/rubygems/installer.rb | 6 +++---
lib/rubygems/installer_test_case.rb | 6 +++---
lib/rubygems/path_support.rb | 3 ++-
4 files changed, 16 insertions(+), 7 deletions(-)

···

--
2.32.0.rc0

Most people don't install gems in the default directory, but in
the user's directory (--user-install), this can be configured by
distributions using /etc/gemrc. However, that doesn't work for bundler,
since it doesn't read gem configurations.

Many bugs have been opened about this mismatch, but there's no easy
clean solution, except this.

The method Gem.user_install can be overridden by distributions to
enforce that all installs are user installs.

This way both bundler and gem will install gems in the same directory
without users having to manually set GEM_HOME to volatile locations such
as $HOME/.local/share/gem/ruby/3.0.0.

All distributions need to do is turn this on.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>

···

---
lib/rubygems/defaults.rb | 8 ++++++++
lib/rubygems/path_support.rb | 3 ++-
2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb
index e95bc06792..5961882d40 100644
--- a/lib/rubygems/defaults.rb
+++ b/lib/rubygems/defaults.rb
@@ -8,6 +8,14 @@ module Gem
   @pre_uninstall_hooks ||= []
   @pre_install_hooks ||= []

+ ##
+ # Determines if gems should be installed in user_dir instead of default_dir by
+ # default
+
+ def self.user_install
+ false
+ end
+
   ##
   # An Array of the default sources that come with RubyGems

diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb
index 8103caf324..a71265ce12 100644
--- a/lib/rubygems/path_support.rb
+++ b/lib/rubygems/path_support.rb
@@ -23,7 +23,8 @@ class Gem::PathSupport
   # hashtable, or defaults to ENV, the system environment.
   #
   def initialize(env)
- @home = env["GEM_HOME"] || Gem.default_dir
+ gem_dir = Gem.user_install ? Gem.user_dir : Gem.default_dir
+ @home = env["GEM_HOME"] || gem_dir

     if File::ALT_SEPARATOR
       @home = @home.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
--
2.32.0.rc0

Currently --no-user-install does nothing because the code assumes
@gem_home is correctly set to Gem.dir, which by default is
Gem.default_dir.

But when `Gem.user_install` is enabled that's not true, so the user has
no way of installing to `Gem.default_dir` other than using the exactly
directory in --install-dir.

Let's help the user by making --no-user-install set @gem_home to
`@Gem.default_dir`.

The tests need to be fixed because they pass both --user-install and
--install-dir, so one is overriding the other.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>

···

---
lib/rubygems/installer.rb | 6 +++---
lib/rubygems/installer_test_case.rb | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb
index 7af51056b7..1f28384aa9 100644
--- a/lib/rubygems/installer.rb
+++ b/lib/rubygems/installer.rb
@@ -180,11 +180,11 @@ def initialize(package, options={})
     @package.prog_mode = options[:prog_mode]
     @package.data_mode = options[:data_mode]

- if options[:user_install]
- @gem_home = Gem.user_dir
+ if options[:user_install] != nil
+ @gem_home = options[:user_install] ? Gem.user_dir : Gem.default_dir
       @bin_dir = Gem.bindir gem_home unless options[:bin_dir]
       @plugins_dir = Gem.plugindir(gem_home)
- check_that_user_bin_dir_is_in_path
+ check_that_user_bin_dir_is_in_path if options[:user_install]
     end
   end

diff --git a/lib/rubygems/installer_test_case.rb b/lib/rubygems/installer_test_case.rb
index 416dac7be6..02bcfceec1 100644
--- a/lib/rubygems/installer_test_case.rb
+++ b/lib/rubygems/installer_test_case.rb
@@ -110,7 +110,7 @@ def util_make_exec(spec = @spec, shebang = "#!/usr/bin/ruby", bindir = "bin")

   def setup_base_installer(force = true)
     @gem = setup_base_gem
- util_installer @spec, @gemhome, false, force
+ util_installer @spec, @gemhome, nil, force
   end

   ##
@@ -221,9 +221,9 @@ def util_setup_gem(ui = @ui, force = true)
   # Creates an installer for +spec+ that will install into +gem_home+. If
   # +user+ is true a user-install will be performed.

- def util_installer(spec, gem_home, user=false, force=true)
+ def util_installer(spec, gem_home, user=nil, force=true)
     Gem::Installer.at(spec.cache_file,
- :install_dir => gem_home,
+ :install_dir => !user && gem_home,
                        :user_install => user,
                        :force => force)
   end
--
2.32.0.rc0