From 486c3765ced95f6432e2de22566269e9f87a3cfb Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Fri, 14 Apr 2023 15:33:40 +0200 Subject: [PATCH] Add `--os=all` and `--arch=all` options. --- Library/Homebrew/cli/args.rb | 58 ++++-- Library/Homebrew/cli/args.rbi | 6 + Library/Homebrew/cli/named_args.rb | 46 +++-- Library/Homebrew/cmd/--cache.rb | 65 ++++++- Library/Homebrew/cmd/fetch.rb | 129 ++++++++----- Library/Homebrew/dev-cmd/audit.rb | 129 +++++++------ Library/Homebrew/dev-cmd/irb.rb | 1 + Library/Homebrew/extend/os/mac/diagnostic.rb | 2 +- .../Homebrew/extend/os/mac/utils/bottles.rb | 13 +- Library/Homebrew/fetch.rb | 6 +- Library/Homebrew/formula.rb | 2 +- Library/Homebrew/formula_installer.rb | 2 +- Library/Homebrew/formula_versions.rb | 8 +- Library/Homebrew/formulary.rb | 176 ++++++++++++++---- Library/Homebrew/sorbet/parlour/attr.rb | 49 ++++- Library/Homebrew/test/cli/named_args_spec.rb | 6 +- .../Homebrew/test/support/helper/formula.rb | 2 +- Library/Homebrew/utils/bottles.rb | 18 +- 18 files changed, 534 insertions(+), 184 deletions(-) diff --git a/Library/Homebrew/cli/args.rb b/Library/Homebrew/cli/args.rb index cf49a4dcc2..02e66905cf 100644 --- a/Library/Homebrew/cli/args.rb +++ b/Library/Homebrew/cli/args.rb @@ -33,13 +33,15 @@ module Homebrew end def freeze_named_args!(named_args, cask_options:) + options = {} + options[:force_bottle] = true if self[:force_bottle?] + options[:override_spec] = :head if self[:HEAD?] + options[:flags] = flags_only unless flags_only.empty? self[:named] = NamedArgs.new( *named_args.freeze, - override_spec: spec(nil), - force_bottle: self[:force_bottle?], - flags: flags_only, - cask_options: cask_options, - parent: self, + parent: self, + cask_options: cask_options, + **options, ) end @@ -98,6 +100,44 @@ module Homebrew return :cask if cask? && !formula? end + sig { returns(T::Array[[Symbol, Symbol]]) } + def os_arch_combinations + skip_invalid_combinations = false + + oses = case (os_sym = os&.to_sym) + when nil + [SimulateSystem.current_os] + when :all + skip_invalid_combinations = true + + [ + *MacOSVersions::SYMBOLS.keys, + :linux, + ] + else + [os_sym] + end + + arches = case (arch_sym = arch&.to_sym) + when nil + [SimulateSystem.current_arch] + when :all + skip_invalid_combinations = true + OnSystem::ARCH_OPTIONS + else + [arch_sym] + end + + oses.product(arches).select do |os, arch| + if skip_invalid_combinations + bottle_tag = Utils::Bottles::Tag.new(system: os, arch: arch) + bottle_tag.valid_combination? + else + true + end + end + end + private def option_to_name(option) @@ -124,14 +164,6 @@ module Homebrew @cli_args.freeze end - def spec(default = :stable) - if self[:HEAD?] - :head - else - default - end - end - def respond_to_missing?(method_name, *) @table.key?(method_name) end diff --git a/Library/Homebrew/cli/args.rbi b/Library/Homebrew/cli/args.rbi index 37d4ac897a..e5394d4d7b 100644 --- a/Library/Homebrew/cli/args.rbi +++ b/Library/Homebrew/cli/args.rbi @@ -240,6 +240,9 @@ module Homebrew sig { returns(T.nilable(T::Array[String])) } def only; end + sig { returns(T.nilable(String)) } + def os; end + sig { returns(T.nilable(T::Array[String])) } def except; end @@ -270,6 +273,9 @@ module Homebrew sig { returns(T::Boolean) } def s?; end + sig { returns(T.nilable(String)) } + def arch; end + sig { returns(T.nilable(String)) } def appdir; end diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 078e9cfd80..3e68e848ce 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -11,7 +11,24 @@ module Homebrew # # @api private class NamedArgs < Array - def initialize(*args, parent: Args.new, override_spec: nil, force_bottle: false, flags: [], cask_options: false) + sig { + params( + args: String, + parent: Args, + override_spec: Symbol, + force_bottle: T::Boolean, + flags: T::Array[String], + cask_options: T::Boolean, + ).void + } + def initialize( + *args, + parent: Args.new, + override_spec: T.unsafe(nil), + force_bottle: T.unsafe(nil), + flags: T.unsafe(nil), + cask_options: false + ) require "cask/cask" require "cask/cask_loader" require "formulary" @@ -50,11 +67,17 @@ module Homebrew warn: T::Boolean, ).returns(T::Array[T.any(Formula, Keg, Cask::Cask)]) } - def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true, - warn: true) + def to_formulae_and_casks( + only: parent&.only_formula_or_cask, + ignore_unavailable: nil, + method: T.unsafe(nil), + uniq: true, + warn: T.unsafe(nil) + ) @to_formulae_and_casks ||= {} @to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name| - load_formula_or_cask(name, only: only, method: method, warn: warn) + options = { warn: warn }.compact + load_formula_or_cask(name, only: only, method: method, **options) rescue FormulaUnreadableError, FormulaClassUnavailableError, TapFormulaUnreadableError, TapFormulaClassUnavailableError, Cask::CaskUnreadableError @@ -88,14 +111,15 @@ module Homebrew end.uniq.freeze end - def load_formula_or_cask(name, only: nil, method: nil, warn: true) + def load_formula_or_cask(name, only: nil, method: nil, warn: nil) unreadable_error = nil if only != :cask begin formula = case method when nil, :factory - Formulary.factory(name, *spec, force_bottle: @force_bottle, flags: @flags, warn: warn) + options = { warn: warn, force_bottle: @force_bottle, flags: @flags }.compact + Formulary.factory(name, *@override_spec, **options) when :resolve resolve_formula(name) when :latest_kegs @@ -126,7 +150,8 @@ module Homebrew begin config = Cask::Config.from_args(@parent) if @cask_options - cask = Cask::CaskLoader.load(name, config: config, warn: warn) + options = { warn: warn }.compact + cask = Cask::CaskLoader.load(name, config: config, **options) if unreadable_error.present? onoe <<~EOS @@ -177,7 +202,7 @@ module Homebrew private :load_formula_or_cask def resolve_formula(name) - Formulary.resolve(name, spec: spec, force_bottle: @force_bottle, flags: @flags) + Formulary.resolve(name, **{ spec: @override_spec, force_bottle: @force_bottle, flags: @flags }.compact) end private :resolve_formula @@ -306,11 +331,6 @@ module Homebrew end.uniq end - def spec - @override_spec - end - private :spec - def resolve_kegs(name) raise UsageError if name.blank? diff --git a/Library/Homebrew/cmd/--cache.rb b/Library/Homebrew/cmd/--cache.rb index c359612c65..89e517383f 100644 --- a/Library/Homebrew/cmd/--cache.rb +++ b/Library/Homebrew/cmd/--cache.rb @@ -16,6 +16,12 @@ module Homebrew If is provided, display the file or directory used to cache . EOS + flag "--os=", + description: "Show cache file for the given operating system." \ + "(Pass `all` to show cache files for all operating systems.)" + flag "--arch=", + description: "Show cache file for the given CPU architecture." \ + "(Pass `all` to show cache files for all architectures.)" switch "-s", "--build-from-source", description: "Show the cache file used when building from source." switch "--force-bottle", @@ -31,6 +37,8 @@ module Homebrew conflicts "--build-from-source", "--force-bottle", "--bottle-tag", "--HEAD", "--cask" conflicts "--formula", "--cask" + conflicts "--os", "--bottle-tag" + conflicts "--arch", "--bottle-tag" named_args [:formula, :cask] end @@ -46,21 +54,62 @@ module Homebrew end formulae_or_casks = args.named.to_formulae_and_casks + os_arch_combinations = args.os_arch_combinations formulae_or_casks.each do |formula_or_cask| - if formula_or_cask.is_a? Formula - print_formula_cache formula_or_cask, args: args + case formula_or_cask + when Formula + formula = T.cast(formula_or_cask, Formula) + ref = formula.loaded_from_api? ? formula.full_name : formula.path + + os_arch_combinations.each do |os, arch| + SimulateSystem.with os: os, arch: arch do + Formulary.clear_cache + formula = Formulary.factory(ref) + print_formula_cache(formula, os: os, arch: arch, args: args) + end + end else - print_cask_cache formula_or_cask + cask = formula_or_cask + ref = cask.loaded_from_api? ? cask.full_token : cask.sourcefile_path + + os_arch_combinations.each do |os, arch| + next if os == :linux + + SimulateSystem.with os: os, arch: arch do + cask = Cask::CaskLoader.load(ref) + print_cask_cache(cask) + end + end end end end - sig { params(formula: Formula, args: CLI::Args).void } - def self.print_formula_cache(formula, args:) - if fetch_bottle?(formula, force_bottle: args.force_bottle?, bottle_tag: args.bottle_tag&.to_sym, - build_from_source_formulae: args.build_from_source_formulae) - puts formula.bottle_for_tag(args.bottle_tag&.to_sym)&.cached_download + sig { params(formula: Formula, os: Symbol, arch: Symbol, args: CLI::Args).void } + def self.print_formula_cache(formula, os:, arch:, args:) + if fetch_bottle?( + formula, + force_bottle: args.force_bottle?, + bottle_tag: args.bottle_tag&.to_sym, + build_from_source_formulae: args.build_from_source_formulae, + os: args.os&.to_sym, + arch: args.arch&.to_sym, + ) + bottle_tag = if (bottle_tag = args.bottle_tag&.to_sym) + # TODO: odeprecate "--bottle-tag" + Utils::Bottles::Tag.from_symbol(bottle_tag) + else + Utils::Bottles::Tag.new(system: os, arch: arch) + end + + bottle = formula.bottle_for_tag(bottle_tag) + + if bottle.nil? + opoo "Bottle for tag #{bottle_tag.to_sym.inspect} is unavailable." + return + end + + puts bottle.cached_download elsif args.HEAD? puts formula.head.cached_download else diff --git a/Library/Homebrew/cmd/fetch.rb b/Library/Homebrew/cmd/fetch.rb index 700a9dce6f..fc0bb91d03 100644 --- a/Library/Homebrew/cmd/fetch.rb +++ b/Library/Homebrew/cmd/fetch.rb @@ -18,12 +18,14 @@ module Homebrew Download a bottle (if available) or source packages for e and binaries for s. For files, also print SHA-256 checksums. EOS - # This is needed for downloading ARM casks in CI. - flag "--arch=", - description: "Download for the given CPU architecture.", - hidden: true - flag "--bottle-tag=", - description: "Download a bottle for given tag." + flag "--os=", + description: "Download for the given operating system." \ + "(Pass `all` to download for all operating systems.)" + flag "--arch=", + description: "Download for the given CPU architecture." \ + "(Pass `all` to download for all architectures.)" + flag "--bottle-tag=", + description: "Download a bottle for given tag." switch "--HEAD", description: "Fetch HEAD version instead of stable version." switch "-f", "--force", @@ -60,6 +62,8 @@ module Homebrew conflicts "--cask", "--force-bottle" conflicts "--cask", "--bottle-tag" conflicts "--formula", "--cask" + conflicts "--os", "--bottle-tag" + conflicts "--arch", "--bottle-tag" named_args [:formula, :cask], min: 1 end @@ -68,17 +72,14 @@ module Homebrew def self.fetch args = fetch_args.parse - if (arch = args.arch) - SimulateSystem.arch = arch.to_sym - end + Formulary.enable_factory_cache! bucket = if args.deps? args.named.to_formulae_and_casks.flat_map do |formula_or_cask| case formula_or_cask when Formula - f = formula_or_cask - - [f, *f.recursive_dependencies.map(&:to_formula)] + formula = formula_or_cask + [formula, *formula.recursive_dependencies.map(&:to_formula)] else formula_or_cask end @@ -87,52 +88,92 @@ module Homebrew args.named.to_formulae_and_casks end.uniq + os_arch_combinations = args.os_arch_combinations + puts "Fetching: #{bucket * ", "}" if bucket.size > 1 bucket.each do |formula_or_cask| case formula_or_cask when Formula - f = formula_or_cask + formula = T.cast(formula_or_cask, Formula) + ref = formula.loaded_from_api? ? formula.full_name : formula.path - f.print_tap_action verb: "Fetching" + os_arch_combinations.each do |os, arch| + SimulateSystem.with os: os, arch: arch do + Formulary.clear_cache + formula = Formulary.factory(ref) - fetched_bottle = false - if fetch_bottle?(f, force_bottle: args.force_bottle?, bottle_tag: args.bottle_tag&.to_sym, - build_from_source_formulae: args.build_from_source_formulae) - begin - f.clear_cache if args.force? - f.fetch_bottle_tab - fetch_formula(f.bottle_for_tag(args.bottle_tag&.to_sym), args: args) - rescue Interrupt - raise - rescue => e - raise if Homebrew::EnvConfig.developer? + formula.print_tap_action verb: "Fetching" fetched_bottle = false - onoe e.message - opoo "Bottle fetch failed, fetching the source instead." - else - fetched_bottle = true + if fetch_bottle?( + formula, + force_bottle: args.force_bottle?, + bottle_tag: args.bottle_tag&.to_sym, + build_from_source_formulae: args.build_from_source_formulae, + os: args.os&.to_sym, + arch: args.arch&.to_sym, + ) + begin + formula.clear_cache if args.force? + + # TODO: Deprecate `--bottle-tag`. + bottle_tag = if (bottle_tag = args.bottle_tag&.to_sym) + Utils::Bottles::Tag.from_symbol(bottle_tag) + else + Utils::Bottles::Tag.new(system: os, arch: arch) + end + + bottle = formula.bottle_for_tag(bottle_tag) + + if bottle.nil? + opoo "Bottle for tag #{bottle_tag.to_sym.inspect} is unavailable." + next + end + + formula.fetch_bottle_tab + fetch_formula(bottle, args: args) + rescue Interrupt + raise + rescue => e + raise if Homebrew::EnvConfig.developer? + + fetched_bottle = false + onoe e.message + opoo "Bottle fetch failed, fetching the source instead." + else + fetched_bottle = true + end + end + + next if fetched_bottle + + fetch_formula(formula, args: args) + + formula.resources.each do |r| + fetch_resource(r, args: args) + r.patches.each { |p| fetch_patch(p, args: args) if p.external? } + end + + formula.patchlist.each { |p| fetch_patch(p, args: args) if p.external? } end end - - next if fetched_bottle - - fetch_formula(f, args: args) - - f.resources.each do |r| - fetch_resource(r, args: args) - r.patches.each { |p| fetch_patch(p, args: args) if p.external? } - end - - f.patchlist.each { |p| fetch_patch(p, args: args) if p.external? } else cask = formula_or_cask + ref = cask.loaded_from_api? ? cask.full_token : cask.sourcefile_path - quarantine = args.quarantine? - quarantine = true if quarantine.nil? + os_arch_combinations.each do |os, arch| + next if os == :linux - download = Cask::Download.new(cask, quarantine: quarantine) - fetch_cask(download, args: args) + SimulateSystem.with os: os, arch: arch do + cask = Cask::CaskLoader.load(ref) + + quarantine = args.quarantine? + quarantine = true if quarantine.nil? + + download = Cask::Download.new(cask, quarantine: quarantine) + fetch_cask(download, args: args) + end + end end end end diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index 79ce9ef31b..671c5f31f3 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -29,10 +29,10 @@ module Homebrew locally available formulae and casks and skip style checks. Will exit with a non-zero status if any errors are found. EOS - # This is needed for auditing ARM casks in CI. - flag "--arch=", - description: "Audit the given CPU architecture.", - hidden: true + flag "--os=", + description: "Audit the given operating system. (Pass `all` to audit all operating systems.)" + flag "--arch=", + description: "Audit the given CPU architecture. (Pass `all` to audit all architectures.)" switch "--strict", description: "Run additional, stricter style checks." switch "--git", @@ -106,9 +106,9 @@ module Homebrew def self.audit args = audit_args.parse - if (arch = args.arch) - SimulateSystem.arch = arch.to_sym - end + Formulary.enable_factory_cache! + + os_arch_combinations = args.os_arch_combinations Homebrew.auditing = true inject_dump_stats!(FormulaAuditor, /^audit_/) if args.audit_debug? @@ -200,7 +200,12 @@ module Homebrew spdx_license_data = SPDX.license_data spdx_exception_data = SPDX.exception_data new_formula_problem_lines = T.let([], T::Array[String]) - formula_results = audit_formulae.sort.to_h do |f| + + formula_results = {} + + audit_formulae.sort.each do |f| + path = f.path + only = only_cops ? ["style"] : args.only options = { new_formula: new_formula, @@ -214,63 +219,83 @@ module Homebrew style_offenses: style_offenses&.for_path(f.path), }.compact - audit_proc = proc { FormulaAuditor.new(f, **options).tap(&:audit) } + os_arch_combinations.each do |os, arch| + SimulateSystem.with os: os, arch: arch do + odebug "Auditing Formula #{f} on os #{os} and arch #{arch}" - # Audit requires full Ruby source so disable API. - # We shouldn't do this for taps however so that we don't unnecessarily require a full Homebrew/core clone. - fa = if f.core_formula? - without_api(&audit_proc) - else - audit_proc.call - end + Formulary.clear_cache + f = Formulary.factory(path) - if fa.problems.any? || fa.new_formula_problems.any? - formula_count += 1 - problem_count += fa.problems.size - problem_lines = format_problem_lines(fa.problems) - corrected_problem_count += options.fetch(:style_offenses, []).count(&:corrected?) - new_formula_problem_lines += format_problem_lines(fa.new_formula_problems) - if args.display_filename? - puts problem_lines.map { |s| "#{f.path}: #{s}" } - else - puts "#{f.full_name}:", problem_lines.map { |s| " #{s}" } + audit_proc = proc { FormulaAuditor.new(f, **options).tap(&:audit) } + + # Audit requires full Ruby source so disable API. + # We shouldn't do this for taps however so that we don't unnecessarily require a full Homebrew/core clone. + fa = if f.core_formula? + without_api(&audit_proc) + else + audit_proc.call + end + + if fa.problems.any? || fa.new_formula_problems.any? + formula_count += 1 + problem_count += fa.problems.size + problem_lines = format_problem_lines(fa.problems) + corrected_problem_count += options.fetch(:style_offenses, []).count(&:corrected?) + new_formula_problem_lines += format_problem_lines(fa.new_formula_problems) + if args.display_filename? + puts problem_lines.map { |s| "#{f.path}: #{s}" } + else + puts "#{f.full_name}:", problem_lines.map { |s| " #{s}" } + end + end + + formula_results.deep_merge!({ f.path => fa.problems + fa.new_formula_problems }) end end - - [f.path, fa.problems + fa.new_formula_problems] end - cask_results = if audit_casks.empty? - {} - else + cask_results = {} + + if audit_casks.any? + require "cask/auditor" if args.display_failures_only? odeprecated "`brew audit --display-failures-only`", "`brew audit ` without the argument" end + end - require "cask/auditor" + audit_casks.each do |cask| + path = cask.sourcefile_path - audit_casks.to_h do |cask| - odebug "Auditing Cask #{cask}" - errors = Cask::Auditor.audit( - cask, - # For switches, we add `|| nil` so that `nil` will be passed - # instead of `false` if they aren't set. - # This way, we can distinguish between "not set" and "set to false". - audit_online: (args.online? || nil), - audit_strict: (args.strict? || nil), + os_arch_combinations.each do |os, arch| + next if os == :linux - # No need for `|| nil` for `--[no-]signing` - # because boolean switches are already `nil` if not passed - audit_signing: args.signing?, - audit_new_cask: (args.new_cask? || nil), - audit_token_conflicts: (args.token_conflicts? || nil), - quarantine: true, - any_named_args: !no_named_args, - only: args.only, - except: args.except, - ) - [cask.sourcefile_path, errors] + SimulateSystem.with os: os, arch: arch do + odebug "Auditing Cask #{cask} on os #{os} and arch #{arch}" + + cask = Cask::CaskLoader.load(path) + + errors = Cask::Auditor.audit( + cask, + # For switches, we add `|| nil` so that `nil` will be passed + # instead of `false` if they aren't set. + # This way, we can distinguish between "not set" and "set to false". + audit_online: (args.online? || nil), + audit_strict: (args.strict? || nil), + + # No need for `|| nil` for `--[no-]signing` + # because boolean switches are already `nil` if not passed + audit_signing: args.signing?, + audit_new_cask: (args.new_cask? || nil), + audit_token_conflicts: (args.token_conflicts? || nil), + quarantine: true, + any_named_args: !no_named_args, + only: args.only, + except: args.except, + ) + + cask_results.deep_merge!({ cask.sourcefile_path => errors }) + end end end diff --git a/Library/Homebrew/dev-cmd/irb.rb b/Library/Homebrew/dev-cmd/irb.rb index 8b80ed8bb3..5254507e30 100644 --- a/Library/Homebrew/dev-cmd/irb.rb +++ b/Library/Homebrew/dev-cmd/irb.rb @@ -7,6 +7,7 @@ require "cli/parser" class String def f(*args) + require "formula" Formulary.factory(self, *args) end diff --git a/Library/Homebrew/extend/os/mac/diagnostic.rb b/Library/Homebrew/extend/os/mac/diagnostic.rb index 95f8e42e7a..35b969cada 100644 --- a/Library/Homebrew/extend/os/mac/diagnostic.rb +++ b/Library/Homebrew/extend/os/mac/diagnostic.rb @@ -343,7 +343,7 @@ module Homebrew nil end if libiconv&.linked_keg&.directory? - unless libiconv.keg_only? + unless libiconv&.keg_only? <<~EOS A libiconv formula is installed and linked. This will break stuff. For serious. Unlink it. diff --git a/Library/Homebrew/extend/os/mac/utils/bottles.rb b/Library/Homebrew/extend/os/mac/utils/bottles.rb index d0a11ad720..a06d0db5da 100644 --- a/Library/Homebrew/extend/os/mac/utils/bottles.rb +++ b/Library/Homebrew/extend/os/mac/utils/bottles.rb @@ -4,13 +4,16 @@ module Utils module Bottles class << self - undef tag + module MacOSOverride + sig { params(tag: T.nilable(T.any(Symbol, Tag))).returns(Tag) } + def tag(tag = nil) + return Tag.new(system: MacOS.version.to_sym, arch: Hardware::CPU.arch) if tag.nil? - def tag(symbol = nil) - return Utils::Bottles::Tag.from_symbol(symbol) if symbol.present? - - Utils::Bottles::Tag.new(system: MacOS.version.to_sym, arch: Hardware::CPU.arch) + super + end end + + prepend MacOSOverride end class Collector diff --git a/Library/Homebrew/fetch.rb b/Library/Homebrew/fetch.rb index f7ece30356..edb17c0e05 100644 --- a/Library/Homebrew/fetch.rb +++ b/Library/Homebrew/fetch.rb @@ -10,12 +10,16 @@ module Homebrew force_bottle: T::Boolean, bottle_tag: T.nilable(Symbol), build_from_source_formulae: T::Array[String], + os: T.nilable(Symbol), + arch: T.nilable(Symbol), ).returns(T::Boolean) } - def fetch_bottle?(formula, force_bottle:, bottle_tag:, build_from_source_formulae:) + def fetch_bottle?(formula, force_bottle:, bottle_tag:, build_from_source_formulae:, os:, arch:) bottle = formula.bottle return true if force_bottle && bottle.present? + return true if os.present? + return true if arch.present? return true if bottle_tag.present? && formula.bottled?(bottle_tag) bottle.present? && diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 1d3e93ed00..e71bc9d6b8 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -385,7 +385,7 @@ class Formula # The Bottle object for given tag. # @private - sig { params(tag: T.nilable(Symbol)).returns(T.nilable(Bottle)) } + sig { params(tag: T.nilable(Utils::Bottles::Tag)).returns(T.nilable(Bottle)) } def bottle_for_tag(tag = nil) Bottle.new(self, bottle_specification, tag) if bottled?(tag) end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 03325f26b6..7dac07c7e4 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -165,7 +165,7 @@ class FormulaInstaller return true if formula.local_bottle_path.present? - bottle = formula.bottle_for_tag(Utils::Bottles.tag.to_sym) + bottle = formula.bottle_for_tag(Utils::Bottles.tag) return false if bottle.nil? unless bottle.compatible_locations? diff --git a/Library/Homebrew/formula_versions.rb b/Library/Homebrew/formula_versions.rb index d7551d48b5..2ff982bf63 100644 --- a/Library/Homebrew/formula_versions.rb +++ b/Library/Homebrew/formula_versions.rb @@ -38,11 +38,17 @@ class FormulaVersions end end + sig { params(rev: String).returns(String) } def file_contents_at_revision(rev) repository.cd { Utils.popen_read("git", "cat-file", "blob", "#{rev}:#{entry_name}") } end - def formula_at_revision(rev) + sig { + type_parameters(:U) + .params(rev: String, _block: T.proc.params(arg0: Formula).returns(T.type_parameter(:U))) + .returns(T.nilable(T.type_parameter(:U))) + } + def formula_at_revision(rev, &_block) Homebrew.raise_deprecation_exceptions = true yield @formula_at_revision[rev] ||= begin diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index f4ebad35a4..58915dd9f1 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -51,7 +51,12 @@ module Formulary next if type == :formulary_factory cached_objects.each_value do |klass| - namespace = Utils.deconstantize(klass.name) + class_name = klass.name + + # Already removed from namespace. + next if class_name.nil? + + namespace = Utils.deconstantize(class_name) next if Utils.deconstantize(namespace) != name remove_const(Utils.demodulize(namespace).to_sym) @@ -119,6 +124,10 @@ module Formulary end end + sig { + params(name: String, path: Pathname, flags: T::Array[String], ignore_errors: T::Boolean) + .returns(T.class_of(Formula)) + } def self.load_formula_from_path(name, path, flags:, ignore_errors:) contents = path.open("r") { |f| ensure_utf8_encoding(f).read } namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}" @@ -127,6 +136,7 @@ module Formulary cache[:path][path] = klass end + sig { params(name: String, flags: T::Array[String]).returns(T.class_of(Formula)) } def self.load_formula_from_api(name, flags:) namespace = :"FormulaNamespaceAPI#{Digest::MD5.hexdigest(name)}" @@ -313,15 +323,27 @@ module Formulary end end + klass = T.cast(klass, T.class_of(Formula)) mod.const_set(class_name, klass) cache[:api] ||= {} cache[:api][name] = klass end - def self.resolve(name, spec: nil, force_bottle: false, flags: []) + sig { params(name: String, spec: Symbol, force_bottle: T::Boolean, flags: T::Array[String]).returns(Formula) } + def self.resolve( + name, + spec: T.unsafe(nil), + force_bottle: T.unsafe(nil), + flags: T.unsafe(nil) + ) + options = { + force_bottle: force_bottle, + flags: flags, + }.compact + if name.include?("/") || File.exist?(name) - f = factory(name, *spec, force_bottle: force_bottle, flags: flags) + f = factory(name, *spec, **options) if f.any_version_installed? tab = Tab.for_formula(f) resolved_spec = spec || tab.spec @@ -334,8 +356,10 @@ module Formulary end else rack = to_rack(name) - alias_path = factory(name, force_bottle: force_bottle, flags: flags).alias_path - f = from_rack(rack, *spec, alias_path: alias_path, force_bottle: force_bottle, flags: flags) + if (alias_path = factory(name, **options).alias_path) + options[:alias_path] = alias_path + end + f = from_rack(rack, *spec, **options) end # If this formula was installed with an alias that has since changed, @@ -524,12 +548,12 @@ module Formulary # Loads tapped formulae. class TapLoader < FormulaLoader - def initialize(tapped_name, from: nil, warn: true) + def initialize(tapped_name, from:, warn:) name, path, tap = formula_name_path(tapped_name, warn: warn) super name, path, tap: tap end - def formula_name_path(tapped_name, warn: true) + def formula_name_path(tapped_name, warn:) user, repo, name = tapped_name.split("/", 3).map(&:downcase) tap = Tap.fetch user, repo path = find_formula_from_name(name, tap) @@ -648,25 +672,46 @@ module Formulary # * a formula pathname # * a formula URL # * a local bottle reference + sig { + params( + ref: T.nilable(T.any(Pathname, String)), + spec: Symbol, + alias_path: Pathname, + from: Symbol, + warn: T::Boolean, + force_bottle: T::Boolean, + flags: T::Array[String], + ignore_errors: T::Boolean, + ).returns(Formula) + } def self.factory( - ref, spec = :stable, alias_path: nil, from: nil, warn: true, - force_bottle: false, flags: [], ignore_errors: false + ref, + spec = :stable, + alias_path: T.unsafe(nil), + from: T.unsafe(nil), + warn: T.unsafe(nil), + force_bottle: T.unsafe(nil), + flags: T.unsafe(nil), + ignore_errors: T.unsafe(nil) ) raise ArgumentError, "Formulae must have a ref!" unless ref cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}" - if factory_cached? && cache[:formulary_factory] && - cache[:formulary_factory][cache_key] - return cache[:formulary_factory][cache_key] - end + return cache[:formulary_factory][cache_key] if factory_cached? && cache[:formulary_factory]&.key?(cache_key) + + loader_options = { from: from, warn: warn }.compact + formula_options = { alias_path: alias_path, + force_bottle: force_bottle, + flags: flags, + ignore_errors: ignore_errors }.compact + formula = loader_for(ref, **loader_options) + .get_formula(spec, **formula_options) - formula = loader_for(ref, from: from, warn: warn).get_formula(spec, alias_path: alias_path, - force_bottle: force_bottle, flags: flags, - ignore_errors: ignore_errors) if factory_cached? cache[:formulary_factory] ||= {} cache[:formulary_factory][cache_key] ||= formula end + formula end @@ -676,15 +721,35 @@ module Formulary # @param :alias_path will be used if the formula is found not to be # installed, and discarded if it is installed because the `alias_path` used # to install the formula will be set instead. - def self.from_rack(rack, spec = nil, alias_path: nil, force_bottle: false, flags: []) + sig { + params( + rack: Pathname, + # Automatically resolves the formula's spec if not specified. + spec: Symbol, + alias_path: Pathname, + force_bottle: T::Boolean, + flags: T::Array[String], + ).returns(Formula) + } + def self.from_rack( + rack, spec = T.unsafe(nil), + alias_path: T.unsafe(nil), + force_bottle: T.unsafe(nil), + flags: T.unsafe(nil) + ) kegs = rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : [] keg = kegs.find(&:linked?) || kegs.find(&:optlinked?) || kegs.max_by(&:version) + options = { + alias_path: alias_path, + force_bottle: force_bottle, + flags: flags, + }.compact + if keg - from_keg(keg, spec, alias_path: alias_path, force_bottle: force_bottle, flags: flags) + from_keg(keg, *spec, **options) else - factory(rack.basename.to_s, spec || :stable, alias_path: alias_path, from: :rack, warn: false, - force_bottle: force_bottle, flags: flags) + factory(rack.basename.to_s, *spec, from: :rack, warn: false, **options) end end @@ -696,24 +761,45 @@ module Formulary end # Return a {Formula} instance for the given keg. - # - # @param spec when nil, will auto resolve the formula's spec. - def self.from_keg(keg, spec = nil, alias_path: nil, force_bottle: false, flags: []) + sig { + params( + keg: Keg, + # Automatically resolves the formula's spec if not specified. + spec: Symbol, + alias_path: Pathname, + force_bottle: T::Boolean, + flags: T::Array[String], + ).returns(Formula) + } + def self.from_keg( + keg, + spec = T.unsafe(nil), + alias_path: T.unsafe(nil), + force_bottle: T.unsafe(nil), + flags: T.unsafe(nil) + ) tab = Tab.for_keg(keg) tap = tab.tap spec ||= tab.spec + formula_name = keg.rack.basename.to_s + + options = { + alias_path: alias_path, + from: :keg, + warn: false, + force_bottle: force_bottle, + flags: flags, + }.compact + f = if tap.nil? - factory(keg.rack.basename.to_s, spec, alias_path: alias_path, from: :keg, warn: false, - force_bottle: force_bottle, flags: flags) + factory(formula_name, spec, **options) else begin - factory("#{tap}/#{keg.rack.basename}", spec, alias_path: alias_path, from: :keg, warn: false, - force_bottle: force_bottle, flags: flags) + factory("#{tap}/#{formula_name}", spec, **options) rescue FormulaUnavailableError # formula may be migrated to different tap. Try to search in core and all taps. - factory(keg.rack.basename.to_s, spec, alias_path: alias_path, from: :keg, warn: false, - force_bottle: force_bottle, flags: flags) + factory(formula_name, spec, **options) end end f.build = tab @@ -723,13 +809,35 @@ module Formulary end # Return a {Formula} instance directly from contents. + sig { + params( + name: String, + path: Pathname, + contents: String, + spec: Symbol, + alias_path: Pathname, + force_bottle: T::Boolean, + flags: T::Array[String], + ignore_errors: T::Boolean, + ).returns(Formula) + } def self.from_contents( - name, path, contents, spec = :stable, alias_path: nil, - force_bottle: false, flags: [], ignore_errors: false + name, + path, + contents, + spec = :stable, + alias_path: T.unsafe(nil), + force_bottle: T.unsafe(nil), + flags: T.unsafe(nil), + ignore_errors: T.unsafe(nil) ) - FormulaContentsLoader.new(name, path, contents) - .get_formula(spec, alias_path: alias_path, force_bottle: force_bottle, - flags: flags, ignore_errors: ignore_errors) + options = { + alias_path: alias_path, + force_bottle: force_bottle, + flags: flags, + ignore_errors: ignore_errors, + }.compact + FormulaContentsLoader.new(name, path, contents).get_formula(spec, **options) end def self.to_rack(ref) diff --git a/Library/Homebrew/sorbet/parlour/attr.rb b/Library/Homebrew/sorbet/parlour/attr.rb index 8737806de8..1771e164da 100644 --- a/Library/Homebrew/sorbet/parlour/attr.rb +++ b/Library/Homebrew/sorbet/parlour/attr.rb @@ -71,16 +71,37 @@ class Attr < Parlour::Plugin tree << element elsif node.type == :send && children.shift.nil? method_name = children.shift - if [:attr_rw, :attr_predicate].include?(method_name) + + case method_name + when :attr_rw, :attr_predicate children.each do |name_node| tree << [method_name, name_node.children.first.to_s] end + when :delegate + children.each do |name_node| + name_node.children.each do |pair| + delegated_method = pair.children.first + delegated_methods = if delegated_method.type == :array + delegated_method.children + else + [delegated_method] + end + + delegated_methods.each do |delegated_method_sym| + tree << [method_name, delegated_method_sym.children.first.to_s] + end + end + end end end tree end + ARRAY_METHODS = T.let(["to_a", "to_ary"].freeze, T::Array[String]) + HASH_METHODS = T.let(["to_h", "to_hash"].freeze, T::Array[String]) + STRING_METHODS = T.let(["to_s", "to_str", "to_json"].freeze, T::Array[String]) + sig { params(tree: T::Array[T.untyped], namespace: Parlour::RbiGenerator::Namespace, sclass: T::Boolean).void } def process_custom_attr(tree, namespace, sclass: false) tree.each do |node| @@ -107,6 +128,32 @@ class Attr < Parlour::Plugin name = node.shift name = "self.#{name}" if sclass namespace.create_method(name, return_type: "T::Boolean") + when :delegate + name = node.shift + + return_type = if name.end_with?("?") + "T::Boolean" + elsif ARRAY_METHODS.include?(name) + "Array" + elsif HASH_METHODS.include?(name) + "Hash" + elsif STRING_METHODS.include?(name) + "String" + else + "T.untyped" + end + + name = "self.#{name}" if sclass + + namespace.create_method( + name, + parameters: [ + Parlour::RbiGenerator::Parameter.new("*args"), + Parlour::RbiGenerator::Parameter.new("**options"), + Parlour::RbiGenerator::Parameter.new("&block"), + ], + return_type: return_type, + ) else raise "Malformed tree." end diff --git a/Library/Homebrew/test/cli/named_args_spec.rb b/Library/Homebrew/test/cli/named_args_spec.rb index 7ed7836ae6..6328379e36 100644 --- a/Library/Homebrew/test/cli/named_args_spec.rb +++ b/Library/Homebrew/test/cli/named_args_spec.rb @@ -4,17 +4,17 @@ require "cli/named_args" def setup_unredable_formula(name) error = FormulaUnreadableError.new(name, RuntimeError.new("testing")) - allow(Formulary).to receive(:factory).with(name, force_bottle: false, flags: [], warn: true).and_raise(error) + allow(Formulary).to receive(:factory).with(name, {}).and_raise(error) end def setup_unredable_cask(name) error = Cask::CaskUnreadableError.new(name, "testing") allow(Cask::CaskLoader).to receive(:load).with(name).and_raise(error) - allow(Cask::CaskLoader).to receive(:load).with(name, config: nil, warn: true).and_raise(error) + allow(Cask::CaskLoader).to receive(:load).with(name, config: nil).and_raise(error) config = instance_double(Cask::Config) allow(Cask::Config).to receive(:from_args).and_return(config) - allow(Cask::CaskLoader).to receive(:load).with(name, config: config, warn: true).and_raise(error) + allow(Cask::CaskLoader).to receive(:load).with(name, config: config).and_raise(error) end describe Homebrew::CLI::NamedArgs do diff --git a/Library/Homebrew/test/support/helper/formula.rb b/Library/Homebrew/test/support/helper/formula.rb index 347bf511d7..b919f77831 100644 --- a/Library/Homebrew/test/support/helper/formula.rb +++ b/Library/Homebrew/test/support/helper/formula.rb @@ -16,7 +16,7 @@ module Test loader = double(get_formula: formula) allow(Formulary).to receive(:loader_for).with(ref, from: :keg, warn: false).and_return(loader) - allow(Formulary).to receive(:loader_for).with(ref, from: nil, warn: true).and_return(loader) + allow(Formulary).to receive(:loader_for).with(ref, {}).and_return(loader) end end end diff --git a/Library/Homebrew/utils/bottles.rb b/Library/Homebrew/utils/bottles.rb index d4537872a0..2b9a178d72 100644 --- a/Library/Homebrew/utils/bottles.rb +++ b/Library/Homebrew/utils/bottles.rb @@ -10,11 +10,19 @@ module Utils module Bottles class << self # Gets the tag for the running OS. - def tag(symbol = nil) - return Tag.from_symbol(symbol) if symbol.present? - - @tag ||= Tag.new(system: HOMEBREW_SYSTEM.downcase.to_sym, - arch: HOMEBREW_PROCESSOR.downcase.to_sym) + sig { params(tag: T.nilable(T.any(Symbol, Tag))).returns(Tag) } + def tag(tag = nil) + case tag + when Symbol + Tag.from_symbol(tag) + when Tag + tag + else + @tag ||= Tag.new( + system: HOMEBREW_SYSTEM.downcase.to_sym, + arch: HOMEBREW_PROCESSOR.downcase.to_sym, + ) + end end def built_as?(formula)