# typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true module Homebrew module Bundle class FormulaInstaller def self.reset! @installed_formulae = nil @outdated_formulae = nil @pinned_formulae = nil end def self.preinstall(name, no_upgrade: false, verbose: false, **options) new(name, options).preinstall(no_upgrade:, verbose:) end def self.install(name, preinstall: true, no_upgrade: false, verbose: false, force: false, **options) new(name, options).install(preinstall:, no_upgrade:, verbose:, force:) end def initialize(name, options = {}) @full_name = name @name = name.split("/").last @args = options.fetch(:args, []).map { |arg| "--#{arg}" } @conflicts_with_arg = options.fetch(:conflicts_with, []) @restart_service = options[:restart_service] @start_service = options.fetch(:start_service, @restart_service) @link = options.fetch(:link, nil) @postinstall = options.fetch(:postinstall, nil) @version_file = options.fetch(:version_file, nil) @changed = nil end def preinstall(no_upgrade: false, verbose: false) if installed? && (self.class.no_upgrade_with_args?(no_upgrade, @name) || !upgradable?) puts "Skipping install of #{@name} formula. It is already installed." if verbose @changed = nil return false end true end def install(preinstall: true, no_upgrade: false, verbose: false, force: false) install_result = if preinstall install_change_state!(no_upgrade:, verbose:, force:) else true end result = install_result if installed? service_result = service_change_state!(verbose:) result &&= service_result link_result = link_change_state!(verbose:) result &&= link_result postinstall_result = postinstall_change_state!(verbose:) result &&= postinstall_result if result && @version_file.present? # Use the version from the environment if it hasn't changed. # Strip the revision number because it's not part of the non-Homebrew version. version = if !changed? && (env_version = Bundle.formula_versions_from_env(@name)) PkgVersion.parse(env_version).version else Formula[@full_name].version end.to_s File.write(@version_file, "#{version}\n") puts "Wrote #{@name} version #{version} to #{@version_file}" if verbose end end result end def install_change_state!(no_upgrade:, verbose:, force:) return false unless resolve_conflicts!(verbose:) if installed? upgrade!(verbose:, force:) else install!(verbose:, force:) end end def start_service? @start_service.present? end def start_service_needed? require "bundle/brew_services" start_service? && !BrewServices.started?(@full_name) end def restart_service? @restart_service.present? end def restart_service_needed? return false unless restart_service? # Restart if `restart_service: :always`, or if the formula was installed or upgraded @restart_service.to_s == "always" || changed? end def changed? @changed.present? end def service_change_state!(verbose:) require "bundle/brew_services" file = Bundle::BrewServices.versioned_service_file(@name) if restart_service_needed? puts "Restarting #{@name} service." if verbose BrewServices.restart(@full_name, file:, verbose:) elsif start_service_needed? puts "Starting #{@name} service." if verbose BrewServices.start(@full_name, file:, verbose:) else true end end def link_change_state!(verbose: false) link_args = [] link_args << "--force" if unlinked_and_keg_only? cmd = case @link when :overwrite link_args << "--overwrite" "link" unless linked? when true "link" unless linked? when false "unlink" if linked? when nil if keg_only? "unlink" if linked? else "link" unless linked? end end if cmd.present? verb = "#{cmd}ing".capitalize with_args = " with #{link_args.join(" ")}" if link_args.present? puts "#{verb} #{@name} formula#{with_args}." if verbose return Bundle.brew(cmd, *link_args, @name, verbose:) end true end def postinstall_change_state!(verbose:) return true if @postinstall.blank? return true unless changed? puts "Running postinstall for #{@name}: #{@postinstall}" if verbose Kernel.system(@postinstall) end def self.formula_installed_and_up_to_date?(formula, no_upgrade: false) return false unless formula_installed?(formula) return true if no_upgrade_with_args?(no_upgrade, formula) !formula_upgradable?(formula) end def self.no_upgrade_with_args?(no_upgrade, formula_name) no_upgrade && Bundle.upgrade_formulae.exclude?(formula_name) end def self.formula_in_array?(formula, array) return true if array.include?(formula) return true if array.include?(formula.split("/").last) require "bundle/formula_dumper" old_names = Homebrew::Bundle::FormulaDumper.formula_oldnames old_name = old_names[formula] old_name ||= old_names[formula.split("/").last] return true if old_name && array.include?(old_name) resolved_full_name = Homebrew::Bundle::FormulaDumper.formula_aliases[formula] return false unless resolved_full_name return true if array.include?(resolved_full_name) return true if array.include?(resolved_full_name.split("/").last) false end def self.formula_installed?(formula) formula_in_array?(formula, installed_formulae) end def self.formula_upgradable?(formula) # Check local cache first and then authoritative Homebrew source. formula_in_array?(formula, upgradable_formulae) && Formula[formula].outdated? end def self.installed_formulae @installed_formulae ||= formulae.map { |f| f[:name] } end def self.upgradable_formulae outdated_formulae - pinned_formulae end def self.outdated_formulae @outdated_formulae ||= formulae.filter_map { |f| f[:name] if f[:outdated?] } end def self.pinned_formulae @pinned_formulae ||= formulae.filter_map { |f| f[:name] if f[:pinned?] } end def self.formulae require "bundle/formula_dumper" Homebrew::Bundle::FormulaDumper.formulae end private def installed? FormulaInstaller.formula_installed?(@name) end def linked? Formula[@full_name].linked? end def keg_only? Formula[@full_name].keg_only? end def unlinked_and_keg_only? !linked? && keg_only? end def upgradable? FormulaInstaller.formula_upgradable?(@name) end def conflicts_with @conflicts_with ||= begin conflicts_with = Set.new conflicts_with += @conflicts_with_arg require "bundle/formula_dumper" if (formula = Homebrew::Bundle::FormulaDumper.formulae_by_full_name(@full_name)) && (formula_conflicts_with = formula[:conflicts_with]) conflicts_with += formula_conflicts_with end conflicts_with.to_a end end def resolve_conflicts!(verbose:) conflicts_with.each do |conflict| next unless FormulaInstaller.formula_installed?(conflict) if verbose puts <<~EOS Unlinking #{conflict} formula. It is currently installed and conflicts with #{@name}. EOS end return false unless Bundle.brew("unlink", conflict, verbose:) next unless restart_service? require "bundle/brew_services" puts "Stopping #{conflict} service (if it is running)." if verbose BrewServices.stop(conflict, verbose:) end true end def install!(verbose:, force:) install_args = @args.dup install_args << "--force" << "--overwrite" if force install_args << "--skip-link" if @link == false with_args = " with #{install_args.join(" ")}" if install_args.present? puts "Installing #{@name} formula#{with_args}. It is not currently installed." if verbose unless Bundle.brew("install", "--formula", @full_name, *install_args, verbose:) @changed = nil return false end FormulaInstaller.installed_formulae << @name @changed = true true end def upgrade!(verbose:, force:) upgrade_args = [] upgrade_args << "--force" if force with_args = " with #{upgrade_args.join(" ")}" if upgrade_args.present? puts "Upgrading #{@name} formula#{with_args}. It is installed but not up-to-date." if verbose unless Bundle.brew("upgrade", "--formula", @name, *upgrade_args, verbose:) @changed = nil return false end @changed = true true end end end end