# typed: strict # frozen_string_literal: true require "diagnostic" require "diagnostic/finding" require "fileutils" require "hardware" require "development_tools" require "upgrade" require "download_queue" require "ask" require "utils/output" require "utils/topological_hash" module Homebrew # Helper module for performing (pre-)install checks. module Install extend Utils::Output::Mixin class << self sig { params(all_fatal: T::Boolean).void } def perform_preinstall_checks_once(all_fatal: false) @perform_preinstall_checks_once ||= T.let({}, T.nilable(T::Hash[T::Boolean, TrueClass])) @perform_preinstall_checks_once[all_fatal] ||= begin perform_preinstall_checks(all_fatal:) true end end sig { params(cc: T.nilable(String)).void } def check_cc_argv(cc) return unless cc opoo <<~EOS You passed `--cc=#{cc}`. #{Diagnostic::Finding.support_tier_message(tier: 3)} EOS end sig { params(all_fatal: T::Boolean).void } def perform_build_from_source_checks(all_fatal: false) Diagnostic.checks(:fatal_build_from_source_checks) Diagnostic.checks(:build_from_source_checks, fatal: all_fatal) end sig { void } def global_post_install; end sig { void } def check_prefix if (Hardware::CPU.intel? || Hardware::CPU.in_rosetta2?) && HOMEBREW_PREFIX.to_s == HOMEBREW_MACOS_ARM_DEFAULT_PREFIX if Hardware::CPU.in_rosetta2? odie <<~EOS Cannot install under Rosetta 2 in ARM default prefix (#{HOMEBREW_PREFIX})! To rerun under ARM use: arch -arm64 brew install ... To install under x86_64, install Homebrew into #{HOMEBREW_DEFAULT_PREFIX}. EOS else odie "Cannot install on Intel processor in ARM default prefix (#{HOMEBREW_PREFIX})!" end elsif Hardware::CPU.arm? && HOMEBREW_PREFIX.to_s == HOMEBREW_DEFAULT_PREFIX odie <<~EOS Cannot install in Homebrew on ARM processor in Intel default prefix (#{HOMEBREW_PREFIX})! Please create a new installation in #{HOMEBREW_MACOS_ARM_DEFAULT_PREFIX} using one of the "Alternative Installs" from: #{Formatter.url("https://docs.brew.sh/Installation")} You can migrate your previously installed formula list with: brew bundle dump EOS end end sig { params(formula: Formula, head: T::Boolean, fetch_head: T::Boolean, only_dependencies: T::Boolean, force: T::Boolean, quiet: T::Boolean, skip_link: T::Boolean, overwrite: T::Boolean).returns(T::Boolean) } def install_formula?( formula, head: false, fetch_head: false, only_dependencies: false, force: false, quiet: false, skip_link: false, overwrite: false ) # HEAD-only without --HEAD is an error if !head && formula.stable.nil? odie <<~EOS #{formula.full_name} is a HEAD-only formula. To install it, run: brew install --HEAD #{formula.full_name} EOS end # --HEAD, fail with no head defined odie "No head is defined for #{formula.full_name}" if head && formula.head.nil? installed_head_version = formula.latest_head_version if installed_head_version && !formula.head_version_outdated?(installed_head_version, fetch_head:) new_head_installed = true end prefix_installed = formula.prefix.exist? && !formula.prefix.empty? # Check if the installed formula is from a different tap if formula.any_version_installed? && (current_tap_name = formula.tap&.name.presence) && (installed_keg_tab = formula.any_installed_keg&.tab.presence) && (installed_tap_name = installed_keg_tab.tap&.name.presence) && installed_tap_name != current_tap_name odie <<~EOS #{formula.name} was installed from the #{Formatter.identifier(installed_tap_name)} tap but you are trying to install it from the #{Formatter.identifier(current_tap_name)} tap. Formulae with the same name from different taps cannot be installed at the same time. To install this version, you must first uninstall the existing formula: brew uninstall #{formula.name} Then you can install the desired version: brew install #{formula.full_name} EOS end if formula.keg_only? && formula.any_version_installed? && formula.optlinked? && !force # keg-only install is only possible when no other version is # linked to opt, because installing without any warnings can break # dependencies. Therefore before performing other checks we need to be # sure the --force switch is passed. if formula.outdated? if !Homebrew::EnvConfig.no_install_upgrade? && !formula.pinned? name = formula.name version = formula.linked_version puts "#{name} #{version} is already installed but outdated (so it will be upgraded)." return true end unpin_cmd_if_needed = ("brew unpin #{formula.full_name} && " if formula.pinned?) optlinked_version = Keg.for(formula.opt_prefix).version onoe <<~EOS #{formula.full_name} #{optlinked_version} is already installed. To upgrade to #{formula.version}, run: #{unpin_cmd_if_needed}brew upgrade #{formula.full_name} EOS elsif only_dependencies return true elsif !quiet opoo_without_github_actions_annotation <<~EOS #{formula.full_name} #{formula.pkg_version} is already installed and up-to-date. To reinstall #{formula.pkg_version}, run: brew reinstall #{formula.name} EOS end elsif (head && new_head_installed) || prefix_installed # After we're sure the --force switch was passed for linking to opt # keg-only we need to be sure that the version we're attempting to # install is not already installed. installed_version = if head formula.latest_head_version else formula.pkg_version end msg = "#{formula.full_name} #{installed_version} is already installed" linked_not_equals_installed = formula.linked_version != installed_version if formula.linked? && linked_not_equals_installed msg = if quiet nil else <<~EOS #{msg}. The currently linked version is: #{formula.linked_version} EOS end elsif only_dependencies || (!formula.linked? && overwrite) msg = nil return true elsif !formula.linked? || formula.keg_only? msg = <<~EOS #{msg}, it's just not linked. To link this version, run: brew link #{formula.full_name} EOS else unless quiet opoo_without_github_actions_annotation <<~EOS #{msg} and up-to-date. To reinstall #{formula.pkg_version}, run: brew reinstall #{formula.name} EOS end msg = nil end opoo msg if msg elsif !formula.any_version_installed? && (old_formula = formula.old_installed_formulae.first) msg = "#{old_formula.full_name} #{old_formula.any_installed_version} already installed" msg = if !old_formula.linked? && !old_formula.keg_only? <<~EOS #{msg}, it's just not linked. To link this version, run: brew link #{old_formula.full_name} EOS elsif quiet nil else "#{msg}." end opoo msg if msg elsif formula.migration_needed? && !force # Check if the formula we try to install is the same as installed # but not migrated one. If --force is passed then install anyway. opoo <<~EOS #{formula.oldnames_to_migrate.first} is already installed, it's just not migrated. To migrate this formula, run: brew migrate #{formula} Or to force-install it, run: brew install #{formula} --force EOS elsif formula.linked? message = "#{formula.name} #{formula.linked_version} is already installed" if formula.outdated? && !head if !Homebrew::EnvConfig.no_install_upgrade? && !formula.pinned? puts "#{message} but outdated (so it will be upgraded)." return true end unpin_cmd_if_needed = ("brew unpin #{formula.full_name} && " if formula.pinned?) onoe <<~EOS #{message} To upgrade to #{formula.pkg_version}, run: #{unpin_cmd_if_needed}brew upgrade #{formula.full_name} EOS elsif only_dependencies || skip_link return true else onoe <<~EOS #{message} To install #{formula.pkg_version}, first run: brew unlink #{formula.name} EOS end else # If none of the above is true and the formula is linked, then # FormulaInstaller will handle this case. return true end # Even if we don't install this formula mark it as no longer just # installed as a dependency. return false unless formula.opt_prefix.directory? keg = Keg.new(formula.opt_prefix.resolved_path) tab = keg.tab unless tab.installed_on_request tab.installed_on_request = true tab.write end false end sig { params(formulae_to_install: T::Array[Formula], installed_on_request: T::Boolean, build_bottle: T::Boolean, force_bottle: T::Boolean, bottle_arch: T.nilable(String), ignore_deps: T::Boolean, only_deps: T::Boolean, include_test_formulae: T::Array[String], build_from_source_formulae: T::Array[String], cc: T.nilable(String), git: T::Boolean, interactive: T::Boolean, keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean, overwrite: T::Boolean, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean, dry_run: T::Boolean, skip_post_install: T::Boolean, skip_link: T::Boolean).returns(T::Array[FormulaInstaller]) } def formula_installers( formulae_to_install, installed_on_request: true, build_bottle: false, force_bottle: false, bottle_arch: nil, ignore_deps: false, only_deps: false, include_test_formulae: [], build_from_source_formulae: [], cc: nil, git: false, interactive: false, keep_tmp: false, debug_symbols: false, force: false, overwrite: false, debug: false, quiet: false, verbose: false, dry_run: false, skip_post_install: false, skip_link: false ) formulae_to_install.filter_map do |formula| Migrator.migrate_if_needed(formula, force:, dry_run:) build_options = formula.build FormulaInstaller.new( formula, options: build_options.used_options, installed_on_request:, build_bottle:, force_bottle:, bottle_arch:, ignore_deps:, only_deps:, include_test_formulae:, build_from_source_formulae:, cc:, git:, interactive:, keep_tmp:, debug_symbols:, force:, overwrite:, debug:, quiet:, verbose:, skip_post_install:, skip_link:, ) end end sig { params( formula_installers: T::Array[FormulaInstaller], download_queue: T.nilable(Homebrew::DownloadQueue), fetch_after_enqueue: T::Boolean, shutdown_download_queue: T::Boolean, show_downloads_heading: T::Boolean, ).returns(T::Array[FormulaInstaller]) } def fetch_formulae( formula_installers, download_queue: nil, fetch_after_enqueue: true, shutdown_download_queue: true, show_downloads_heading: true ) return formula_installers if formula_installers.empty? download_queue = T.let(download_queue || Homebrew::DownloadQueue.new(pour: true), Homebrew::DownloadQueue) begin valid_formula_installers = prelude_fetch_formulae(formula_installers, download_queue:) # Wait on just the bottle manifests dependency resolution needs so # in-flight bottles are only reported under the downloads heading. download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests", allow_failures: true) [:prelude, :enqueue_fetch].each do |step| valid_formula_installers = select_formula_installers(valid_formula_installers, step:) next if step == :enqueue_fetch && !fetch_after_enqueue if step == :prelude download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests", allow_failures: true) else heading = if show_downloads_heading combined_fetch_downloads_heading(formula_names: valid_formula_installers.map { |fi| fi.formula.name }) end download_queue.fetch(heading:) end end ensure download_queue.shutdown if shutdown_download_queue end valid_formula_installers end sig { params( formula_installers: T::Array[FormulaInstaller], download_queue: Homebrew::DownloadQueue, metadata_only: T::Boolean, ).returns(T::Array[FormulaInstaller]) } def prelude_fetch_formulae(formula_installers, download_queue:, metadata_only: false) formula_installers.each do |fi| fi.download_queue = download_queue end # Only pass the keyword when limiting the fetch so mocks and # overrides expecting the historical no-argument call keep working. action = ->(fi) { metadata_only ? fi.prelude_fetch(metadata_only: true) : fi.prelude_fetch } select_formula_installers(formula_installers, action:) end sig { params( formula_installers: T::Array[FormulaInstaller], step: T.nilable(Symbol), action: T.nilable(T.proc.params(formula_installer: FormulaInstaller).void), ).returns(T::Array[FormulaInstaller]) } def select_formula_installers(formula_installers, step: nil, action: nil) formula_installers.select do |fi| if action action.call(fi) elsif step fi.public_send(step) end true rescue CannotInstallFormulaError => e ofail e.message false rescue => e ofail "#{fi.formula}: #{e}" false end end sig { params(formula_installers: T::Array[FormulaInstaller], download_queue: Homebrew::DownloadQueue).returns(T::Array[FormulaInstaller]) } def enqueue_formulae(formula_installers, download_queue:) fetch_formulae( formula_installers, download_queue:, fetch_after_enqueue: false, shutdown_download_queue: false, show_downloads_heading: false, ) end sig { params(formula_names: T::Array[String], cask_names: T::Array[String]).returns(T.nilable(String)) } def combined_fetch_downloads_heading(formula_names: [], cask_names: []) combined_fetch_targets = formula_names.map { |name| Formatter.identifier(name) } + cask_names.map { |name| Formatter.identifier(name) } return if combined_fetch_targets.empty? "Fetching downloads for: #{combined_fetch_targets.to_sentence}" end sig { params(cask_installers: T::Array[T.untyped], download_queue: Homebrew::DownloadQueue).void } def enqueue_cask_installers(cask_installers, download_queue:) source_downloads = [] valid_cask_installers = cask_installers.select do |cask_installer| if cask_installer.source_download_requires_pre_fetch? && (source_download = cask_installer.prelude_fetch_download) source_downloads << source_download end true rescue => e ofail "#{cask_installer.cask}: #{e}" false end if source_downloads.any? source_downloads.each { |source_download| download_queue.enqueue(source_download) } download_queue.fetch(only: Cask::Download, heading: "Downloading Cask files") end valid_cask_installers.each do |cask_installer| cask_installer.enqueue_downloads rescue => e ofail "#{cask_installer.cask}: #{e}" end end sig { params(formula_installers: T::Array[FormulaInstaller], installed_on_request: T::Boolean, build_bottle: T::Boolean, force_bottle: T::Boolean, bottle_arch: T.nilable(String), ignore_deps: T::Boolean, only_deps: T::Boolean, include_test_formulae: T::Array[String], build_from_source_formulae: T::Array[String], cc: T.nilable(String), git: T::Boolean, interactive: T::Boolean, keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean, overwrite: T::Boolean, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean, dry_run: T::Boolean, dry_run_action: String, skip_post_install: T::Boolean, skip_link: T::Boolean).void } def install_formulae( formula_installers, installed_on_request: true, build_bottle: false, force_bottle: false, bottle_arch: nil, ignore_deps: false, only_deps: false, include_test_formulae: [], build_from_source_formulae: [], cc: nil, git: false, interactive: false, keep_tmp: false, debug_symbols: false, force: false, overwrite: false, debug: false, quiet: false, verbose: false, dry_run: false, dry_run_action: "install", skip_post_install: false, skip_link: false ) formulae_names_to_install = formula_installers.map { |fi| fi.formula.name } return if formulae_names_to_install.empty? if dry_run ohai "Would #{dry_run_action} #{Utils.pluralize("formula", formulae_names_to_install.count, include_count: true)}:" puts formulae_names_to_install.join(" ") formula_installers.each do |fi| next if fi.ignore_deps? print_dry_run_dependencies(fi.formula, fi.compute_dependencies, &:name) end return end formula_installers.each do |fi| formula = fi.formula upgrade = formula.linked? && formula.outdated? && !formula.head? && !Homebrew::EnvConfig.no_install_upgrade? install_formula(fi, upgrade:) Cleanup.install_formula_clean!(formula) end end sig { params( formula: Formula, dependencies: T::Array[Dependency], skip_formula_names: T::Array[String], _block: T.proc.params(arg0: Formula).returns(String), ).void } def print_dry_run_dependencies(formula, dependencies, skip_formula_names: [], &_block) return if dependencies.empty? entries = dependencies.filter_map do |dep| dependency = dep.to_formula next if skip_formula_names.include?(dependency.full_name) [dependency.any_version_installed?, yield(dependency)] end upgrade, install = entries.partition(&:first) { install:, upgrade: }.each do |verb, group| next if group.empty? ohai "Would #{verb} #{Utils.pluralize("dependency", group.count, include_count: true)} " \ "for #{formula.name}:" puts Upgrade.format_upgrade_summary(group.map(&:last)) end end # If asking the user is enabled, show dry-run information. sig { params( formulae_installer: T::Array[FormulaInstaller], dependants: Homebrew::Upgrade::Dependents, flags: T::Array[String], force_bottle: T::Boolean, build_from_source_formulae: T::Array[String], interactive: T::Boolean, keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean, prompt: T::Boolean, action: String, ).void } def ask_formulae(formulae_installer, dependants, flags: [], force_bottle: false, build_from_source_formulae: [], interactive: false, keep_tmp: false, debug_symbols: false, force: false, debug: false, quiet: false, verbose: false, prompt: true, action: "installation") return if formulae_installer.empty? formula_names = formulae_installer.map { |formula_installer| formula_installer.formula.full_name } install_formulae(formulae_installer, dry_run: true, dry_run_action: dry_run_action(action)) Upgrade.upgrade_dependents( Homebrew::Upgrade::Dependents.new( upgradeable: dependants.upgradeable.dup, pinned: dependants.pinned.dup, skipped: dependants.skipped.dup, ), formulae_installer.map(&:formula), flags:, dry_run: true, force_bottle:, build_from_source_formulae:, interactive:, keep_tmp:, debug_symbols:, force:, debug:, quiet:, verbose:, ) ask_input(action:) if prompt && ask_prompt_needed?( planned_names: formula_names, requested_names: formula_names, force: formulae_ask_prompt_needed?(formulae_installer, dependants), ) end sig { params( casks: T::Array[Cask::Cask], action: String, prompt: T::Boolean, skip_cask_deps: T::Boolean, ).void } def ask_casks(casks, action: "installation", prompt: true, skip_cask_deps: false) return if casks.empty? cask_names = casks.map(&:full_name) dependency_names = print_dry_run_casks(casks, action: dry_run_action(action), skip_cask_deps:) ask_input(action:) if prompt && ask_prompt_needed?( planned_names: cask_names + dependency_names, requested_names: cask_names, ) end sig { params( casks: T::Array[Cask::Cask], action: String, skip_cask_deps: T::Boolean, include_installed: T::Boolean, ).returns(T::Array[String]) } def print_dry_run_casks(casks, action: "install", skip_cask_deps: false, include_installed: true) if (casks_to_print = (include_installed ? casks : casks.reject(&:installed?)).presence) ohai "Would #{action} #{::Utils.pluralize("cask", casks_to_print.count, include_count: true)}:" puts casks_to_print.map(&:full_name).join(" ") end casks.flat_map do |cask| dep_names = T.let([], T::Array[String]) unless skip_cask_deps dep_names.concat( ::Utils::TopologicalHash.graph_package_dependencies([cask]).tsort.grep(Cask::Cask).filter_map do |dep| next if dep.full_name == cask.full_name next if dep.installed? dep.full_name end, ) end dep_names.concat( CaskDependent.new(cask) .runtime_dependencies(read_from_tab: false, undeclared: false) .reject(&:installed?) .map(&:name), ) dep_names.uniq! next [] if dep_names.blank? ohai "Would install #{::Utils.pluralize("dependency", dep_names.count, include_count: true)} " \ "for #{cask.full_name}:" puts dep_names.join(" ") dep_names end end sig { params( planned_names: T::Array[String], requested_names: T::Array[String], force: T::Boolean, named: T::Boolean, ).returns(T::Boolean) } def ask_prompt_needed?(planned_names:, requested_names:, force: false, named: true) return false if planned_names.empty? return true if force return true unless named planned_names.any? { |planned_name| requested_names.exclude?(planned_name) } end sig { params( formulae_installer: T::Array[FormulaInstaller], dependants: Homebrew::Upgrade::Dependents, ).returns(T::Boolean) } def formulae_ask_prompt_needed?(formulae_installer, dependants) formulae_installer.any? do |formula_installer| !formula_installer.ignore_deps? && formula_installer.compute_dependencies.present? end || dependants.upgradeable.present? end sig { params(formula_installer: FormulaInstaller, upgrade: T::Boolean).void } def install_formula(formula_installer, upgrade:) formula = formula_installer.formula formula_installer.check_installation_already_attempted if upgrade Upgrade.print_upgrade_message(formula, formula_installer.options) kegs = Upgrade.outdated_kegs(formula) linked_kegs = kegs.select(&:linked?) else formula.print_tap_action end # first we unlink the currently active keg for this formula otherwise it is # possible for the existing build to interfere with the build we are about to # do! Seriously, it happens! kegs.each(&:unlink) if kegs.present? formula_installer.install formula_installer.finish rescue FormulaInstallationAlreadyAttemptedError # We already attempted to upgrade f as part of the dependency tree of # another formula. In that case, don't generate an error, just move on. nil ensure # restore previous installation state if build failed begin linked_kegs&.each(&:link) unless formula&.latest_version_installed? rescue nil end end sig { params(action: String).void } def ask(action: "installation") ask_input(action:) end sig { params(all_fatal: T::Boolean).void } def perform_preinstall_checks(all_fatal: false) check_prefix check_cpu attempt_directory_creation Diagnostic.checks(:supported_configuration_checks, fatal: all_fatal) Diagnostic.checks(:preinstall_checks, fatal: false) Diagnostic.checks(:fatal_preinstall_checks) end private sig { params(action: String).returns(String) } def dry_run_action(action) case action when "reinstallation" "reinstall" when "upgrade" "upgrade" else "install" end end sig { params(formula: Formula).returns(T::Array[Keg]) } def outdated_kegs(formula) [formula, *formula.old_installed_formulae].map(&:linked_keg) .select(&:directory?) .map { |k| Keg.new(k.resolved_path) } end sig { void } def attempt_directory_creation Keg.must_exist_directories.each do |dir| FileUtils.mkdir_p(dir) unless dir.exist? rescue nil end end sig { void } def check_cpu return unless Hardware::CPU.ppc? odie <<~EOS Sorry, Homebrew does not support your computer's CPU architecture! For PowerPC Mac (PPC32/PPC64BE) support, see: #{Formatter.url("https://github.com/mistydemeo/tigerbrew")} EOS end sig { params(action: String).void } def ask_input(action: "installation") Homebrew::Ask.confirm?(action:) nil end # Compute the total sizes (download and installed) for the given formulae. sig { params(sized_formulae: T::Array[Formula], debug: T::Boolean).returns(T::Hash[Symbol, Integer]) } def compute_total_sizes(sized_formulae, debug: false) total_download_size = 0 total_installed_size = 0 sized_formulae.each do |formula| bottle = formula.bottle next unless bottle # Fetch additional bottle metadata (if necessary). bottle.fetch_tab(quiet: !debug) total_download_size += bottle.bottle_size.to_i if bottle.bottle_size total_installed_size += bottle.installed_size.to_i if bottle.installed_size end { download: total_download_size, installed: total_installed_size } end sig { params(formulae_installer: T::Array[FormulaInstaller], dependants: Homebrew::Upgrade::Dependents).returns(T::Array[Formula]) } def collect_dependencies(formulae_installer, dependants) formulae_dependencies = formulae_installer.flat_map do |f| [f.formula, f.compute_dependencies.flatten.grep(Dependency).flat_map(&:to_formula)] end.flatten.uniq formulae_dependencies.concat(dependants.upgradeable) if dependants.upgradeable formulae_dependencies.uniq end end end end require "extend/os/install"