# 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