diff options
| author | Alyssa Ross | 2018-03-01 17:24:34 +0000 | 
|---|---|---|
| committer | GitHub | 2018-03-01 17:24:34 +0000 | 
| commit | 43c658a8fa966c9cb66386961a77c041ebca89dd (patch) | |
| tree | 4795accfe1defa1f4a9aac9127a497bb6ea9def0 /Library/Homebrew/linkage_checker.rb | |
| parent | 9225dac27f88b325692b5b66a59c498cb48180a1 (diff) | |
| parent | 97932c1abfbeeeeb57c36de3382451d7167d7c7b (diff) | |
| download | brew-43c658a8fa966c9cb66386961a77c041ebca89dd.tar.bz2 | |
Merge pull request #3852 from sjackman/linkage_checker1.5.6
Move linkage_checker from os/mac to generic
Diffstat (limited to 'Library/Homebrew/linkage_checker.rb')
| -rw-r--r-- | Library/Homebrew/linkage_checker.rb | 178 | 
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 | 
