brew/Library/Homebrew/dependency.rb
Martin Afanasjew ea4d137e87 dependency: fix merging tags in 'merge_repeats'
While it may suffice to merge string and non-reserved tags by forming a
union of all tags of dependencies of the same name, this approach fails
to work for the reserved tags. These are now merged such that the most
restrictive tag (meaning sometimes an empty tag) is preserved.

The previous behavior caused essential dependencies to be omitted and
builds to fail in response. E.g., multiple `:fortran` dependencies with
tags `[]`, `[:recommended]`, and `[:optional]` would have been expanded
and merged to `"gcc"` with tags `[:recommended, :optional]`, causing it
to be no longer seen as a required dependency.

Closes Homebrew/homebrew#47040.

Signed-off-by: Martin Afanasjew <martin@afanasjew.de>
2015-12-17 13:50:38 +01:00

177 lines
4.2 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)
expanded_deps = []
deps.each do |dep|
# FIXME: don't hide cyclic dependencies
next if dependent.name == dep.name
case action(dependent, dep, &block)
when :prune
next
when :skip
expanded_deps.concat(expand(dep.to_formula, &block))
when :keep_but_prune_recursive_deps
expanded_deps << dep
else
expanded_deps.concat(expand(dep.to_formula, &block))
expanded_deps << dep
end
end
merge_repeats(expanded_deps)
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