brew/Library/Homebrew/linkage_checker.rb
Mike McQuaid 4a03145c1c linkage: fix --test exit code.
Ensure that a non-zero exit code is set both for missing random dylibs
and random missing dependencies.

Additionally, while we are here, drastically trim down the public
interface for this class to the bare minimum and allow getting the
output from `display_test_output` as a variable.

Fixes issue mentioned by @ilovezfs in:
https://github.com/Homebrew/brew/pull/3940#issuecomment-383794520
2018-04-24 09:52:51 +01:00

187 lines
5.7 KiB
Ruby

require "keg"
require "formula"
class LinkageChecker
def initialize(keg, formula = nil)
@keg = keg
@formula = formula || resolve_formula(keg)
@brewed_dylibs = Hash.new { |h, k| h[k] = Set.new }
@system_dylibs = Set.new
@broken_dylibs = []
@broken_deps = Hash.new { |h, k| h[k] = [] }
@variable_dylibs = Set.new
@indirect_deps = []
@undeclared_deps = []
@reverse_links = Hash.new { |h, k| h[k] = Set.new }
@unnecessary_deps = []
check_dylibs
end
def display_normal_output
display_items "System libraries", @system_dylibs
display_items "Homebrew libraries", @brewed_dylibs
display_items "Indirect dependencies with linkage", @indirect_deps
display_items "Variable-referenced libraries", @variable_dylibs
display_items "Missing libraries", @broken_dylibs
display_items "Broken dependencies", @broken_deps
display_items "Undeclared dependencies with linkage", @undeclared_deps
display_items "Dependencies with no linkage", @unnecessary_deps
end
def display_reverse_output
return if @reverse_links.empty?
sorted = @reverse_links.sort
sorted.each do |dylib, files|
puts dylib
files.each do |f|
unprefixed = f.to_s.strip_prefix "#{@keg}/"
puts " #{unprefixed}"
end
puts unless dylib == sorted.last[0]
end
end
def display_test_output(puts_output: true)
display_items "Missing libraries", @broken_dylibs, puts_output: puts_output
display_items "Broken dependencies", @broken_deps, puts_output: puts_output
puts "No broken library linkage" unless broken_library_linkage?
end
def broken_library_linkage?
!@broken_dylibs.empty? || !@broken_deps.empty?
end
private
attr_reader :keg, :formula
def dylib_to_dep(dylib)
dylib =~ %r{#{Regexp.escape(HOMEBREW_PREFIX)}/(opt|Cellar)/([\w+-.@]+)/}
Regexp.last_match(2)
end
def check_dylibs
checked_dylibs = Set.new
@keg.find do |file|
next if file.symlink? || file.directory?
next unless file.dylib? || file.binary_executable? || file.mach_o_bundle?
# weakly loaded dylibs may not actually exist on disk, so skip them
# when checking for broken linkage
file.dynamically_linked_libraries(except: :LC_LOAD_WEAK_DYLIB).each do |dylib|
@reverse_links[dylib] << file
next if checked_dylibs.include? dylib
if dylib.start_with? "@"
@variable_dylibs << dylib
else
begin
owner = Keg.for Pathname.new(dylib)
rescue NotAKegError
@system_dylibs << dylib
rescue Errno::ENOENT
next if harmless_broken_link?(dylib)
if (dep = dylib_to_dep(dylib))
@broken_deps[dep] |= [dylib]
else
@broken_dylibs << dylib
end
else
tap = Tab.for_keg(owner).tap
f = if tap.nil? || tap.core_tap?
owner.name
else
"#{tap}/#{owner.name}"
end
@brewed_dylibs[f] << dylib
end
end
checked_dylibs << dylib
end
end
@indirect_deps, @undeclared_deps, @unnecessary_deps = check_undeclared_deps if formula
end
def check_undeclared_deps
filter_out = proc do |dep|
next true if dep.build?
next false unless dep.optional? || dep.recommended?
formula.build.without?(dep)
end
declared_deps = formula.deps.reject { |dep| filter_out.call(dep) }.map(&:name)
runtime_deps = keg.to_formula.runtime_dependencies(read_from_tab: false)
recursive_deps = runtime_deps.map { |dep| dep.to_formula.name }
declared_dep_names = declared_deps.map { |dep| dep.split("/").last }
indirect_deps = []
undeclared_deps = []
@brewed_dylibs.each_key do |full_name|
name = full_name.split("/").last
next if name == formula.name
if recursive_deps.include?(name)
indirect_deps << full_name unless declared_dep_names.include?(name)
else
undeclared_deps << full_name
end
end
sort_by_formula_full_name!(indirect_deps)
sort_by_formula_full_name!(undeclared_deps)
unnecessary_deps = declared_dep_names.reject do |full_name|
name = full_name.split("/").last
next true if Formula[name].bin.directory?
@brewed_dylibs.keys.map { |x| x.split("/").last }.include?(name)
end
missing_deps = @broken_deps.values.flatten.map { |d| dylib_to_dep(d) }
unnecessary_deps -= missing_deps
[indirect_deps, undeclared_deps, unnecessary_deps]
end
def sort_by_formula_full_name!(arr)
arr.sort! do |a, b|
if a.include?("/") && !b.include?("/")
1
elsif !a.include?("/") && b.include?("/")
-1
else
a <=> b
end
end
end
# Whether or not dylib is a harmless broken link, meaning that it's
# okay to skip (and not report) as broken.
def harmless_broken_link?(dylib)
# libgcc_s_* is referenced by programs that use the Java Service Wrapper,
# and is harmless on x86(_64) machines
[
"/usr/lib/libgcc_s_ppc64.1.dylib",
"/opt/local/lib/libgcc/libgcc_s.1.dylib",
].include?(dylib)
end
# Display a list of things.
# Things may either be an array, or a hash of (label -> array)
def display_items(label, things, puts_output: true)
return if things.empty?
output = "#{label}:"
if things.is_a? Hash
things.keys.sort.each do |list_label|
things[list_label].sort.each do |item|
output += "\n #{item} (#{list_label})"
end
end
else
things.sort.each do |item|
output += "\n #{item}"
end
end
puts output if puts_output
output
end
def resolve_formula(keg)
Formulary.from_keg(keg)
rescue FormulaUnavailableError
opoo "Formula unavailable: #{keg.name}"
end
end