mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
327 lines
10 KiB
Ruby
327 lines
10 KiB
Ruby
# 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
|
|
|
|
|
|
# Ending with a `!` is more appropriate here given that this affects the state of the user's system.
|
|
# rubocop:disable Naming/PredicateMethod
|
|
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
|
|
# rubocop:enable Naming/PredicateMethod
|
|
|
|
# Ending with a `!` is more appropriate here given that this affects the state of the user's system.
|
|
# rubocop:disable Naming/PredicateMethod
|
|
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
|
|
# rubocop:enable Naming/PredicateMethod
|
|
|
|
# Ending with a `!` is more appropriate here given that this affects the state of the user's system.
|
|
# rubocop:disable Naming/PredicateMethod
|
|
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
|
|
# rubocop:enable Naming/PredicateMethod
|
|
end
|
|
end
|
|
end
|