# typed: strict # frozen_string_literal: true require "formula" 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 "install" require "messages" require "cask/cask_loader" require "cmd/install" require "find" require "utils/spdx" require "deprecate_disable" require "unlink" require "service" require "attestation" require "sbom" require "utils/fork" # Installer for a formula. class FormulaInstaller include FormulaCellarChecks 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 { params( formula: Formula, link_keg: T::Boolean, installed_as_dependency: 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[Formula], build_from_source_formulae: T::Array[Formula], 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_as_dependency: 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 @link_keg = T.let(!formula.keg_only? || link_keg, T::Boolean) @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 @installed_as_dependency = installed_as_dependency @installed_on_request = installed_on_request @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]]) @hold_locks = T.let(false, T::Boolean) @show_summary_heading = T.let(false, T::Boolean) @etc_var_preinstall = T.let([], T::Array[Pathname]) # 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 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_as_dependency? = @installed_as_dependency 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::Set[Formula]) } def self.attempted @attempted ||= T.let(Set.new, T.nilable(T::Set[Formula])) end sig { void } def self.clear_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 { void } def self.clear_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 { void } def self.clear_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.present? 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? end sig { void } def prelude 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 Tab.clear_cache verify_deps_exist unless ignore_deps? forbidden_license_check forbidden_tap_check forbidden_formula_check check_install_sanity install_fetch_deps unless ignore_deps? end sig { void } def verify_deps_exist begin compute_dependencies rescue TapFormulaUnavailableError => e raise if e.tap.installed? e.tap.ensure_installed! retry if e.tap.installed? # It may have not installed if it's a core tap. end 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 { |dep| [dep, dep.to_formula.recursive_dependencies] } 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 # Merge into one list recursive_deps = recursive_dep_map.flat_map { |dep, rdeps| [dep] + rdeps } Dependency.merge_repeats(recursive_deps) else recursive_deps = 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?(inherited_options_for(dep)) next if dep.build? && pour_bottle? next if dep.test? 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 install_fetch_deps return if @compute_dependencies.blank? compute_dependencies(use_cache: false) if @compute_dependencies.any? do |dep, options| next false unless dep.implicit? fetch_dependencies install_dependency(dep, options) 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| Pathname.new(file).cp_path_sub(HOMEBREW_PREFIX, formula.bottle_prefix) end end sig { void } def install lock start_time = Time.now Homebrew::Install.perform_build_from_source_checks if !pour_bottle? && DevelopmentTools.installed? # 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 ignore_interrupts do begin FileUtils.rm_r(formula.prefix) if formula.prefix.directory? rescue Errno::EACCES, Errno::ENOTEMPTY odie <<~EOS Could not remove #{formula.prefix.basename} keg! Do so manually: sudo rm -rf #{formula.prefix} EOS end formula.rack.rmdir_if_possible end 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 formula.local_bottle_path Utils::Bottles.formula_contents formula.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_as_dependency = installed_as_dependency? 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? conflicts = formula.conflicts.select do |c| 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, Options]]) } def compute_dependencies(use_cache: true) @compute_dependencies = T.let(nil, T.nilable(T::Array[[Dependency, Options]])) 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, Options]]).returns(T::Array[Formula]) } def unbottled_dependencies(deps) deps.map { |(dep, _options)| dep.to_formula }.reject do |dep_f| next false unless dep_f.pour_bottle? dep_f.bottled? end end sig { void } def compute_and_install_dependencies deps = compute_dependencies install_dependencies(deps) 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, _| Requirement.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 .each_with_object({}) { |dep, h| h[dep.name] = dep } while (f = formulae.pop) runtime_requirements = runtime_requirements(f) f.recursive_requirements do |dependent, req| 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) Requirement.prune else unsatisfied_reqs[dependent] << req end end end unsatisfied_reqs end sig { params(formula: Formula, inherited_options: T::Hash[String, Options]).returns(T::Array[Dependency]) } def expand_dependencies_for_formula(formula, inherited_options) # 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}" Dependency.expand(formula, cache_key:) do |dependent, dep| inherited_options[dep.name] |= inherited_options_for(dep) build = effective_build_options_for( dependent, inherited_options.fetch(dependent.name, Options.new), ) 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?) bottle_runtime_version = @bottle_tab_runtime_dependencies.dig(dep.name, "version").presence bottle_runtime_version = Version.new(bottle_runtime_version) if bottle_runtime_version bottle_runtime_revision = @bottle_tab_runtime_dependencies.dig(dep.name, "revision") if dep.prune_from_option?(build) || ((dep.build? || dep.test?) && !keep_build_test) Dependency.prune elsif dep.satisfied?(inherited_options[dep.name], minimum_version: bottle_runtime_version, minimum_revision: bottle_runtime_revision) Dependency.skip end end end sig { returns(T::Array[[Dependency, Options]]) } def expand_dependencies inherited_options = Hash.new { |hash, key| hash[key] = Options.new } expanded_deps = expand_dependencies_for_formula(formula, inherited_options) expanded_deps.map { |dep| [dep, inherited_options[dep.name]] } end sig { params(dependent: Formula, inherited_options: Options).returns(BuildOptions) } def effective_build_options_for(dependent, inherited_options = Options.new) args = dependent.build.used_options args |= (dependent == formula) ? options : inherited_options 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 options end sig { params(dep: Dependency).returns(Options) } def inherited_options_for(dep) inherited_options = Options.new u = Option.new("universal") if (options.include?(u) || formula.require_universal_deps?) && !dep.build? && dep.to_formula.option_defined?(u) inherited_options << u end inherited_options end sig { params(deps: T::Array[[Dependency, Options]]).void } def install_dependencies(deps) if deps.empty? && only_deps? puts "All dependencies for #{formula.full_name} are satisfied." elsif !deps.empty? oh1 "Installing dependencies for #{formula.full_name}: " \ "#{deps.map(&:first).map { Formatter.identifier(_1) }.to_sentence}", truncate: false deps.each { |dep, options| install_dependency(dep, options) } 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?, installed_as_dependency: true, 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.prelude fi.fetch end sig { params(dep: Dependency, inherited_options: Options).void } def install_dependency(dep, inherited_options) df = dep.to_formula if df.linked_keg.directory? linked_keg = Keg.new(df.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 df.latest_version_installed? installed_keg = Keg.new(df.prefix) tab ||= installed_keg.tab tmp_keg = Pathname.new("#{installed_keg}.tmp") installed_keg.rename(tmp_keg) end if df.tap.present? && tab.present? && (tab_tap = tab.source["tap"].presence) && df.tap.to_s != tab_tap.to_s odie <<~EOS #{df} is already installed from #{tab_tap}! Please `brew uninstall #{df}` first." EOS end options = Options.new options |= tab.used_options if tab.present? options |= Tab.remap_deprecated_options(df.deprecated_options, dep.options) options |= inherited_options options &= df.options installed_on_request = df.any_version_installed? && tab.present? && tab.installed_on_request installed_on_request ||= false fi = FormulaInstaller.new( df, options:, link_keg: keg_had_linked_keg && keg_was_linked, installed_as_dependency: true, 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)}" 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 if !installed_on_request? || installed_as_dependency? 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 { void } def finish return if only_deps? ohai "Finishing up" if verbose? keg = Keg.new(formula.prefix) if skip_link? unless quiet? ohai "Skipping 'link' on request" puts "You can run it manually using:" puts " brew link #{formula.full_name}" end else link(keg) end install_service fix_dynamic_linkage(keg) if !@poured_bottle || !formula.bottle_specification.skip_relocation? 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_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? LinkageChecker.new(keg, formula, cache_db: 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 # write/update a SBOM file (if we aren't bottling) unless build_bottle? 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" # 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 # 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 args = [ "nice", *HOMEBREW_RUBY_EXEC_ARGS, "--", HOMEBREW_LIBRARY_PATH/"build.rb", formula.specified_path, ].concat(build_argv) if Sandbox.available? sandbox = Sandbox.new formula.logs.mkpath sandbox.record_log(formula.logs/"build.sandbox.log") sandbox.allow_write_path(Dir.home) if interactive? 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 unless link_keg 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 cask_installed_with_formula_name = begin Cask::CaskLoader.load(formula.name, warn: false).installed? rescue Cask::CaskUnavailableError, Cask::CaskInvalidError false end if cask_installed_with_formula_name ohai "#{formula.name} cask is installed, skipping link." return end if keg.linked? opoo "This keg was marked linked already, continuing anyway" keg.remove_linked_keg_record end Homebrew::Unlink.unlink_versioned_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 # * 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. keg_formula_path = formula.opt_prefix/".brew/#{formula.name}.rb" return keg_formula_path if formula.loaded_from_api? return keg_formula_path if formula.local_bottle_path.present? tap_formula_path = T.must(formula.specified_path) 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 if Sandbox.available? 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.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 # 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, _options)| self.class.fetched.include?(dep.to_formula) end return if deps.empty? oh1 "Fetching dependencies for #{formula.full_name}: " \ "#{deps.map(&:first).map { Formatter.identifier(_1) }.to_sentence}", truncate: false deps.each { |(dep, _options)| fetch_dependency(dep) } 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).void } def fetch_bottle_tab(quiet: false) return if @fetch_bottle_tab begin formula.fetch_bottle_tab(quiet: quiet) @bottle_tab_runtime_dependencies = formula.bottle_tab_attributes .fetch("runtime_dependencies", []).then { |deps| deps || [] } .each_with_object({}) { |dep, h| h[dep["full_name"]] = dep } .freeze rescue DownloadError, Resource::BottleManifest::Error # do nothing end @fetch_bottle_tab = T.let(true, T.nilable(TrueClass)) end sig { void } def fetch return if previously_fetched_formula fetch_dependencies return if only_deps? return if formula.local_bottle_path.present? oh1 "Fetching #{Formatter.identifier(formula.full_name)}".strip downloadable_object = downloadable check_attestation = if pour_bottle?(output_warning: true) fetch_bottle_tab !downloadable_object.cached_download.exist? else @formula = Homebrew::API::Formula.source_download(formula) if formula.loaded_from_api? formula.fetch_patches formula.resources.each(&:fetch) downloadable_object = downloadable false end downloadable_object.fetch # 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. if check_attestation && Homebrew::Attestation.enabled? && formula.tap&.core_tap? && formula.name != "gh" ohai "Verifying attestation for #{formula.name}" begin Homebrew::Attestation.check_core_attestation T.cast(downloadable_object, Bottle) rescue Homebrew::Attestation::GhIncompatible # A small but significant number of users have developer mode enabled # but *also* haven't upgraded in a long time, meaning that their `gh` # version is too old to perform attestations. raise CannotInstallFormulaError, <<~EOS The bottle for #{formula.name} could not be verified. This typically indicates an outdated or incompatible `gh` CLI. Please confirm that you're running the latest version of `gh` by performing an upgrade before retrying: brew update brew upgrade gh EOS rescue Homebrew::Attestation::GhAuthInvalid # Only raise an error if we explicitly opted-in to verification. raise CannotInstallFormulaError, <<~EOS if Homebrew::EnvConfig.verify_attestations? The bottle for #{formula.name} could not be verified. This typically indicates an invalid GitHub API token. If you have `HOMEBREW_GITHUB_API_TOKEN` set, check it is correct or unset it and instead run: gh auth login EOS # If we didn't explicitly opt-in, then quietly opt-out in the case of invalid credentials. # Based on user reports, a significant number of users are running with stale tokens. ENV["HOMEBREW_NO_VERIFY_ATTESTATIONS"] = "1" rescue Homebrew::Attestation::GhAuthNeeded raise CannotInstallFormulaError, <<~EOS The bottle for #{formula.name} could not be verified. This typically indicates a missing GitHub API token, which you can resolve either by setting `HOMEBREW_GITHUB_API_TOKEN` or by running: gh auth login EOS rescue Homebrew::Attestation::MissingAttestationError, Homebrew::Attestation::InvalidAttestationError => e raise CannotInstallFormulaError, <<~EOS The bottle for #{formula.name} has an invalid build provenance attestation. This may indicate that the bottle was not produced by the expected tap, or was maliciously inserted into the expected tap's bottle storage. Additional context: #{e} EOS end end 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) elsif pour_bottle? T.must(formula.bottle) else T.must(formula.resource) end end sig { void } def pour HOMEBREW_CELLAR.cd do downloadable.downloader.stage 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.installed_as_dependency = installed_as_dependency? 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? keg.replace_placeholders_with_locations(tab.changed_files, skip_linkage:) cellar = formula.bottle_specification.tag_to_cellar(Utils::Bottles.tag) return if [:any, :any_skip_relocation].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| 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 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, _options)| 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 { void } def forbidden_tap_check 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 unless ignore_deps? compute_dependencies.each do |(dep, _options)| 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 return if only_deps? formula_tap = formula.tap return if formula_tap.blank? || (formula_tap.allowed_by_env? && !formula_tap.forbidden_by_env?) error_message = "The installation of #{formula.full_name} has the tap #{formula_tap}\n" \ "but #{owner} " error_message << "has not allowed this tap in `HOMEBREW_ALLOWED_TAPS`" unless formula_tap.allowed_by_env? error_message << " and\n" if !formula_tap.allowed_by_env? && formula_tap.forbidden_by_env? error_message << "has forbidden this tap in `HOMEBREW_FORBIDDEN_TAPS`" if formula_tap.forbidden_by_env? error_message << ".#{owner_contact}" raise CannotInstallFormulaError, error_message end sig { void } def forbidden_formula_check 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 ignore_deps? compute_dependencies.each do |(dep, _options)| 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 return if only_deps? formula_name = if forbidden_formulae.include?(formula.name) formula.name elsif forbidden_formulae.include?(formula.full_name) formula.full_name else return end raise CannotInstallFormulaError, <<~EOS The installation of #{formula_name} was forbidden by #{owner} in `HOMEBREW_FORBIDDEN_FORMULAE`.#{owner_contact} EOS end private 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"