2

I'd like to clean a little system-house. Essentially,

(Gem.all_system_gems - Bundler.referenced_gems(array_of_gemfile_locks)).each do |gem, ver|
  `gem uninstall #{gem} -v #{ver}
end

Any such RubyGems/Bundler methods? Or any known/efficient way of accomplishing the same?

Thanks, Ben

btd
  • 404
  • 3
  • 12
  • Let me be clear for those googling... the above are not real methods (AFAIK). Just pseudo-code to get the idea across. – btd May 10 '13 at 06:17
  • You could add this in the question itself ;) – Kashyap May 10 '13 at 06:25
  • @btd Possible duplicate question [http://stackoverflow.com/questions/7905114/rails-bundle-clean](http://stackoverflow.com/questions/7905114/rails-bundle-clean) – Viren May 10 '13 at 07:25
  • @Viren, the other question proposes solutions where RVM gemsets or bundle --path are used. I like the selected solution because in my case I'm not using either. – btd May 10 '13 at 17:42

4 Answers4

9

Bundler has a clean command to remove unused gems.

bundle clean --force

This will remove all gems that are not needed by the current project.

If you want to keep your system's gem repository clean you should consider using the --path option with bundle install. This will allow you to keep project dependencies outside of the system's gem repository.

georgehemmings
  • 488
  • 4
  • 7
  • +100_000_000 upvotes. This is exactly what I was hoping for. Thank you! – steve May 14 '14 at 20:31
  • This is way cleaner and easier (and might I add "official"? :)) `bundle clean --dry-run` might be helpful to see which ones get removed. – Kashyap Jan 16 '15 at 09:09
3

If you're on *nix or Mac OS, you can put the names of the gems you want to remove in a text file. Then run this command:

xargs gem uninstall < path/to/text/file

xargs is a great tool for processing long lists of files. In this case, it takes the contents of the text file, when its piped in via STDIN, and puts each line read into the command-line of gem uninstall. It will continue to do that until the text file is exhausted.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
3

Caution: Severe brain-damage possible.

I put up a version here explaining each function.


# gem_cleaner.rb

require 'bundler'

`touch Gemfile` unless File.exists?("Gemfile")

dot_lockfiles = [ "/path/to/gemfile1.lock", "/path/to/gemfile2.lock" 
  # ..and so on...
]

lockfile_parser = ->(path) do
  Bundler::LockfileParser.new(File.read(path))
end

lockfile_specs = ->(lockfile) { lockfile.specs.map(&:to_s) }

de_parenthesize = ->(string) { string.gsub(/\,|\(|\)/, "") }

uninstaller = ->(string) do
  `gem uninstall #{string.split(" ").map(&de_parenthesize).join(" -v ")}`
end

splitter = ->(string) do
  temp = string.split(" ").map(&de_parenthesize)
  gem_name = temp.shift
  temp.map {|x| "#{gem_name} (#{x})"}
end

# Remove #lazy and #to_a if on Ruby version < 2.0
gems_to_be_kept    = dot_lockfiles.lazy.map(&lockfile_parser).map(&lockfile_specs).to_a.uniq
all_installed_gems = `gem list`.split("\n").map(&splitter).flatten
gems_to_be_uninstalled = all_installed_gems - gems_to_be_kept
gems_to_be_uninstalled.map(&uninstaller)

Why did I write this snippet this way? I happened to see this the other day: http://www.confreaks.com/videos/2382-rmw2013-functional-principles-for-oo-development

Kashyap
  • 4,696
  • 24
  • 26
0

This code is based off of what @Kashyap answered (Bundler::LockfileParser is a good find). I ended up changing it a bit and wanted to share what I ended up using.

require 'rubygems'
require 'bundler'

LOCK_FILES = %w(/path/to/first/Gemfile.lock /path/to/second/Gemfile.lock)
REVIEWABLE_SHELL_SCRIPT = 'gem_cleaner.csh'

class GemCleaner
  def initialize lock_files, output_file
    @lock_files = lock_files
    @output_file = output_file
  end
  def lock_file_gems
    @lock_files.map do |lock_file| 
      Bundler::LockfileParser.new(File.read(lock_file)).specs.
        map {|s| [s.name, s.version.version] } 
    end.flatten(1).uniq
  end
  def installed_gems
    Gem::Specification.find_all.map {|s| [s.name, s.version.version] }
  end
  def gems_to_uninstall
    installed_gems - lock_file_gems
  end
  def create_shell_script
    File.open(@output_file, 'w', 0744) do |f|
      f.puts "#!/bin/csh"
      gems_to_uninstall.sort.each {|g| f.puts "gem uninstall #{g[0]} -v #{g[1]}" }
    end
  end
end

gc = GemCleaner.new(LOCK_FILES, REVIEWABLE_SHELL_SCRIPT)
gc.create_shell_script

Primary differences are use of Gem::Specification.find_all and output to a shell script so I could review the gems before uninstalling. Oh, and still doing it the old-fashioned OO-way. :)

Leaving selected answer with @Kashyap. Props.

btd
  • 404
  • 3
  • 12