brew/Library/Homebrew/dependency.rb
Xu Cheng 8749ecc383 Dependency.expand: ensure pop stack
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.
2016-02-07 14:58:05 +08:00

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