2025-03-18 17:38:37 +00:00
|
|
|
# typed: true # rubocop:todo Sorbet/StrictSigil
|
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "utils/formatter"
|
|
|
|
|
|
|
|
module Homebrew
|
|
|
|
module Bundle
|
|
|
|
module Commands
|
|
|
|
# TODO: refactor into multiple modules
|
|
|
|
module Cleanup
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.reset!
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/cask_dumper"
|
2025-07-04 11:25:41 +01:00
|
|
|
require "bundle/formula_dumper"
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/tap_dumper"
|
|
|
|
require "bundle/vscode_extension_dumper"
|
|
|
|
require "bundle/brew_services"
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
@dsl = nil
|
|
|
|
@kept_casks = nil
|
|
|
|
@kept_formulae = nil
|
|
|
|
Homebrew::Bundle::CaskDumper.reset!
|
2025-07-04 11:25:41 +01:00
|
|
|
Homebrew::Bundle::FormulaDumper.reset!
|
2025-03-18 17:38:37 +00:00
|
|
|
Homebrew::Bundle::TapDumper.reset!
|
|
|
|
Homebrew::Bundle::VscodeExtensionDumper.reset!
|
|
|
|
Homebrew::Bundle::BrewServices.reset!
|
|
|
|
end
|
|
|
|
|
2025-05-23 06:16:22 +01:00
|
|
|
def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
|
2025-07-04 11:25:41 +01:00
|
|
|
formulae: true, casks: true, taps: true, vscode: true)
|
2025-03-18 17:38:37 +00:00
|
|
|
@dsl ||= dsl
|
|
|
|
|
2025-05-23 06:16:22 +01:00
|
|
|
casks = casks ? casks_to_uninstall(global:, file:) : []
|
2025-07-04 11:25:41 +01:00
|
|
|
formulae = formulae ? formulae_to_uninstall(global:, file:) : []
|
2025-05-23 06:16:22 +01:00
|
|
|
taps = taps ? taps_to_untap(global:, file:) : []
|
|
|
|
vscode_extensions = vscode ? vscode_extensions_to_uninstall(global:, file:) : []
|
2025-03-18 17:38:37 +00:00
|
|
|
if force
|
|
|
|
if casks.any?
|
|
|
|
args = zap ? ["--zap"] : []
|
|
|
|
Kernel.system HOMEBREW_BREW_FILE, "uninstall", "--cask", *args, "--force", *casks
|
2025-07-12 02:06:37 +08:00
|
|
|
puts "Uninstalled #{casks.size} cask#{"s" if casks.size != 1}"
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
if formulae.any?
|
|
|
|
Kernel.system HOMEBREW_BREW_FILE, "uninstall", "--formula", "--force", *formulae
|
2025-07-12 02:06:37 +08:00
|
|
|
puts "Uninstalled #{formulae.size} formula#{"e" if formulae.size != 1}"
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
Kernel.system HOMEBREW_BREW_FILE, "untap", *taps if taps.any?
|
|
|
|
|
|
|
|
Bundle.exchange_uid_if_needed! do
|
|
|
|
vscode_extensions.each do |extension|
|
2025-04-02 17:15:32 +01:00
|
|
|
Kernel.system(T.must(Bundle.which_vscode).to_s, "--uninstall-extension", extension)
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup")
|
|
|
|
puts cleanup unless cleanup.empty?
|
|
|
|
else
|
|
|
|
would_uninstall = false
|
|
|
|
|
|
|
|
if casks.any?
|
|
|
|
puts "Would uninstall casks:"
|
|
|
|
puts Formatter.columns casks
|
|
|
|
would_uninstall = true
|
|
|
|
end
|
|
|
|
|
|
|
|
if formulae.any?
|
|
|
|
puts "Would uninstall formulae:"
|
|
|
|
puts Formatter.columns formulae
|
|
|
|
would_uninstall = true
|
|
|
|
end
|
|
|
|
|
|
|
|
if taps.any?
|
|
|
|
puts "Would untap:"
|
|
|
|
puts Formatter.columns taps
|
|
|
|
would_uninstall = true
|
|
|
|
end
|
|
|
|
|
|
|
|
if vscode_extensions.any?
|
|
|
|
puts "Would uninstall VSCode extensions:"
|
|
|
|
puts Formatter.columns vscode_extensions
|
|
|
|
would_uninstall = true
|
|
|
|
end
|
|
|
|
|
|
|
|
cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup", "--dry-run")
|
|
|
|
unless cleanup.empty?
|
|
|
|
puts "Would `brew cleanup`:"
|
|
|
|
puts cleanup
|
|
|
|
end
|
|
|
|
|
|
|
|
puts "Run `brew bundle cleanup --force` to make these changes." if would_uninstall || !cleanup.empty?
|
|
|
|
exit 1 if would_uninstall
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.casks_to_uninstall(global: false, file: nil)
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/cask_dumper"
|
2025-03-18 17:38:37 +00:00
|
|
|
Homebrew::Bundle::CaskDumper.cask_names - kept_casks(global:, file:)
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.formulae_to_uninstall(global: false, file: nil)
|
2025-03-18 17:38:37 +00:00
|
|
|
kept_formulae = self.kept_formulae(global:, file:)
|
|
|
|
|
2025-07-04 11:25:41 +01:00
|
|
|
require "bundle/formula_dumper"
|
|
|
|
require "bundle/formula_installer"
|
|
|
|
current_formulae = Homebrew::Bundle::FormulaDumper.formulae
|
2025-03-18 17:38:37 +00:00
|
|
|
current_formulae.reject! do |f|
|
2025-07-04 11:25:41 +01:00
|
|
|
Homebrew::Bundle::FormulaInstaller.formula_in_array?(f[:full_name], kept_formulae)
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
2025-04-01 15:12:12 +01:00
|
|
|
|
|
|
|
# Don't try to uninstall formulae with keepme references
|
|
|
|
current_formulae.reject! do |f|
|
|
|
|
Formula[f[:full_name]].installed_kegs.any? do |keg|
|
|
|
|
keg.keepme_refs.present?
|
|
|
|
end
|
|
|
|
end
|
2025-03-18 17:38:37 +00:00
|
|
|
current_formulae.map { |f| f[:full_name] }
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
private_class_method def self.kept_formulae(global: false, file: nil)
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brewfile"
|
2025-07-04 11:25:41 +01:00
|
|
|
require "bundle/formula_dumper"
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/cask_dumper"
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
@kept_formulae ||= begin
|
|
|
|
@dsl ||= Brewfile.read(global:, file:)
|
|
|
|
|
|
|
|
kept_formulae = @dsl.entries.select { |e| e.type == :brew }.map(&:name)
|
|
|
|
kept_formulae += Homebrew::Bundle::CaskDumper.formula_dependencies(kept_casks)
|
|
|
|
kept_formulae.map! do |f|
|
2025-07-10 08:05:36 +00:00
|
|
|
Homebrew::Bundle::FormulaDumper.formula_aliases.fetch(
|
|
|
|
f,
|
|
|
|
Homebrew::Bundle::FormulaDumper.formula_oldnames.fetch(f, f),
|
|
|
|
)
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
|
2025-07-04 11:25:41 +01:00
|
|
|
kept_formulae + recursive_dependencies(Homebrew::Bundle::FormulaDumper.formulae, kept_formulae)
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
private_class_method def self.kept_casks(global: false, file: nil)
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brewfile"
|
2025-03-18 17:38:37 +00:00
|
|
|
return @kept_casks if @kept_casks
|
|
|
|
|
|
|
|
@dsl ||= Brewfile.read(global:, file:)
|
2025-07-10 08:05:36 +00:00
|
|
|
kept_casks = @dsl.entries.select { |e| e.type == :cask }.flat_map(&:name)
|
|
|
|
kept_casks.map! do |c|
|
|
|
|
Homebrew::Bundle::CaskDumper.cask_oldnames.fetch(c, c)
|
|
|
|
end
|
|
|
|
@kept_casks = kept_casks
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
private_class_method def self.recursive_dependencies(current_formulae, formulae_names, top_level: true)
|
2025-03-18 17:38:37 +00:00
|
|
|
@checked_formulae_names = [] if top_level
|
|
|
|
dependencies = T.let([], T::Array[Formula])
|
|
|
|
|
|
|
|
formulae_names.each do |name|
|
|
|
|
next if @checked_formulae_names.include?(name)
|
|
|
|
|
|
|
|
formula = current_formulae.find { |f| f[:full_name] == name }
|
|
|
|
next unless formula
|
|
|
|
|
|
|
|
f_deps = formula[:dependencies]
|
|
|
|
unless formula[:poured_from_bottle?]
|
|
|
|
f_deps += formula[:build_dependencies]
|
|
|
|
f_deps.uniq!
|
|
|
|
end
|
|
|
|
next unless f_deps
|
|
|
|
next if f_deps.empty?
|
|
|
|
|
|
|
|
@checked_formulae_names << name
|
|
|
|
f_deps += recursive_dependencies(current_formulae, f_deps, top_level: false)
|
|
|
|
dependencies += f_deps
|
|
|
|
end
|
|
|
|
|
|
|
|
dependencies.uniq
|
|
|
|
end
|
|
|
|
|
2025-03-19 09:51:39 +00:00
|
|
|
IGNORED_TAPS = %w[homebrew/core].freeze
|
2025-03-18 17:38:37 +00:00
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.taps_to_untap(global: false, file: nil)
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brewfile"
|
|
|
|
require "bundle/tap_dumper"
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
@dsl ||= Brewfile.read(global:, file:)
|
|
|
|
kept_formulae = self.kept_formulae(global:, file:).filter_map(&method(:lookup_formula))
|
|
|
|
kept_taps = @dsl.entries.select { |e| e.type == :tap }.map(&:name)
|
|
|
|
kept_taps += kept_formulae.filter_map(&:tap).map(&:name)
|
|
|
|
current_taps = Homebrew::Bundle::TapDumper.tap_names
|
|
|
|
current_taps - kept_taps - IGNORED_TAPS
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.lookup_formula(formula)
|
2025-03-18 17:38:37 +00:00
|
|
|
Formulary.factory(formula)
|
|
|
|
rescue TapFormulaUnavailableError
|
|
|
|
# ignore these as an unavailable formula implies there is no tap to worry about
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.vscode_extensions_to_uninstall(global: false, file: nil)
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brewfile"
|
2025-03-18 17:38:37 +00:00
|
|
|
@dsl ||= Brewfile.read(global:, file:)
|
|
|
|
kept_extensions = @dsl.entries.select { |e| e.type == :vscode }.map { |x| x.name.downcase }
|
|
|
|
|
|
|
|
# To provide a graceful migration from `Brewfile`s that don't yet or
|
|
|
|
# don't want to use `vscode`: don't remove any extensions if we don't
|
|
|
|
# find any in the `Brewfile`.
|
|
|
|
return [].freeze if kept_extensions.empty?
|
|
|
|
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/vscode_extension_dumper"
|
2025-03-18 17:38:37 +00:00
|
|
|
current_extensions = Homebrew::Bundle::VscodeExtensionDumper.extensions
|
|
|
|
current_extensions - kept_extensions
|
|
|
|
end
|
|
|
|
|
2025-03-21 04:24:55 +00:00
|
|
|
def self.system_output_no_stderr(cmd, *args)
|
2025-03-18 17:38:37 +00:00
|
|
|
IO.popen([cmd, *args], err: :close).read
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|