From 05c7b65f540708e2f3a3de31b197e205d2bd170b Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Tue, 24 Jun 2025 12:06:55 +0100 Subject: [PATCH] tab, cask/tab: add more Sorbet types and signatures. I bailed before going all the way to `typed: strict` but this should at least improve things and fix: `Library/Homebrew/tab.rb:111: warning: The class Tab reached 8 shape variations, instance variables accesses will be slower and memory usage increased.` --- Library/Homebrew/cask/tab.rb | 23 ++++++- Library/Homebrew/dev-cmd/bottle.rb | 2 +- Library/Homebrew/formula.rb | 4 +- Library/Homebrew/formula_installer.rb | 3 +- Library/Homebrew/tab.rb | 89 ++++++++++++++++++++++++--- 5 files changed, 106 insertions(+), 15 deletions(-) diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index f7e12a3bcb..b6bc7a54da 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -5,10 +5,24 @@ require "tab" module Cask class Tab < ::AbstractTab - attr_accessor :uninstall_flight_blocks, :uninstall_artifacts + sig { returns(T.nilable(T::Boolean)) } + attr_accessor :uninstall_flight_blocks + + sig { returns(T.nilable(T::Array[T.untyped])) } + attr_accessor :uninstall_artifacts + + sig { params(attributes: T::Hash[String, T.untyped]).void } + def initialize(attributes = {}) + @uninstall_flight_blocks = T.let(nil, T.nilable(T::Boolean)) + @uninstall_artifacts = T.let(nil, T.nilable(T::Array[T.untyped])) + + super + end # Instantiates a {Tab} for a new installation of a cask. - def self.create(cask) + sig { override.params(formula_or_cask: T.any(Formula, Cask)).returns(T.attached_class) } + def self.create(formula_or_cask) + cask = T.cast(formula_or_cask, Cask) tab = super tab.tabfile = cask.metadata_main_container_path/FILENAME @@ -23,6 +37,7 @@ module Cask # Returns a {Tab} for an already installed cask, # or a fake one if the cask is not installed. + sig { params(cask: Cask).returns(T.attached_class) } def self.for_cask(cask) path = cask.metadata_main_container_path/FILENAME @@ -40,6 +55,7 @@ module Cask tab end + sig { returns(T.attached_class) } def self.empty tab = super tab.uninstall_flight_blocks = false @@ -76,10 +92,12 @@ module Cask runtime_deps end + sig { returns(T.nilable(String)) } def version source["version"] end + sig { params(_args: T.untyped).returns(String) } def to_json(*_args) attributes = { "homebrew_version" => homebrew_version, @@ -98,6 +116,7 @@ module Cask JSON.pretty_generate(attributes) end + sig { returns(String) } def to_s s = ["Installed"] s << "using the formulae.brew.sh API" if loaded_from_api diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb index cb69bb464c..d512b65190 100644 --- a/Library/Homebrew/dev-cmd/bottle.rb +++ b/Library/Homebrew/dev-cmd/bottle.rb @@ -675,7 +675,7 @@ module Homebrew "filename" => filename.url_encode, "local_filename" => filename.to_s, "sha256" => sha256, - "tab" => tab.to_bottle_hash, + "tab" => T.must(tab).to_bottle_hash, "path_exec_files" => path_exec_files, "all_files" => all_files, "installed_size" => installed_size, diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index bf570f6c1e..7b20a26dfc 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -730,7 +730,9 @@ class Formula tab = Tab.for_keg(prefix(version)) return true if tab.version_scheme < version_scheme - return true if stable && tab.stable_version && tab.stable_version < T.must(stable).version + + tab_stable_version = tab.stable_version + return true if stable && tab_stable_version && tab_stable_version < T.must(stable).version return false unless fetch_head return false unless head&.downloader.is_a?(VCSDownloadStrategy) diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 56d9094ac2..691abffc19 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -420,7 +420,8 @@ class FormulaInstaller invalid_arch_dependencies = [] pinned_unsatisfied_deps = [] recursive_deps.each do |dep| - if (tab = Tab.for_formula(dep.to_formula)) && tab.arch.present? && tab.arch.to_s != Hardware::CPU.arch.to_s + tab = Tab.for_formula(dep.to_formula) + if tab.arch.present? && tab.arch.to_s != Hardware::CPU.arch.to_s invalid_arch_dependencies << "#{dep} was built for #{tab.arch}" end diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index dc57bb4d20..9acea83252 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -10,6 +10,9 @@ require "cachable" # Rather than calling `new` directly, use one of the class methods like {Tab.create}. class AbstractTab extend Cachable + extend T::Helpers + + abstract! FILENAME = "INSTALL_RECEIPT.json" @@ -25,14 +28,34 @@ class AbstractTab sig { returns(T.nilable(T::Boolean)) } # TODO: change this to always return a boolean attr_accessor :installed_on_request - attr_accessor :homebrew_version, :tabfile, :loaded_from_api, :time, :arch, :source, :built_on + sig { returns(T.nilable(String)) } + attr_accessor :homebrew_version + + attr_accessor :tabfile, :loaded_from_api, :time, :arch, :source, :built_on # Returns the formula or cask runtime dependencies. # # @api internal attr_accessor :runtime_dependencies + sig { params(attributes: T::Hash[String, T.untyped]).void } + def initialize(attributes = {}) + @installed_as_dependency = T.let(nil, T.nilable(T::Boolean)) + @installed_on_request = T.let(nil, T.nilable(T::Boolean)) + @homebrew_version = T.let(nil, T.nilable(String)) + @tabfile = T.let(nil, T.nilable(Pathname)) + @loaded_from_api = T.let(nil, T.nilable(T::Boolean)) + @time = T.let(nil, T.nilable(Integer)) + @arch = T.let(nil, T.nilable(String)) + @source = T.let(nil, T.nilable(T::Hash[String, T.untyped])) + @built_on = T.let(nil, T.nilable(T::Hash[String, T.untyped])) + @runtime_dependencies = T.let(nil, T.nilable(T::Array[T.untyped])) + + attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } + end + # Instantiates a {Tab} for a new installation of a formula or cask. + sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(T.attached_class) } def self.create(formula_or_cask) attributes = { "homebrew_version" => HOMEBREW_VERSION, @@ -54,6 +77,7 @@ class AbstractTab # Returns the {Tab} for a formula or cask install receipt at `path`. # # NOTE: Results are cached. + sig { params(path: T.any(Pathname, String)).returns(T.attached_class) } def self.from_file(path) cache.fetch(path) do |p| content = File.read(p) @@ -64,6 +88,7 @@ class AbstractTab end # Like {from_file}, but bypass the cache. + sig { params(content: String, path: T.any(Pathname, String)).returns(T.attached_class) } def self.from_file_content(content, path) attributes = begin JSON.parse(content) @@ -75,6 +100,7 @@ class AbstractTab new(attributes) end + sig { returns(T.attached_class) } def self.empty attributes = { "homebrew_version" => HOMEBREW_VERSION, @@ -107,11 +133,9 @@ class AbstractTab end private_class_method :formula_to_dep_hash - def initialize(attributes = {}) - attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } - end - + sig { returns(Version) } def parsed_homebrew_version + homebrew_version = self.homebrew_version return Version::NULL if homebrew_version.nil? Version.new(homebrew_version) @@ -123,11 +147,13 @@ class AbstractTab Tap.fetch(tap_name) if tap_name end + sig { params(tap: T.nilable(T.any(Tap, String))).void } def tap=(tap) - tap_name = tap.respond_to?(:name) ? tap.name : tap + tap_name = tap.is_a?(Tap) ? tap.name : tap source["tap"] = tap_name end + sig { void } def write self.class.cache[tabfile] = self tabfile.atomic_write(to_json) @@ -144,8 +170,30 @@ class Tab < AbstractTab attr_writer :used_options, :unused_options, :compiler, :source_modified_time attr_reader :tapped_from + sig { params(attributes: T::Hash[String, T.untyped]).void } + def initialize(attributes = {}) + @poured_from_bottle = T.let(nil, T.nilable(T::Boolean)) + @built_as_bottle = T.let(nil, T.nilable(T::Boolean)) + @changed_files = T.let(nil, T.nilable(T::Array[Pathname])) + @stdlib = T.let(nil, T.nilable(String)) + @aliases = T.let(nil, T.nilable(T::Array[String])) + @used_options = T.let(nil, T.nilable(T::Array[String])) + @unused_options = T.let(nil, T.nilable(T::Array[String])) + @compiler = T.let(nil, T.nilable(String)) + @source_modified_time = T.let(nil, T.nilable(Integer)) + @tapped_from = T.let(nil, T.nilable(String)) + + super + end + # Instantiates a {Tab} for a new installation of a formula. - def self.create(formula, compiler, stdlib) + sig { + override.params(formula_or_cask: T.any(Formula, Cask::Cask), compiler: T.any(Symbol, String), + stdlib: T.nilable(T.any(String, Symbol))).returns(T.attached_class) + } + def self.create(formula_or_cask, compiler = DevelopmentTools.default_compiler, stdlib = nil) + formula = T.cast(formula_or_cask, Formula) + tab = super(formula) build = formula.build runtime_deps = formula.runtime_dependencies(undeclared: false) @@ -172,10 +220,10 @@ class Tab < AbstractTab end # Like {from_file}, but bypass the cache. + sig { params(content: String, path: T.any(Pathname, String)).returns(T.attached_class) } def self.from_file_content(content, path) tab = super - tab.source_modified_time ||= 0 tab.source ||= {} tab.tap = tab.tapped_from if !tab.tapped_from.nil? && tab.tapped_from != "path or URL" @@ -220,6 +268,7 @@ class Tab < AbstractTab # Returns a {Tab} for the named formula's installation, # or a fake one if the formula is not installed. + sig { params(name: String).returns(T.attached_class) } def self.for_name(name) for_formula(Formulary.factory(name)) end @@ -237,6 +286,7 @@ class Tab < AbstractTab # Returns a {Tab} for an already installed formula, # or a fake one if the formula is not installed. + sig { params(formula: Formula).returns(T.attached_class) } def self.for_formula(formula) paths = [] @@ -276,6 +326,7 @@ class Tab < AbstractTab tab end + sig { returns(T.attached_class) } def self.empty tab = super @@ -293,6 +344,7 @@ class Tab < AbstractTab tab end + sig { returns(T::Hash[String, T.untyped]) } def self.empty_source_versions { "stable" => nil, @@ -308,6 +360,7 @@ class Tab < AbstractTab end end + sig { returns(T::Boolean) } def any_args_or_options? !used_options.empty? || !unused_options.empty? end @@ -324,14 +377,17 @@ class Tab < AbstractTab !with?(val) end + sig { params(opt: String).returns(T::Boolean) } def include?(opt) used_options.include? opt end + sig { returns(T::Boolean) } def head? spec == :head end + sig { returns(T::Boolean) } def stable? spec == :stable end @@ -344,10 +400,12 @@ class Tab < AbstractTab Options.create(@used_options) end + sig { returns(Options) } def unused_options Options.create(@unused_options) end + sig { returns(T.any(String, Symbol)) } def compiler @compiler || DevelopmentTools.default_compiler end @@ -358,36 +416,44 @@ class Tab < AbstractTab @runtime_dependencies if parsed_homebrew_version >= "1.1.6" end + sig { returns(CxxStdlib) } def cxxstdlib # Older tabs won't have these values, so provide sensible defaults lib = stdlib.to_sym if stdlib CxxStdlib.create(lib, compiler.to_sym) end + sig { returns(T::Boolean) } def built_bottle? built_as_bottle && !poured_from_bottle end + sig { returns(T::Boolean) } def bottle? built_as_bottle end + sig { returns(Symbol) } def spec source["spec"].to_sym end + sig { returns(T::Hash[String, T.untyped]) } def versions source["versions"] end + sig { returns(T.nilable(Version)) } def stable_version versions["stable"]&.then { Version.new(_1) } end + sig { returns(T.nilable(Version)) } def head_version versions["head"]&.then { Version.new(_1) } end + sig { returns(Integer) } def version_scheme versions["version_scheme"] || 0 end @@ -397,6 +463,7 @@ class Tab < AbstractTab Time.at(@source_modified_time || 0) end + sig { params(options: T.nilable(T::Hash[String, T.untyped])).returns(String) } def to_json(options = nil) attributes = { "homebrew_version" => homebrew_version, @@ -411,7 +478,7 @@ class Tab < AbstractTab "time" => time, "source_modified_time" => source_modified_time.to_i, "stdlib" => stdlib&.to_s, - "compiler" => compiler&.to_s, + "compiler" => compiler.to_s, "aliases" => aliases, "runtime_dependencies" => runtime_dependencies, "source" => source, @@ -424,13 +491,14 @@ class Tab < AbstractTab end # A subset of to_json that we care about for bottles. + sig { returns(T::Hash[String, T.untyped]) } def to_bottle_hash attributes = { "homebrew_version" => homebrew_version, "changed_files" => changed_files&.map(&:to_s), "source_modified_time" => source_modified_time.to_i, "stdlib" => stdlib&.to_s, - "compiler" => compiler&.to_s, + "compiler" => compiler.to_s, "runtime_dependencies" => runtime_dependencies, "arch" => arch, "built_on" => built_on, @@ -439,6 +507,7 @@ class Tab < AbstractTab attributes end + sig { void } def write # If this is a new installation, the cache of installed formulae # will no longer be valid.