Merge pull request #20643 from Homebrew/dug/typed-utils-bottles

Enable strict typing in Utils::Bottles
This commit is contained in:
Douglas Eichelberger 2025-09-08 18:03:39 +00:00 committed by GitHub
commit 6e6c06f5a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 30 deletions

View File

@ -182,7 +182,8 @@ module Homebrew
sig { sig {
params(old_keys: T::Array[String], old_bottle_spec: BottleSpecification, params(old_keys: T::Array[String], old_bottle_spec: BottleSpecification,
new_bottle_hash: T::Hash[String, T.untyped]).returns(T::Array[T::Array[String]]) new_bottle_hash: T::Hash[String, T.untyped])
.returns([T::Array[String], T::Array[T::Hash[Symbol, T.any(String, Symbol)]]])
} }
def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash) def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash)
mismatches = [] mismatches = []
@ -409,7 +410,7 @@ module Homebrew
bottle_tag, rebuild = if local_bottle_json bottle_tag, rebuild = if local_bottle_json
_, tag_string, rebuild_string = Utils::Bottles.extname_tag_rebuild(formula.local_bottle_path.to_s) _, tag_string, rebuild_string = Utils::Bottles.extname_tag_rebuild(formula.local_bottle_path.to_s)
[tag_string.to_sym, rebuild_string.to_i] [T.must(tag_string).to_sym, rebuild_string.to_i]
end end
bottle_tag = if bottle_tag bottle_tag = if bottle_tag
@ -860,8 +861,8 @@ module Homebrew
end end
sig { sig {
params(formula: Formula, formula_ast: Utils::AST::FormulaAST, params(formula: Formula, formula_ast: Utils::AST::FormulaAST, bottle_hash: T::Hash[String, T.untyped])
bottle_hash: T::Hash[String, T.untyped]).returns(T.nilable(T::Array[String])) .returns(T.nilable(T::Array[T::Hash[Symbol, T.any(String, Symbol)]]))
} }
def old_checksums(formula, formula_ast, bottle_hash) def old_checksums(formula, formula_ast, bottle_hash)
bottle_node = T.cast(formula_ast.bottle_block, T.nilable(RuboCop::AST::BlockNode)) bottle_node = T.cast(formula_ast.bottle_block, T.nilable(RuboCop::AST::BlockNode))

View File

