Mike McQuaid e8bfa23877
Support Cask renames when installing/dumping
This adds support for Cask old tokens used for renames of Casks.

We'll now correctly check these at installation time to avoid repeatedly
installing renamed Casks and dump them in the Brewfile. We also use this
logic to avoid cleaning up renamed Casks.
2025-07-10 08:05:36 +00:00

222 lines
8.0 KiB
Ruby

# 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
def self.reset!
require "bundle/cask_dumper"
require "bundle/formula_dumper"
require "bundle/tap_dumper"
require "bundle/vscode_extension_dumper"
require "bundle/brew_services"
@dsl = nil
@kept_casks = nil
@kept_formulae = nil
Homebrew::Bundle::CaskDumper.reset!
Homebrew::Bundle::FormulaDumper.reset!
Homebrew::Bundle::TapDumper.reset!
Homebrew::Bundle::VscodeExtensionDumper.reset!
Homebrew::Bundle::BrewServices.reset!
end
def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
formulae: true, casks: true, taps: true, vscode: true)
@dsl ||= dsl
casks = casks ? casks_to_uninstall(global:, file:) : []
formulae = formulae ? formulae_to_uninstall(global:, file:) : []
taps = taps ? taps_to_untap(global:, file:) : []
vscode_extensions = vscode ? vscode_extensions_to_uninstall(global:, file:) : []
if force
if casks.any?
args = zap ? ["--zap"] : []
Kernel.system HOMEBREW_BREW_FILE, "uninstall", "--cask", *args, "--force", *casks
puts "Uninstalled #{casks.size} cask#{(casks.size == 1) ? "" : "s"}"
end
if formulae.any?
Kernel.system HOMEBREW_BREW_FILE, "uninstall", "--formula", "--force", *formulae
puts "Uninstalled #{formulae.size} formula#{(formulae.size == 1) ? "" : "e"}"
end
Kernel.system HOMEBREW_BREW_FILE, "untap", *taps if taps.any?
Bundle.exchange_uid_if_needed! do
vscode_extensions.each do |extension|
Kernel.system(T.must(Bundle.which_vscode).to_s, "--uninstall-extension", extension)
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
def self.casks_to_uninstall(global: false, file: nil)
require "bundle/cask_dumper"
Homebrew::Bundle::CaskDumper.cask_names - kept_casks(global:, file:)
end
def self.formulae_to_uninstall(global: false, file: nil)
kept_formulae = self.kept_formulae(global:, file:)
require "bundle/formula_dumper"
require "bundle/formula_installer"
current_formulae = Homebrew::Bundle::FormulaDumper.formulae
current_formulae.reject! do |f|
Homebrew::Bundle::FormulaInstaller.formula_in_array?(f[:full_name], kept_formulae)
end
# 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
current_formulae.map { |f| f[:full_name] }
end
private_class_method def self.kept_formulae(global: false, file: nil)
require "bundle/brewfile"
require "bundle/formula_dumper"
require "bundle/cask_dumper"
@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|
Homebrew::Bundle::FormulaDumper.formula_aliases.fetch(
f,
Homebrew::Bundle::FormulaDumper.formula_oldnames.fetch(f, f),
)
end
kept_formulae + recursive_dependencies(Homebrew::Bundle::FormulaDumper.formulae, kept_formulae)
end
end
private_class_method def self.kept_casks(global: false, file: nil)
require "bundle/brewfile"
return @kept_casks if @kept_casks
@dsl ||= Brewfile.read(global:, file:)
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
end
private_class_method def self.recursive_dependencies(current_formulae, formulae_names, top_level: true)
@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
IGNORED_TAPS = %w[homebrew/core].freeze
def self.taps_to_untap(global: false, file: nil)
require "bundle/brewfile"
require "bundle/tap_dumper"
@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
def self.lookup_formula(formula)
Formulary.factory(formula)
rescue TapFormulaUnavailableError
# ignore these as an unavailable formula implies there is no tap to worry about
nil
end
def self.vscode_extensions_to_uninstall(global: false, file: nil)
require "bundle/brewfile"
@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?
require "bundle/vscode_extension_dumper"
current_extensions = Homebrew::Bundle::VscodeExtensionDumper.extensions
current_extensions - kept_extensions
end
def self.system_output_no_stderr(cmd, *args)
IO.popen([cmd, *args], err: :close).read
end
end
end
end
end