From 472cb0ad01304d8ada0872c598e96759b80b738e Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Wed, 3 Dec 2025 22:49:18 -0500 Subject: [PATCH] Use `FormulaStruct` when loading formulae from the API --- Library/Homebrew/api.rb | 1 + Library/Homebrew/api/formula.rb | 143 +++++++++++ Library/Homebrew/api/formula_struct.rb | 202 +++++++++++++++ Library/Homebrew/dependency_collector.rb | 2 +- Library/Homebrew/formula.rb | 8 +- Library/Homebrew/formulary.rb | 229 ++++-------------- .../rbi/dsl/homebrew/api/formula_struct.rbi | 41 ++++ .../sorbet/tapioca/compilers/api_structs.rb | 25 ++ Library/Homebrew/test/formulary_spec.rb | 18 +- Library/Homebrew/utils/spdx.rb | 20 +- 10 files changed, 491 insertions(+), 198 deletions(-) create mode 100644 Library/Homebrew/api/formula_struct.rb create mode 100644 Library/Homebrew/sorbet/rbi/dsl/homebrew/api/formula_struct.rbi create mode 100644 Library/Homebrew/sorbet/tapioca/compilers/api_structs.rb diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index fc48ac542334e..d830e20f41079 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -5,6 +5,7 @@ require "api/cask" require "api/formula" require "api/internal" +require "api/formula_struct" require "base64" require "utils/output" diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index dc24481b10ca7..b5fa440b07450 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -5,6 +5,7 @@ require "api" require "api/source_download" require "download_queue" +require "autobump_constants" module Homebrew module API @@ -157,6 +158,148 @@ def self.write_names_and_aliases(regenerate: false) Homebrew::API.write_names_file!(all_formulae.keys, "formula", regenerate:) Homebrew::API.write_aliases_file!(all_aliases, "formula", regenerate:) end + + sig { params(hash: T::Hash[String, T.untyped]).returns(FormulaStruct) } + def self.generate_formula_struct_hash(hash) + hash = Homebrew::API.merge_variations(hash).dup + + if (caveats = hash["caveats"]) + hash["caveats"] = Formulary.replace_placeholders(caveats) + end + + hash["bottle_checksums"] = begin + files = hash.dig("bottle", "stable", "files") || {} + files.map do |tag, bottle_spec| + { + cellar: Formulary.convert_to_string_or_symbol(bottle_spec.fetch("cellar")), + tag.to_sym => bottle_spec.fetch("sha256"), + } + end + end + + hash["bottle_rebuild"] = hash.dig("bottle", "stable", "rebuild") + + conflicts_with = hash["conflicts_with"] || [] + conflicts_with_reasons = hash["conflicts_with_reasons"] || [] + hash["conflicts"] = conflicts_with.zip(conflicts_with_reasons).map do |name, reason| + if reason.present? + [name, { because: reason }] + else + [name, {}] + end + end + + if (deprecation_date = hash["deprecation_date"]) + hash["deprecate_args"] = { + date: deprecation_date, + because: DeprecateDisable.to_reason_string_or_symbol(hash["deprecation_reason"], + type: :formula), + replacement_formula: hash["deprecation_replacement_formula"], + replacement_cask: hash["deprecation_replacement_cask"], + } + end + + if (disable_date = hash["disable_date"]) + hash["disable_args"] = { + date: disable_date, + because: DeprecateDisable.to_reason_string_or_symbol(hash["disable_reason"], type: :formula), + replacement_formula: hash["disable_replacement_formula"], + replacement_cask: hash["disable_replacement_cask"], + } + end + + hash["head_dependency_hash"] = hash["head_dependencies"] + + hash["head_url_args"] = begin + url = hash.dig("urls", "head", "url") + specs = { + branch: hash.dig("urls", "head", "branch"), + using: hash.dig("urls", "head", "using")&.to_sym, + }.compact_blank + [url, specs] + end + + if (keg_only_hash = hash["keg_only_reason"]) + reason = Formulary.convert_to_string_or_symbol(keg_only_hash.fetch("reason")) + explanation = keg_only_hash["explanation"] + hash["keg_only_args"] = [reason, explanation].compact + end + + hash["license"] = SPDX.string_to_license_expression(hash["license"]) + + hash["link_overwrite_paths"] = hash["link_overwrite"] + + if (reason = hash["no_autobump_message"]) + reason = reason.to_sym if NO_AUTOBUMP_REASONS_LIST.key?(reason.to_sym) + hash["no_autobump_args"] = { because: reason } + end + + if (condition = hash["pour_bottle_only_if"]) + hash["pour_bottle_args"] = { only_if: condition.to_sym } + end + + hash["requirements_array"] = hash["requirements"] + + hash["ruby_source_checksum"] = hash.dig("ruby_source_checksum", "sha256") + + if (service_hash = hash["service"]) + service_hash = Homebrew::Service.from_hash(service_hash) + + hash["service_run_args"], hash["service_run_kwargs"] = case (run = service_hash[:run]) + when Hash + [[], run] + when Array, String + [[run], {}] + else + [[], {}] + end + + hash["service_name_args"] = service_hash[:name] + + hash["service_args"] = service_hash.filter_map do |key, arg| + [key.to_sym, arg] if key != :name && key != :run + end + end + + hash["stable_checksum"] = hash.dig("urls", "stable", "checksum") + + hash["stable_dependency_hash"] = { + "dependencies" => hash["dependencies"] || [], + "build_dependencies" => hash["build_dependencies"] || [], + "test_dependencies" => hash["test_dependencies"] || [], + "recommended_dependencies" => hash["recommended_dependencies"] || [], + "optional_dependencies" => hash["optional_dependencies"] || [], + "uses_from_macos" => hash["uses_from_macos"] || [], + "uses_from_macos_bounds" => hash["uses_from_macos_bounds"] || [], + } + + hash["stable_url_args"] = begin + url = hash.dig("urls", "stable", "url") + specs = { + tag: hash.dig("urls", "stable", "tag"), + revision: hash.dig("urls", "stable", "revision"), + using: hash.dig("urls", "stable", "using")&.to_sym, + }.compact_blank + [url, specs] + end + + hash["stable_version"] = hash.dig("versions", "stable") + + # Should match FormulaStruct::PREDICATES + hash["bottle_present"] = hash["bottle"].present? + hash["deprecated_present"] = hash["deprecation_date"].present? + hash["disabled_present"] = hash["disable_date"].present? + hash["head_present"] = hash.dig("urls", "head").present? + hash["keg_only_present"] = hash["keg_only_reason"].present? + hash["no_autobump_message_present"] = hash["no_autobump_message"].present? + hash["pour_bottle_present"] = hash["pour_bottle_only_if"].present? + hash["service_present"] = hash["service"].present? + hash["service_run_present"] = hash.dig("service", "run").present? + hash["service_name_present"] = hash.dig("service", "name").present? + hash["stable_present"] = hash.dig("urls", "stable").present? + + FormulaStruct.from_hash(hash) + end end end end diff --git a/Library/Homebrew/api/formula_struct.rb b/Library/Homebrew/api/formula_struct.rb new file mode 100644 index 0000000000000..739729b9ab1a7 --- /dev/null +++ b/Library/Homebrew/api/formula_struct.rb @@ -0,0 +1,202 @@ +# typed: strict +# frozen_string_literal: true + +require "service" +require "utils/spdx" + +module Homebrew + module API + class FormulaStruct < T::Struct + PREDICATES = [ + :bottle, + :deprecated, + :disabled, + :head, + :keg_only, + :no_autobump_message, + :pour_bottle, + :service, + :service_run, + :service_name, + :stable, + ].freeze + + # `:codesign` and custom requirement classes are not supported. + API_SUPPORTED_REQUIREMENTS = [:arch, :linux, :macos, :maximum_macos, :xcode].freeze + private_constant :API_SUPPORTED_REQUIREMENTS + + DependencyArgs = T.type_alias do + T.any( + # Formula name: "foo" + String, + # Formula name and dependency type: { "foo" => :build } + T::Hash[String, Symbol], + ) + end + + RequirementArgs = T.type_alias do + T.any( + # Requirement name: :macos + Symbol, + # Requirement name and other info: { macos: :build } + T::Hash[Symbol, T::Array[T.anything]], + ) + end + + UsesFromMacOSArgs = T.type_alias do + [ + T.any( + # Formula name: "foo" + String, + # Formula name and dependency type: { "foo" => :build } + # Formula name, dependency type, and version bounds: { "foo" => :build, since: :catalina } + T::Hash[T.any(String, Symbol), T.any(Symbol, T::Array[Symbol])], + ), + # If the first argument is only a name, this argument contains the version bounds: { since: :catalina } + T::Hash[Symbol, Symbol], + ] + end + + PREDICATES.each do |predicate_name| + present_method_name = :"#{predicate_name}_present" + predicate_method_name = :"#{predicate_name}?" + + const present_method_name, T::Boolean, default: false + + define_method(predicate_method_name) do + send(present_method_name) + end + end + + # Changes to this struct must be mirrored in Homebrew::API::Formula.generate_formula_struct_hash + const :aliases, T::Array[String], default: [] + const :bottle, T::Hash[String, T.anything], default: {} + const :bottle_checksums, T::Array[T::Hash[String, T.anything]], default: [] + const :bottle_rebuild, Integer, default: 0 + const :caveats, T.nilable(String) + const :conflicts, T::Array[[String, T::Hash[Symbol, String]]], default: [] + const :deprecate_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {} + const :desc, String + const :disable_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {} + const :head_url_args, [String, T::Hash[Symbol, T.anything]] + const :homepage, String + const :keg_only_args, T::Array[T.any(String, Symbol)], default: [] + const :license, SPDX::LicenseExpression + const :link_overwrite_paths, T::Array[String], default: [] + const :no_autobump_args, T::Hash[Symbol, T.any(String, Symbol)], default: {} + const :oldnames, T::Array[String], default: [] + const :post_install_defined, T::Boolean, default: true + const :pour_bottle_args, T::Hash[Symbol, Symbol], default: {} + const :revision, Integer, default: 0 + const :ruby_source_checksum, String + const :ruby_source_path, String + const :service_args, T::Array[[Symbol, BasicObject]], default: [] + const :service_name_args, T::Hash[Symbol, String], default: {} + const :service_run_args, T::Array[Homebrew::Service::RunParam], default: [] + const :service_run_kwargs, T::Hash[Symbol, Homebrew::Service::RunParam], default: {} + const :stable_checksum, T.nilable(String) + const :stable_url_args, [String, T::Hash[Symbol, T.anything]] + const :stable_version, String + const :tap_git_head, String + const :version_scheme, Integer, default: 0 + const :versioned_formulae, T::Array[String], default: [] + + sig { returns(T::Array[T.any(DependencyArgs, RequirementArgs)]) } + def head_dependencies + spec_dependencies(:head) + spec_requirements(:head) + end + + sig { returns(T::Array[T.any(DependencyArgs, RequirementArgs)]) } + def stable_dependencies + spec_dependencies(:stable) + spec_requirements(:stable) + end + + sig { returns(T::Array[UsesFromMacOSArgs]) } + def head_uses_from_macos + spec_uses_from_macos(:head) + end + + sig { returns(T::Array[UsesFromMacOSArgs]) } + def stable_uses_from_macos + spec_uses_from_macos(:stable) + end + + private + + const :stable_dependency_hash, T::Hash[String, T::Array[String]], default: {} + const :head_dependency_hash, T::Hash[String, T::Array[String]], default: {} + const :requirements_array, T::Array[T::Hash[String, T.untyped]], default: [] + + sig { params(spec: Symbol).returns(T::Array[DependencyArgs]) } + def spec_dependencies(spec) + deps_hash = send("#{spec}_dependency_hash") + dependencies = deps_hash.fetch("dependencies", []) + dependencies + [:build, :test, :recommended, :optional].filter_map do |type| + deps_hash["#{type}_dependencies"]&.map do |dep| + { dep => type } + end + end.flatten(1) + end + + sig { params(spec: Symbol).returns(T::Array[UsesFromMacOSArgs]) } + def spec_uses_from_macos(spec) + deps_hash = send("#{spec}_dependency_hash") + zipped_array = deps_hash["uses_from_macos"]&.zip(deps_hash["uses_from_macos_bounds"]) + return [] unless zipped_array + + zipped_array.map do |entry, bounds| + bounds ||= {} + bounds = bounds.transform_keys(&:to_sym).transform_values(&:to_sym) + + if entry.is_a?(Hash) + # The key is the dependency name, the value is the dep type. Only the type should be a symbol + entry = entry.deep_transform_values(&:to_sym) + # When passing both a dep type and bounds, uses_from_macos expects them both in the first argument + entry = entry.merge(bounds) + [entry, {}] + else + [entry, bounds] + end + end + end + + sig { params(spec: Symbol).returns(T::Array[RequirementArgs]) } + def spec_requirements(spec) + requirements_array.filter_map do |req| + next unless req["specs"].include?(spec.to_s) + + req_name = req["name"].to_sym + next if API_SUPPORTED_REQUIREMENTS.exclude?(req_name) + + req_version = case req_name + when :arch + req["version"]&.to_sym + when :macos, :maximum_macos + MacOSVersion::SYMBOLS.key(req["version"]) + else + req["version"] + end + + req_tags = [] + req_tags << req_version if req_version.present? + req_tags += req["contexts"]&.map do |tag| + case tag + when String + tag.to_sym + when Hash + tag.deep_transform_keys(&:to_sym) + else + tag + end + end + + if req_tags.empty? + req_name + else + { req_name => req_tags } + end + end + end + end + end +end diff --git a/Library/Homebrew/dependency_collector.rb b/Library/Homebrew/dependency_collector.rb index 2daf26c644221..8a9a793d63e38 100644 --- a/Library/Homebrew/dependency_collector.rb +++ b/Library/Homebrew/dependency_collector.rb @@ -160,7 +160,7 @@ def parse_string_spec(spec, tags) def parse_symbol_spec(spec, tags) # When modifying this list of supported requirements, consider - # whether `Formulary::API_SUPPORTED_REQUIREMENTS` should also be changed. + # whether `FormulaStruct::API_SUPPORTED_REQUIREMENTS` should also be changed. case spec when :arch then ArchRequirement.new(tags) when :codesign then CodesignRequirement.new(tags) diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 72b36545ba81f..1bf749d91e581 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -3602,14 +3602,14 @@ def desc(val = T.unsafe(nil)) # @see https://spdx.github.io/spdx-spec/latest/annexes/spdx-license-expressions/ SPDX license expression guide # @api public sig { - params(args: T.nilable(T.any(String, Symbol, T::Hash[T.any(String, Symbol), T.anything]))) - .returns(T.nilable(T.any(String, Symbol, T::Hash[T.any(String, Symbol), T.anything]))) + params(args: T.nilable(SPDX::LicenseExpression)) + .returns(T.nilable(SPDX::LicenseExpression)) } def license(args = nil) if args.nil? @licenses else - @licenses = T.let(args, T.nilable(T.any(String, Symbol, T::Hash[T.any(String, Symbol), T.anything]))) + @licenses = T.let(args, T.nilable(SPDX::LicenseExpression)) end end @@ -3936,7 +3936,7 @@ def build_flags # ``` # # @api public - sig { params(block: T.nilable(T.proc.returns(SoftwareSpec))).returns(T.untyped) } + sig { params(block: T.nilable(T.proc.void)).returns(T.untyped) } def stable(&block) return T.must(@stable) unless block diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index ce33c1dce83e1..3e2c3dbb32f67 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -26,10 +26,6 @@ module Formulary ALLOWED_URL_SCHEMES = %w[file].freeze private_constant :ALLOWED_URL_SCHEMES - # `:codesign` and custom requirement classes are not supported. - API_SUPPORTED_REQUIREMENTS = [:arch, :linux, :macos, :maximum_macos, :xcode].freeze - private_constant :API_SUPPORTED_REQUIREMENTS - # Enable the factory cache. # # @api internal @@ -227,251 +223,132 @@ def self.load_formula_from_json!(name, json_formula_with_variations, flags:) mod.const_set(:BUILD_FLAGS, flags) class_name = class_s(name) - json_formula = Homebrew::API.merge_variations(json_formula_with_variations) - - caveats_string = (replace_placeholders(json_formula["caveats"]) if json_formula["caveats"]) - - uses_from_macos_names = json_formula.fetch("uses_from_macos", []).map do |dep| - next dep unless dep.is_a? Hash - - dep.keys.first - end - - requirements = {} - json_formula["requirements"]&.map do |req| - req_name = req["name"].to_sym - next if API_SUPPORTED_REQUIREMENTS.exclude?(req_name) - - req_version = case req_name - when :arch - req["version"]&.to_sym - when :macos, :maximum_macos - MacOSVersion::SYMBOLS.key(req["version"]) - else - req["version"] - end - - req_tags = [] - req_tags << req_version if req_version.present? - req_tags += req["contexts"]&.map do |tag| - case tag - when String - tag.to_sym - when Hash - tag.deep_transform_keys(&:to_sym) - else - tag - end - end - - spec_hash = req_tags.empty? ? req_name : { req_name => req_tags } - - specs = req["specs"] - specs ||= ["stable", "head"] # backwards compatibility - specs.each do |spec| - requirements[spec.to_sym] ||= [] - requirements[spec.to_sym] << spec_hash - end - end - - add_deps = lambda do |spec| - T.bind(self, SoftwareSpec) - - dep_json = json_formula.fetch("#{spec}_dependencies", json_formula) - - dep_json["dependencies"]&.each do |dep| - # Backwards compatibility check - uses_from_macos used to be a part of dependencies on Linux - next if !json_formula.key?("uses_from_macos_bounds") && uses_from_macos_names.include?(dep) && - !Homebrew::SimulateSystem.simulating_or_running_on_macos? - - depends_on dep - end - - [:build, :test, :recommended, :optional].each do |type| - dep_json["#{type}_dependencies"]&.each do |dep| - # Backwards compatibility check - uses_from_macos used to be a part of dependencies on Linux - next if !json_formula.key?("uses_from_macos_bounds") && uses_from_macos_names.include?(dep) && - !Homebrew::SimulateSystem.simulating_or_running_on_macos? - - depends_on dep => type - end - end - - dep_json["uses_from_macos"]&.each_with_index do |dep, index| - bounds = dep_json.fetch("uses_from_macos_bounds", [])[index].dup || {} - bounds.deep_transform_keys!(&:to_sym) - bounds.deep_transform_values!(&:to_sym) - - if dep.is_a?(Hash) - uses_from_macos dep.deep_transform_values(&:to_sym).merge(bounds) - else - uses_from_macos dep, bounds - end - end - end + formula_struct = Homebrew::API::Formula.generate_formula_struct_hash(json_formula_with_variations) klass = Class.new(::Formula) do @loaded_from_api = T.let(true, T.nilable(T::Boolean)) @api_source = T.let(json_formula_with_variations, T.nilable(T::Hash[String, T.untyped])) - desc json_formula["desc"] - homepage json_formula["homepage"] - license SPDX.string_to_license_expression(json_formula["license"]) - revision json_formula.fetch("revision", 0) - version_scheme json_formula.fetch("version_scheme", 0) + desc formula_struct.desc + homepage formula_struct.homepage + license formula_struct.license + revision formula_struct.revision + version_scheme formula_struct.version_scheme - if (urls_stable = json_formula["urls"]["stable"].presence) + if formula_struct.stable? stable do - url_spec = { - tag: urls_stable["tag"], - revision: urls_stable["revision"], - using: urls_stable["using"]&.to_sym, - }.compact - url urls_stable["url"], **url_spec - version json_formula["versions"]["stable"] - sha256 urls_stable["checksum"] if urls_stable["checksum"].present? - - instance_exec(:stable, &add_deps) - requirements[:stable]&.each do |req| - depends_on req + url(*formula_struct.stable_url_args) + version formula_struct.stable_version + if (checksum = formula_struct.stable_checksum) + sha256 checksum + end + + formula_struct.stable_dependencies.each do |dep| + depends_on dep + end + + formula_struct.stable_uses_from_macos.each do |args| + uses_from_macos(*args) end end end - if (urls_head = json_formula["urls"]["head"].presence) + if formula_struct.head? head do - url_spec = { - branch: urls_head["branch"], - using: urls_head["using"]&.to_sym, - }.compact - url urls_head["url"], **url_spec - - instance_exec(:head, &add_deps) - requirements[:head]&.each do |req| - depends_on req + url(*formula_struct.head_url_args) + + formula_struct.head_dependencies.each do |dep| + depends_on dep end - end - end - if (because = json_formula["no_autobump_msg"]) - because = because.to_sym if NO_AUTOBUMP_REASONS_LIST.key?(because.to_sym) - no_autobump!(because:) + formula_struct.head_uses_from_macos.each do |args| + uses_from_macos(*args) + end + end end - bottles_stable = json_formula["bottle"]["stable"].presence + no_autobump!(**formula_struct.no_autobump_args) if formula_struct.no_autobump_message? - if bottles_stable + if formula_struct.bottle? bottle do if Homebrew::EnvConfig.bottle_domain == HOMEBREW_BOTTLE_DEFAULT_DOMAIN root_url HOMEBREW_BOTTLE_DEFAULT_DOMAIN else root_url Homebrew::EnvConfig.bottle_domain end - rebuild bottles_stable["rebuild"] - bottles_stable["files"].each do |tag, bottle_spec| - cellar = Formulary.convert_to_string_or_symbol bottle_spec["cellar"] - sha256 cellar:, tag.to_sym => bottle_spec["sha256"] + rebuild formula_struct.bottle_rebuild + formula_struct.bottle_checksums.each do |args| + sha256(**args) end end end - if (pour_bottle_only_if = json_formula["pour_bottle_only_if"]) - pour_bottle? only_if: pour_bottle_only_if.to_sym - end - - if (keg_only_reason = json_formula["keg_only_reason"].presence) - reason = Formulary.convert_to_string_or_symbol keg_only_reason["reason"] - keg_only reason, keg_only_reason["explanation"] - end + pour_bottle?(**formula_struct.pour_bottle_args) if formula_struct.pour_bottle? - if (deprecation_date = json_formula["deprecation_date"].presence) - reason = DeprecateDisable.to_reason_string_or_symbol json_formula["deprecation_reason"], type: :formula - replacement_formula = json_formula["deprecation_replacement_formula"] - replacement_cask = json_formula["deprecation_replacement_cask"] - deprecate! date: deprecation_date, because: reason, replacement_formula:, replacement_cask: - end + keg_only(*formula_struct.keg_only_args) if formula_struct.keg_only? - if (disable_date = json_formula["disable_date"].presence) - reason = DeprecateDisable.to_reason_string_or_symbol json_formula["disable_reason"], type: :formula - replacement_formula = json_formula["disable_replacement_formula"] - replacement_cask = json_formula["disable_replacement_cask"] - disable! date: disable_date, because: reason, replacement_formula:, replacement_cask: - end + deprecate!(**formula_struct.deprecate_args) if formula_struct.deprecated? + disable!(**formula_struct.disable_args) if formula_struct.disabled? - json_formula["conflicts_with"]&.each_with_index do |conflict, index| - conflicts_with conflict, because: json_formula.dig("conflicts_with_reasons", index) + formula_struct.conflicts.each do |name, args| + conflicts_with(name, **args) end - json_formula["link_overwrite"]&.each do |overwrite_path| - link_overwrite overwrite_path + formula_struct.link_overwrite_paths.each do |path| + link_overwrite path end define_method(:install) do raise NotImplementedError, "Cannot build from source from abstract formula." end - @post_install_defined_boolean = T.let(json_formula["post_install_defined"], T.nilable(T::Boolean)) - @post_install_defined_boolean = true if @post_install_defined_boolean.nil? # Backwards compatibility + @post_install_defined_boolean = T.let(formula_struct.post_install_defined, T.nilable(T::Boolean)) define_method(:post_install_defined?) do self.class.instance_variable_get(:@post_install_defined_boolean) end - if (service_hash = json_formula["service"].presence) - service_hash = Homebrew::Service.from_hash(service_hash) + if formula_struct.service? service do - T.bind(self, Homebrew::Service) - - if (run_params = service_hash.delete(:run).presence) - case run_params - when Hash - run(**run_params) - when Array, String - run run_params - end - end - - if (name_params = service_hash.delete(:name).presence) - name(**name_params) - end + run(*formula_struct.service_run_args, **formula_struct.service_run_kwargs) if formula_struct.service_run? + name(**formula_struct.service_name_args) if formula_struct.service_name? - service_hash.each do |key, arg| + formula_struct.service_args.each do |key, arg| public_send(key, arg) end end end - @caveats_string = T.let(caveats_string, T.nilable(String)) + @caveats_string = T.let(formula_struct.caveats, T.nilable(String)) define_method(:caveats) do self.class.instance_variable_get(:@caveats_string) end - @tap_git_head_string = T.let(json_formula["tap_git_head"], T.nilable(String)) + @tap_git_head_string = T.let(formula_struct.tap_git_head, T.nilable(String)) define_method(:tap_git_head) do self.class.instance_variable_get(:@tap_git_head_string) end - @oldnames_array = T.let(json_formula["oldnames"] || [json_formula["oldname"]].compact, T.nilable(T::Array[String])) + @oldnames_array = T.let(formula_struct.oldnames, T.nilable(T::Array[String])) define_method(:oldnames) do self.class.instance_variable_get(:@oldnames_array) end - @aliases_array = T.let(json_formula.fetch("aliases", []), T.nilable(T::Array[String])) + @aliases_array = T.let(formula_struct.aliases, T.nilable(T::Array[String])) define_method(:aliases) do self.class.instance_variable_get(:@aliases_array) end - @versioned_formulae_array = T.let(json_formula.fetch("versioned_formulae", []), T.nilable(T::Array[String])) + @versioned_formulae_array = T.let(formula_struct.versioned_formulae, T.nilable(T::Array[String])) define_method(:versioned_formulae_names) do self.class.instance_variable_get(:@versioned_formulae_array) end - @ruby_source_path_string = T.let(json_formula["ruby_source_path"], T.nilable(String)) + @ruby_source_path_string = T.let(formula_struct.ruby_source_path, T.nilable(String)) define_method(:ruby_source_path) do self.class.instance_variable_get(:@ruby_source_path_string) end - @ruby_source_checksum_string = T.let(json_formula.dig("ruby_source_checksum", "sha256"), T.nilable(String)) - @ruby_source_checksum_string ||= json_formula["ruby_source_sha256"] + @ruby_source_checksum_string = T.let(formula_struct.ruby_source_checksum, T.nilable(String)) define_method(:ruby_source_checksum) do checksum = self.class.instance_variable_get(:@ruby_source_checksum_string) Checksum.new(checksum) if checksum diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/api/formula_struct.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/api/formula_struct.rbi new file mode 100644 index 0000000000000..fd6e0296be16f --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/api/formula_struct.rbi @@ -0,0 +1,41 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Homebrew::API::FormulaStruct`. +# Please instead update this file by running `bin/tapioca dsl Homebrew::API::FormulaStruct`. + + +class Homebrew::API::FormulaStruct + sig { returns(T::Boolean) } + def bottle?; end + + sig { returns(T::Boolean) } + def deprecated?; end + + sig { returns(T::Boolean) } + def disabled?; end + + sig { returns(T::Boolean) } + def head?; end + + sig { returns(T::Boolean) } + def keg_only?; end + + sig { returns(T::Boolean) } + def no_autobump_message?; end + + sig { returns(T::Boolean) } + def pour_bottle?; end + + sig { returns(T::Boolean) } + def service?; end + + sig { returns(T::Boolean) } + def service_name?; end + + sig { returns(T::Boolean) } + def service_run?; end + + sig { returns(T::Boolean) } + def stable?; end +end diff --git a/Library/Homebrew/sorbet/tapioca/compilers/api_structs.rb b/Library/Homebrew/sorbet/tapioca/compilers/api_structs.rb new file mode 100644 index 0000000000000..61cd17cc636b8 --- /dev/null +++ b/Library/Homebrew/sorbet/tapioca/compilers/api_structs.rb @@ -0,0 +1,25 @@ +# typed: strict +# frozen_string_literal: true + +require_relative "../../../global" +require "api/formula_struct" + +module Tapioca + module Compilers + class ApiStructs < Tapioca::Dsl::Compiler + ConstantType = type_member { { fixed: T.class_of(T::Struct) } } + + sig { override.returns(T::Enumerable[T::Module[T.anything]]) } + def self.gather_constants = [::Homebrew::API::FormulaStruct] + + sig { override.void } + def decorate + root.create_class(T.must(constant.name)) do |klass| + ::Homebrew::API::FormulaStruct::PREDICATES.each do |predicate_name| + klass.create_method("#{predicate_name}?", return_type: "T::Boolean") + end + end + end + end + end +end diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 08ad75ca27c91..4f6c9a9e197ef 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -362,6 +362,7 @@ def formula_json_contents(extra_items = {}) "download" => nil, "version" => "1.0", "contexts" => ["build"], + "specs" => ["stable"], }, ], "conflicts_with" => ["conflicting_formula"], @@ -377,21 +378,28 @@ def formula_json_contents(extra_items = {}) }, "ruby_source_path" => "Formula/#{formula_name}.rb", "ruby_source_checksum" => { "sha256" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }, + "tap_git_head" => "0000000000000000000000000000000000000000", }.merge(extra_items), } end let(:deprecate_json) do { - "deprecation_date" => "2022-06-15", - "deprecation_reason" => "repo_archived", + "deprecated" => true, + "deprecation_date" => "2022-06-15", + "deprecation_reason" => "repo_archived", + "deprecation_replacement_formula" => nil, + "deprecation_replacement_cask" => nil, } end let(:disable_json) do { - "disable_date" => "2022-06-15", - "disable_reason" => "requires something else", + "disabled" => true, + "disable_date" => "2022-06-15", + "disable_reason" => "requires something else", + "disable_replacement_formula" => nil, + "disable_replacement_cask" => nil, } end @@ -419,7 +427,7 @@ def formula_json_contents(extra_items = {}) { "variations" => { "x86_64_linux" => { - "dependencies" => ["dep", "uses_from_macos_dep"], + "dependencies" => ["dep"], }, }, } diff --git a/Library/Homebrew/utils/spdx.rb b/Library/Homebrew/utils/spdx.rb index 48eb050b0d2f6..5f333b09abac3 100644 --- a/Library/Homebrew/utils/spdx.rb +++ b/Library/Homebrew/utils/spdx.rb @@ -154,19 +154,15 @@ def license_expression_to_string(license_expression, bracket: false, hash_type: end end - sig { - params( - string: T.nilable(String), - ).returns( - T.nilable( - T.any( - String, - Symbol, - T::Hash[T.any(String, Symbol), T.untyped], - ), - ), + LicenseExpression = T.type_alias do + T.any( + String, + Symbol, + T::Hash[T.any(String, Symbol), T.anything], ) - } + end + + sig { params(string: T.nilable(String)).returns(T.nilable(LicenseExpression)) } def string_to_license_expression(string) return if string.blank?