-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Fix plugins uninstallation for user installed gems #6456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This is just initial attempt, I have not even run the test suite locally. But there is more unfortunately.
|
d33c536
to
48ef371
Compare
lib/rubygems/installer.rb
Outdated
@@ -514,12 +513,10 @@ def generate_plugins # :nodoc: | |||
latest = Gem::Specification.latest_spec_for(spec.name) | |||
return if latest && latest.version > spec.version | |||
|
|||
ensure_writable_dir @plugins_dir |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was introduced in #3144, but I don't think this is the right place. If directories are missing during RubyGems update, then RubyGems update should fix them. No other directory (with exception to bindir, which is possibly out of RubyGems structure) is ensured this way.
On top of this, now it causes unfortunate test failures :/
cf9ee26
to
bd6bb2b
Compare
I'm starting to be afraid there is no way to cover the whole dependency matrix between RubyGems/Bundler 😭 |
adbcab7
to
eada332
Compare
While I am not really happy about it, I have reverted back to the simpler approach. There is one test failure:
And I think that the test case does not make too much sense in the context of the current code. I don't even think that the current implementation should be problematic in the context of original issue it covers. @deivid-rodriguez could you PTAL? The problem is that as far as I understand the logic, the uninstaller always removes content from the location the gemspec was loaded from. The |
@voxik Shame on me that I didn't include more details of my real usage at the time when I wrote that test 😞. Anyways, I think we can introduce your patch without breaking my test: diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb
index 5883ed1c41..a0e2af65bc 100644
--- a/lib/rubygems/uninstaller.rb
+++ b/lib/rubygems/uninstaller.rb
@@ -48,8 +48,8 @@ def initialize(gem, options = {})
# TODO document the valid options
@gem = gem
@version = options[:version] || Gem::Requirement.default
- @gem_home = File.realpath(options[:install_dir] || Gem.dir)
- @plugins_dir = Gem.plugindir(@gem_home)
+ @install_dir = File.realpath(options[:install_dir]) if options[:install_dir]
+ @gem_home = @install_dir || File.realpath(Gem.dir)
@force_executables = options[:executables]
@force_all = options[:all]
@force_ignore = options[:ignore]
@@ -69,7 +69,7 @@ def initialize(gem, options = {})
# only add user directory if install_dir is not set
@user_install = false
- @user_install = options[:user_install] unless options[:install_dir]
+ @user_install = options[:user_install] unless @install_dir
# Optimization: populated during #uninstall
@default_specs_matching_uninstall_params = []
@@ -285,7 +285,7 @@ def remove(spec)
def remove_plugins(spec) # :nodoc:
return if spec.plugins.empty?
- remove_plugins_for(spec, @plugins_dir)
+ remove_plugins_for(spec, plugin_dir_for(spec))
end
##
@@ -295,7 +295,7 @@ def regenerate_plugins
latest = Gem::Specification.latest_spec_for(@spec.name)
return if latest.nil?
- regenerate_plugins_for(latest, @plugins_dir)
+ regenerate_plugins_for(latest, plugin_dir_for(@spec))
end
##
@@ -407,4 +407,8 @@ def warn_cannot_uninstall_default_gems(specs)
say "Gem #{spec.full_name} cannot be uninstalled because it is a default gem"
end
end
+
+ def plugin_dir_for(spec)
+ Gem.plugindir(@install_dir || spec.base_dir)
+ end
end Can you write a test for the issue this is fixing, by the way? Ideally you would also provide a reproducible example of the realworld issue and how this patch fixes the problem (what I didn't do when I introduced that test 🙏). I can't reproduce your original issue:
So my understanding is that it should be related to Fedora customizations in the Thanks for your work! |
I'll take look tomorrow but
I think the right command to reproduce is:
|
This is the full reproducer with plain upstream Ruby:
|
TL;DR: @deivid-rodriguez the point I am trying to make is that the The general problem I am facing here is that we have arrived at the current implementation by evolution, not by design. My PR at this stage tries to return back to what could have been the original design. Previously, I was experimenting with bigger changes, but that would be much larger effort (which I cannot undertake due to other workload). I need to split my comment into several chapters, because it grown long (sorry). How
|
@plugins_dir = Gem.plugindir(@gem_home) |
All the other real removal action is driven via Gem::Specification
method calls, which are derived from #base_dir
.
From this POV, your original implementation is non-systematic and what I am actually proposing is to address this exception. That is why I am questioning the test_remove_plugins_with_install_dir(TestGemUninstaller)
, because it only cements in the exception.
What are the expectations recorded in test suite?
If I check the current --install-dir
test coverage, it is basically non-existing. It does not really focus on some real utility of --install-dir
, it just focus on making sure that the install_dir == gem_home
. Let me elaborate in detail.
This test just some implementation detail and would be probably better to drop it immediately:
rubygems/test/rubygems/test_gem_uninstaller.rb
Lines 25 to 30 in 696264f
def test_initialize_expand_path | |
FileUtils.mkdir_p "foo/bar" | |
uninstaller = Gem::Uninstaller.new nil, :install_dir => "foo//bar" | |
assert_match %r{foo/bar$}, uninstaller.instance_variable_get(:@gem_home) | |
end |
This is the install_dir == gem_home
case:
rubygems/test/rubygems/test_gem_uninstaller.rb
Lines 137 to 153 in 696264f
def test_remove_not_in_home | |
Dir.mkdir "#{@gemhome}2" | |
uninstaller = Gem::Uninstaller.new nil, :install_dir => "#{@gemhome}2" | |
e = assert_raise Gem::GemNotInHomeException do | |
use_ui ui do | |
uninstaller.remove @spec | |
end | |
end | |
expected = | |
"Gem '#{@spec.full_name}' is not installed in directory #{@gemhome}2" | |
assert_equal expected, e.message | |
assert_path_exist @spec.gem_dir | |
end |
The following checks special case with symlinks, but after all, it works with the original location, so does not test any real --install-dir
use case, where the gem would be installed somewhere outside of some standard and expected location:
rubygems/test/rubygems/test_gem_uninstaller.rb
Lines 155 to 171 in 696264f
def test_remove_symlinked_gem_home | |
pend "Symlinks not supported or not enabled" unless symlink_supported? | |
Dir.mktmpdir("gem_home") do |dir| | |
symlinked_gem_home = "#{dir}/#{File.basename(@gemhome)}" | |
FileUtils.ln_s(@gemhome, dir) | |
uninstaller = Gem::Uninstaller.new nil, :install_dir => symlinked_gem_home | |
use_ui ui do | |
uninstaller.remove @spec | |
end | |
assert_path_not_exist @spec.gem_dir | |
end | |
end |
and then there is the origin test_remove_plugins_with_install_dir(TestGemUninstaller)
. Nothing more.
The situation with --user-install
is not any better ...
History
And here is the history lesson.
The --install-dir
option was introduced here ~16 year ago. But at that era, the Gem::Specification
was just pretty basic. All the paths were constructed in the uninstaller and the option could influence spec_dir
:
rubygems/lib/rubygems/uninstaller.rb
Line 59 in 62864c3
spec_dir = File.join @gem_home, 'specifications' |
I think that the last time the --install-dir
was useful was prior this commit. This reduced the --install-dir
to the "safeguards".
At this stage ~12 years ago, it can already be seen quite clearly that the --install-dir
is used just to check the install_dir == gem_home
condition.
Specification
for managing pluginseada332
to
991a91a
Compare
Sorry for getting back so late @voxik. I'm sure we're missing some test coverage, but from my realworld testing, the realworld Also, after looking again into my test, I think it makes sense to make sure that when I was able to reproduce your realworld issue though and confirmed that your patch, together with my amend to not break the plugins spec fix it. So I added my suggestion to this PR and also added a test to cover this problem. |
Actually nevermind here. I had a second look and you're rigth. Your original patch is correct and it's the test that needs updating to better simulate I'll fix everything on Monday and restore your original patch since it's indeed correct 👍. |
Heh, that would sound like me 😉 I'd love to remove exceptions and protect core principles Thx for looking into this 👍 |
Instead of unit testing the `remove_plugins` method, test the whole removal process.
The plugin loader from `@gem_home` was removed during uninstallation. However, this could leave behind the plugins for `--user-install` installed gems. Use `Gem::Specifictaions#base_dir` instead. This ensures that the plugin loader for associated .gemspec is uninstalled.
991a91a
to
6047f78
Compare
I improved the existing test to better represent realworld, and added a new test covering this fix. It should be ready now. |
Fix plugins uninstallation for user installed gems (cherry picked from commit e933219)
What was the end-user or developer problem that led to this PR?
Originally, when the plugins system was reworked as part of #3108, the plugin directory was considered global. But that is mistake. The plugins are always related to specific gems and therefore the directories should be handled via
Gem::Specification
. This will prevent issues such as leftovers after gem uninstallation as demonstred by #6452:What is your fix for the problem, implemented in this PR?
Gem.Specification
should be responsible for providing plugins path.Fixes #6452.