aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/linkage_checker.rb
diff options
context:
space:
mode:
authorShaun Jackman2018-02-28 09:13:17 -0800
committerShaun Jackman2018-02-28 11:18:27 -0800
commit97932c1abfbeeeeb57c36de3382451d7167d7c7b (patch)
treea6618dd70dab52b04fbd6a0e9825fb8ce88a0cbc /Library/Homebrew/linkage_checker.rb
parent4176399537f6cff28e6e94b5473870f3d89d1024 (diff)
downloadbrew-97932c1abfbeeeeb57c36de3382451d7167d7c7b.tar.bz2
Move linkage_checker from os/mac to generic
Diffstat (limited to 'Library/Homebrew/linkage_checker.rb')
-rw-r--r--Library/Homebrew/linkage_checker.rb178
1 files changed, 178 insertions, 0 deletions
diff --git a/Library/Homebrew/linkage_checker.rb b/Library/Homebrew/linkage_checker.rb
new file mode 100644
index 000000000..cf6c12f22
--- /dev/null
+++ b/Library/Homebrew/linkage_checker.rb
@@ -0,0 +1,178 @@
+require "set"
+require "keg"
+require "formula"
+
+class LinkageChecker
+ attr_reader :keg, :formula
+ attr_reader :brewed_dylibs, :system_dylibs, :broken_dylibs, :variable_dylibs
+ attr_reader :undeclared_deps, :unnecessary_deps, :reverse_links
+
+ 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 = Set.new
+ @variable_dylibs = Set.new
+ @indirect_deps = []
+ @undeclared_deps = []
+ @reverse_links = Hash.new { |h, k| h[k] = Set.new }
+ @unnecessary_deps = []
+ check_dylibs
+ end
+
+ def check_dylibs
+ @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
+ 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)
+ @broken_dylibs << dylib
+ 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
+ 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)
+ recursive_deps = keg.to_formula.runtime_dependencies.map { |dep| dep.to_formula.full_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
+ [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
+
+ 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 "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
+ display_items "Missing libraries", @broken_dylibs
+ display_items "Possible unnecessary dependencies", @unnecessary_deps
+ puts "No broken dylib links" if @broken_dylibs.empty?
+ end
+
+ def broken_dylibs?
+ !@broken_dylibs.empty?
+ end
+
+ def undeclared_deps?
+ !@undeclared_deps.empty?
+ end
+
+ def unnecessary_deps?
+ !@unnecessary_deps.empty?
+ end
+
+ private
+
+ # 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)
+ return if things.empty?
+ puts "#{label}:"
+ if things.is_a? Hash
+ things.sort.each do |list_label, list|
+ list.sort.each do |item|
+ puts " #{item} (#{list_label})"
+ end
+ end
+ else
+ things.sort.each do |item|
+ puts " #{item}"
+ end
+ end
+ end
+
+ def resolve_formula(keg)
+ Formulary.from_keg(keg)
+ rescue FormulaUnavailableError
+ opoo "Formula unavailable: #{keg.name}"
+ end
+end