From 9bdd6619e23f498320c3d3a6be8bd8d095a6d521 Mon Sep 17 00:00:00 2001 From: Baptiste Fontaine Date: Tue, 29 Dec 2015 12:57:48 +0100 Subject: [PATCH] cleanup: move code away from cmd/ Closes Homebrew/homebrew#47484. Signed-off-by: Baptiste Fontaine --- Library/Homebrew/cleanup.rb | 156 ++++++++++++++++++ Library/Homebrew/cmd/cleanup.rb | 189 +--------------------- Library/Homebrew/cmd/upgrade.rb | 6 +- Library/Homebrew/formula.rb | 44 +++++ Library/Homebrew/test/test_cleanup.rb | 76 +++++++++ Library/Homebrew/test/test_cmd_cleanup.rb | 30 ---- Library/Homebrew/test/test_formula.rb | 22 +++ 7 files changed, 307 insertions(+), 216 deletions(-) create mode 100644 Library/Homebrew/cleanup.rb create mode 100644 Library/Homebrew/test/test_cleanup.rb delete mode 100644 Library/Homebrew/test/test_cmd_cleanup.rb diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb new file mode 100644 index 0000000000..15805ae8e7 --- /dev/null +++ b/Library/Homebrew/cleanup.rb @@ -0,0 +1,156 @@ +require "bottles" +require "formula" +require "thread" + +module Homebrew + module Cleanup + @@disk_cleanup_size = 0 + + def self.cleanup + cleanup_cellar + cleanup_cache + cleanup_logs + unless ARGV.dry_run? + cleanup_lockfiles + rm_DS_Store + end + end + + def self.update_disk_cleanup_size(path_size) + @@disk_cleanup_size += path_size + end + + def self.disk_cleanup_size + @@disk_cleanup_size + end + + def self.cleanup_formula(formula) + formula.eligible_kegs_for_cleanup.each do |keg| + cleanup_path(keg) { keg.uninstall } + end + end + + def self.cleanup_logs + return unless HOMEBREW_LOGS.directory? + HOMEBREW_LOGS.subdirs.each do |dir| + cleanup_path(dir) { dir.rmtree } if prune?(dir, :days_default => 14) + end + end + + def self.cleanup_cellar + Formula.installed.each do |formula| + cleanup_formula formula + end + end + + def self.cleanup_cache + return unless HOMEBREW_CACHE.directory? + HOMEBREW_CACHE.children.each do |path| + if path.to_s.end_with? ".incomplete" + cleanup_path(path) { path.unlink } + next + end + if path.basename.to_s == "java_cache" && path.directory? + cleanup_path(path) { FileUtils.rm_rf path } + next + end + if prune?(path) + if path.file? + cleanup_path(path) { path.unlink } + elsif path.directory? && path.to_s.include?("--") + cleanup_path(path) { FileUtils.rm_rf path } + end + next + end + + next unless path.file? + file = path + + if Pathname::BOTTLE_EXTNAME_RX === file.to_s + version = bottle_resolve_version(file) rescue file.version + else + version = file.version + end + next unless version + next unless (name = file.basename.to_s[/(.*)-(?:#{Regexp.escape(version)})/, 1]) + + next unless HOMEBREW_CELLAR.directory? + + begin + f = Formulary.from_rack(HOMEBREW_CELLAR/name) + rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + next + end + + file_is_stale = if PkgVersion === version + f.pkg_version > version + else + f.version > version + end + + if file_is_stale || ARGV.switch?("s") && !f.installed? || bottle_file_outdated?(f, file) + cleanup_path(file) { file.unlink } + end + end + end + + def self.cleanup_path(path) + if ARGV.dry_run? + puts "Would remove: #{path} (#{path.abv})" + else + puts "Removing: #{path}... (#{path.abv})" + yield + end + + update_disk_cleanup_size(path.disk_usage) + end + + def self.cleanup_lockfiles + return unless HOMEBREW_CACHE_FORMULA.directory? + candidates = HOMEBREW_CACHE_FORMULA.children + lockfiles = candidates.select { |f| f.file? && f.extname == ".brewing" } + lockfiles.each do |file| + next unless file.readable? + file.open.flock(File::LOCK_EX | File::LOCK_NB) && file.unlink + end + end + + def self.rm_DS_Store + paths = Queue.new + %w[Cellar Frameworks Library bin etc include lib opt sbin share var]. + map { |p| HOMEBREW_PREFIX/p }.each { |p| paths << p if p.exist? } + workers = (0...Hardware::CPU.cores).map do + Thread.new do + begin + while p = paths.pop(true) + quiet_system "find", p, "-name", ".DS_Store", "-delete" + end + rescue ThreadError # ignore empty queue error + end + end + end + workers.map(&:join) + end + + def self.prune?(path, options = {}) + @time ||= Time.now + + path_modified_time = path.mtime + days_default = options[:days_default] + + prune = ARGV.value "prune" + + return true if prune == "all" + + prune_time = if prune + @time - 60 * 60 * 24 * prune.to_i + elsif days_default + @time - 60 * 60 * 24 * days_default.to_i + end + + return false unless prune_time + + path_modified_time < prune_time + end + end +end diff --git a/Library/Homebrew/cmd/cleanup.rb b/Library/Homebrew/cmd/cleanup.rb index e01a809b59..7d891787a7 100644 --- a/Library/Homebrew/cmd/cleanup.rb +++ b/Library/Homebrew/cmd/cleanup.rb @@ -1,31 +1,16 @@ -require "formula" -require "keg" -require "bottles" -require "thread" +require "cleanup" require "utils" module Homebrew - @@disk_cleanup_size = 0 - - def update_disk_cleanup_size(path_size) - @@disk_cleanup_size += path_size - end - def cleanup if ARGV.named.empty? - cleanup_cellar - cleanup_cache - cleanup_logs - unless ARGV.dry_run? - cleanup_lockfiles - rm_DS_Store - end + Cleanup.cleanup else - ARGV.resolved_formulae.each { |f| cleanup_formula(f) } + ARGV.resolved_formulae.each { |f| Cleanup.cleanup_formula f } end - if @@disk_cleanup_size > 0 - disk_space = disk_usage_readable(@@disk_cleanup_size) + if Cleanup.disk_cleanup_size > 0 + disk_space = disk_usage_readable(Cleanup.disk_cleanup_size) if ARGV.dry_run? ohai "This operation would free approximately #{disk_space} of disk space." else @@ -33,168 +18,4 @@ module Homebrew end end end - - def cleanup_logs - return unless HOMEBREW_LOGS.directory? - HOMEBREW_LOGS.subdirs.each do |dir| - cleanup_path(dir) { dir.rmtree } if prune?(dir, :days_default => 14) - end - end - - def cleanup_cellar - Formula.installed.each do |formula| - cleanup_formula formula - end - end - - def cleanup_formula(f) - if f.installed? - eligible_kegs = f.installed_kegs.select { |k| f.pkg_version > k.version } - if eligible_kegs.any? && eligible_for_cleanup?(f) - eligible_kegs.each { |keg| cleanup_keg(keg) } - else - eligible_kegs.each { |keg| opoo "Skipping (old) keg-only: #{keg}" } - end - elsif f.installed_prefixes.any? && !f.pinned? - # If the cellar only has one version installed, don't complain - # that we can't tell which one to keep. Don't complain at all if the - # only installed version is a pinned formula. - opoo "Skipping #{f.full_name}: most recent version #{f.pkg_version} not installed" - end - end - - def cleanup_keg(keg) - if keg.linked? - opoo "Skipping (old) #{keg} due to it being linked" - else - cleanup_path(keg) { keg.uninstall } - end - end - - def cleanup_cache - return unless HOMEBREW_CACHE.directory? - HOMEBREW_CACHE.children.each do |path| - if path.to_s.end_with? ".incomplete" - cleanup_path(path) { path.unlink } - next - end - if path.basename.to_s == "java_cache" && path.directory? - cleanup_path(path) { FileUtils.rm_rf path } - next - end - if prune?(path) - if path.file? - cleanup_path(path) { path.unlink } - elsif path.directory? && path.to_s.include?("--") - cleanup_path(path) { FileUtils.rm_rf path } - end - next - end - - next unless path.file? - file = path - - if Pathname::BOTTLE_EXTNAME_RX === file.to_s - version = bottle_resolve_version(file) rescue file.version - else - version = file.version - end - next unless version - next unless (name = file.basename.to_s[/(.*)-(?:#{Regexp.escape(version)})/, 1]) - - next unless HOMEBREW_CELLAR.directory? - - begin - f = Formulary.from_rack(HOMEBREW_CELLAR/name) - rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError - next - end - - file_is_stale = if PkgVersion === version - f.pkg_version > version - else - f.version > version - end - - if file_is_stale || ARGV.switch?("s") && !f.installed? || bottle_file_outdated?(f, file) - cleanup_path(file) { file.unlink } - end - end - end - - def cleanup_path(path) - if ARGV.dry_run? - puts "Would remove: #{path} (#{path.abv})" - else - puts "Removing: #{path}... (#{path.abv})" - yield - end - update_disk_cleanup_size(path.disk_usage) - end - - def cleanup_lockfiles - return unless HOMEBREW_CACHE_FORMULA.directory? - candidates = HOMEBREW_CACHE_FORMULA.children - lockfiles = candidates.select { |f| f.file? && f.extname == ".brewing" } - lockfiles.each do |file| - next unless file.readable? - file.open.flock(File::LOCK_EX | File::LOCK_NB) && file.unlink - end - end - - def rm_DS_Store - paths = Queue.new - %w[Cellar Frameworks Library bin etc include lib opt sbin share var]. - map { |p| HOMEBREW_PREFIX/p }.each { |p| paths << p if p.exist? } - workers = (0...Hardware::CPU.cores).map do - Thread.new do - begin - while p = paths.pop(true) - quiet_system "find", p, "-name", ".DS_Store", "-delete" - end - rescue ThreadError # ignore empty queue error - end - end - end - workers.map(&:join) - end - - def prune?(path, options = {}) - @time ||= Time.now - - path_modified_time = path.mtime - days_default = options[:days_default] - - prune = ARGV.value "prune" - - return true if prune == "all" - - prune_time = if prune - @time - 60 * 60 * 24 * prune.to_i - elsif days_default - @time - 60 * 60 * 24 * days_default.to_i - end - - return false unless prune_time - - path_modified_time < prune_time - end - - def eligible_for_cleanup?(formula) - # It used to be the case that keg-only kegs could not be cleaned up, because - # older brews were built against the full path to the keg-only keg. Then we - # introduced the opt symlink, and built against that instead. So provided - # no brew exists that was built against an old-style keg-only keg, we can - # remove it. - if !formula.keg_only? || ARGV.force? - true - elsif formula.opt_prefix.directory? - # SHA records were added to INSTALL_RECEIPTS the same day as opt symlinks - Formula.installed.select do |f| - f.deps.any? do |d| - d.to_formula.full_name == formula.full_name rescue d.name == formula.name - end - end.all? { |f| f.installed_prefixes.all? { |keg| Tab.for_keg(keg).HEAD } } - end - end end diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index 9c71d40571..62ef0bf811 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -1,5 +1,5 @@ require "cmd/install" -require "cmd/cleanup" +require "cleanup" module Homebrew def upgrade @@ -44,7 +44,9 @@ module Homebrew outdated.each do |f| upgrade_formula(f) - cleanup_formula(f) if ARGV.include?("--cleanup") && f.installed? + next unless ARGV.include?("--cleanup") + next unless f.installed? + Homebrew::Cleanup.cleanup_formula f end end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index b12b9c01b5..e2f5bd9613 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1475,6 +1475,50 @@ class Formula end end + # @private + def eligible_kegs_for_cleanup + eligible_for_cleanup = [] + if installed? + eligible_kegs = installed_kegs.select { |k| pkg_version > k.version } + if eligible_kegs.any? && eligible_for_cleanup? + eligible_kegs.each do |keg| + if keg.linked? + opoo "Skipping (old) #{keg} due to it being linked" + else + eligible_for_cleanup << keg + end + end + else + eligible_kegs.each { |keg| opoo "Skipping (old) keg-only: #{keg}" } + end + elsif installed_prefixes.any? && !pinned? + # If the cellar only has one version installed, don't complain + # that we can't tell which one to keep. Don't complain at all if the + # only installed version is a pinned formula. + opoo "Skipping #{full_name}: most recent version #{f.pkg_version} not installed" + end + eligible_for_cleanup + end + + # @private + def eligible_for_cleanup? + # It used to be the case that keg-only kegs could not be cleaned up, because + # older brews were built against the full path to the keg-only keg. Then we + # introduced the opt symlink, and built against that instead. So provided + # no brew exists that was built against an old-style keg-only keg, we can + # remove it. + if !keg_only? || ARGV.force? + true + elsif opt_prefix.directory? + # SHA records were added to INSTALL_RECEIPTS the same day as opt symlinks + Formula.installed.select do |f| + f.deps.any? do |d| + d.to_formula.full_name == full_name rescue d.name == name + end + end.all? { |f| f.installed_prefixes.all? { |keg| Tab.for_keg(keg).HEAD } } + end + end + private def exec_cmd(cmd, args, out, logfn) diff --git a/Library/Homebrew/test/test_cleanup.rb b/Library/Homebrew/test/test_cleanup.rb new file mode 100644 index 0000000000..bccfa1e54e --- /dev/null +++ b/Library/Homebrew/test/test_cleanup.rb @@ -0,0 +1,76 @@ +require "testing_env" +require "testball" +require "cleanup" +require "fileutils" +require "pathname" + +class CleanupTests < Homebrew::TestCase + def setup + @ds_store = Pathname.new "#{HOMEBREW_PREFIX}/Library/.DS_Store" + FileUtils.touch @ds_store + end + + def teardown + FileUtils.rm_f @ds_store + ARGV.delete "--dry-run" + ARGV.delete "--prune=all" + end + + def test_cleanup + shutup { Homebrew::Cleanup.cleanup } + refute_predicate @ds_store, :exist? + end + + def test_cleanup_dry_run + ARGV << "--dry-run" + shutup { Homebrew::Cleanup.cleanup } + assert_predicate @ds_store, :exist? + end + + def test_cleanup_formula + f1 = Class.new(Testball) { version "0.1" }.new + f2 = Class.new(Testball) { version "0.2" }.new + f3 = Class.new(Testball) { version "0.3" }.new + + shutup do + f1.brew { f1.install } + f2.brew { f2.install } + f3.brew { f3.install } + end + + assert_predicate f1, :installed? + assert_predicate f2, :installed? + assert_predicate f3, :installed? + + shutup { Homebrew::Cleanup.cleanup_formula f3 } + + refute_predicate f1, :installed? + refute_predicate f2, :installed? + assert_predicate f3, :installed? + ensure + [f1, f2, f3].each(&:clear_cache) + f3.rack.rmtree + end + + def test_cleanup_logs + path = (HOMEBREW_LOGS/"delete_me") + path.mkpath + ARGV << "--prune=all" + shutup { Homebrew::Cleanup.cleanup_logs } + refute_predicate path, :exist? + end + + def test_cleanup_cache_incomplete_downloads + incomplete = (HOMEBREW_CACHE/"something.incomplete") + incomplete.mkpath + shutup { Homebrew::Cleanup.cleanup_cache } + refute_predicate incomplete, :exist? + end + + def test_cleanup_cache_java_cache + java_cache = (HOMEBREW_CACHE/"java_cache") + java_cache.mkpath + shutup { Homebrew::Cleanup.cleanup_cache } + refute_predicate java_cache, :exist? + end +end diff --git a/Library/Homebrew/test/test_cmd_cleanup.rb b/Library/Homebrew/test/test_cmd_cleanup.rb deleted file mode 100644 index 109f27eba6..0000000000 --- a/Library/Homebrew/test/test_cmd_cleanup.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "testing_env" -require "testball" -require "cmd/cleanup" - -class CleanupTests < Homebrew::TestCase - def test_cleanup - f1 = Class.new(Testball) { version "0.1" }.new - f2 = Class.new(Testball) { version "0.2" }.new - f3 = Class.new(Testball) { version "0.3" }.new - - shutup do - f1.brew { f1.install } - f2.brew { f2.install } - f3.brew { f3.install } - end - - assert_predicate f1, :installed? - assert_predicate f2, :installed? - assert_predicate f3, :installed? - - shutup { Homebrew.cleanup_formula(f3) } - - refute_predicate f1, :installed? - refute_predicate f2, :installed? - assert_predicate f3, :installed? - ensure - [f1, f2, f3].each(&:clear_cache) - f3.rack.rmtree - end -end diff --git a/Library/Homebrew/test/test_formula.rb b/Library/Homebrew/test/test_formula.rb index e6b5396b80..8f372f6f72 100644 --- a/Library/Homebrew/test/test_formula.rb +++ b/Library/Homebrew/test/test_formula.rb @@ -1,5 +1,6 @@ require "testing_env" require "testball" +require "formula" class FormulaTests < Homebrew::TestCase def test_formula_instantiation @@ -345,4 +346,25 @@ class FormulaTests < Homebrew::TestCase assert h.is_a?(Hash), "Formula#to_hash should return a Hash" assert h["versions"]["bottle"], "The hash should say the formula is bottled" end + + def test_eligible_kegs_for_cleanup + f1 = Class.new(Testball) { version "0.1" }.new + f2 = Class.new(Testball) { version "0.2" }.new + f3 = Class.new(Testball) { version "0.3" }.new + + shutup do + f1.brew { f1.install } + f2.brew { f2.install } + f3.brew { f3.install } + end + + assert_predicate f1, :installed? + assert_predicate f2, :installed? + assert_predicate f3, :installed? + + assert_equal f3.installed_kegs[0..1], f3.eligible_kegs_for_cleanup + ensure + [f1, f2, f3].each(&:clear_cache) + f3.rack.rmtree + end end