mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

During the dependencies expansion, there may be errors (e.g. FormulaUnavaiableError). As result, some deps will be left behind in the stack and interfere afterwards dependencies expansion. So let's ensure stack clean for each expansions. Fixes Homebrew/homebrew#48834.
184 lines
4.4 KiB
Ruby
184 lines
4.4 KiB
Ruby
require "dependable"
|
|
|
|
# A dependency on another Homebrew formula.
|
|
class Dependency
|
|
include Dependable
|
|
|
|
attr_reader :name, :tags, :env_proc, :option_names
|
|
|
|
DEFAULT_ENV_PROC = proc {}
|
|
|
|
def initialize(name, tags = [], env_proc = DEFAULT_ENV_PROC, option_names = [name])
|
|
@name = name
|
|
@tags = tags
|
|
@env_proc = env_proc
|
|
@option_names = option_names
|
|
end
|
|
|
|
def to_s
|
|
name
|
|
end
|
|
|
|
def ==(other)
|
|
instance_of?(other.class) && name == other.name && tags == other.tags
|
|
end
|
|
alias_method :eql?, :==
|
|
|
|
def hash
|
|
name.hash ^ tags.hash
|
|
end
|
|
|
|
def to_formula
|
|
formula = Formulary.factory(name)
|
|
formula.build = BuildOptions.new(options, formula.options)
|
|
formula
|
|
end
|
|
|
|
def installed?
|
|
to_formula.installed?
|
|
end
|
|
|
|
def satisfied?(inherited_options)
|
|
installed? && missing_options(inherited_options).empty?
|
|
end
|
|
|
|
def missing_options(inherited_options)
|
|
required = options | inherited_options
|
|
required - Tab.for_formula(to_formula).used_options
|
|
end
|
|
|
|
def modify_build_environment
|
|
env_proc.call unless env_proc.nil?
|
|
end
|
|
|
|
def inspect
|
|
"#<#{self.class.name}: #{name.inspect} #{tags.inspect}>"
|
|
end
|
|
|
|
# Define marshaling semantics because we cannot serialize @env_proc
|
|
def _dump(*)
|
|
Marshal.dump([name, tags])
|
|
end
|
|
|
|
def self._load(marshaled)
|
|
new(*Marshal.load(marshaled))
|
|
end
|
|
|
|
class << self
|
|
# Expand the dependencies of dependent recursively, optionally yielding
|
|
# [dependent, dep] pairs to allow callers to apply arbitrary filters to
|
|
# the list.
|
|
# The default filter, which is applied when a block is not given, omits
|
|
# optionals and recommendeds based on what the dependent has asked for.
|
|
def expand(dependent, deps = dependent.deps, &block)
|
|
# Keep track dependencies to avoid infinite cyclic dependency recursion.
|
|
@expand_stack ||= []
|
|
@expand_stack.push dependent.name
|
|
|
|
expanded_deps = []
|
|
|
|
deps.each do |dep|
|
|
next if dependent.name == dep.name
|
|
|
|
case action(dependent, dep, &block)
|
|
when :prune
|
|
next
|
|
when :skip
|
|
next if @expand_stack.include? dep.name
|
|
expanded_deps.concat(expand(dep.to_formula, &block))
|
|
when :keep_but_prune_recursive_deps
|
|
expanded_deps << dep
|
|
else
|
|
next if @expand_stack.include? dep.name
|
|
expanded_deps.concat(expand(dep.to_formula, &block))
|
|
expanded_deps << dep
|
|
end
|
|
end
|
|
|
|
merge_repeats(expanded_deps)
|
|
ensure
|
|
@expand_stack.pop
|
|
end
|
|
|
|
def action(dependent, dep, &_block)
|
|
catch(:action) do
|
|
if block_given?
|
|
yield dependent, dep
|
|
elsif dep.optional? || dep.recommended?
|
|
prune unless dependent.build.with?(dep)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Prune a dependency and its dependencies recursively
|
|
def prune
|
|
throw(:action, :prune)
|
|
end
|
|
|
|
# Prune a single dependency but do not prune its dependencies
|
|
def skip
|
|
throw(:action, :skip)
|
|
end
|
|
|
|
# Keep a dependency, but prune its dependencies
|
|
def keep_but_prune_recursive_deps
|
|
throw(:action, :keep_but_prune_recursive_deps)
|
|
end
|
|
|
|
def merge_repeats(all)
|
|
grouped = all.group_by(&:name)
|
|
|
|
all.map(&:name).uniq.map do |name|
|
|
deps = grouped.fetch(name)
|
|
dep = deps.first
|
|
tags = merge_tags(deps)
|
|
option_names = deps.flat_map(&:option_names).uniq
|
|
dep.class.new(name, tags, dep.env_proc, option_names)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def merge_tags(deps)
|
|
options = deps.flat_map(&:option_tags).uniq
|
|
merge_necessity(deps) + merge_temporality(deps) + options
|
|
end
|
|
|
|
def merge_necessity(deps)
|
|
# Cannot use `deps.any?(&:required?)` here due to its definition.
|
|
if deps.any? { |dep| !dep.recommended? && !dep.optional? }
|
|
[] # Means required dependency.
|
|
elsif deps.any?(&:recommended?)
|
|
[:recommended]
|
|
else # deps.all?(&:optional?)
|
|
[:optional]
|
|
end
|
|
end
|
|
|
|
def merge_temporality(deps)
|
|
if deps.all?(&:build?)
|
|
[:build]
|
|
elsif deps.all?(&:run?)
|
|
[:run]
|
|
else
|
|
[] # Means both build and runtime dependency.
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class TapDependency < Dependency
|
|
attr_reader :tap
|
|
|
|
def initialize(name, tags = [], env_proc = DEFAULT_ENV_PROC, option_names = [name.split("/").last])
|
|
@tap = name.rpartition("/").first
|
|
super(name, tags, env_proc, option_names)
|
|
end
|
|
|
|
def installed?
|
|
super
|
|
rescue FormulaUnavailableError
|
|
false
|
|
end
|
|
end
|