brew/Library/Homebrew/install.rb

605 lines
22 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "diagnostic"
require "fileutils"
require "hardware"
require "development_tools"
require "upgrade"
require "download_queue"
module Homebrew
2020-08-17 18:39:55 +02:00
# Helper module for performing (pre-)install checks.
module Install
class << self
sig { params(all_fatal: T::Boolean).void }
def perform_preinstall_checks_once(all_fatal: false)
@perform_preinstall_checks_once ||= {}
@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
@checks ||= Diagnostic::Checks.new
opoo <<~EOS
You passed `--cc=#{cc}`.
#{@checks.support_tier_message(tier: 3)}
EOS
2023-04-17 10:59:47 -07:00
end
2020-08-17 18:39:55 +02:00
sig { params(all_fatal: T::Boolean).void }
2023-04-17 10:59:47 -07:00
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 }
2023-04-17 10:59:47 -07:00
def global_post_install; end
sig { void }
2023-04-17 10:59:47 -07:00
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
2023-04-17 10:59:47 -07:00
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)
}
2023-04-17 10:59:47 -07:00
def install_formula?(
formula,
head: false,
fetch_head: false,
only_dependencies: false,
force: false,
quiet: false,
skip_link: false,
overwrite: false
2023-04-17 10:59:47 -07:00
)
2025-08-15 22:33:23 -04:00
# HEAD-only without --HEAD is an error
2023-04-17 10:59:47 -07:00
if !head && formula.stable.nil?
odie <<~EOS
2025-08-15 22:33:23 -04:00
#{formula.full_name} is a HEAD-only formula.
2023-04-17 10:59:47 -07:00
To install it, run:
brew install --HEAD #{formula.full_name}
2021-06-10 10:50:05 -04:00
EOS
end
2023-04-17 10:59:47 -07:00
# --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 &&
2024-03-07 16:20:20 +00:00
!formula.head_version_outdated?(installed_head_version, fetch_head:)
2023-04-17 10:59:47 -07:00
new_head_installed = true
2021-06-10 10:50:05 -04:00
end
2023-04-17 10:59:47 -07:00
prefix_installed = formula.prefix.exist? && !formula.prefix.children.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
2023-04-17 10:59:47 -07:00
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.
2023-04-17 10:59:47 -07:00
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 <<~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
2023-04-17 10:59:47 -07:00
# keg-only we need to be sure that the version we're attempting to
# install is not already installed.
2021-06-10 10:50:05 -04:00
2023-04-17 10:59:47 -07:00
installed_version = if head
formula.latest_head_version
2021-06-10 10:50:05 -04:00
else
2023-04-17 10:59:47 -07:00
formula.pkg_version
2021-06-10 10:50:05 -04:00
end
2023-04-17 10:59:47 -07:00
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)
2023-04-17 10:59:47 -07:00
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}
EOS
2021-06-10 10:50:05 -04:00
else
2023-04-17 10:59:47 -07:00
msg = if quiet
nil
else
<<~EOS
#{msg} and up-to-date.
To reinstall #{formula.pkg_version}, run:
brew reinstall #{formula.name}
EOS
end
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?
2021-06-10 10:50:05 -04:00
<<~EOS
2023-04-17 10:59:47 -07:00
#{msg}, it's just not linked.
To link this version, run:
brew link #{old_formula.full_name}
2021-06-10 10:50:05 -04:00
EOS
2023-04-17 10:59:47 -07:00
elsif quiet
nil
else
"#{msg}."
2021-06-10 10:50:05 -04:00
end
2023-04-17 10:59:47 -07:00
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
2023-04-27 04:09:28 +01:00
#{formula.oldnames_to_migrate.first} is already installed, it's just not migrated.
2023-04-17 10:59:47 -07:00
To migrate this formula, run:
brew migrate #{formula}
Or to force-install it, run:
brew install #{formula} --force
2021-06-10 10:50:05 -04:00
EOS
2023-04-17 10:59:47 -07:00
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
2023-04-17 10:59:47 -07:00
else
onoe <<~EOS
#{message}
To install #{formula.pkg_version}, first run:
brew unlink #{formula.name}
EOS
end
else
2023-04-17 10:59:47 -07:00
# If none of the above is true and the formula is linked, then
# FormulaInstaller will handle this case.
return true
end
2021-06-10 10:50:05 -04:00
2023-04-17 10:59:47 -07:00
# 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?
2021-06-10 10:50:05 -04:00
2023-04-17 10:59:47 -07:00
keg = Keg.new(formula.opt_prefix.resolved_path)
tab = keg.tab
2023-04-17 10:59:47 -07:00
unless tab.installed_on_request
tab.installed_on_request = true
tab.write
end
2021-06-10 10:50:05 -04:00
2023-04-17 10:59:47 -07:00
false
end
2021-06-10 10:50:05 -04:00
sig {
params(formulae_to_install: T::Array[Formula], installed_on_request: T::Boolean,
installed_as_dependency: 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(
2023-04-17 10:59:47 -07:00
formulae_to_install,
installed_on_request: true,
installed_as_dependency: false,
2023-04-17 10:59:47 -07:00
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
2023-04-17 10:59:47 -07:00
)
2025-06-08 12:34:24 -04:00
formulae_to_install.filter_map do |formula|
2024-03-07 16:20:20 +00:00
Migrator.migrate_if_needed(formula, force:, dry_run:)
2023-04-17 10:59:47 -07:00
build_options = formula.build
2025-06-08 12:34:24 -04:00
FormulaInstaller.new(
2023-04-17 10:59:47 -07:00
formula,
options: build_options.used_options,
installed_on_request:,
installed_as_dependency:,
2024-03-07 16:20:20 +00:00
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:,
2023-04-17 10:59:47 -07:00
)
end
2025-06-08 12:34:24 -04:00
end
sig { params(formula_installers: T::Array[FormulaInstaller]).returns(T::Array[FormulaInstaller]) }
def fetch_formulae(formula_installers)
formulae_names_to_install = formula_installers.map { |fi| fi.formula.name }
return formula_installers if formulae_names_to_install.empty?
formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence
oh1 "Fetching downloads for: #{formula_sentence}", truncate: false
if EnvConfig.download_concurrency > 1
download_queue = Homebrew::DownloadQueue.new(pour: true)
formula_installers.each do |fi|
fi.download_queue = download_queue
end
end
valid_formula_installers = formula_installers.dup
begin
[:prelude_fetch, :prelude, :fetch].each do |step|
valid_formula_installers.select! do |fi|
fi.public_send(step)
true
rescue CannotInstallFormulaError => e
ofail e.message
false
rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e
ofail "#{fi.formula}: #{e}"
false
end
download_queue&.fetch
end
ensure
download_queue&.shutdown
end
valid_formula_installers
end
sig {
params(formula_installers: T::Array[FormulaInstaller], installed_on_request: T::Boolean,
installed_as_dependency: 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).void
}
2025-06-08 12:34:24 -04:00
def install_formulae(
formula_installers,
installed_on_request: true,
installed_as_dependency: false,
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_names_to_install = formula_installers.map { |fi| fi.formula.name }
return if formulae_names_to_install.empty?
if dry_run
ohai "Would install #{Utils.pluralize("formula", formulae_names_to_install.count,
plural: "e", include_count: true)}:"
puts formulae_names_to_install.join(" ")
formula_installers.each do |fi|
print_dry_run_dependencies(fi.formula, fi.compute_dependencies, &:name)
end
return
end
valid_formula_installers = fetch_formulae(formula_installers)
2025-07-23 21:13:22 +01:00
valid_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)
2023-04-17 10:59:47 -07:00
end
end
sig { params(formula: Formula, dependencies: T::Array[[Dependency, Options]]).void }
def print_dry_run_dependencies(formula, dependencies)
2023-04-17 10:59:47 -07:00
return if dependencies.empty?
2023-04-17 10:59:47 -07:00
ohai "Would install #{Utils.pluralize("dependenc", dependencies.count, plural: "ies", singular: "y",
include_count: true)} for #{formula.name}:"
formula_names = dependencies.map { |(dep, _options)| yield dep.to_formula }
2023-04-17 10:59:47 -07:00
puts formula_names.join(" ")
end
2023-04-17 11:02:59 -07:00
# If asking the user is enabled, show dependency and size information.
sig { params(formulae_installer: T::Array[FormulaInstaller], dependants: Homebrew::Upgrade::Dependents, args: Homebrew::CLI::Args).void }
def ask_formulae(formulae_installer, dependants, args:)
return if formulae_installer.empty?
formulae = collect_dependencies(formulae_installer, dependants)
2025-05-03 15:39:43 -04:00
2025-03-05 17:38:18 -05:00
ohai "Looking for bottles..."
2025-06-08 11:32:21 -04:00
sizes = compute_total_sizes(formulae, debug: args.debug?)
2025-03-05 17:38:18 -05:00
2025-06-08 11:32:21 -04:00
puts "#{::Utils.pluralize("Formula", formulae.count, plural: "e")} \
(#{formulae.count}): #{formulae.join(", ")}\n\n"
2025-07-31 17:31:27 +01:00
puts "Download Size: #{disk_usage_readable(sizes.fetch(:download))}"
puts "Install Size: #{disk_usage_readable(sizes.fetch(:installed))}"
if (net_install_size = sizes[:net]) && net_install_size != 0
puts "Net Install Size: #{disk_usage_readable(net_install_size)}"
end
2025-03-05 17:38:18 -05:00
ask_input
end
sig { params(casks: T::Array[Cask::Cask]).void }
def ask_casks(casks)
return if casks.empty?
puts "#{::Utils.pluralize("Cask", casks.count, plural: "s")} \
(#{casks.count}): #{casks.join(", ")}\n\n"
ask_input
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
2025-03-05 17:38:18 -05:00
private
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 { params(all_fatal: T::Boolean).void }
2025-03-05 17:38:18 -05:00
def perform_preinstall_checks(all_fatal: false)
check_prefix
check_cpu
attempt_directory_creation
Diagnostic.checks(:supported_configuration_checks, fatal: all_fatal)
Diagnostic.checks(:fatal_preinstall_checks)
end
sig { void }
2025-03-05 17:38:18 -05:00
def attempt_directory_creation
Keg.must_exist_directories.each do |dir|
FileUtils.mkdir_p(dir) unless dir.exist?
rescue
nil
end
end
sig { void }
2025-03-05 17:38:18 -05:00
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 { void }
def ask_input
ohai "Do you want to proceed with the installation? [Y/y/yes/N/n/no]"
accepted_inputs = %w[y yes]
declined_inputs = %w[n no]
loop do
2025-03-05 17:38:18 -05:00
result = $stdin.gets
return unless result
result = result.chomp.strip.downcase
if accepted_inputs.include?(result)
break
elsif declined_inputs.include?(result)
2025-03-05 17:38:18 -05:00
exit 1
else
puts "Invalid input. Please enter 'Y', 'y', or 'yes' to proceed, or 'N' to abort."
end
end
end
# Compute the total sizes (download, installed, and net) 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
total_net_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
# Sum disk usage for all installed kegs of the formula.
next if formula.installed_kegs.none?
kegs_dep_size = formula.installed_kegs.sum { |keg| keg.disk_usage.to_i }
total_net_size += bottle.installed_size.to_i - kegs_dep_size if bottle.installed_size
end
{ download: total_download_size,
installed: total_installed_size,
net: total_net_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
2025-06-29 23:29:18 -04:00
formulae_dependencies.uniq
end
end
end
end
2018-09-28 14:01:09 -07:00
require "extend/os/install"