Files
brew/Library/Homebrew/formula_installer.rb
T
Mike McQuaid 204ea7e964 Make tests use public APIs and enable their cops
- Make every statically-poked method public and call it directly,
  keeping `public_send` only for dynamically-named public methods.
- Read and write state through public `attr_*` accessors instead of
  instance variable reflection.
- Enable `Homebrew/NoSendInTests` and
  `Homebrew/NoInstanceVariableAccessInTests` now the test suite is
  clean, so neither pattern creeps back in.
- Keep rare justified disables, e.g. `Module#remove_const` is
  private core Ruby and raw memoisation state has no accessor.
2026-07-27 08:11:46 +01:00

1931 lines
63 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "formula"
require "api/formula_bottle"
require "keg"
require "tab"
require "utils/bottles"
require "caveats"
require "cleaner"
require "formula_cellar_checks"
require "install_renamed"
require "sandbox"
require "development_tools"
require "cache_store"
require "linkage_checker"
require "messages"
require "cask/caskroom"
require "cmd/install"
require "find"
require "utils/spdx"
require "deprecate_disable"
require "unlink"
require "service"
require "attestation"
require "utils/fork"
require "utils/output"
require "utils/attestation"
# Installer for a formula.
class FormulaInstaller
include FormulaCellarChecks
include Utils::Output::Mixin
ETC_VAR_DIRS = T.let([HOMEBREW_PREFIX/"etc", HOMEBREW_PREFIX/"var"].freeze, T::Array[Pathname])
sig { override.returns(Formula) }
attr_reader :formula
sig { returns(T::Hash[String, T::Hash[String, String]]) }
attr_reader :bottle_tab_runtime_dependencies
sig { returns(Options) }
attr_accessor :options
sig { returns(T::Boolean) }
attr_accessor :link_keg
sig { returns(Homebrew::DownloadQueue) }
attr_accessor :download_queue
sig { params(ran_prelude: T::Boolean).void }
attr_writer :ran_prelude
sig {
params(
formula: Formula,
link_keg: T::Boolean,
installed_on_request: T::Boolean,
show_header: T::Boolean,
build_bottle: T::Boolean,
skip_post_install: T::Boolean,
skip_link: 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],
env: T.nilable(String),
git: T::Boolean,
interactive: T::Boolean,
keep_tmp: T::Boolean,
debug_symbols: T::Boolean,
cc: T.nilable(String),
options: Options,
force: T::Boolean,
overwrite: T::Boolean,
debug: T::Boolean,
quiet: T::Boolean,
verbose: T::Boolean,
).void
}
def initialize(
formula,
link_keg: false,
installed_on_request: false,
show_header: false,
build_bottle: false,
skip_post_install: false,
skip_link: false,
force_bottle: false,
bottle_arch: nil,
ignore_deps: false,
only_deps: false,
include_test_formulae: [],
build_from_source_formulae: [],
env: nil,
git: false,
interactive: false,
keep_tmp: false,
debug_symbols: false,
cc: nil,
options: Options.new,
force: false,
overwrite: false,
debug: false,
quiet: false,
verbose: false
)
@formula = formula
@env = env
@force = force
@overwrite = overwrite
@keep_tmp = keep_tmp
@debug_symbols = debug_symbols
@installed_on_request = installed_on_request
link_keg ||= !formula.keg_only? || auto_link_versioned_keg_only?
@link_keg = link_keg
@show_header = show_header
@ignore_deps = ignore_deps
@only_deps = only_deps
@build_from_source_formulae = build_from_source_formulae
@build_bottle = build_bottle
@skip_post_install = skip_post_install
@skip_link = skip_link
@bottle_arch = bottle_arch
@formula.force_bottle ||= force_bottle
@force_bottle = T.let(@formula.force_bottle, T::Boolean)
@include_test_formulae = include_test_formulae
@interactive = interactive
@git = git
@cc = cc
@verbose = verbose
@quiet = quiet
@debug = debug
@options = options
@requirement_messages = T.let([], T::Array[String])
@poured_bottle = T.let(false, T::Boolean)
@start_time = T.let(nil, T.nilable(Time))
@bottle_tab_runtime_dependencies = T.let({}.freeze, T::Hash[String, T::Hash[String, String]])
@bottle_built_os_version = T.let(nil, T.nilable(String))
@hold_locks = T.let(false, T::Boolean)
@show_summary_heading = T.let(false, T::Boolean)
@etc_var_preinstall = T.let([], T::Array[Pathname])
@download_queue = T.let(Homebrew.default_download_queue, Homebrew::DownloadQueue)
@api_bottle = T.let(nil, T.nilable(Bottle))
@api_bottle_loaded = T.let(false, T::Boolean)
# Take the original formula instance, which might have been swapped from an API instance to a source instance
@formula = T.let(T.must(previously_fetched_formula), Formula) if previously_fetched_formula
@ran_prelude_fetch = T.let(false, T::Boolean)
@ran_prelude = T.let(false, T::Boolean)
end
sig { returns(T::Boolean) }
def debug? = @debug
sig { returns(T::Boolean) }
def debug_symbols? = @debug_symbols
sig { returns(T::Boolean) }
def force? = @force
sig { returns(T::Boolean) }
def force_bottle? = @force_bottle
sig { returns(T::Boolean) }
def git? = @git
sig { returns(T::Boolean) }
def ignore_deps? = @ignore_deps
sig { returns(T::Boolean) }
def installed_on_request? = @installed_on_request
sig { returns(T::Boolean) }
def interactive? = @interactive
sig { returns(T::Boolean) }
def keep_tmp? = @keep_tmp
sig { returns(T::Boolean) }
def only_deps? = @only_deps
sig { returns(T::Boolean) }
def overwrite? = @overwrite
sig { returns(T::Boolean) }
def quiet? = @quiet
sig { returns(T::Boolean) }
def show_header? = @show_header
sig { returns(T::Boolean) }
def show_summary_heading? = @show_summary_heading
sig { returns(T::Boolean) }
def verbose? = @verbose
sig { returns(T::Boolean) }
def self.show_missing_bottle_metadata_warning?
return false if @missing_bottle_metadata_warning_shown
@missing_bottle_metadata_warning_shown = T.let(true, T.nilable(TrueClass))
true
end
sig { returns(T::Set[Formula]) }
def self.attempted
@attempted ||= T.let(Set.new, T.nilable(T::Set[Formula]))
end
sig { returns(T::Set[Formula]) }
def self.installed
@installed ||= T.let(Set.new, T.nilable(T::Set[Formula]))
end
sig { returns(T::Set[Formula]) }
def self.fetched
@fetched ||= T.let(Set.new, T.nilable(T::Set[Formula]))
end
sig { returns(T::Boolean) }
def build_from_source?
@build_from_source_formulae.include?(formula.full_name)
end
sig { returns(T::Boolean) }
def include_test?
@include_test_formulae.include?(formula.full_name)
end
sig { returns(T::Boolean) }
def build_bottle?
@build_bottle.present?
end
sig { returns(T::Boolean) }
def skip_post_install?
@skip_post_install.present?
end
sig { returns(T::Boolean) }
def skip_link?
@skip_link.present?
end
sig { params(output_warning: T::Boolean).returns(T::Boolean) }
def pour_bottle?(output_warning: false)
return false if !formula.bottle_tag? && !formula.local_bottle_path
return true if force_bottle?
return false if build_from_source? || build_bottle? || interactive?
return false if @cc
return false unless options.empty?
unless formula.pour_bottle?
if output_warning && formula.pour_bottle_check_unsatisfied_reason
opoo <<~EOS
Building #{formula.full_name} from source:
#{formula.pour_bottle_check_unsatisfied_reason}
EOS
end
return false
end
return true if formula.local_bottle_path
bottle = api_bottle || formula.bottle_for_tag(Utils::Bottles.tag)
return false if bottle.nil?
unless bottle.compatible_locations?
if output_warning
prefix = Pathname(bottle.cellar.to_s).parent
opoo <<~EOS
Building #{formula.full_name} from source as the bottle needs:
- `HOMEBREW_CELLAR=#{bottle.cellar}` (yours is #{HOMEBREW_CELLAR})
- `HOMEBREW_PREFIX=#{prefix}` (yours is #{HOMEBREW_PREFIX})
EOS
end
return false
end
true
end
sig { params(dep: Formula, build: BuildOptions).returns(T::Boolean) }
def install_bottle_for?(dep, build)
return pour_bottle? if dep == formula
(
@build_from_source_formulae.exclude?(dep.full_name) &&
dep.bottle.present? &&
dep.pour_bottle? &&
build.used_options.empty? &&
dep.bottle&.compatible_locations?
) || false
end
sig { void }
def prelude_fetch
return if @ran_prelude_fetch
deprecate_disable_type = DeprecateDisable.type(formula)
if deprecate_disable_type.present?
message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}"
case deprecate_disable_type
when :deprecated
opoo message
when :disabled
if force?
opoo message
else
GitHub::Actions.puts_annotation_if_env_set!(:error, message)
raise CannotInstallFormulaError, message
end
end
end
# Run the formula-self forbidden checks before any source or bottle
# download is enqueued so a forbidden formula never triggers a fetch.
forbidden_tap_check(formula_only: true)
forbidden_formula_check(formula_only: true)
if pour_bottle?
# Needs to be done before expand_dependencies for compute_dependencies
fetch_bottle_tab(enqueue: true)
elsif formula.loaded_from_api?
Homebrew::API::Formula.source_download(formula, download_queue:, enqueue: true)
end
fetch_fetch_deps unless ignore_deps?
@ran_prelude_fetch = true
end
sig { void }
def prelude
prelude_fetch unless @ran_prelude_fetch
determine_bottle_tab_attributes
verify_deps_exist unless ignore_deps?
forbidden_license_check
forbidden_tap_check
forbidden_formula_check
check_install_sanity
install_fetch_deps if !ignore_deps? && Homebrew::EnvConfig.download_concurrency <= 1
@ran_prelude = true
end
sig { void }
def determine_bottle_tab_attributes
Tab.clear_cache
# Setup bottle_tab_runtime_dependencies for compute_dependencies and
# bottle_built_os_version for dependency resolution.
begin
bottle_tab_attributes = formula.bottle_tab_attributes
raw_deps = bottle_tab_attributes.fetch("runtime_dependencies", []).then { |deps| deps || [] }
@bottle_tab_runtime_dependencies = raw_deps.to_h { |dep| [dep["full_name"], dep] }.freeze
if (bottle_tag = formula.bottle_for_tag(Utils::Bottles.tag)&.tag) &&
bottle_tag.system != :all
# Extract the OS version the bottle was built on.
# This ensures that when installing older bottles (e.g. Sonoma bottle on Sequoia),
# we resolve dependencies according to the bottle's built OS, not the current OS.
@bottle_built_os_version = bottle_tab_attributes.dig("built_on", "os_version")
end
rescue Resource::BottleManifest::Error
# If we can't get the bottle manifest, assume a full dependencies install.
end
end
sig { void }
def verify_deps_exist
compute_dependencies
rescue FormulaUnavailableError => e
e.dependent = formula.full_name
raise
end
sig { void }
def check_installation_already_attempted
raise FormulaInstallationAlreadyAttemptedError, formula if self.class.attempted.include?(formula)
end
sig { void }
def check_install_sanity
check_installation_already_attempted
if force_bottle? && !pour_bottle?
raise CannotInstallFormulaError, "`--force-bottle` passed but #{formula.full_name} has no bottle!"
end
if Homebrew.default_prefix? &&
!build_from_source? && !build_bottle? && !formula.head? && formula.tap&.core_tap? &&
# Integration tests override homebrew-core locations
ENV["HOMEBREW_INTEGRATION_TEST"].nil? &&
!pour_bottle?
message = if !formula.pour_bottle? && formula.pour_bottle_check_unsatisfied_reason
formula_message = formula.pour_bottle_check_unsatisfied_reason
formula_message[0] = formula_message[0].downcase
<<~EOS
#{formula}: #{formula_message}
EOS
# don't want to complain about no bottle available if doing an
# upgrade/reinstall/dependency install (but do in the case the bottle
# check fails)
elsif fresh_install?(formula)
<<~EOS
#{formula}: no bottle available!
EOS
end
if message
message += <<~EOS
If you're feeling brave, you can try to install from source with:
brew install --build-from-source #{formula}
This is a Tier 3 configuration:
#{Formatter.url("https://docs.brew.sh/Support-Tiers#tier-3")}
#{Formatter.bold("Do not report any issues to Homebrew/* repositories!")}
Read the above document instead before opening any issues or PRs.
EOS
raise CannotInstallFormulaError, message
end
end
return if ignore_deps?
if Homebrew::EnvConfig.developer?
# `recursive_dependencies` trims cyclic dependencies, so we do one level and take the recursive deps of that.
# Mapping direct dependencies to deeper dependencies in a hash is also useful for the cyclic output below.
recursive_dep_map = formula.deps.to_h do |dep|
# We cheat a bit with bubblewrap. We eagerly add it to build dependencies on tier-one systems.
# But this cyclic dependency check is (intentionally) overly strict and forbids cyclic build dependencies,
# to help prevent cases that would break, for example, mass bottling.
recursive_deps = if dep.name == "bubblewrap" && dep.implicit?
[]
else
dep.to_formula.recursive_dependencies do |_dependent, recursive_dep|
Dependable::PRUNE if recursive_dep.name == "bubblewrap" && recursive_dep.implicit?
end
end
[dep, recursive_deps]
end
cyclic_dependencies = []
recursive_dep_map.each do |dep, recursive_deps|
if [formula.name, formula.full_name].include?(dep.name)
cyclic_dependencies << "#{formula.full_name} depends on itself directly"
elsif recursive_deps.any? { |rdep| [formula.name, formula.full_name].include?(rdep.name) }
cyclic_dependencies << "#{formula.full_name} depends on itself via #{dep.name}"
end
end
if cyclic_dependencies.present?
raise CannotInstallFormulaError, <<~EOS
#{formula.full_name} contains a recursive dependency on itself:
#{cyclic_dependencies.join("\n ")}
EOS
end
end
recursive_deps = if pour_bottle?
# Include implicit dependencies (except duplicates) in formulae to check
(formula.runtime_dependencies + formula.deps.select(&:implicit?)).uniq(&:name)
else
formula.recursive_dependencies
end
invalid_arch_dependencies = []
pinned_unsatisfied_deps = []
recursive_deps.each do |dep|
tab = Tab.for_formula(dep.to_formula)
if tab.arch.present? && tab.arch.to_s != Hardware::CPU.arch.to_s
invalid_arch_dependencies << "#{dep} was built for #{tab.arch}"
end
next unless dep.to_formula.pinned?
next if dep.satisfied?
pinned_unsatisfied_deps << dep
end
if invalid_arch_dependencies.present?
raise CannotInstallFormulaError, <<~EOS
#{formula.full_name} dependencies not built for the #{Hardware::CPU.arch} CPU architecture:
#{invalid_arch_dependencies.join("\n ")}
EOS
end
return if pinned_unsatisfied_deps.empty?
raise CannotInstallFormulaError,
"You must `brew unpin #{pinned_unsatisfied_deps * " "}` as installing " \
"#{formula.full_name} requires the latest version of pinned dependencies."
end
sig { params(_formula: Formula).returns(T.nilable(T::Boolean)) }
def fresh_install?(_formula) = false
sig { void }
def fetch_fetch_deps
return if @compute_dependencies.blank?
compute_dependencies(use_cache: false) if @compute_dependencies.any? do |dep|
next false unless dep.implicit?
fetch_dependencies
true
end
end
sig { void }
def install_fetch_deps
return if @compute_dependencies.blank?
compute_dependencies(use_cache: false) if @compute_dependencies.any? do |dep|
next false unless dep.implicit?
fetch_dependencies
install_dependency(dep)
true
end
end
sig { void }
def build_bottle_preinstall
@etc_var_preinstall = Find.find(*ETC_VAR_DIRS.select(&:directory?)).to_a
end
sig { void }
def build_bottle_postinstall
etc_var_postinstall = Find.find(*ETC_VAR_DIRS.select(&:directory?)).to_a
(etc_var_postinstall - @etc_var_preinstall).each do |file|
# Keep new `etc`/`var` files in `.bottle` so `Formula#install_etc_var`
# can restore them later with `InstallRenamed` config handling.
Pathname.new(file).cp_path_sub(HOMEBREW_PREFIX, formula.bottle_prefix)
end
end
sig { void }
def install
lock
start_time = Time.now
unless pour_bottle?
require "install"
Homebrew::Install.perform_build_from_source_checks
end
# Warn if a more recent version of this formula is available in the tap.
begin
if !quiet? &&
formula.pkg_version < (v = Formulary.factory(formula.full_name, force_bottle: force_bottle?).pkg_version)
opoo "#{formula.full_name} #{v} is available and more recent than version #{formula.pkg_version}."
end
rescue FormulaUnavailableError
nil
end
check_conflicts
raise UnbottledError, [formula] if !pour_bottle? && !DevelopmentTools.installed?
unless ignore_deps?
deps = compute_dependencies(use_cache: false)
if ((pour_bottle? && !DevelopmentTools.installed?) || build_bottle?) &&
(unbottled = unbottled_dependencies(deps)).presence
# Check that each dependency in deps has a bottle available, terminating
# abnormally with a UnbottledError if one or more don't.
raise UnbottledError, unbottled
end
install_dependencies(deps)
end
return if only_deps?
formula.deprecated_flags.each do |deprecated_option|
old_flag = deprecated_option.old_flag
new_flag = deprecated_option.current_flag
opoo "#{formula.full_name}: #{old_flag} was deprecated; using #{new_flag} instead!"
end
options = display_options(formula).join(" ")
oh1 "Installing #{Formatter.identifier(formula.full_name)} #{options}".strip if show_header?
if (tap = formula.tap) && tap.should_report_analytics?
require "utils/analytics"
Utils::Analytics.report_package_event(:formula_install, package_name: formula.name, tap_name: tap.name,
on_request: installed_on_request?, options:)
end
self.class.attempted << formula
if pour_bottle?
begin
pour
# Catch any other types of exceptions as they leave us with nothing installed.
rescue Exception # rubocop:disable Lint/RescueException
Keg.new(formula.prefix).ignore_interrupts_and_uninstall! if formula.prefix.exist?
raise
else
@poured_bottle = true
end
end
puts_requirement_messages
build_bottle_preinstall if build_bottle?
unless @poured_bottle
build
clean
# Store the formula used to build the keg in the keg.
formula_contents = if (local_bottle_path = formula.local_bottle_path)
Utils::Bottles.formula_contents local_bottle_path, name: formula.name
else
formula.path.read
end
s = formula_contents.gsub(/ bottle do.+?end\n\n?/m, "")
brew_prefix = formula.prefix/".brew"
brew_prefix.mkpath
Pathname(brew_prefix/"#{formula.name}.rb").atomic_write(s)
keg = Keg.new(formula.prefix)
tab = keg.tab
tab.installed_on_request = installed_on_request?
tab.write
end
build_bottle_postinstall if build_bottle?
opoo "Nothing was installed to #{formula.prefix}" unless formula.latest_version_installed?
end_time = Time.now
Homebrew.messages.package_installed(formula.name, end_time - start_time)
end
sig { void }
def check_conflicts
return if force?
return if skip_link?
return unless link_keg
conflicts = formula.conflicts.select do |c|
next false if c.name == formula.name || c.name == formula.full_name
f = Formulary.factory(c.name)
rescue TapFormulaUnavailableError
# If the formula name is a fully-qualified name let's silently
# ignore it as we don't care about things used in taps that aren't
# currently tapped.
false
rescue FormulaUnavailableError => e
# If the formula name doesn't exist any more then complain but don't
# stop installation from continuing.
opoo <<~EOS
#{formula}: #{e.message}
'conflicts_with "#{c.name}"' should be removed from #{formula.path.basename}.
EOS
raise if Homebrew::EnvConfig.developer?
$stderr.puts "Please report this issue to the #{formula.tap&.full_name} tap".squeeze(" ")
$stderr.puts " (not Homebrew/* repositories)!" unless formula.core_formula?
false
else
f.linked_keg.exist? && f.opt_prefix.exist?
end
raise FormulaConflictError.new(formula, conflicts) unless conflicts.empty?
end
# Compute and collect the dependencies needed by the formula currently
# being installed.
sig { params(use_cache: T::Boolean).returns(T::Array[Dependency]) }
def compute_dependencies(use_cache: true)
@compute_dependencies = T.let(nil, T.nilable(T::Array[Dependency])) unless use_cache
@compute_dependencies ||= begin
# Needs to be done before expand_dependencies
fetch_bottle_tab if pour_bottle?
check_requirements(expand_requirements)
expand_dependencies
end
end
sig { params(deps: T::Array[Dependency]).returns(T::Array[Formula]) }
def unbottled_dependencies(deps)
deps.map(&:to_formula).reject do |dep_f|
next false unless dep_f.pour_bottle?
dep_f.bottled?
end
end
sig { params(req_map: T::Hash[Formula, T::Array[Requirement]]).void }
def check_requirements(req_map)
@requirement_messages = []
fatals = []
req_map.each_pair do |dependent, reqs|
reqs.each do |req|
next if dependent.latest_version_installed? && req.is_a?(MacOSRequirement) && req.comparator == "<="
@requirement_messages << "#{dependent}: #{req.message}"
fatals << req if req.fatal?
end
end
return if fatals.empty?
puts_requirement_messages
raise UnsatisfiedRequirements, fatals
end
sig { params(formula: Formula).returns(T::Array[Requirement]) }
def runtime_requirements(formula)
runtime_deps = formula.runtime_formula_dependencies(undeclared: false)
recursive_requirements = formula.recursive_requirements do |dependent, _|
next Dependable::PRUNE unless runtime_deps.include?(dependent)
end
(recursive_requirements.to_a + formula.requirements.to_a).reject(&:build?).uniq
end
sig { returns(T::Hash[Formula, T::Array[Requirement]]) }
def expand_requirements
unsatisfied_reqs = Hash.new { |h, k| h[k] = [] }
formulae = [formula]
formula_deps_map = formula.recursive_dependencies
.to_h { |dep| [dep.name, dep] }
while (f = formulae.pop)
runtime_requirements = runtime_requirements(f)
f.recursive_requirements do |dependent, req|
dependent = T.cast(dependent, Formula)
build = effective_build_options_for(dependent)
install_bottle_for_dependent = install_bottle_for?(dependent, build)
keep_build_test = false
keep_build_test ||= runtime_requirements.include?(req)
keep_build_test ||= req.test? && include_test? && dependent == f
keep_build_test ||= req.build? && !install_bottle_for_dependent && !dependent.latest_version_installed?
if req.prune_from_option?(build) ||
req.satisfied?(env: @env, cc: @cc, build_bottle: @build_bottle, bottle_arch: @bottle_arch) ||
((req.build? || req.test?) && !keep_build_test) ||
formula_deps_map[dependent.name]&.build? ||
(only_deps? && f == dependent)
next Dependable::PRUNE
else
unsatisfied_reqs[dependent] << req
nil # Return nil to satisfy T.nilable(Symbol) block sig (Array from << would violate it).
end
end
end
unsatisfied_reqs
end
sig { params(formula: Formula).returns(T::Array[Dependency]) }
def expand_dependencies_for_formula(formula)
# Cache for this expansion only. FormulaInstaller has a lot of inputs which can alter expansion.
cache_key = "FormulaInstaller-#{formula.full_name}-#{Time.now.to_f}"
formula_cache = T.let({}, T::Hash[Dependency, Formula])
satisfied_cache = T.let(
{},
T::Hash[T::Array[T.nilable(T.any(Dependency, String, Integer))], T::Boolean],
)
Dependency.expand(formula, cache_key:, formula_cache:) do |dependent, dep|
dependent = T.cast(dependent, Formula)
build = effective_build_options_for(dependent)
keep_build_test = false
keep_build_test ||= dep.test? && include_test? && @include_test_formulae.include?(dependent.full_name)
keep_build_test ||= dep.build? && !install_bottle_for?(dependent, build) &&
(formula.head? || !dependent.latest_version_installed?)
minimum_version = @bottle_tab_runtime_dependencies.dig(dep.name, "version").presence
minimum_version = Version.new(minimum_version) if minimum_version
minimum_revision = @bottle_tab_runtime_dependencies.dig(dep.name, "revision")&.to_i
bottle_os_version = @bottle_built_os_version
next Dependable::PRUNE if dep.prune_from_option?(build) || ((dep.build? || dep.test?) && !keep_build_test)
satisfied_cache_key = T.let([
dep,
minimum_version&.to_s,
minimum_revision,
bottle_os_version,
], T::Array[T.nilable(T.any(Dependency, String, Integer))])
satisfied = satisfied_cache.fetch(satisfied_cache_key) do
satisfied_cache[satisfied_cache_key] = dep.satisfied?(minimum_version:, minimum_revision:, bottle_os_version:)
end
next Dependable::SKIP if satisfied
end
end
sig { returns(T::Array[Dependency]) }
def expand_dependencies = expand_dependencies_for_formula(formula)
sig { params(dependent: Formula).returns(BuildOptions) }
def effective_build_options_for(dependent)
args = dependent.build.used_options
args |= options if dependent == formula
args |= Tab.for_formula(dependent).used_options
args &= dependent.options
BuildOptions.new(args, dependent.options)
end
sig { params(formula: Formula).returns(T::Array[String]) }
def display_options(formula)
options = if formula.head?
["--HEAD"]
else
[]
end
options += effective_build_options_for(formula).used_options.to_a.map(&:to_s)
options
end
sig { params(deps: T::Array[Dependency]).void }
def install_dependencies(deps)
if deps.empty? && only_deps?
puts "All dependencies for #{formula.full_name} are satisfied."
elsif !deps.empty?
deps_with_formulae = deps.map { |dep| [dep, dep.to_formula] }
if deps.length > 1
names = deps_with_formulae.map do |dep, dep_formula|
installed = dep_formula.any_version_installed?
pretty_install_status(Formatter.identifier(dep), installed:,
outdated: installed && dep_formula.outdated?, mark_uninstalled: false,
bold: false)
end
oh1 "Installing dependencies for #{formula.full_name}:#{Tty.reset} #{names.to_sentence}",
truncate: false
end
deps_with_formulae.each { |dep, dep_formula| install_dependency(dep, dep_formula) }
end
@show_header = true unless deps.empty?
end
sig { params(dep: Dependency).void }
def fetch_dependency(dep)
df = dep.to_formula
fi = FormulaInstaller.new(
df,
force_bottle: false,
# When fetching we don't need to recurse the dependency tree as it's already
# been done for us in `compute_dependencies` and there's no requirement to
# fetch in a particular order.
# Note, this tree can vary when pouring bottles so we need to check it then.
ignore_deps: !pour_bottle?,
include_test_formulae: @include_test_formulae,
build_from_source_formulae: @build_from_source_formulae,
keep_tmp: keep_tmp?,
debug_symbols: debug_symbols?,
force: force?,
debug: debug?,
quiet: quiet?,
verbose: verbose?,
)
fi.download_queue = download_queue
fi.prelude
fi.enqueue_fetch
end
sig { params(dep: Dependency, dep_formula: Formula).void }
def install_dependency(dep, dep_formula = dep.to_formula)
if dep_formula.linked_keg.directory?
linked_keg = Keg.new(dep_formula.linked_keg.resolved_path)
tab = linked_keg.tab
keg_had_linked_keg = true
keg_was_linked = linked_keg.linked?
linked_keg.unlink
else
keg_had_linked_keg = false
end
if dep_formula.latest_version_installed?
installed_keg = Keg.new(dep_formula.prefix)
tab ||= installed_keg.tab
tmp_keg = Pathname.new("#{installed_keg}.tmp")
installed_keg.rename(tmp_keg) unless tmp_keg.directory?
end
if dep_formula.tap.present? && tab.present? && (tab_tap = tab.source["tap"].presence) &&
dep_formula.tap.to_s != tab_tap.to_s
odie <<~EOS
#{dep_formula} is already installed from #{tab_tap}!
Please `brew uninstall #{dep_formula}` first."
EOS
end
options = Options.new
options |= tab.used_options if tab.present?
options |= Tab.remap_deprecated_options(dep_formula.deprecated_options, dep.options)
options &= dep_formula.options
installed_on_request = dep_formula.any_version_installed? && tab.present? && tab.installed_on_request
installed_on_request ||= false
fi = FormulaInstaller.new(
dep_formula,
options:,
link_keg: keg_had_linked_keg && keg_was_linked,
installed_on_request:,
force_bottle: false,
include_test_formulae: @include_test_formulae,
build_from_source_formulae: @build_from_source_formulae,
keep_tmp: keep_tmp?,
debug_symbols: debug_symbols?,
force: force?,
debug: debug?,
quiet: quiet?,
verbose: verbose?,
)
oh1 "Installing #{formula.full_name} dependency: #{Formatter.identifier(dep.name)}"
# prelude only needed to populate bottle_tab_runtime_dependencies, fetching has already been done.
fi.prelude
fi.install
fi.finish
# Handle all possible exceptions installing deps.
rescue Exception => e # rubocop:disable Lint/RescueException
ignore_interrupts do
tmp_keg.rename(installed_keg.to_path) if tmp_keg && !installed_keg.directory?
linked_keg.link(verbose: verbose?) if keg_was_linked
end
raise unless e.is_a? FormulaInstallationAlreadyAttemptedError
# We already attempted to install f as part of another formula's
# dependency tree. In that case, don't generate an error, just move on.
nil
else
ignore_interrupts { FileUtils.rm_r(tmp_keg) if tmp_keg&.directory? }
end
sig { void }
def caveats
return if only_deps?
audit_installed if Homebrew::EnvConfig.developer?
return unless installed_on_request?
return if quiet?
caveats = Caveats.new(formula)
return if caveats.empty?
Homebrew.messages.record_completions_and_elisp(caveats.completions_and_elisp)
return if caveats.caveats.empty?
@show_summary_heading = true
ohai "Caveats", caveats.to_s
Homebrew.messages.record_caveats(formula.name, caveats)
end
sig { returns(T.nilable(String)) }
def link_manual_command_warning
return unless installed_on_request?
return unless formula.keg_only?
return unless formula.keg_only_reason.versioned_formula?
return if link_keg
return if formula.linked?
reason = formula.link_overwrite_reason
return if reason.blank?
<<~EOS
#{formula.full_name} was installed but not linked because #{reason}.
To link this version, run:
brew link #{formula.full_name}
EOS
end
sig { void }
def finish
return if only_deps?
ohai "Finishing up" if verbose?
keg = Keg.new(formula.prefix)
link(keg)
warning = link_manual_command_warning
opoo warning if !quiet? && warning.present?
install_service
fix_dynamic_linkage(keg) if !@poured_bottle || !formula.bottle_specification.skip_relocation?(tab: keg.tab)
require "install"
Homebrew::Install.global_post_install
if build_bottle? || skip_post_install?
unless quiet?
if build_bottle?
ohai "Not running 'post_install' as we're building a bottle"
elsif skip_post_install?
ohai "Skipping 'post_install' on request"
end
puts "You can run it manually using:"
puts " brew postinstall #{formula.full_name}"
end
else
formula.install_etc_var
post_install if formula.post_install_steps_defined? || formula.post_install_defined?
end
keg.prepare_debug_symbols if debug_symbols?
# Updates the cache for a particular formula after doing an install
CacheStoreDatabase.use(:linkage) do |db|
break unless db.created?
typed_db = T.cast(db, CacheStoreDatabase[String, T::Hash[T.any(String, Symbol), T.anything]])
LinkageChecker.new(keg, formula, cache_db: typed_db, rebuild_cache: true)
end
# Update tab with actual runtime dependencies
tab = keg.tab
Tab.clear_cache
f_runtime_deps = formula.runtime_dependencies(read_from_tab: false)
tab.runtime_dependencies = Tab.runtime_deps_hash(formula, f_runtime_deps)
tab.write
# Update packaged SBOM metadata or write a source-install SBOM.
if @poured_bottle
if (install_time = tab.time)
require "sbom"
SBOM.update_pour_metadata(
SBOM.spdxfile(formula),
homebrew_version: HOMEBREW_VERSION,
time: install_time,
supplement: (api_bottle || formula.bottle)&.sbom_supplement,
)
end
elsif Homebrew::EnvConfig.sbom? && !build_bottle?
require "sbom"
sbom = SBOM.create(formula, tab)
sbom.write(validate: Homebrew::EnvConfig.developer?)
end
# let's reset Utils::Git.available? if we just installed git
Utils::Git.clear_available_cache if formula.name == "git"
Sandbox.reset_state! if formula.name == "bubblewrap"
# use installed ca-certificates when it's needed and available
if formula.name == "ca-certificates" &&
!DevelopmentTools.ca_file_handles_most_https_certificates?
ENV["SSL_CERT_FILE"] = ENV["GIT_SSL_CAINFO"] = (formula.pkgetc/"cert.pem").to_s
ENV["GIT_SSL_CAPATH"] = formula.pkgetc.to_s
end
# use installed curl when it's needed and available
if formula.name == "curl" &&
!DevelopmentTools.curl_handles_most_https_certificates?
ENV["HOMEBREW_CURL"] = (formula.opt_bin/"curl").to_s
Utils::Curl.clear_path_cache
end
caveats
ohai "Summary" if verbose? || show_summary_heading?
puts summary
self.class.installed << formula
ensure
unlock
end
sig { returns(String) }
def summary
s = +""
s << "#{Homebrew::EnvConfig.install_badge} " unless Homebrew::EnvConfig.no_emoji?
s << "#{formula.prefix.resolved_path}: #{formula.prefix.abv}"
s << ", built in #{pretty_duration build_time}" if build_time
s.freeze
end
sig { returns(T.nilable(Float)) }
def build_time
@build_time ||= T.let(Time.now - @start_time, T.nilable(Float)) if @start_time && !interactive?
end
sig { returns(T::Array[String]) }
def sanitized_argv_options
args = []
args << "--ignore-dependencies" if ignore_deps?
if build_bottle?
args << "--build-bottle"
args << "--bottle-arch=#{@bottle_arch}" if @bottle_arch
end
args << "--git" if git?
args << "--interactive" if interactive?
args << "--verbose" if verbose?
args << "--debug" if debug?
args << "--cc=#{@cc}" if @cc
args << "--keep-tmp" if keep_tmp?
if debug_symbols?
args << "--debug-symbols"
args << "--build-from-source"
end
if @env.present?
args << "--env=#{@env}"
elsif formula.env.std? || formula.deps.select(&:build?).any? { |d| d.name == "scons" }
args << "--env=std"
end
args << "--HEAD" if formula.head?
args
end
sig { returns(T::Array[String]) }
def build_argv = sanitized_argv_options + options.as_flags
sig { void }
def build
FileUtils.rm_rf(formula.logs)
@start_time = Time.now
# If the formula is still loaded from the API (i.e. the source .rb was never
# fetched), attempt to download the source now. Without this, specified_path
# would point at a JSON file (e.g. formula.jws.json) which build.rb cannot
# load. See: https://github.com/orgs/Homebrew/discussions/6455
@formula = Homebrew::API::Formula.source_download_formula(formula) if formula.loaded_from_api?
# 1. formulae can modify ENV, so we must ensure that each
# installation has a pristine ENV when it starts, forking now is
# the easiest way to do this
formula_path = formula.specified_path
args = [
"nice",
*HOMEBREW_RUBY_EXEC_ARGS,
"--",
HOMEBREW_LIBRARY_PATH/"build.rb",
formula_path,
].concat(build_argv)
if use_sandbox?("building")
sandbox = Sandbox.new
sandbox.allow_read_if_exists path: formula_path
if Homebrew::EnvConfig.require_tap_trust?
require "trust"
sandbox.allow_read_if_exists path: Homebrew::Trust.trust_file
end
formula.logs.mkpath
sandbox.record_log(formula.logs/"build.sandbox.log")
if interactive?
sandbox.allow_write_path(Dir.home)
else
sandbox.deny_read_home
end
sandbox.allow_write_temp_and_cache
sandbox.allow_write_log(formula)
sandbox.allow_cvs
sandbox.allow_fossil
sandbox.allow_write_xcode
sandbox.allow_write_cellar(formula)
sandbox.deny_all_network unless formula.network_access_allowed?(:build)
sandbox.run(*args)
else
Utils.safe_fork do
exec(*args)
end
end
formula.update_head_version
raise "Empty installation" if !formula.prefix.directory? || Keg.new(formula.prefix).empty_installation?
# Handle all possible exceptions when building.
rescue Exception => e # rubocop:disable Lint/RescueException
if e.is_a? BuildError
e.formula = formula
e.options = display_options(formula)
end
ignore_interrupts do
# any exceptions must leave us with nothing installed
formula.update_head_version
FileUtils.rm_r(formula.prefix) if formula.prefix.directory?
formula.rack.rmdir_if_possible
end
raise e
end
sig { params(keg: Keg).void }
def link(keg)
Formula.clear_cache
cask_installed_with_formula_name = Cask::Caskroom.cask_installed?(formula.name)
if cask_installed_with_formula_name
ohai "#{formula.name} cask is installed, skipping link."
@link_keg = false
elsif skip_link? && !quiet?
ohai "Skipping 'link' on request"
puts "You can run it manually using:"
puts " brew link #{formula.full_name}"
end
if !link_keg || skip_link?
begin
keg.optlink(verbose: verbose?, overwrite: overwrite?)
rescue Keg::LinkError => e
ofail "Failed to create #{formula.opt_prefix}"
puts "Things that depend on #{formula.full_name} will probably not build."
puts e
end
return
end
if keg.linked?
opoo "This keg was marked linked already, continuing anyway"
keg.remove_linked_keg_record
end
Homebrew::Unlink.unlink_link_overwrite_formulae(formula, verbose: verbose?)
link_overwrite_backup = {} # Hash: conflict file -> backup file
backup_dir = HOMEBREW_CACHE/"Backup"
begin
keg.link(verbose: verbose?, overwrite: overwrite?)
rescue Keg::ConflictError => e
conflict_file = e.dst
if formula.link_overwrite?(conflict_file) && !link_overwrite_backup.key?(conflict_file)
backup_file = backup_dir/conflict_file.relative_path_from(HOMEBREW_PREFIX).to_s
backup_file.parent.mkpath
FileUtils.mv conflict_file, backup_file
link_overwrite_backup[conflict_file] = backup_file
retry
end
ofail "The `brew link` step did not complete successfully"
puts "The formula built, but is not symlinked into #{HOMEBREW_PREFIX}"
puts e
puts
puts "Possible conflicting files are:"
keg.link(dry_run: true, overwrite: true, verbose: verbose?)
@show_summary_heading = true
rescue Keg::LinkError => e
ofail "The `brew link` step did not complete successfully"
puts "The formula built, but is not symlinked into #{HOMEBREW_PREFIX}"
puts e
puts
puts "You can try again using:"
puts " brew link #{formula.name}"
@show_summary_heading = true
# Handle all other possible exceptions when linking.
rescue Exception => e # rubocop:disable Lint/RescueException
ofail "An unexpected error occurred during the `brew link` step"
puts "The formula built, but is not symlinked into #{HOMEBREW_PREFIX}"
puts e
if debug?
require "utils/backtrace"
puts Utils::Backtrace.clean(e)
end
@show_summary_heading = true
ignore_interrupts do
keg.unlink
link_overwrite_backup.each do |origin, backup|
origin.parent.mkpath
FileUtils.mv backup, origin
end
end
raise
end
return if link_overwrite_backup.empty?
opoo "These files were overwritten during the `brew link` step:"
puts link_overwrite_backup.keys
puts
puts "They have been backed up to: #{backup_dir}"
@show_summary_heading = true
end
sig { void }
def install_service
service = if formula.service? && formula.service.command?
service_path = formula.systemd_service_path
service_path.atomic_write(formula.service.to_systemd_unit)
service_path.chmod 0644
if formula.service.timed?
timer_path = formula.systemd_timer_path
timer_path.atomic_write(formula.service.to_systemd_timer)
timer_path.chmod 0644
end
formula.service.to_plist
end
return unless service
launchd_service_path = formula.launchd_service_path
launchd_service_path.atomic_write(service)
launchd_service_path.chmod 0644
log = formula.var/"log"
log.mkpath if service.include? log.to_s
# Handle all possible exceptions when installing service files.
rescue Exception => e # rubocop:disable Lint/RescueException
puts e
ofail "Failed to install service files"
require "utils/backtrace"
odebug e, Utils::Backtrace.clean(e)
end
sig { params(keg: Keg).void }
def fix_dynamic_linkage(keg)
keg.fix_dynamic_linkage
# Rescue all possible exceptions when fixing linkage.
rescue Exception => e # rubocop:disable Lint/RescueException
ofail "Failed to fix install linkage"
puts e
puts "The formula built, but you may encounter issues using it or linking other"
puts "formulae against it."
require "utils/backtrace"
odebug "Backtrace", Utils::Backtrace.clean(e)
@show_summary_heading = true
end
sig { void }
def clean
ohai "Cleaning" if verbose?
Cleaner.new(formula).clean
# Handle all possible exceptions when cleaning does not complete.
rescue Exception => e # rubocop:disable Lint/RescueException
opoo "The cleaning step did not complete successfully"
puts "Still, the installation was successful, so we will link it into your prefix."
require "utils/backtrace"
odebug e, Utils::Backtrace.clean(e)
Homebrew.failed = true
@show_summary_heading = true
end
sig { returns(Pathname) }
def post_install_formula_path
# Use the formula from the keg when any of the following is true:
# * We're installing from the JSON API
# * We're installing a local bottle file
# * We're building from source
# * The formula doesn't exist in the tap (or the tap isn't installed)
# * The formula in the tap has a different `pkg_version``.
#
# In all other cases, including if the formula from the keg is unreadable
# (third-party taps may `require` some of their own libraries) or if there
# is no formula present in the keg (as is the case with very old bottles),
# use the formula from the tap.
tap_formula_path = T.must(formula.specified_path)
installed_prefix = formula.any_installed_prefix
return tap_formula_path if installed_prefix.nil?
keg_formula_path = installed_prefix/".brew/#{formula.name}.rb"
return keg_formula_path if formula.loaded_from_api?
return keg_formula_path if formula.local_bottle_path
return keg_formula_path if build_from_source?
return keg_formula_path unless tap_formula_path.exist?
begin
keg_formula = Formulary.factory(keg_formula_path)
tap_formula = Formulary.factory(tap_formula_path)
return keg_formula_path if keg_formula.pkg_version != tap_formula.pkg_version
tap_formula_path
rescue FormulaUnavailableError, FormulaUnreadableError
tap_formula_path
end
end
sig { void }
def post_install
args = [
"nice",
*HOMEBREW_RUBY_EXEC_ARGS,
"-I", $LOAD_PATH.join(File::PATH_SEPARATOR),
"--",
HOMEBREW_LIBRARY_PATH/"postinstall.rb"
]
args << post_install_formula_path
with_preserved_brew_file do
if use_sandbox?("running post-install")
sandbox = Sandbox.new
formula.logs.mkpath
sandbox.record_log(formula.logs/"postinstall.sandbox.log")
sandbox.allow_write_temp_and_cache
sandbox.allow_write_log(formula)
sandbox.allow_write_xcode
sandbox.deny_write_homebrew_repository
sandbox.deny_read_home
sandbox.allow_write_cellar(formula)
sandbox.deny_all_network unless formula.network_access_allowed?(:postinstall)
Keg.keg_link_directories.each do |dir|
sandbox.allow_write_path "#{HOMEBREW_PREFIX}/#{dir}"
end
sandbox.run(*args)
else
Utils.safe_fork do
exec(*args)
end
end
end
# Handle all possible exceptions when postinstall does not complete.
rescue Exception => e # rubocop:disable Lint/RescueException
opoo "The post-install step did not complete successfully"
puts "You can try again using:"
puts " brew postinstall #{formula.full_name}"
require "utils/backtrace"
odebug e, Utils::Backtrace.clean(e), always_display: Homebrew::EnvConfig.developer?
Homebrew.failed = true
@show_summary_heading = true
end
sig { void }
def fetch_dependencies
return if ignore_deps?
# Don't output dependencies if we're explicitly installing them.
deps = compute_dependencies.reject do |dep|
self.class.fetched.include?(dep.to_formula)
end
return if deps.empty?
deps.each { fetch_dependency(it) }
end
sig { returns(T.nilable(Formula)) }
def previously_fetched_formula
# We intentionally don't compare classes here:
# from-API-JSON and from-source formula classes are not equal but we
# want to equate them to be the same thing here given mixing bottle and
# from-source installs of the same formula within the same operation
# doesn't make sense.
self.class.fetched.find do |fetched_formula|
fetched_formula.full_name == formula.full_name && fetched_formula.active_spec_sym == formula.active_spec_sym
end
end
sig { params(quiet: T::Boolean, enqueue: T::Boolean).void }
def fetch_bottle_tab(quiet: false, enqueue: false)
return if @fetch_bottle_tab
return if formula.local_bottle_path
if (bottle = api_bottle || formula.bottle) &&
(manifest_resource = bottle.github_packages_manifest_resource) &&
enqueue
download_queue.enqueue(manifest_resource) unless manifest_resource.downloaded_and_valid?
else
begin
formula.fetch_bottle_tab(quiet: quiet)
rescue DownloadError, Resource::BottleManifest::Error
# do nothing
end
end
@fetch_bottle_tab = T.let(true, T.nilable(TrueClass))
end
sig { void }
def fetch
enqueue_fetch
download_queue.fetch
end
sig { void }
def enqueue_fetch
return if previously_fetched_formula
downloadable_object = T.let(nil, T.nilable(Downloadable))
bottle_download = T.let(nil, T.nilable(Downloadable))
check_attestation = T.let(false, T::Boolean)
local_bottle_path = formula.local_bottle_path
bottle_install = !only_deps? && local_bottle_path.nil? && pour_bottle?(output_warning: true)
# We skip `gh` to avoid a bootstrapping cycle, in the off-chance a user attempts
# to explicitly `brew install gh` without already having a version for bootstrapping.
# We also skip bottle installs from local bottle paths, as these are done in CI
# as part of the build lifecycle before attestations are produced.
verify_attestation = bottle_install &&
Homebrew::EnvConfig.verify_attestations? &&
(formula.tap&.core_tap? || false) &&
formula.name != "gh"
if bottle_install && @ran_prelude
bottle_download = downloadable
check_attestation = verify_attestation && !bottle_download.cached_download.exist?
download_queue.enqueue(bottle_download, check_attestation:, stage: false)
end
fetch_dependencies
return if only_deps?
return if local_bottle_path
downloadable_object = bottle_download || downloadable
if bottle_install
if bottle_download.nil?
fetch_bottle_tab(enqueue: true)
check_attestation = verify_attestation && !downloadable_object.cached_download.exist?
end
else
@formula = Homebrew::API::Formula.source_download_formula(formula) if formula.loaded_from_api?
formula.enqueue_resources_and_patches(download_queue:)
downloadable_object = downloadable
end
# Check attestation after download completes.
download_queue.enqueue(downloadable_object, check_attestation:)
self.class.fetched << formula
rescue CannotInstallFormulaError
if (cached_download = downloadable_object&.cached_download)&.exist?
cached_download.unlink
end
raise
end
sig { returns(Downloadable) }
def downloadable
if (bottle_path = formula.local_bottle_path)
Resource::Local.new(bottle_path.to_s)
elsif pour_bottle?
bottle = api_bottle || formula.bottle
odie "Bottle for #{formula.full_name} is unavailable." if bottle.nil?
bottle
else
resource = formula.resource
odie "Resource for #{formula.full_name} is unavailable." if resource.nil?
resource
end
end
sig { returns(T.nilable(Bottle)) }
def api_bottle
return @api_bottle if @api_bottle_loaded
@api_bottle_loaded = true
return unless formula.loaded_from_internal_api?
return unless formula.core_formula?
@api_bottle = Homebrew::API::FormulaBottle.bottle(
name: formula.name,
formula_struct: Homebrew::API::Internal.formula_struct(formula.name),
)
end
sig { void }
def pour
HOMEBREW_CELLAR.cd do
downloadable_object = downloadable
ohai "Pouring #{downloadable_object.downloader.basename}"
formula.rack.mkpath
# Download queue may have already extracted the bottle to a temporary directory.
# We cannot rely on `download_queue` here as dependencies may be poured by another installer.
if downloadable_object.is_a?(Bottle) &&
(bottle_poured_file = downloadable_object.staged_path_from_download_queue_marker).exist?
bottle_tmp_keg = downloadable_object.staged_path_from_download_queue
FileUtils.rm(bottle_poured_file)
FileUtils.mv(bottle_tmp_keg, formula.prefix)
bottle_tmp_keg.parent.rmdir_if_possible
else
downloadable_object.downloader.stage
end
end
Tab.clear_cache
tab = Utils::Bottles.load_tab(formula)
# fill in missing/outdated parts of the tab
# keep in sync with Tab#to_bottle_hash
tab.used_options = []
tab.unused_options = []
tab.built_as_bottle = true
tab.poured_from_bottle = true
tab.loaded_from_api = formula.loaded_from_api?
tab.loaded_from_internal_api = formula.loaded_from_internal_api?
tab.installed_on_request = installed_on_request?
tab.time = Time.now.to_i
tab.aliases = formula.aliases
tab.arch = Hardware::CPU.arch
tab.source["versions"]["stable"] = T.must(formula.stable).version&.to_s
tab.source["versions"]["version_scheme"] = formula.version_scheme
tab.source["path"] = formula.specified_path.to_s
tab.source["tap_git_head"] = formula.tap&.installed? ? formula.tap&.git_head : nil
tab.tap = formula.tap
tab.write
keg = Keg.new(formula.prefix)
skip_linkage = formula.bottle_specification.skip_relocation?(tab:)
if Homebrew::EnvConfig.bottle_domain_custom? && tab.changed_files.nil?
if self.class.show_missing_bottle_metadata_warning?
opoo <<~EOS
No bottle relocation metadata was found for this `HOMEBREW_BOTTLE_DOMAIN`.
Homebrew will perform full relocation. Ask the mirror operator to provide
an OCI registry proxy of `ghcr.io` that includes manifests and their
`sh.brew.tab` annotations, then use `HOMEBREW_ARTIFACT_DOMAIN` instead.
EOS
end
skip_linkage = false
end
keg.replace_placeholders_with_locations(tab.changed_files, skip_linkage:)
cellar = formula.bottle_specification.tag_to_cellar(Utils::Bottles.tag)
return if BottleSpecification::RELOCATABLE_CELLARS.include?(cellar)
prefix = Pathname(cellar).parent.to_s
return if cellar == HOMEBREW_CELLAR.to_s && prefix == HOMEBREW_PREFIX.to_s
return unless ENV["HOMEBREW_RELOCATE_BUILD_PREFIX"]
keg.relocate_build_prefix(keg, prefix, HOMEBREW_PREFIX)
end
sig { override.params(output: T.nilable(String)).void }
def problem_if_output(output)
return unless output
opoo output
@show_summary_heading = true
end
sig { void }
def audit_installed
unless formula.keg_only?
problem_if_output(check_env_path(formula.bin))
problem_if_output(check_env_path(formula.sbin))
end
super
end
sig { returns(T::Array[Formula]) }
def self.locked
@locked ||= T.let([], T.nilable(T::Array[Formula]))
end
sig { void }
def forbidden_license_check
forbidden_licenses = Homebrew::EnvConfig.forbidden_licenses.to_s.dup
SPDX::ALLOWED_LICENSE_SYMBOLS.each do |s|
pattern = /#{s.to_s.tr("_", " ")}/i
forbidden_licenses.sub!(pattern, s.to_s)
end
invalid_licenses = []
forbidden_licenses = forbidden_licenses.split.each_with_object({}) do |license, hash|
license_sym = license.to_sym
license = license_sym if SPDX::ALLOWED_LICENSE_SYMBOLS.include?(license_sym)
unless SPDX.valid_license?(license)
invalid_licenses << license
next
end
hash[license] = SPDX.license_version_info(license)
end
if invalid_licenses.present?
opoo <<~EOS
`$HOMEBREW_FORBIDDEN_LICENSES` contains invalid license identifiers: #{invalid_licenses.to_sentence}
These licenses will not be forbidden. See the valid SPDX license identifiers at:
#{Formatter.url("https://spdx.org/licenses/")}
And the licenses for a formula with:
brew info <formula>
EOS
end
return if forbidden_licenses.blank?
owner = Homebrew::EnvConfig.forbidden_owner
owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence)
"\n#{contact}"
end
unless ignore_deps?
compute_dependencies.each do |dep|
dep_f = dep.to_formula
next unless SPDX.licenses_forbid_installation? dep_f.license, forbidden_licenses
raise CannotInstallFormulaError, <<~EOS
The installation of #{formula.name} has a dependency on #{dep.name} where all
its licenses were forbidden by #{owner} in `$HOMEBREW_FORBIDDEN_LICENSES`:
#{SPDX.license_expression_to_string dep_f.license}#{owner_contact}
EOS
end
end
return if only_deps?
return unless SPDX.licenses_forbid_installation? formula.license, forbidden_licenses
raise CannotInstallFormulaError, <<~EOS
#{formula.name}'s licenses are all forbidden by #{owner} in `$HOMEBREW_FORBIDDEN_LICENSES`:
#{SPDX.license_expression_to_string formula.license}#{owner_contact}
EOS
end
sig { params(formula_only: T::Boolean).void }
def forbidden_tap_check(formula_only: false)
return if Tap.allowed_taps.blank? && Tap.forbidden_taps.blank?
owner = Homebrew::EnvConfig.forbidden_owner
owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence)
"\n#{contact}"
end
# Check the formula itself before its dependencies, since dependency
# resolution can trigger downloads via `compute_dependencies`.
unless only_deps?
formula_tap = formula.tap
if formula_tap.present? && (!formula_tap.allowed_by_env? || formula_tap.forbidden_by_env?)
formula_error_message = "The installation of #{formula.full_name} has the tap #{formula_tap}\n" \
"but #{owner} "
unless formula_tap.allowed_by_env?
formula_error_message << "has not allowed this tap in `$HOMEBREW_ALLOWED_TAPS`"
end
formula_error_message << " and\n" if !formula_tap.allowed_by_env? && formula_tap.forbidden_by_env?
if formula_tap.forbidden_by_env?
formula_error_message << "has forbidden this tap in `$HOMEBREW_FORBIDDEN_TAPS`"
end
formula_error_message << ".#{owner_contact}"
raise CannotInstallFormulaError, formula_error_message
end
end
return if formula_only
return if ignore_deps?
compute_dependencies.each do |dep|
dep_tap = dep.tap
next if dep_tap.blank? || (dep_tap.allowed_by_env? && !dep_tap.forbidden_by_env?)
error_message = "The installation of #{formula.name} has a dependency #{dep.name}\n" \
"from the #{dep_tap} tap but #{owner} "
error_message << "has not allowed this tap in `$HOMEBREW_ALLOWED_TAPS`" unless dep_tap.allowed_by_env?
error_message << " and\n" if !dep_tap.allowed_by_env? && dep_tap.forbidden_by_env?
error_message << "has forbidden this tap in `$HOMEBREW_FORBIDDEN_TAPS`" if dep_tap.forbidden_by_env?
error_message << ".#{owner_contact}"
raise CannotInstallFormulaError, error_message
end
end
sig { params(formula_only: T::Boolean).void }
def forbidden_formula_check(formula_only: false)
forbidden_formulae = Set.new(Homebrew::EnvConfig.forbidden_formulae.to_s.split)
return if forbidden_formulae.blank?
owner = Homebrew::EnvConfig.forbidden_owner
owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence)
"\n#{contact}"
end
unless only_deps?
formula_name = if forbidden_formulae.include?(formula.name)
formula.name
elsif forbidden_formulae.include?(formula.full_name)
formula.full_name
end
if formula_name
raise CannotInstallFormulaError, <<~EOS
The installation of #{formula_name} was forbidden by #{owner}
in `$HOMEBREW_FORBIDDEN_FORMULAE`.#{owner_contact}
EOS
end
end
return if formula_only
return if ignore_deps?
compute_dependencies.each do |dep|
dep_name = if forbidden_formulae.include?(dep.name)
dep.name
elsif dep.tap.present? &&
(dep_full_name = "#{dep.tap}/#{dep.name}") &&
forbidden_formulae.include?(dep_full_name)
dep_full_name
else
next
end
raise CannotInstallFormulaError, <<~EOS
The installation of #{formula.name} has a dependency #{dep_name}
but the #{dep_name} formula was forbidden by #{owner} in `$HOMEBREW_FORBIDDEN_FORMULAE`.#{owner_contact}
EOS
end
end
private
# Landlock cannot protect `bin/brew` while allowing post-install writes to
# `bin`, so a malicious post-install could replace `brew` to persist into
# later invocations. Snapshot and restore the entry, using a pre-opened `bin`
# descriptor to restore its mode before repairing the entry if needed.
sig { params(block: T.proc.void).void }
def with_preserved_brew_file(&block)
return yield if Sandbox.full_write_isolation?
brew_file = HOMEBREW_PREFIX/"bin/brew"
File.open(brew_file.dirname) do |brew_directory|
brew_directory_mode = brew_directory.stat.mode & 07777
symlink = brew_file.symlink?
contents = if symlink
brew_file.readlink.to_s
else
brew_file.binread
end
brew_file_mode = brew_file.lstat.mode & 07777
begin
yield
ensure
brew_directory.chmod brew_directory_mode
if symlink && (!brew_file.symlink? || brew_file.readlink.to_s != contents)
FileUtils.rm_rf brew_file
brew_file.make_symlink contents
elsif !symlink && (brew_file.symlink? || !brew_file.file? || brew_file.binread != contents ||
(brew_file.lstat.mode & 07777) != brew_file_mode)
FileUtils.rm_rf brew_file
brew_file.atomic_write contents
brew_file.chmod brew_file_mode
end
end
end
end
# Whether to run the given install `step` (e.g. `"building"`) inside
# Homebrew's sandbox. Warns when it will not: noting reliance on the outer
# sandbox when `$HOMEBREW_AVOID_NESTED_SANDBOXING` skips a nested sandbox,
# otherwise that no sandbox is available.
sig { params(step: String).returns(T::Boolean) }
def use_sandbox?(step)
unless Sandbox.available?
opoo "Sandbox unavailable: #{step} without sandboxing!"
return false
end
if Sandbox.avoid_nested_sandboxing?
opoo "#{step.capitalize} without Homebrew's sandbox; relying on the outer sandbox."
return false
end
true
end
sig { returns(T::Boolean) }
def auto_link_versioned_keg_only?
return false unless installed_on_request?
return false unless formula.keg_only?
return false unless formula.keg_only_reason.versioned_formula?
return false if formula.any_version_installed?
return false if formula.link_overwrite_formulae.any? do |related_formula|
related_formula.any_version_installed? ||
(related_formula.name == formula.unversioned_formula_name && related_formula.keg_only?)
end
true
end
sig { void }
def lock
return unless self.class.locked.empty?
unless ignore_deps?
formula.recursive_dependencies.each do |dep|
self.class.locked << dep.to_formula
end
end
self.class.locked.unshift(formula)
self.class.locked.uniq!
self.class.locked.each(&:lock)
@hold_locks = true
end
sig { void }
def unlock
return unless @hold_locks
self.class.locked.each(&:unlock)
self.class.locked.clear
@hold_locks = false
end
sig { void }
def puts_requirement_messages
return if @requirement_messages.empty?
$stderr.puts @requirement_messages
end
end
require "extend/os/formula_installer"