@ -609,8 +609,8 @@ on_request: installed_on_request?, options:)
clean clean
# Store the formula used to build the keg in the keg. # Store the formula used to build the keg in the keg.
formula_contents = if formula.local_bottle_path formula_contents = if (local_bottle_path = formula.local_bottle_path)
Utils::Bottles.formula_contents formula.local_bottle_path, name: formula.name Utils::Bottles.formula_contents local_bottle_path, name: formula.name
else else
formula.path.read formula.path.read
end end

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "tab" require "tab"
@ -20,13 +20,14 @@ module Utils
when Tag when Tag
tag tag
else else
@tag ||= Tag.new( @tag ||= T.let(Tag.new(
system: HOMEBREW_SYSTEM.downcase.to_sym, system: HOMEBREW_SYSTEM.downcase.to_sym,
arch: HOMEBREW_PROCESSOR.downcase.to_sym, arch: HOMEBREW_PROCESSOR.downcase.to_sym,
) ), T.nilable(Tag))
end end
end end
sig { params(formula: Formula).returns(T::Boolean) }
def built_as?(formula) def built_as?(formula)
return false unless formula.latest_version_installed? return false unless formula.latest_version_installed?
@ -34,34 +35,40 @@ module Utils
tab.built_as_bottle tab.built_as_bottle
end end
sig { params(formula: Formula, file: Pathname).returns(T::Boolean) }
def file_outdated?(formula, file) def file_outdated?(formula, file)
file = file.resolved_path file = file.resolved_path
filename = file.basename.to_s filename = file.basename.to_s
return false if formula.bottle.blank? bottle = formula.bottle
return false unless bottle
_, bottle_tag, bottle_rebuild = extname_tag_rebuild(filename) _, bottle_tag, bottle_rebuild = extname_tag_rebuild(filename)
return false if bottle_tag.blank? return false if bottle_tag.blank?
bottle_tag != formula.bottle.tag.to_s || bottle_rebuild.to_i != formula.bottle.rebuild bottle_tag != bottle.tag.to_s || bottle_rebuild.to_i != bottle.rebuild
end end
sig { params(filename: String).returns(T::Array[String]) }
def extname_tag_rebuild(filename) def extname_tag_rebuild(filename)
HOMEBREW_BOTTLES_EXTNAME_REGEX.match(filename).to_a HOMEBREW_BOTTLES_EXTNAME_REGEX.match(filename).to_a
end end
sig { params(bottle_file: Pathname).returns(T.nilable(String)) }
def receipt_path(bottle_file) def receipt_path(bottle_file)
bottle_file_list(bottle_file).find do |line| bottle_file_list(bottle_file).find do |line|
%r{.+/.+/INSTALL_RECEIPT.json}.match?(line) %r{.+/.+/INSTALL_RECEIPT.json}.match?(line)
end end
end end
sig { params(bottle_file: Pathname, file_path: String).returns(String) }
def file_from_bottle(bottle_file, file_path) def file_from_bottle(bottle_file, file_path)
Utils.popen_read("tar", "--extract", "--to-stdout", "--file", bottle_file, file_path) Utils.popen_read("tar", "--extract", "--to-stdout", "--file", bottle_file, file_path)
end end
sig { params(bottle_file: Pathname).returns([String, String]) }
def resolve_formula_names(bottle_file) def resolve_formula_names(bottle_file)
name = bottle_file_list(bottle_file).first.to_s.split("/").first name = bottle_file_list(bottle_file).first.to_s.split("/").fetch(0)
full_name = if (receipt_file_path = receipt_path(bottle_file)) full_name = if (receipt_file_path = receipt_path(bottle_file))
receipt_file = file_from_bottle(bottle_file, receipt_file_path) receipt_file = file_from_bottle(bottle_file, receipt_file_path)
tap = Tab.from_file_content(receipt_file, "#{bottle_file}/#{receipt_file_path}").tap tap = Tab.from_file_content(receipt_file, "#{bottle_file}/#{receipt_file_path}").tap
@ -80,13 +87,14 @@ module Utils
[name, full_name] [name, full_name]
end end
sig { params(bottle_file: Pathname).returns(PkgVersion) }
def resolve_version(bottle_file) def resolve_version(bottle_file)
version = bottle_file_list(bottle_file).first.to_s.split("/").second version = bottle_file_list(bottle_file).first.to_s.split("/").fetch(1)
PkgVersion.parse(version) PkgVersion.parse(version)
end end
def formula_contents(bottle_file, sig { params(bottle_file: Pathname, name: String).returns(String) }
name: resolve_formula_names(bottle_file)[0]) def formula_contents(bottle_file, name: resolve_formula_names(bottle_file)[0])
bottle_version = resolve_version bottle_file bottle_version = resolve_version bottle_file
formula_path = "#{name}/#{bottle_version}/.brew/#{name}.rb" formula_path = "#{name}/#{bottle_version}/.brew/#{name}.rb"
contents = file_from_bottle(bottle_file, formula_path) contents = file_from_bottle(bottle_file, formula_path)
@ -95,6 +103,10 @@ module Utils
contents contents
end end
sig {
params(root_url: String, name: String, checksum: T.any(Checksum, String),
filename: T.nilable(Bottle::Filename)).returns(T.any([String, T.nilable(String)], String))
}
def path_resolved_basename(root_url, name, checksum, filename) def path_resolved_basename(root_url, name, checksum, filename)
if root_url.match?(GitHubPackages::URL_REGEX) if root_url.match?(GitHubPackages::URL_REGEX)
image_name = GitHubPackages.image_formula_name(name) image_name = GitHubPackages.image_formula_name(name)
@ -104,6 +116,7 @@ module Utils
end end
end end
sig { params(formula: Formula).returns(Tab) }
def load_tab(formula) def load_tab(formula)
keg = Keg.new(formula.prefix) keg = Keg.new(formula.prefix)
tabfile = keg/AbstractTab::FILENAME tabfile = keg/AbstractTab::FILENAME
@ -112,7 +125,7 @@ module Utils
if (tab_attributes = formula.bottle_tab_attributes.presence) if (tab_attributes = formula.bottle_tab_attributes.presence)
Tab.from_file_content(tab_attributes.to_json, tabfile) Tab.from_file_content(tab_attributes.to_json, tabfile)
elsif !tabfile.exist? && bottle_json_path&.exist? elsif !tabfile.exist? && bottle_json_path&.exist?
_, tag, = Utils::Bottles.extname_tag_rebuild(formula.local_bottle_path) _, tag, = Utils::Bottles.extname_tag_rebuild(formula.local_bottle_path.to_s)
bottle_hash = JSON.parse(File.read(bottle_json_path)) bottle_hash = JSON.parse(File.read(bottle_json_path))
tab_json = bottle_hash[formula.full_name]["bottle"]["tags"][tag]["tab"].to_json tab_json = bottle_hash[formula.full_name]["bottle"]["tags"][tag]["tab"].to_json
Tab.from_file_content(tab_json, tabfile) Tab.from_file_content(tab_json, tabfile)
@ -130,8 +143,9 @@ module Utils
private private
sig { params(bottle_file: Pathname).returns(T::Array[String]) }
def bottle_file_list(bottle_file) def bottle_file_list(bottle_file)
@bottle_file_list ||= {} @bottle_file_list ||= T.let({}, T.nilable(T::Hash[Pathname, T::Array[String]]))
@bottle_file_list[bottle_file] ||= Utils.popen_read("tar", "--list", "--file", bottle_file) @bottle_file_list[bottle_file] ||= Utils.popen_read("tar", "--list", "--file", bottle_file)
.lines .lines
.map(&:chomp) .map(&:chomp)
@ -140,23 +154,24 @@ module Utils
# Denotes the arch and OS of a bottle. # Denotes the arch and OS of a bottle.
class Tag class Tag
sig { returns(Symbol) }
attr_reader :system, :arch attr_reader :system, :arch
sig { params(value: Symbol).returns(T.attached_class) } sig { params(value: Symbol).returns(T.attached_class) }
def self.from_symbol(value) def self.from_symbol(value)
return new(system: :all, arch: :all) if value == :all return new(system: :all, arch: :all) if value == :all
@all_archs_regex ||= begin @all_archs_regex ||= T.let(begin
all_archs = Hardware::CPU::ALL_ARCHS.map(&:to_s) all_archs = Hardware::CPU::ALL_ARCHS.map(&:to_s)
/ /
^((?<arch>#{Regexp.union(all_archs)})_)? ^((?<arch>#{Regexp.union(all_archs)})_)?
(?<system>[\w.]+)$ (?<system>[\w.]+)$
/x /x
end end, T.nilable(Regexp))
match = @all_archs_regex.match(value.to_s) match = @all_archs_regex.match(value.to_s)
raise ArgumentError, "Invalid bottle tag symbol" unless match raise ArgumentError, "Invalid bottle tag symbol" unless match
system = match[:system].to_sym system = T.must(match[:system]).to_sym
arch = match[:arch]&.to_sym || :x86_64 arch = match[:arch]&.to_sym || :x86_64
new(system:, arch:) new(system:, arch:)
end end
@ -167,18 +182,27 @@ module Utils
@arch = arch @arch = arch
end end
sig { override.params(other: BasicObject).returns(T::Boolean) }
def ==(other) def ==(other)
if other.is_a?(Symbol) case other
when Symbol
to_sym == other to_sym == other
else when self.class
self.class == other.class && system == other.system && standardized_arch == other.standardized_arch system == other.system && standardized_arch == other.standardized_arch
else false
end end
end end
sig { override.params(other: BasicObject).returns(T::Boolean) }
def eql?(other) def eql?(other)
self.class == other.class && self == other case other
when self.class
self == other
else false
end
end end
sig { override.returns(Integer) }
def hash def hash
[system, standardized_arch].hash [system, standardized_arch].hash
end end
@ -196,11 +220,12 @@ module Utils
arch_to_symbol(standardized_arch) arch_to_symbol(standardized_arch)
end end
sig { returns(String) } sig { override.returns(String) }
def to_s def to_s
to_sym.to_s to_sym.to_s
end end
sig { returns(Symbol) }
def to_unstandardized_sym def to_unstandardized_sym
# Never allow these generic names # Never allow these generic names
return to_sym if [:intel, :arm].include? arch return to_sym if [:intel, :arm].include? arch
@ -211,7 +236,7 @@ module Utils
sig { returns(MacOSVersion) } sig { returns(MacOSVersion) }
def to_macos_version def to_macos_version
@to_macos_version ||= MacOSVersion.from_symbol(system) @to_macos_version ||= T.let(MacOSVersion.from_symbol(system), T.nilable(MacOSVersion))
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
@ -280,14 +305,20 @@ module Utils
sig { returns(T.any(Symbol, String)) } sig { returns(T.any(Symbol, String)) }
attr_reader :cellar attr_reader :cellar
sig { params(tag: Utils::Bottles::Tag, checksum: Checksum, cellar: T.any(Symbol, String)).void }
def initialize(tag:, checksum:, cellar:) def initialize(tag:, checksum:, cellar:)
@tag = tag @tag = tag
@checksum = checksum @checksum = checksum
@cellar = cellar @cellar = cellar
end end
sig { override.params(other: BasicObject).returns(T::Boolean) }
def ==(other) def ==(other)
self.class == other.class && tag == other.tag && checksum == other.checksum && cellar == other.cellar case other
when self.class
tag == other.tag && checksum == other.checksum && cellar == other.cellar
else false
end
end end
alias eql? == alias eql? ==
end end
@ -304,8 +335,13 @@ module Utils
@tag_specs.keys @tag_specs.keys
end end
sig { override.params(other: BasicObject).returns(T::Boolean) }
def ==(other) def ==(other)
self.class == other.class && @tag_specs == other.instance_variable_get(:@tag_specs) case other
when self.class
@tag_specs == other.tag_specs
else false
end
end end
alias eql? == alias eql? ==
@ -335,8 +371,14 @@ module Utils
@tag_specs[tag] if tag @tag_specs[tag] if tag
end end
protected
sig { returns(T::Hash[Utils::Bottles::Tag, Utils::Bottles::TagSpecification]) }
attr_reader :tag_specs
private private
sig { params(tag: Utils::Bottles::Tag, no_older_versions: T::Boolean).returns(T.nilable(Utils::Bottles::Tag)) }
def find_matching_tag(tag, no_older_versions: false) def find_matching_tag(tag, no_older_versions: false)
if @tag_specs.key?(tag) if @tag_specs.key?(tag)
tag tag