| 
									
										
										
										
											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-14 14:48:08 +01: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-14 14:48:08 +01: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:) | 
					
						
							| 
									
										
										
										
											2025-09-07 13:20:45 -07:00
										 |  |  |           kept_formulae = self.kept_formulae(global:, file:).filter_map(&method(:lookup_formula)) | 
					
						
							| 
									
										
										
										
											2025-03-18 17:38:37 +00:00
										 |  |  |           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 |