diff --git a/Library/Homebrew/dev-cmd/linkage.rb b/Library/Homebrew/dev-cmd/linkage.rb index 31e9bd1036c829024c274a057b2311faa6c3fe0b..0ade2bc646f0a77e14c77ced41493acc37a9e2a9 100644 --- a/Library/Homebrew/dev-cmd/linkage.rb +++ b/Library/Homebrew/dev-cmd/linkage.rb @@ -1,4 +1,4 @@ -#: * `linkage` [`--test`] [`--reverse`] <formula>: +#: * `linkage` [`--test`] [`--reverse`] [`--rebuild`] <formula>: #: Checks the library links of an installed formula. #: #: Only works on installed formulae. An error is raised if it is run on @@ -9,6 +9,9 @@ #: #: If `--reverse` is passed, print the dylib followed by the binaries #: which link to it for each library the keg references. +#: +#: If `--rebuild` is passed, flushes the `LinkageStore` cache for each +#: 'keg.name' and forces a check on the dylibs. require "os/mac/linkage_checker" @@ -18,7 +21,10 @@ module Homebrew def linkage ARGV.kegs.each do |keg| ohai "Checking #{keg.name} linkage" if ARGV.kegs.size > 1 - result = LinkageChecker.new(keg) + database_cache = DatabaseCache.new("linkage") + result = LinkageChecker.new(keg, database_cache) + result.flush_cache_and_check_dylibs if ARGV.include?("--rebuild") + if ARGV.include?("--test") result.display_test_output Homebrew.failed = true if result.broken_dylibs? @@ -27,6 +33,8 @@ module Homebrew else result.display_normal_output end + + database_cache.close end end end diff --git a/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb b/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb index 901d8945fe3a3178716cb9c78474533a43f8aba9..00b151b641af20df21f10bbf23c5af39be09a602 100644 --- a/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb +++ b/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb @@ -64,7 +64,10 @@ module FormulaCellarChecks def check_linkage return unless formula.prefix.directory? keg = Keg.new(formula.prefix) - checker = LinkageChecker.new(keg, formula) + database_cache = DatabaseCache.new("linkage") + checker = LinkageChecker.new(keg, database_cache, formula) + checker.flush_cache_and_check_dylibs + database_cache.close return unless checker.broken_dylibs? output = <<~EOS diff --git a/Library/Homebrew/os/mac/cache_store.rb b/Library/Homebrew/os/mac/cache_store.rb new file mode 100644 index 0000000000000000000000000000000000000000..49b341dcc99cdbf0aa58d00e461df6abde05dbd6 --- /dev/null +++ b/Library/Homebrew/os/mac/cache_store.rb @@ -0,0 +1,207 @@ +require "dbm" +require "json" + +# +# `DatabaseCache` is a class acting as an interface to a persistent storage +# mechanism residing in the `HOMEBREW_CACHE` +# +class DatabaseCache + # Name of the database cache file located at <HOMEBREW_CACHE>/<name>.db + # + # @return [String] + attr_accessor :name + + # Instantiates new `DatabaseCache` object + # + # @param [String] name + # @return [nil] + def initialize(name) + @name = name + end + + # Memoized `DBM` database object with on-disk database located in the + # `HOMEBREW_CACHE` + # + # @return [DBM] db + def db + @db ||= DBM.open("#{HOMEBREW_CACHE}/#{name}", 0666, DBM::WRCREAT) + end + + # Close the `DBM` database object after usage + # + # @return [nil] + def close + db.close + end +end + +# +# `CacheStore` is an abstract base class which provides methods to mutate and +# fetch data from a persistent storage mechanism +# +# @abstract +# +class CacheStore + # Instantiates a new `CacheStore` class + # + # @param [DatabaseCache] database_cache + # @return [nil] + def initialize(database_cache) + @db = database_cache.db + end + + # Inserts new values or updates existing cached values to persistent storage + # mechanism + # + # @abstract + # @param [Any] + # @return [nil] + def update!(*) + raise NotImplementedError + end + + # Fetches cached values in persistent storage according to the type of data + # stored + # + # @abstract + # @param [Any] + # @return [Any] + def fetch(*) + raise NotImplementedError + end + + # Deletes data from the cache based on a condition defined in a concrete class + # + # @abstract + # @return [nil] + def flush_cache! + raise NotImplementedError + end + + protected + + # A class instance providing access to the `DBM` database object + # + # @return [DBM] + attr_reader :db +end + +# +# `LinkageStore` is a concrete class providing methods to fetch and mutate +# linkage-specific data used by the `brew linkage` command +# +# If the cache hasn't changed, don't do extra processing in `LinkageChecker`. +# Instead, just fetch the data stored in the cache +# +class LinkageStore < CacheStore + # Types of dylibs of the form (label -> array) + HASH_LINKAGE_TYPES = %w[brewed_dylibs reverse_links].freeze + + # The keg name for the `LinkageChecker` class + # + # @return [String] + attr_reader :key + + # Initializes new `LinkageStore` class + # + # @param [String] keg_name + # @param [DatabaseCache] database_cache + # @return [nil] + def initialize(keg_name, database_cache) + @key = keg_name + super(database_cache) + end + + # Inserts new values or updates existing cached values to persistent storage + # mechanism according to the type of data + # + # @param [Hash] path_values + # @param [Hash] hash_values + # @return [nil] + def update!( + path_values: { + "system_dylibs" => %w[], "variable_dylibs" => %w[], "broken_dylibs" => %w[], + "indirect_deps" => %w[], "undeclared_deps" => %w[], "unnecessary_deps" => %w[] + }, + hash_values: { + "brewed_dylibs" => {}, "reverse_links" => {} + } + ) + db[key] = { + "path_values" => format_path_values(path_values), + "hash_values" => format_hash_values(hash_values), + } + end + + # Fetches cached values in persistent storage according to the type of data + # stored + # + # @param [String] type + # @return [Any] + def fetch(type:) + if HASH_LINKAGE_TYPES.include?(type) + fetch_hash_values(type: type) + else + fetch_path_values(type: type) + end + end + + # A condition for where to flush the cache + # + # @return [String] + def flush_cache! + db.delete(key) + end + + private + + # Fetches a subset of paths where the name = `key` + # + # @param [String] type + # @return [Array[String]] + def fetch_path_values(type:) + return [] unless db.key?(key) && !db[key].nil? + string_to_hash(db[key])["path_values"][type] + end + + # Fetches a subset of paths and labels where the name = `key`. Formats said + # paths/labels into `key => [value]` syntax expected by `LinkageChecker` + # + # @param [String] type + # @return [Hash] + def fetch_hash_values(type:) + return {} unless db.key?(key) && !db[key].nil? + string_to_hash(db[key])["hash_values"][type] + end + + # Parses `DBM` stored `String` into ruby `Hash` + # + # @param [String] string + # @return [Hash] + def string_to_hash(string) + JSON.parse(string.gsub("=>", ":")) + end + + # Formats the linkage data for `path_values` into a kind which can be parsed + # by the `string_to_hash` method. Converts ruby `Set`s to `Array`s. + # + # @param [Hash(String, Set(String))] hash + # @return [Hash(String, Array(String))] + def format_path_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 `string_to_hash` method. Converts ruby `Set`s to `Array`s, and + # converts ruby `Pathname`s to `String`s + # + # @param [Hash(String, Set(Pathname))] hash + # @return [Hash(String, Array(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 diff --git a/Library/Homebrew/os/mac/linkage_checker.rb b/Library/Homebrew/os/mac/linkage_checker.rb index cf6c12f22c35fde0ab8f3b224209e88c01e44b50..c123d72905d4fe0027732217d62bd773e454b1e9 100644 --- a/Library/Homebrew/os/mac/linkage_checker.rb +++ b/Library/Homebrew/os/mac/linkage_checker.rb @@ -1,27 +1,56 @@ require "set" require "keg" require "formula" +require "os/mac/cache_store" class LinkageChecker - attr_reader :keg, :formula - attr_reader :brewed_dylibs, :system_dylibs, :broken_dylibs, :variable_dylibs - attr_reader :undeclared_deps, :unnecessary_deps, :reverse_links + attr_reader :keg, :formula, :store - def initialize(keg, formula = nil) + def initialize(keg, db, 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 + @store = LinkageStore.new(keg.name, db) + end + + # '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 + + # '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 check_dylibs + def undeclared_deps + @undeclared_deps ||= store.fetch(type: "undeclared_deps") + end + + def indirect_deps + @indirect_deps ||= store.fetch(type: "indirect_deps") + end + + def unnecessary_deps + @unnecessary_deps ||= store.fetch(type: "unnecessary_deps") + end + + def flush_cache_and_check_dylibs + reset_dylibs! + @keg.find do |file| next if file.symlink? || file.directory? next unless file.dylib? || file.binary_executable? || file.mach_o_bundle? @@ -54,6 +83,7 @@ class LinkageChecker end @indirect_deps, @undeclared_deps, @unnecessary_deps = check_undeclared_deps if formula + store_dylibs! end def check_undeclared_deps @@ -99,18 +129,18 @@ class LinkageChecker 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 + 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 + return if reverse_links.empty? + sorted = reverse_links.sort sorted.each do |dylib, files| puts dylib files.each do |f| @@ -122,21 +152,21 @@ class LinkageChecker 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? + 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? + !broken_dylibs.empty? end def undeclared_deps? - !@undeclared_deps.empty? + !undeclared_deps.empty? end def unnecessary_deps? - !@unnecessary_deps.empty? + !unnecessary_deps.empty? end private @@ -175,4 +205,39 @@ class LinkageChecker rescue FormulaUnavailableError opoo "Formula unavailable: #{keg.name}" end + + # Helper function to reset dylib values when building cache + # + # @return [nil] + 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 } + @indirect_deps = [] + @undeclared_deps = [] + @unnecessary_deps = [] + end + + # Updates data store with package path values + # + # @return [nil] + def store_dylibs! + store.update!( + path_values: { + "system_dylibs" => @system_dylibs, + "variable_dylibs" => @variable_dylibs, + "broken_dylibs" => @broken_dylibs, + "indirect_deps" => @indirect_deps, + "undeclared_deps" => @undeclared_deps, + "unnecessary_deps" => @unnecessary_deps, + }, + hash_values: { + "brewed_dylibs" => @brewed_dylibs, + "reverse_links" => @reverse_links, + }, + ) + end end