Merge pull request #20165 from Homebrew/more_tab_types

tab, cask/tab: add more Sorbet types and signatures.
This commit is contained in:
Patrick Linnane 2025-06-24 20:27:11 +00:00 committed by GitHub
commit f32f391fc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 106 additions and 15 deletions

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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.