brew/Library/Homebrew/cleanup.rb

603 lines
18 KiB
Ruby
Raw Normal View History

2020-10-10 14:16:11 +02:00
# typed: false
# frozen_string_literal: true
2016-04-25 17:57:51 +01:00
require "utils/bottles"
require "formula"
require "cask/cask_loader"
2018-08-10 00:54:03 +02:00
require "set"
2020-08-17 03:52:17 +02:00
module Homebrew
# Helper class for cleaning up the Homebrew cache.
#
# @api private
class Cleanup
CLEANUP_DEFAULT_DAYS = Homebrew::EnvConfig.cleanup_periodic_full_days.to_i.freeze
2020-08-17 03:52:17 +02:00
private_constant :CLEANUP_DEFAULT_DAYS
# {Pathname} refinement with helper functions for cleaning up files.
2020-08-17 03:52:17 +02:00
module CleanupRefinement
refine Pathname do
def incomplete?
extname.end_with?(".incomplete")
end
2020-08-17 03:52:17 +02:00
def nested_cache?
directory? && %w[
cargo_cache
go_cache
go_mod_cache
glide_home
java_cache
npm_cache
gclient_cache
].include?(basename.to_s)
end
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
def go_cache_directory?
# Go makes its cache contents read-only to ensure cache integrity,
# which makes sense but is something we need to undo for cleanup.
directory? && %w[go_cache go_mod_cache].include?(basename.to_s)
end
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
def prune?(days)
return false unless days
return true if days.zero?
2018-08-30 05:19:20 +01:00
2020-08-17 03:52:17 +02:00
return true if symlink? && !exist?
2018-08-08 09:43:38 +02:00
2023-02-04 13:51:35 -08:00
days_ago = (DateTime.now - days).to_time
mtime < days_ago && ctime < days_ago
2020-08-17 03:52:17 +02:00
end
2018-09-01 06:42:47 +02:00
2020-08-19 17:12:32 +01:00
def stale?(scrub: false)
2020-08-17 03:52:17 +02:00
return false unless resolved_path.file?
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
if dirname.basename.to_s == "Cask"
stale_cask?(scrub)
else
stale_formula?(scrub)
end
end
2018-09-17 02:45:00 +02:00
2020-08-17 03:52:17 +02:00
private
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
def stale_formula?(scrub)
return false unless HOMEBREW_CELLAR.directory?
2018-08-08 09:43:38 +02:00
version = if HOMEBREW_BOTTLES_EXTNAME_REGEX.match?(to_s)
2020-08-17 03:52:17 +02:00
begin
Utils::Bottles.resolve_version(self)
rescue
nil
end
end
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
version ||= basename.to_s[/\A.*(?:--.*?)*--(.*?)#{Regexp.escape(extname)}\Z/, 1]
version ||= basename.to_s[/\A.*--?(.*?)#{Regexp.escape(extname)}\Z/, 1]
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
return false unless version
2020-08-17 03:52:17 +02:00
version = Version.new(version)
return false unless (formula_name = basename.to_s[/\A(.*?)(?:--.*?)*--?(?:#{Regexp.escape(version)})/, 1])
2020-08-17 03:52:17 +02:00
formula = begin
Formulary.from_rack(HOMEBREW_CELLAR/formula_name)
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
nil
2020-08-17 03:52:17 +02:00
end
2018-08-08 09:43:38 +02:00
return false if formula.blank?
2020-08-17 03:52:17 +02:00
resource_name = basename.to_s[/\A.*?--(.*?)--?(?:#{Regexp.escape(version)})/, 1]
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
if resource_name == "patch"
patch_hashes = formula.stable&.patches&.select(&:external?)&.map(&:resource)&.map(&:version)
return true unless patch_hashes&.include?(Checksum.new(version.to_s))
elsif resource_name && (resource_version = formula.stable&.resources&.dig(resource_name)&.version)
2020-08-17 03:52:17 +02:00
return true if resource_version != version
elsif version.is_a?(PkgVersion)
return true if formula.pkg_version > version
elsif formula.version > version
return true
end
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
return true if scrub && !formula.latest_version_installed?
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
return true if Utils::Bottles.file_outdated?(formula, self)
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
false
end
2020-08-17 03:52:17 +02:00
def stale_cask?(scrub)
return false unless (name = basename.to_s[/\A(.*?)--/, 1])
2020-08-17 03:52:17 +02:00
cask = begin
Cask::CaskLoader.load(name)
rescue Cask::CaskError
nil
2020-08-17 03:52:17 +02:00
end
return false if cask.blank?
2020-08-17 03:52:17 +02:00
return true unless basename.to_s.match?(/\A#{Regexp.escape(name)}--#{Regexp.escape(cask.version)}\b/)
2020-12-01 17:04:59 +00:00
return true if scrub && cask.versions.exclude?(cask.version)
2020-08-17 03:52:17 +02:00
if cask.version.latest?
2023-02-04 13:51:35 -08:00
cleanup_threshold = (DateTime.now - CLEANUP_DEFAULT_DAYS).to_time
return mtime < cleanup_threshold && ctime < cleanup_threshold
2020-08-17 03:52:17 +02:00
end
2020-08-17 03:52:17 +02:00
false
end
end
end
2018-08-08 09:43:38 +02:00
2020-08-17 03:52:17 +02:00
using CleanupRefinement
2018-08-08 09:43:38 +02:00
2018-08-08 11:20:53 +02:00
extend Predicable
2019-04-19 21:46:20 +09:00
PERIODIC_CLEAN_FILE = (HOMEBREW_CACHE/".cleaned").freeze
attr_predicate :dry_run?, :scrub?, :prune?
2020-07-07 11:29:33 +01:00
attr_reader :args, :days, :cache, :disk_cleanup_size
2018-08-08 11:20:53 +02:00
def initialize(*args, dry_run: false, scrub: false, days: nil, cache: HOMEBREW_CACHE)
@disk_cleanup_size = 0
@args = args
@dry_run = dry_run
@scrub = scrub
@prune = days.present?
@days = days || Homebrew::EnvConfig.cleanup_max_age_days.to_i
2018-08-08 11:20:53 +02:00
@cache = cache
2018-08-10 00:54:03 +02:00
@cleaned_up_paths = Set.new
2017-08-13 04:21:07 +05:30
end
def self.install_formula_clean!(f, dry_run: false)
2020-04-05 15:44:50 +01:00
return if Homebrew::EnvConfig.no_install_cleanup?
return unless f.latest_version_installed?
return if skip_clean_formula?(f)
if dry_run
ohai "Would run `brew cleanup #{f}`"
else
ohai "Running `brew cleanup #{f}`..."
end
puts_no_install_cleanup_disable_message_if_not_already!
return if dry_run
Cleanup.new.cleanup_formula(f)
end
def self.puts_no_install_cleanup_disable_message
return if Homebrew::EnvConfig.no_env_hints?
return if Homebrew::EnvConfig.no_install_cleanup?
puts "Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP."
puts "Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`)."
end
def self.puts_no_install_cleanup_disable_message_if_not_already!
return if @puts_no_install_cleanup_disable_message_if_not_already
puts_no_install_cleanup_disable_message
@puts_no_install_cleanup_disable_message_if_not_already = true
end
def self.skip_clean_formula?(f)
return false if Homebrew::EnvConfig.no_cleanup_formulae.blank?
@skip_clean_formulae ||= Homebrew::EnvConfig.no_cleanup_formulae.split(",")
@skip_clean_formulae.include?(f.name) || (@skip_clean_formulae & f.aliases).present?
end
def self.periodic_clean_due?
2020-04-05 15:44:50 +01:00
return false if Homebrew::EnvConfig.no_install_cleanup?
unless PERIODIC_CLEAN_FILE.exist?
HOMEBREW_CACHE.mkpath
FileUtils.touch PERIODIC_CLEAN_FILE
return false
end
2023-02-04 13:51:35 -08:00
PERIODIC_CLEAN_FILE.mtime < (DateTime.now - CLEANUP_DEFAULT_DAYS).to_time
end
def self.periodic_clean!(dry_run: false)
return if Homebrew::EnvConfig.no_install_cleanup?
return unless periodic_clean_due?
if dry_run
oh1 "Would run `brew cleanup` which has not been run in the last #{CLEANUP_DEFAULT_DAYS} days"
else
oh1 "`brew cleanup` has not been run in the last #{CLEANUP_DEFAULT_DAYS} days, running now..."
end
puts_no_install_cleanup_disable_message
return if dry_run
Cleanup.new.clean!(quiet: true, periodic: true)
end
def clean!(quiet: false, periodic: false)
2018-08-08 11:20:53 +02:00
if args.empty?
Formula.installed
.sort_by(&:name)
.reject { |f| Cleanup.skip_clean_formula?(f) }
.each do |formula|
cleanup_formula(formula, quiet: quiet, ds_store: false, cache_db: false)
2018-08-08 11:20:53 +02:00
end
Cleanup.autoremove(dry_run: dry_run?) if Homebrew::EnvConfig.autoremove?
2018-08-08 11:20:53 +02:00
cleanup_cache
cleanup_logs
2018-10-14 00:13:04 +02:00
cleanup_lockfiles
cleanup_python_site_packages
prune_prefix_symlinks_and_directories
unless dry_run?
cleanup_cache_db
rm_ds_store
HOMEBREW_CACHE.mkpath
FileUtils.touch PERIODIC_CLEAN_FILE
end
# Cleaning up Ruby needs to be done last to avoid requiring additional
# files afterwards. Additionally, don't allow it on periodic cleans to
# avoid having to try to do a `brew install` when we've just deleted
# the running Ruby process...
return if periodic
cleanup_portable_ruby
2021-02-02 11:51:08 +00:00
cleanup_bootsnap
2018-08-08 11:20:53 +02:00
else
args.each do |arg|
formula = begin
2018-09-11 17:44:18 +02:00
Formulary.resolve(arg)
2018-08-08 11:20:53 +02:00
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
nil
end
cask = begin
2018-09-06 08:29:14 +02:00
Cask::CaskLoader.load(arg)
rescue Cask::CaskError
2018-08-08 11:20:53 +02:00
nil
end
if formula && Cleanup.skip_clean_formula?(formula)
onoe "Refusing to clean #{formula} because it is listed in " \
"#{Tty.bold}HOMEBREW_NO_CLEANUP_FORMULAE#{Tty.reset}!"
elsif formula
cleanup_formula(formula)
end
2018-08-08 11:20:53 +02:00
cleanup_cask(cask) if cask
end
end
end
def unremovable_kegs
@unremovable_kegs ||= []
end
def cleanup_formula(formula, quiet: false, ds_store: true, cache_db: true)
formula.eligible_kegs_for_cleanup(quiet: quiet)
.each(&method(:cleanup_keg))
cleanup_cache(Pathname.glob(cache/"#{formula.name}--*"))
rm_ds_store([formula.rack]) if ds_store
cleanup_cache_db(formula.rack) if cache_db
2018-08-09 16:46:39 +02:00
cleanup_lockfiles(FormulaLock.new(formula.name).path)
end
def cleanup_cask(cask, ds_store: true)
cleanup_cache(Pathname.glob(cache/"Cask/#{cask.token}--*"))
rm_ds_store([cask.caskroom_path]) if ds_store
2018-08-09 16:46:39 +02:00
cleanup_lockfiles(CaskLock.new(cask.token).path)
end
2018-08-08 11:20:53 +02:00
def cleanup_keg(keg)
cleanup_path(keg) { keg.uninstall(raise_failures: true) }
2021-10-19 23:39:58 +09:00
rescue Errno::EACCES, Errno::ENOTEMPTY => e
opoo e.message
unremovable_kegs << keg
end
def cleanup_logs
return unless HOMEBREW_LOGS.directory?
2023-01-02 19:18:51 +00:00
logs_days = [days, CLEANUP_DEFAULT_DAYS].min
2018-09-17 02:45:00 +02:00
HOMEBREW_LOGS.subdirs.each do |dir|
cleanup_path(dir) { dir.rmtree } if dir.prune?(logs_days)
end
end
def cleanup_unreferenced_downloads
return if dry_run?
return unless (cache/"downloads").directory?
2018-10-14 00:13:04 +02:00
downloads = (cache/"downloads").children
referenced_downloads = [cache, cache/"Cask"].select(&:directory?)
.flat_map(&:children)
.select(&:symlink?)
.map(&:resolved_path)
2018-10-14 00:13:04 +02:00
(downloads - referenced_downloads).each do |download|
if download.incomplete?
begin
LockFile.new(download.basename).with_lock do
download.unlink
end
rescue OperationInProgressError
# Skip incomplete downloads which are still in progress.
next
end
elsif download.directory?
FileUtils.rm_rf download
2018-10-14 00:13:04 +02:00
else
download.unlink
end
end
end
def cleanup_cache(entries = nil)
entries ||= [cache, cache/"Cask"].select(&:directory?).flat_map(&:children)
entries.each do |path|
next if path == PERIODIC_CLEAN_FILE
2018-08-30 05:19:20 +01:00
FileUtils.chmod_R 0755, path if path.go_cache_directory? && !dry_run?
2018-08-08 09:43:38 +02:00
next cleanup_path(path) { path.unlink } if path.incomplete?
next cleanup_path(path) { FileUtils.rm_rf path } if path.nested_cache?
2018-08-08 11:20:53 +02:00
if path.prune?(days)
2018-08-25 22:06:24 +02:00
if path.file? || path.symlink?
cleanup_path(path) { path.unlink }
elsif path.directory? && path.to_s.include?("--")
cleanup_path(path) { FileUtils.rm_rf path }
end
next
end
2021-05-08 11:20:01 +10:00
# If we've specified --prune don't do the (expensive) .stale? check.
cleanup_path(path) { path.unlink } if !prune? && path.stale?(scrub: scrub?)
end
cleanup_unreferenced_downloads
end
def cleanup_path(path)
return unless path.exist?
2018-08-10 00:54:03 +02:00
return unless @cleaned_up_paths.add?(path)
@disk_cleanup_size += path.disk_usage
2018-08-08 09:43:38 +02:00
2018-08-08 11:20:53 +02:00
if dry_run?
puts "Would remove: #{path} (#{path.abv})"
else
puts "Removing: #{path}... (#{path.abv})"
yield
end
end
2018-08-09 16:46:39 +02:00
def cleanup_lockfiles(*lockfiles)
2018-08-16 05:55:17 +02:00
return if dry_run?
2018-08-09 16:46:39 +02:00
lockfiles = HOMEBREW_LOCKS.children.select(&:file?) if lockfiles.empty? && HOMEBREW_LOCKS.directory?
2018-08-09 16:46:39 +02:00
lockfiles.each do |file|
next unless file.readable?
2018-08-09 16:46:39 +02:00
next unless file.open(File::RDWR).flock(File::LOCK_EX | File::LOCK_NB)
2018-08-11 17:23:35 +02:00
begin
2018-08-16 05:55:17 +02:00
file.unlink
2018-08-11 17:23:35 +02:00
ensure
file.open(File::RDWR).flock(File::LOCK_UN) if file.exist?
end
end
end
def cleanup_portable_ruby
vendor_dir = HOMEBREW_LIBRARY/"Homebrew/vendor"
portable_ruby_latest_version = (vendor_dir/"portable-ruby-version").read.chomp
portable_rubies_to_remove = []
Pathname.glob(vendor_dir/"portable-ruby/*.*").select(&:directory?).each do |path|
2022-11-20 10:49:53 -08:00
next if !use_system_ruby? && portable_ruby_latest_version == path.basename.to_s
portable_rubies_to_remove << path
end
return if portable_rubies_to_remove.empty?
bundler_path = vendor_dir/"bundle/ruby"
if dry_run?
puts Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "clean", "-nx", bundler_path).chomp
else
puts Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "clean", "-ffqx", bundler_path).chomp
end
portable_rubies_to_remove.each do |portable_ruby|
cleanup_path(portable_ruby) { portable_ruby.rmtree }
end
end
2022-11-20 10:49:53 -08:00
def use_system_ruby?; end
2021-02-02 11:51:08 +00:00
def cleanup_bootsnap
bootsnap = cache/"bootsnap"
return unless bootsnap.exist?
cleanup_path(bootsnap) { bootsnap.rmtree }
2021-02-02 11:51:08 +00:00
end
def cleanup_cache_db(rack = nil)
FileUtils.rm_rf [
cache/"desc_cache.json",
cache/"linkage.db",
cache/"linkage.db.db",
]
CacheStoreDatabase.use(:linkage) do |db|
break unless db.created?
db.each_key do |keg|
next if rack.present? && !keg.start_with?("#{rack}/")
next if File.directory?(keg)
LinkageCacheStore.new(keg, db).delete!
end
end
end
2018-08-09 01:55:43 +02:00
def rm_ds_store(dirs = nil)
2021-03-26 14:11:03 +00:00
dirs ||= Keg::MUST_EXIST_DIRECTORIES + [
HOMEBREW_PREFIX/"Caskroom",
]
dirs.select(&:directory?)
.flat_map { |d| Pathname.glob("#{d}/**/.DS_Store") }
.each do |dir|
dir.unlink
rescue Errno::EACCES
# don't care if we can't delete a .DS_Store
nil
end
end
def cleanup_python_site_packages
pyc_files = Hash.new { |h, k| h[k] = [] }
seen_non_pyc_file = Hash.new { |h, k| h[k] = false }
unused_pyc_files = []
HOMEBREW_PREFIX.glob("lib/python*/site-packages").each do |site_packages|
site_packages.each_child do |child|
next unless child.directory?
# TODO: Work out a sensible way to clean up pip's, setuptools', and wheel's
# {dist,site}-info directories. Alternatively, consider always removing
# all `-info` directories, because we may not be making use of them.
next if child.basename.to_s.end_with?("-info")
# Clean up old *.pyc files in the top-level __pycache__.
if child.basename.to_s == "__pycache__"
child.find do |path|
next unless path.extname == ".pyc"
next unless path.prune?(days)
unused_pyc_files << path
end
next
end
# Look for directories that contain only *.pyc files.
child.find do |path|
next if path.directory?
if path.extname == ".pyc"
pyc_files[child] << path
else
seen_non_pyc_file[child] = true
break
end
end
end
end
unused_pyc_files += pyc_files.reject { |k,| seen_non_pyc_file[k] }
.values
2022-08-31 16:04:21 +08:00
.flatten
return if unused_pyc_files.blank?
unused_pyc_files.each do |pyc|
cleanup_path(pyc) { pyc.unlink }
end
end
def prune_prefix_symlinks_and_directories
ObserverPathnameExtension.reset_counts!
dirs = []
Keg::MUST_EXIST_SUBDIRECTORIES.each do |dir|
next unless dir.directory?
dir.find do |path|
path.extend(ObserverPathnameExtension)
if path.symlink?
unless path.resolved_path_exists?
2020-09-01 14:05:52 +01:00
path.uninstall_info if path.to_s.match?(Keg::INFOFILE_RX) && !dry_run?
if dry_run?
puts "Would remove (broken link): #{path}"
else
path.unlink
end
end
2020-12-01 17:04:59 +00:00
elsif path.directory? && Keg::MUST_EXIST_SUBDIRECTORIES.exclude?(path)
dirs << path
end
end
end
dirs.reverse_each do |d|
if dry_run? && d.children.empty?
puts "Would remove (empty directory): #{d}"
else
d.rmdir_if_possible
end
end
return if dry_run?
return if ObserverPathnameExtension.total.zero?
n, d = ObserverPathnameExtension.counts
print "Pruned #{n} symbolic links "
print "and #{d} directories " if d.positive?
puts "from #{HOMEBREW_PREFIX}"
end
def self.autoremove(dry_run: false)
require "utils/autoremove"
require "cask/caskroom"
# If this runs after install, uninstall, reinstall or upgrade,
# the cache of installed formulae may no longer be valid.
Formula.clear_cache unless dry_run
formulae = Formula.installed
# Remove formulae listed in HOMEBREW_NO_CLEANUP_FORMULAE and their dependencies.
if Homebrew::EnvConfig.no_cleanup_formulae.present?
formulae -= formulae.select(&method(:skip_clean_formula?))
.flat_map { |f| [f, *f.runtime_formula_dependencies] }
end
casks = Cask::Caskroom.casks
removable_formulae = Utils::Autoremove.removable_formulae(formulae, casks)
return if removable_formulae.blank?
formulae_names = removable_formulae.map(&:full_name).sort
verb = dry_run ? "Would autoremove" : "Autoremoving"
oh1 "#{verb} #{formulae_names.count} unneeded #{"formula".pluralize(formulae_names.count)}:"
puts formulae_names.join("\n")
return if dry_run
require "uninstall"
kegs_by_rack = removable_formulae.map(&:any_installed_keg).group_by(&:rack)
Uninstall.uninstall_kegs(kegs_by_rack)
# The installed formula cache will be invalid after uninstalling.
Formula.clear_cache
end
end
end
2022-11-20 10:49:53 -08:00
require "extend/os/cleanup"