Merge pull request #3720 from AndrewMcBurney/cache-optimization
Cache optimization for `brew linkage` command.
This commit is contained in:
		
						commit
						97645d061d
					
				
							
								
								
									
										148
									
								
								Library/Homebrew/cache_store.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								Library/Homebrew/cache_store.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,148 @@
 | 
			
		||||
require "dbm"
 | 
			
		||||
require "json"
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# `CacheStoreDatabase` acts as an interface to a persistent storage mechanism
 | 
			
		||||
# residing in the `HOMEBREW_CACHE`
 | 
			
		||||
#
 | 
			
		||||
class CacheStoreDatabase
 | 
			
		||||
  # Yields the cache store database.
 | 
			
		||||
  # Closes the database after use if it has been loaded.
 | 
			
		||||
  #
 | 
			
		||||
  # @param  [Symbol] type
 | 
			
		||||
  # @yield  [CacheStoreDatabase] self
 | 
			
		||||
  def self.use(type)
 | 
			
		||||
    database = CacheStoreDatabase.new(type)
 | 
			
		||||
    return_value = yield(database)
 | 
			
		||||
    database.close_if_open!
 | 
			
		||||
    return_value
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Sets a value in the underlying database (and creates it if necessary).
 | 
			
		||||
  def set(key, value)
 | 
			
		||||
    db[key] = value
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Gets a value from the underlying database (if it already exists).
 | 
			
		||||
  def get(key)
 | 
			
		||||
    return unless created?
 | 
			
		||||
    db[key]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Gets a value from the underlying database (if it already exists).
 | 
			
		||||
  def delete(key)
 | 
			
		||||
    return unless created?
 | 
			
		||||
    db.delete(key)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Closes the underlying database (if it created and open).
 | 
			
		||||
  def close_if_open!
 | 
			
		||||
    @db&.close
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns `true` if the cache file has been created for the given `@type`
 | 
			
		||||
  #
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def created?
 | 
			
		||||
    File.exist?(cache_path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  # The mode of any created files will be 0664 (that is, readable and writable
 | 
			
		||||
  # by the owner and the group, and readable by everyone else). Files created
 | 
			
		||||
  # will also be modified by the process' umask value at the time of creation:
 | 
			
		||||
  #   https://docs.oracle.com/cd/E17276_01/html/api_reference/C/envopen.html
 | 
			
		||||
  DATABASE_MODE = 0664
 | 
			
		||||
 | 
			
		||||
  # Lazily loaded database in read/write mode. If this method is called, a
 | 
			
		||||
  # database file with be created in the `HOMEBREW_CACHE` with name
 | 
			
		||||
  # corresponding to the `@type` instance variable
 | 
			
		||||
  #
 | 
			
		||||
  # @return [DBM] db
 | 
			
		||||
  def db
 | 
			
		||||
    # DBM::WRCREAT: Creates the database if it does not already exist
 | 
			
		||||
    @db ||= DBM.open(dbm_file_path, DATABASE_MODE, DBM::WRCREAT)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Creates a CacheStoreDatabase
 | 
			
		||||
  #
 | 
			
		||||
  # @param  [Symbol] type
 | 
			
		||||
  # @return [nil]
 | 
			
		||||
  def initialize(type)
 | 
			
		||||
    @type = type
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # `DBM` appends `.db` file extension to the path provided, which is why it's
 | 
			
		||||
  # not included
 | 
			
		||||
  #
 | 
			
		||||
  # @return [String]
 | 
			
		||||
  def dbm_file_path
 | 
			
		||||
    File.join(HOMEBREW_CACHE, @type.to_s)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # The path where the database resides in the `HOMEBREW_CACHE` for the given
 | 
			
		||||
  # `@type`
 | 
			
		||||
  #
 | 
			
		||||
  # @return [String]
 | 
			
		||||
  def cache_path
 | 
			
		||||
    "#{dbm_file_path}.db"
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# `CacheStore` provides methods to mutate and fetch data from a persistent
 | 
			
		||||
# storage mechanism
 | 
			
		||||
#
 | 
			
		||||
class CacheStore
 | 
			
		||||
  # @param  [CacheStoreDatabase] database
 | 
			
		||||
  # @return [nil]
 | 
			
		||||
  def initialize(database)
 | 
			
		||||
    @database = database
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Inserts new values or updates existing cached values to persistent storage
 | 
			
		||||
  # mechanism
 | 
			
		||||
  #
 | 
			
		||||
  # @abstract
 | 
			
		||||
  def update!(*)
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Fetches cached values in persistent storage according to the type of data
 | 
			
		||||
  # stored
 | 
			
		||||
  #
 | 
			
		||||
  # @abstract
 | 
			
		||||
  def fetch_type(*)
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Deletes data from the cache based on a condition defined in a concrete class
 | 
			
		||||
  #
 | 
			
		||||
  # @abstract
 | 
			
		||||
  def flush_cache!
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  protected
 | 
			
		||||
 | 
			
		||||
  # @return [CacheStoreDatabase]
 | 
			
		||||
  attr_reader :database
 | 
			
		||||
 | 
			
		||||
  # DBM stores ruby objects as a ruby `String`. Hence, when fetching the data,
 | 
			
		||||
  # to convert the ruby string back into a ruby `Hash`, the string is converted
 | 
			
		||||
  # into a JSON compatible string in `ruby_hash_to_json_string`, where it may
 | 
			
		||||
  # later be parsed by `JSON.parse` in the `json_string_to_ruby_hash` method
 | 
			
		||||
  #
 | 
			
		||||
  # @param  [Hash] ruby `Hash` to be converted to `JSON` string
 | 
			
		||||
  # @return [String]
 | 
			
		||||
  def ruby_hash_to_json_string(hash)
 | 
			
		||||
    hash.to_json
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @param  [String] `JSON` string to be converted to ruby `Hash`
 | 
			
		||||
  # @return [Hash]
 | 
			
		||||
  def json_string_to_ruby_hash(string)
 | 
			
		||||
    JSON.parse(string)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
#:  * `linkage` [`--test`] [`--reverse`] <formula>:
 | 
			
		||||
#:  * `linkage` [`--test`] [`--reverse`] [`--cached`] <formula>:
 | 
			
		||||
#:    Checks the library links of an installed formula.
 | 
			
		||||
#:
 | 
			
		||||
#:    Only works on installed formulae. An error is raised if it is run on
 | 
			
		||||
@ -9,23 +9,32 @@
 | 
			
		||||
#:
 | 
			
		||||
#:    If `--reverse` is passed, print the dylib followed by the binaries
 | 
			
		||||
#:    which link to it for each library the keg references.
 | 
			
		||||
#:
 | 
			
		||||
#:    If `--cached` is passed, print the cached linkage values stored in
 | 
			
		||||
#:    HOMEBREW_CACHE, set from a previous `brew linkage` run
 | 
			
		||||
 | 
			
		||||
require "cache_store"
 | 
			
		||||
require "linkage_checker"
 | 
			
		||||
 | 
			
		||||
module Homebrew
 | 
			
		||||
  module_function
 | 
			
		||||
 | 
			
		||||
  def linkage
 | 
			
		||||
    ARGV.kegs.each do |keg|
 | 
			
		||||
      ohai "Checking #{keg.name} linkage" if ARGV.kegs.size > 1
 | 
			
		||||
      result = LinkageChecker.new(keg)
 | 
			
		||||
      if ARGV.include?("--test")
 | 
			
		||||
        result.display_test_output
 | 
			
		||||
        Homebrew.failed = true if result.broken_library_linkage?
 | 
			
		||||
      elsif ARGV.include?("--reverse")
 | 
			
		||||
        result.display_reverse_output
 | 
			
		||||
      else
 | 
			
		||||
        result.display_normal_output
 | 
			
		||||
    CacheStoreDatabase.use(:linkage) do |db|
 | 
			
		||||
      ARGV.kegs.each do |keg|
 | 
			
		||||
        ohai "Checking #{keg.name} linkage" if ARGV.kegs.size > 1
 | 
			
		||||
 | 
			
		||||
        use_cache = ARGV.include?("--cached") || ENV["HOMEBREW_LINKAGE_CACHE"]
 | 
			
		||||
        result = LinkageChecker.new(keg, use_cache: use_cache, cache_db: db)
 | 
			
		||||
 | 
			
		||||
        if ARGV.include?("--test")
 | 
			
		||||
          result.display_test_output
 | 
			
		||||
          Homebrew.failed = true if result.broken_library_linkage?
 | 
			
		||||
        elsif ARGV.include?("--reverse")
 | 
			
		||||
          result.display_reverse_output
 | 
			
		||||
        else
 | 
			
		||||
          result.display_normal_output
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
require "cache_store"
 | 
			
		||||
require "linkage_checker"
 | 
			
		||||
 | 
			
		||||
module FormulaCellarChecks
 | 
			
		||||
@ -64,22 +65,28 @@ module FormulaCellarChecks
 | 
			
		||||
  def check_linkage
 | 
			
		||||
    return unless formula.prefix.directory?
 | 
			
		||||
    keg = Keg.new(formula.prefix)
 | 
			
		||||
    checker = LinkageChecker.new(keg, formula)
 | 
			
		||||
 | 
			
		||||
    return unless checker.broken_library_linkage?
 | 
			
		||||
    output = <<~EOS
 | 
			
		||||
      #{formula} has broken dynamic library links:
 | 
			
		||||
        #{checker.display_test_output}
 | 
			
		||||
    EOS
 | 
			
		||||
    tab = Tab.for_keg(keg)
 | 
			
		||||
    if tab.poured_from_bottle
 | 
			
		||||
      output += <<~EOS
 | 
			
		||||
        Rebuild this from source with:
 | 
			
		||||
          brew reinstall --build-from-source #{formula}
 | 
			
		||||
        If that's successful, file an issue#{formula.tap ? " here:\n  #{formula.tap.issues_url}" : "."}
 | 
			
		||||
    CacheStoreDatabase.use(:linkage) do |db|
 | 
			
		||||
      checker = LinkageChecker.new(
 | 
			
		||||
        keg, formula, cache_db: db, use_cache: !ENV["HOMEBREW_LINKAGE_CACHE"].nil?
 | 
			
		||||
      )
 | 
			
		||||
      next unless checker.broken_library_linkage?
 | 
			
		||||
 | 
			
		||||
      output = <<~EOS
 | 
			
		||||
        #{formula} has broken dynamic library links:
 | 
			
		||||
          #{checker.display_test_output}
 | 
			
		||||
      EOS
 | 
			
		||||
 | 
			
		||||
      tab = Tab.for_keg(keg)
 | 
			
		||||
      if tab.poured_from_bottle
 | 
			
		||||
        output += <<~EOS
 | 
			
		||||
          Rebuild this from source with:
 | 
			
		||||
            brew reinstall --build-from-source #{formula}
 | 
			
		||||
          If that's successful, file an issue#{formula.tap ? " here:\n  #{formula.tap.issues_url}" : "."}
 | 
			
		||||
        EOS
 | 
			
		||||
      end
 | 
			
		||||
      problem_if_output output
 | 
			
		||||
    end
 | 
			
		||||
    problem_if_output output
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def audit_installed
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
require "cache_store"
 | 
			
		||||
require "formula_support"
 | 
			
		||||
require "lock_file"
 | 
			
		||||
require "formula_pin"
 | 
			
		||||
@ -1535,8 +1536,14 @@ class Formula
 | 
			
		||||
    keg = opt_or_installed_prefix_keg
 | 
			
		||||
    return [] unless keg
 | 
			
		||||
 | 
			
		||||
    linkage_checker = LinkageChecker.new(keg, self)
 | 
			
		||||
    linkage_checker.undeclared_deps.map { |n| Dependency.new(n) }
 | 
			
		||||
    undeclared_deps = CacheStoreDatabase.use(:linkage) do |db|
 | 
			
		||||
      linkage_checker = LinkageChecker.new(
 | 
			
		||||
        keg, self, cache_db: db, use_cache: !ENV["HOMEBREW_LINKAGE_CACHE"].nil?
 | 
			
		||||
      )
 | 
			
		||||
      linkage_checker.undeclared_deps.map { |n| Dependency.new(n) }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    undeclared_deps
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns a list of formulae depended on by this formula that aren't
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,8 @@ require "debrew"
 | 
			
		||||
require "sandbox"
 | 
			
		||||
require "emoji"
 | 
			
		||||
require "development_tools"
 | 
			
		||||
require "cache_store"
 | 
			
		||||
require "linkage_checker"
 | 
			
		||||
 | 
			
		||||
class FormulaInstaller
 | 
			
		||||
  include FormulaCellarChecks
 | 
			
		||||
@ -607,6 +609,12 @@ class FormulaInstaller
 | 
			
		||||
    ohai "Summary" if verbose? || show_summary_heading?
 | 
			
		||||
    puts summary
 | 
			
		||||
 | 
			
		||||
    # Updates the cache for a particular formula after doing an install
 | 
			
		||||
    CacheStoreDatabase.use(:linkage) do |db|
 | 
			
		||||
      break unless db.created?
 | 
			
		||||
      LinkageChecker.new(keg, formula, cache_db: db)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # let's reset Utils.git_available? if we just installed git
 | 
			
		||||
    Utils.clear_git_available_cache if formula.name == "git"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										123
									
								
								Library/Homebrew/linkage_cache_store.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								Library/Homebrew/linkage_cache_store.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
			
		||||
require "set"
 | 
			
		||||
require "cache_store"
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# `LinkageCacheStore` provides methods to fetch and mutate linkage-specific data used
 | 
			
		||||
# by the `brew linkage` command
 | 
			
		||||
#
 | 
			
		||||
class LinkageCacheStore < CacheStore
 | 
			
		||||
  # @param  [String] keg_name
 | 
			
		||||
  # @param  [CacheStoreDatabase] database
 | 
			
		||||
  # @return [nil]
 | 
			
		||||
  def initialize(keg_name, database)
 | 
			
		||||
    @keg_name = keg_name
 | 
			
		||||
    super(database)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns `true` if the database has any value for the current `keg_name`
 | 
			
		||||
  #
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def keg_exists?
 | 
			
		||||
    !database.get(keg_name).nil?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Inserts dylib-related information into the cache if it does not exist or
 | 
			
		||||
  # updates data into the linkage cache if it does exist
 | 
			
		||||
  #
 | 
			
		||||
  # @param  [Hash] array_values: hash containing KVPs of { :type => Array | Set }
 | 
			
		||||
  # @param  [Hash] hash_values:  hash containing KVPs of { :type => Hash }
 | 
			
		||||
  # @param  [Array[Hash]] values
 | 
			
		||||
  # @raise  [TypeError] error if the values are not `Arary`, `Set`, or `Hash`
 | 
			
		||||
  # @return [nil]
 | 
			
		||||
  def update!(array_values: {}, hash_values: {}, **values)
 | 
			
		||||
    values.each do |key, value|
 | 
			
		||||
      if value.is_a?(Hash)
 | 
			
		||||
        hash_values[key] = value
 | 
			
		||||
      elsif value.is_a?(Array) || value.is_a?(Set)
 | 
			
		||||
        array_values[key] = value
 | 
			
		||||
      else
 | 
			
		||||
        raise TypeError, <<~EOS
 | 
			
		||||
          Can't store types that are not `Array`, `Set` or `Hash` in the
 | 
			
		||||
          linkage store.
 | 
			
		||||
        EOS
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    database.set keg_name, ruby_hash_to_json_string(
 | 
			
		||||
      array_values: format_array_values(array_values),
 | 
			
		||||
      hash_values: format_hash_values(hash_values),
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @param  [Symbol] the type to fetch from the `LinkageCacheStore`
 | 
			
		||||
  # @raise  [TypeError] error if the type is not in `HASH_LINKAGE_TYPES` or
 | 
			
		||||
  #   `ARRAY_LINKAGE_TYPES`
 | 
			
		||||
  # @return [Hash | Array]
 | 
			
		||||
  def fetch_type(type)
 | 
			
		||||
    if HASH_LINKAGE_TYPES.include?(type)
 | 
			
		||||
      fetch_hash_values(type)
 | 
			
		||||
    elsif ARRAY_LINKAGE_TYPES.include?(type)
 | 
			
		||||
      fetch_array_values(type)
 | 
			
		||||
    else
 | 
			
		||||
      raise TypeError, <<~EOS
 | 
			
		||||
        Can't fetch types that are not defined for the linkage store
 | 
			
		||||
      EOS
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @return [nil]
 | 
			
		||||
  def flush_cache!
 | 
			
		||||
    database.delete(keg_name)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  ARRAY_LINKAGE_TYPES = [
 | 
			
		||||
    :system_dylibs, :variable_dylibs, :broken_dylibs, :indirect_deps,
 | 
			
		||||
    :undeclared_deps, :unnecessary_deps
 | 
			
		||||
  ].freeze
 | 
			
		||||
  HASH_LINKAGE_TYPES = [:brewed_dylibs, :reverse_links, :broken_deps].freeze
 | 
			
		||||
 | 
			
		||||
  # @return [String] the key to lookup items in the `CacheStore`
 | 
			
		||||
  attr_reader :keg_name
 | 
			
		||||
 | 
			
		||||
  # @param  [Symbol] the type to fetch from the `LinkageCacheStore`
 | 
			
		||||
  # @return [Array]
 | 
			
		||||
  def fetch_array_values(type)
 | 
			
		||||
    keg_cache = database.get(keg_name)
 | 
			
		||||
    return [] unless keg_cache
 | 
			
		||||
    json_string_to_ruby_hash(keg_cache)["array_values"][type.to_s]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @param  [Symbol] type
 | 
			
		||||
  # @return [Hash]
 | 
			
		||||
  def fetch_hash_values(type)
 | 
			
		||||
    keg_cache = database.get(keg_name)
 | 
			
		||||
    return {} unless keg_cache
 | 
			
		||||
    json_string_to_ruby_hash(keg_cache)["hash_values"][type.to_s]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Formats the linkage data for `array_values` into a kind which can be parsed
 | 
			
		||||
  # by the `json_string_to_ruby_hash` method. Internally converts ruby `Set`s to
 | 
			
		||||
  # `Array`s
 | 
			
		||||
  #
 | 
			
		||||
  # @param  [Hash]
 | 
			
		||||
  # @return [String]
 | 
			
		||||
  def format_array_values(hash)
 | 
			
		||||
    hash.each_with_object({}) { |(k, v), h| h[k] = v.to_a }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Formats the linkage data for `hash_values` into a kind which can be parsed
 | 
			
		||||
  # by the `json_string_to_ruby_hash` method. Converts ruby `Set`s to `Array`s,
 | 
			
		||||
  # and converts ruby `Pathname`s to `String`s
 | 
			
		||||
  #
 | 
			
		||||
  # @param  [Hash]
 | 
			
		||||
  # @return [String]
 | 
			
		||||
  def format_hash_values(hash)
 | 
			
		||||
    hash.each_with_object({}) do |(outer_key, outer_values), outer_hash|
 | 
			
		||||
      outer_hash[outer_key] = outer_values.each_with_object({}) do |(k, v), h|
 | 
			
		||||
        h[k] = v.to_a.map(&:to_s)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -1,42 +1,38 @@
 | 
			
		||||
require "keg"
 | 
			
		||||
require "formula"
 | 
			
		||||
require "linkage_cache_store"
 | 
			
		||||
 | 
			
		||||
class LinkageChecker
 | 
			
		||||
  attr_reader :undeclared_deps
 | 
			
		||||
 | 
			
		||||
  def initialize(keg, formula = nil)
 | 
			
		||||
  def initialize(keg, formula = nil, use_cache: false, cache_db:)
 | 
			
		||||
    @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
 | 
			
		||||
 | 
			
		||||
    if use_cache
 | 
			
		||||
      @store = LinkageCacheStore.new(keg.name, cache_db)
 | 
			
		||||
      flush_cache_and_check_dylibs unless @store.keg_exists?
 | 
			
		||||
    else
 | 
			
		||||
      flush_cache_and_check_dylibs
 | 
			
		||||
    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 "Broken dependencies", @broken_deps
 | 
			
		||||
    display_items "Undeclared dependencies with linkage", @undeclared_deps
 | 
			
		||||
    display_items "Dependencies with no linkage", @unnecessary_deps
 | 
			
		||||
    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
 | 
			
		||||
    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}/"
 | 
			
		||||
        unprefixed = f.to_s.strip_prefix "#{keg}/"
 | 
			
		||||
        puts "  #{unprefixed}"
 | 
			
		||||
      end
 | 
			
		||||
      puts unless dylib == sorted.last[0]
 | 
			
		||||
@ -53,16 +49,58 @@ class LinkageChecker
 | 
			
		||||
    !@broken_dylibs.empty? || !@broken_deps.empty?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def undeclared_deps
 | 
			
		||||
    @undeclared_deps ||= store.fetch_type(:undeclared_deps)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  attr_reader :keg, :formula
 | 
			
		||||
  attr_reader :keg, :formula, :store
 | 
			
		||||
 | 
			
		||||
  # 'Hash-type' cache values
 | 
			
		||||
 | 
			
		||||
  def brewed_dylibs
 | 
			
		||||
    @brewed_dylibs ||= store.fetch_type(:brewed_dylibs)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reverse_links
 | 
			
		||||
    @reverse_links ||= store.fetch_type(:reverse_links)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def broken_deps
 | 
			
		||||
    @broken_deps ||= store.fetch_type(:broken_deps)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # 'Path-type' cached values
 | 
			
		||||
 | 
			
		||||
  def system_dylibs
 | 
			
		||||
    @system_dylibs ||= store.fetch_type(:system_dylibs)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def broken_dylibs
 | 
			
		||||
    @broken_dylibs ||= store.fetch_type(:broken_dylibs)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def variable_dylibs
 | 
			
		||||
    @variable_dylibs ||= store.fetch_type(:variable_dylibs)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def indirect_deps
 | 
			
		||||
    @indirect_deps ||= store.fetch_type(:indirect_deps)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unnecessary_deps
 | 
			
		||||
    @unnecessary_deps ||= store.fetch_type(:unnecessary_deps)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def dylib_to_dep(dylib)
 | 
			
		||||
    dylib =~ %r{#{Regexp.escape(HOMEBREW_PREFIX)}/(opt|Cellar)/([\w+-.@]+)/}
 | 
			
		||||
    Regexp.last_match(2)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_dylibs
 | 
			
		||||
  def flush_cache_and_check_dylibs
 | 
			
		||||
    reset_dylibs!
 | 
			
		||||
 | 
			
		||||
    checked_dylibs = Set.new
 | 
			
		||||
    @keg.find do |file|
 | 
			
		||||
      next if file.symlink? || file.directory?
 | 
			
		||||
@ -70,7 +108,8 @@ class LinkageChecker
 | 
			
		||||
 | 
			
		||||
      # 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|
 | 
			
		||||
      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? "@"
 | 
			
		||||
@ -101,7 +140,11 @@ class LinkageChecker
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    @indirect_deps, @undeclared_deps, @unnecessary_deps = check_undeclared_deps if formula
 | 
			
		||||
    if formula
 | 
			
		||||
      @indirect_deps, @undeclared_deps, @unnecessary_deps =
 | 
			
		||||
        check_undeclared_deps
 | 
			
		||||
    end
 | 
			
		||||
    store_dylibs!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_undeclared_deps
 | 
			
		||||
@ -200,4 +243,33 @@ class LinkageChecker
 | 
			
		||||
  rescue FormulaUnavailableError
 | 
			
		||||
    opoo "Formula unavailable: #{keg.name}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Helper function to reset dylib values
 | 
			
		||||
  def reset_dylibs!
 | 
			
		||||
    store&.flush_cache!
 | 
			
		||||
    @system_dylibs    = Set.new
 | 
			
		||||
    @broken_dylibs    = Set.new
 | 
			
		||||
    @variable_dylibs  = Set.new
 | 
			
		||||
    @brewed_dylibs    = Hash.new { |h, k| h[k] = Set.new }
 | 
			
		||||
    @reverse_links    = Hash.new { |h, k| h[k] = Set.new }
 | 
			
		||||
    @broken_deps      = Hash.new { |h, k| h[k] = [] }
 | 
			
		||||
    @indirect_deps    = []
 | 
			
		||||
    @undeclared_deps  = []
 | 
			
		||||
    @unnecessary_deps = []
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Updates data store with package path values
 | 
			
		||||
  def store_dylibs!
 | 
			
		||||
    store&.update!(
 | 
			
		||||
      system_dylibs: system_dylibs,
 | 
			
		||||
      variable_dylibs: variable_dylibs,
 | 
			
		||||
      broken_dylibs: broken_dylibs,
 | 
			
		||||
      indirect_deps: indirect_deps,
 | 
			
		||||
      broken_deps: broken_deps,
 | 
			
		||||
      undeclared_deps: undeclared_deps,
 | 
			
		||||
      unnecessary_deps: unnecessary_deps,
 | 
			
		||||
      brewed_dylibs: brewed_dylibs,
 | 
			
		||||
      reverse_links: reverse_links,
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										147
									
								
								Library/Homebrew/test/cache_store_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								Library/Homebrew/test/cache_store_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,147 @@
 | 
			
		||||
require "cache_store"
 | 
			
		||||
 | 
			
		||||
describe CacheStoreDatabase do
 | 
			
		||||
  subject { CacheStoreDatabase.new(:sample) }
 | 
			
		||||
 | 
			
		||||
  describe "self.use" do
 | 
			
		||||
    let(:type) { :test }
 | 
			
		||||
 | 
			
		||||
    it "creates a new `DatabaseCache` instance" do
 | 
			
		||||
      cache_store = double("cache_store", close_if_open!: nil)
 | 
			
		||||
      expect(CacheStoreDatabase).to receive(:new).with(type).and_return(cache_store)
 | 
			
		||||
      expect(cache_store).to receive(:close_if_open!)
 | 
			
		||||
      CacheStoreDatabase.use(type) { |_db| }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#set" do
 | 
			
		||||
    let(:db) { double("db", :[]= => nil) }
 | 
			
		||||
 | 
			
		||||
    before(:each) do
 | 
			
		||||
      allow(File).to receive(:write)
 | 
			
		||||
      allow(subject).to receive(:created?).and_return(true)
 | 
			
		||||
      expect(db).to receive(:has_key?).with(:foo).and_return(false)
 | 
			
		||||
      subject.stub(:db).and_return(db)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "sets the value in the `CacheStoreDatabase`" do
 | 
			
		||||
      expect(db).to_not have_key(:foo)
 | 
			
		||||
      subject.set(:foo, "bar")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#get" do
 | 
			
		||||
    context "database created" do
 | 
			
		||||
      let(:db) { double("db", :[] => "bar") }
 | 
			
		||||
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        allow(subject).to receive(:created?).and_return(true)
 | 
			
		||||
        expect(db).to receive(:has_key?).with(:foo).and_return(true)
 | 
			
		||||
        subject.stub(:db).and_return(db)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "gets value in the `CacheStoreDatabase` corresponding to the key" do
 | 
			
		||||
        expect(db).to have_key(:foo)
 | 
			
		||||
        expect(subject.get(:foo)).to eq("bar")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "database not created" do
 | 
			
		||||
      let(:db) { double("db", :[] => nil) }
 | 
			
		||||
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        allow(subject).to receive(:created?).and_return(false)
 | 
			
		||||
        subject.stub(:db).and_return(db)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "does not get value in the `CacheStoreDatabase` corresponding to key" do
 | 
			
		||||
        expect(subject.get(:foo)).to_not be("bar")
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "does not call `db[]` if `CacheStoreDatabase.created?` is `false`" do
 | 
			
		||||
        expect(db).not_to receive(:[])
 | 
			
		||||
        subject.get(:foo)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#delete" do
 | 
			
		||||
    context "database created" do
 | 
			
		||||
      let(:db) { double("db", :[] => { foo: "bar" }) }
 | 
			
		||||
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        allow(subject).to receive(:created?).and_return(true)
 | 
			
		||||
        subject.stub(:db).and_return(db)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "deletes value in the `CacheStoreDatabase` corresponding to the key" do
 | 
			
		||||
        expect(db).to receive(:delete).with(:foo)
 | 
			
		||||
        subject.delete(:foo)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "database not created" do
 | 
			
		||||
      let(:db) { double("db", delete: nil) }
 | 
			
		||||
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        allow(subject).to receive(:created?).and_return(false)
 | 
			
		||||
        subject.stub(:db).and_return(db)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "does not call `db.delete` if `CacheStoreDatabase.created?` is `false`" do
 | 
			
		||||
        expect(db).not_to receive(:delete)
 | 
			
		||||
        subject.delete(:foo)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#close_if_open!" do
 | 
			
		||||
    context "database open" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        subject.instance_variable_set(:@db, instance_double("db", close: nil))
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "does not raise an error when `close` is called on the database" do
 | 
			
		||||
        expect { subject.close_if_open! }.to_not raise_error(NoMethodError)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "database not open" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        subject.instance_variable_set(:@db, nil)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "does not raise an error when `close` is called on the database" do
 | 
			
		||||
        expect { subject.close_if_open! }.to_not raise_error(NoMethodError)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#created?" do
 | 
			
		||||
    let(:cache_path) { "path/to/homebrew/cache/sample.db" }
 | 
			
		||||
 | 
			
		||||
    before(:each) do
 | 
			
		||||
      subject.stub(:cache_path).and_return(cache_path)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "`File.exist?(cache_path)` returns `true`" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        allow(File).to receive(:exist?).with(cache_path).and_return(true)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns `true`" do
 | 
			
		||||
        expect(subject.created?).to be(true)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "`File.exist?(cache_path)` returns `false`" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        allow(File).to receive(:exist?).with(cache_path).and_return(false)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns `false`" do
 | 
			
		||||
        expect(subject.created?).to be(false)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										35
									
								
								Library/Homebrew/test/dev-cmd/linkage_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Library/Homebrew/test/dev-cmd/linkage_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
describe "brew linkage", :integration_test do
 | 
			
		||||
  before do
 | 
			
		||||
    setup_test_formula "testball"
 | 
			
		||||
    (HOMEBREW_CELLAR/"testball/0.0.1/foo").mkpath
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context "no cache" do
 | 
			
		||||
    it "works when no arguments are provided" do
 | 
			
		||||
      expect { brew "linkage" }
 | 
			
		||||
        .to be_a_success
 | 
			
		||||
        .and not_to_output.to_stdout
 | 
			
		||||
        .and not_to_output.to_stderr
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "works when one argument is provided" do
 | 
			
		||||
      expect { brew "linkage", "testball" }
 | 
			
		||||
        .to be_a_success
 | 
			
		||||
        .and not_to_output.to_stderr
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context "cache" do
 | 
			
		||||
    it "works when no arguments are provided" do
 | 
			
		||||
      expect { brew "linkage", "--cached" }
 | 
			
		||||
        .to be_a_success
 | 
			
		||||
        .and not_to_output.to_stderr
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "works when one argument is provided" do
 | 
			
		||||
      expect { brew "linkage", "--cached", "testball" }
 | 
			
		||||
        .to be_a_success
 | 
			
		||||
        .and not_to_output.to_stderr
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										87
									
								
								Library/Homebrew/test/linkage_cache_store_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								Library/Homebrew/test/linkage_cache_store_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
require "linkage_cache_store"
 | 
			
		||||
 | 
			
		||||
describe LinkageCacheStore do
 | 
			
		||||
  let(:keg_name) { "keg_name" }
 | 
			
		||||
  let(:database) { double("database") }
 | 
			
		||||
 | 
			
		||||
  subject { LinkageCacheStore.new(keg_name, database) }
 | 
			
		||||
 | 
			
		||||
  describe "#keg_exists?" do
 | 
			
		||||
    context "`keg_name` exists in cache" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        expect(database).to receive(:get).with(keg_name).and_return("")
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns `true`" do
 | 
			
		||||
        expect(subject.keg_exists?).to be(true)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "`keg_name` does not exist in cache" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        expect(database).to receive(:get).with(keg_name).and_return(nil)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns `false`" do
 | 
			
		||||
        expect(subject.keg_exists?).to be(false)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#update!" do
 | 
			
		||||
    context "a `value` is a `Hash`" do
 | 
			
		||||
      it "sets the cache for the `keg_name`" do
 | 
			
		||||
        expect(database).to receive(:set).with(keg_name, anything)
 | 
			
		||||
        subject.update!(a_value: { key: ["value"] })
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "a `value` is an `Array`" do
 | 
			
		||||
      it "sets the cache for the `keg_name`" do
 | 
			
		||||
        expect(database).to receive(:set).with(keg_name, anything)
 | 
			
		||||
        subject.update!(a_value: ["value"])
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "a `value` is not an `Array` or `Hash`" do
 | 
			
		||||
      it "raises a `TypeError` if a `value` is not an `Array` or `Hash`" do
 | 
			
		||||
        expect { subject.update!(key: 1) }.to raise_error(TypeError)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#flush_cache!" do
 | 
			
		||||
    it "calls `delete` on the `database` with `keg_name` as parameter" do
 | 
			
		||||
      expect(database).to receive(:delete).with(keg_name)
 | 
			
		||||
      subject.flush_cache!
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#fetch_type" do
 | 
			
		||||
    context "`HASH_LINKAGE_TYPES.include?(type)`" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        expect(database).to receive(:get).with(keg_name).and_return(nil)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns a `Hash` of values" do
 | 
			
		||||
        expect(subject.fetch_type(:brewed_dylibs)).to be_an_instance_of(Hash)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "`ARRAY_LINKAGE_TYPES.include?(type)`" do
 | 
			
		||||
      before(:each) do
 | 
			
		||||
        expect(database).to receive(:get).with(keg_name).and_return(nil)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns an `Array` of values" do
 | 
			
		||||
        expect(subject.fetch_type(:system_dylibs)).to be_an_instance_of(Array)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context "`type` not in `HASH_LINKAGE_TYPES` or `ARRAY_LINKAGE_TYPES`" do
 | 
			
		||||
      it "raises a `TypeError` if the `type` is not supported" do
 | 
			
		||||
        expect { subject.fetch_type(:bad_type) }.to raise_error(TypeError)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -779,7 +779,7 @@ With `--verbose` or `-v`, many commands print extra debugging information. Note
 | 
			
		||||
    If `--pry` is passed or HOMEBREW_PRY is set, pry will be
 | 
			
		||||
    used instead of irb.
 | 
			
		||||
 | 
			
		||||
  * `linkage` [`--test`] [`--reverse`] `formula`:
 | 
			
		||||
  * `linkage` [`--test`] [`--reverse`] [`--cached`] `formula`:
 | 
			
		||||
    Checks the library links of an installed formula.
 | 
			
		||||
 | 
			
		||||
    Only works on installed formulae. An error is raised if it is run on
 | 
			
		||||
@ -791,6 +791,9 @@ With `--verbose` or `-v`, many commands print extra debugging information. Note
 | 
			
		||||
    If `--reverse` is passed, print the dylib followed by the binaries
 | 
			
		||||
    which link to it for each library the keg references.
 | 
			
		||||
 | 
			
		||||
    If `--cached` is passed, print the cached linkage values stored in
 | 
			
		||||
    HOMEBREW_CACHE, set from a previous `brew linkage` run
 | 
			
		||||
 | 
			
		||||
  * `man` [`--fail-if-changed`]:
 | 
			
		||||
    Generate Homebrew's manpages.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -795,7 +795,7 @@ Enter the interactive Homebrew Ruby shell\.
 | 
			
		||||
If \fB\-\-examples\fR is passed, several examples will be shown\. If \fB\-\-pry\fR is passed or HOMEBREW_PRY is set, pry will be used instead of irb\.
 | 
			
		||||
.
 | 
			
		||||
.TP
 | 
			
		||||
\fBlinkage\fR [\fB\-\-test\fR] [\fB\-\-reverse\fR] \fIformula\fR
 | 
			
		||||
\fBlinkage\fR [\fB\-\-test\fR] [\fB\-\-reverse\fR] [\fB\-\-cached\fR] \fIformula\fR
 | 
			
		||||
Checks the library links of an installed formula\.
 | 
			
		||||
.
 | 
			
		||||
.IP
 | 
			
		||||
@ -807,6 +807,9 @@ If \fB\-\-test\fR is passed, only display missing libraries and exit with a non\
 | 
			
		||||
.IP
 | 
			
		||||
If \fB\-\-reverse\fR is passed, print the dylib followed by the binaries which link to it for each library the keg references\.
 | 
			
		||||
.
 | 
			
		||||
.IP
 | 
			
		||||
If \fB\-\-cached\fR is passed, print the cached linkage values stored in HOMEBREW_CACHE, set from a previous \fBbrew linkage\fR run
 | 
			
		||||
.
 | 
			
		||||
.TP
 | 
			
		||||
\fBman\fR [\fB\-\-fail\-if\-changed\fR]
 | 
			
		||||
Generate Homebrew\'s manpages\.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user