diff --git a/Library/Homebrew/api/formula_hash.rb b/Library/Homebrew/api/formula_hash.rb new file mode 100644 index 0000000000000..19ba0bfa58e5c --- /dev/null +++ b/Library/Homebrew/api/formula_hash.rb @@ -0,0 +1,224 @@ +# typed: strict +# frozen_string_literal: true + +require "utils/bottles" + +module Homebrew + module API + module CompactSerializable + extend T::Helpers + + requires_ancestor { T::Struct } + + sig { params(args: T.anything).returns(T::Hash[String, T.untyped]) } + def serialize(*args) + super.compact_blank + end + end + + module StringifySymbols + sig { params(value: T.nilable(T.any(String, Symbol))).returns(T.nilable(String)) } + def to_symbolized_string(value) + return value unless value.is_a? Symbol + + ":#{value}" + end + end + + class BottleHash < T::Struct + include CompactSerializable + + const :rebuild, T.nilable(Integer), default: 0 + const :cellar, T.nilable(String), default: ":any" + const :sha256, T.nilable(String) + end + + class DependencyHash < T::Struct + include CompactSerializable + + const :name, String + const :context, T.nilable(Symbol) + end + + class HeadURLHash < T::Struct + include CompactSerializable + + const :url, String + const :branch, T.nilable(String) + const :using, T.nilable(String) + end + + # TODO: simplify this + class RequirementHash < T::Struct + include CompactSerializable + + const :name, String + const :cask, T.nilable(String) + const :download, T.nilable(String) + const :version, T.nilable(String) + const :contexts, T::Array[T.untyped] + const :specs, T::Array[T.untyped] + end + + class ServiceHash < T::Struct + include CompactSerializable + + const :name, String + const :run, T.nilable(String) + const :run_type, T.nilable(String) + const :interval, T.nilable(Integer) + const :cron, T.nilable(String) + const :keep_alive, T.nilable(T::Boolean) + const :launch_only_once, T.nilable(T::Boolean) + const :require_root, T.nilable(T::Boolean) + const :environment_variables, T.nilable(T::Hash[String, String]) + const :working_dir, T.nilable(String) + const :root_dir, T.nilable(String) + const :input_path, T.nilable(String) + const :log_path, T.nilable(String) + const :error_log_path, T.nilable(String) + const :restart_delay, T.nilable(Integer) + const :process_type, T.nilable(String) + const :macos_legacy_timers, T.nilable(T::Boolean) + const :sockets, T.nilable(T::Array[String]) + end + + class StableURLHash < T::Struct + include CompactSerializable + + const :url, String + const :tag, T.nilable(String) + const :revision, T.nilable(String) + const :using, T.nilable(String) + const :checksum, T.nilable(String) + end + + class UsesFromMacOSHash < T::Struct + include CompactSerializable + + const :context, T.nilable(Symbol) + const :since, T.nilable(String) + end + + class FormulaHash < T::Struct + include CompactSerializable + extend StringifySymbols + + DEPENDENCY_CONTEXTS = T.let([:build, :test, :recommended, :optional].freeze, T::Array[Symbol]) + + PROPERTIES = T.let({ + aliases: T::Array[String], + bottle: BottleHash, + caveats: T::Array[String], + conflicts_with: T::Array[String], + conflicts_with_reasons: T::Array[String], + deprecation_date: String, + deprecation_reason: String, + deprecation_replacement_cask: String, + deprecation_replacement_formula: String, + desc: String, + disable_date: String, + disable_reason: String, + disable_replacement_cask: String, + disable_replacement_formula: String, + head_dependencies: T::Array[DependencyHash], + head_url: HeadURLHash, + homepage: String, + keg_only_reason: String, + license: String, + link_overwrite: T::Array[String], + no_autobump_msg: String, + oldnames: T::Array[String], + post_install_defined: T::Boolean, + pour_bottle_only_if: Symbol, + requirements: T::Array[RequirementHash], + revision: Integer, + ruby_source_sha256: String, + ruby_source_path: String, + service: T::Hash[String, T.untyped], + stable_dependencies: T::Array[DependencyHash], + stable_url: StableURLHash, + stable_version: String, + tap_git_head: String, + uses_from_macos: T::Hash[String, UsesFromMacOSHash], + version_scheme: Integer, + versioned_formulae: T::Array[String], + }.freeze, T::Hash[Symbol, T.untyped]) + + PROPERTIES.each do |property, type| + const property, T.nilable(type) + end + + sig { params(hash: T::Hash[String, T.untyped], bottle_tag: ::Utils::Bottles::Tag).returns(FormulaHash) } + def self.from_hash(hash, bottle_tag:) + hash = Homebrew::API.merge_variations(hash, bottle_tag: bottle_tag) + + hash["bottle"] = begin + bottle_collector = ::Utils::Bottles::Collector.new + hash.dig("bottle", "stable", "files")&.each do |tag, data| + tag = ::Utils::Bottles::Tag.from_symbol(tag) + bottle_collector.add tag, checksum: Checksum.new(data["sha256"]), cellar: :any + end + BottleHash.new( + rebuild: hash.dig("bottle", "stable", "rebuild"), + cellar: to_symbolized_string(bottle_collector.specification_for(bottle_tag)&.cellar), + sha256: bottle_collector.specification_for(bottle_tag)&.checksum&.to_s, + ) + end + + head_dependencies = [] + hash.dig("head_dependencies", "dependencies")&.each do |dep_name| + head_dependencies << DependencyHash.new(name: dep_name) + end + DEPENDENCY_CONTEXTS.each do |context| + hash.dig("head_dependencies", "#{context}_dependencies")&.each do |dep_name| + head_dependencies << DependencyHash.new(name: dep_name, context:) + end + end + hash["head_dependencies"] = head_dependencies + + hash["head_url"] = if (specs = hash["head_url"]) + HeadURLHash.new(**specs.transform_keys(&:to_sym)) + end + + hash["requirements"] = hash["requirements"]&.map do |specs| + RequirementHash.new(**specs.transform_keys(&:to_sym)) + end + + hash["service"] = if (specs = hash["service"]) + ServiceHash.new(**specs.transform_keys(&:to_sym)) + end + + hash["stable_url"] = if (specs = hash["stable_url"]) + StableURLHash.new(**specs.transform_keys(&:to_sym)) + end + + hash["ruby_source_sha256"] = hash.dig("ruby_source_checksum", "sha256") + + stable_dependencies = hash.fetch("dependencies", []).map do |dep_name| + DependencyHash.new(name: dep_name) + end + DEPENDENCY_CONTEXTS.each do |context| + hash.fetch("#{context}_dependencies", []).each do |dep_name| + stable_dependencies << DependencyHash.new(name: dep_name, context:) + end + end + hash["stable_dependencies"] = stable_dependencies + + hash["stable_version"] = hash.dig("stable", "version") + + uses_from_macos_bounds = hash.fetch("uses_from_macos_bounds", []) + hash["uses_from_macos"] = hash["uses_from_macos"].zip(uses_from_macos_bounds).to_h do |name, bounds| + name, context = if name.is_a?(Hash) + [name.keys.first, name.values.first.to_sym] + else + [name, nil] + end + [name, UsesFromMacOSHash.new(context:, since: bounds["since"])] + end + + new(**hash.transform_keys(&:to_sym).slice(*PROPERTIES.keys)) + end + end + end +end diff --git a/Library/Homebrew/api/generator.rb b/Library/Homebrew/api/generator.rb new file mode 100644 index 0000000000000..da34776f88573 --- /dev/null +++ b/Library/Homebrew/api/generator.rb @@ -0,0 +1,253 @@ +# typed: strict +# frozen_string_literal: true + +require "tap" +require "formulary" +require "utils/output" +require "formula" +require "context" +require "api/formula_hash" + +module Homebrew + module API + class Generator + include Utils::Output::Mixin + + sig { params(only_core: T::Boolean, only_cask: T::Boolean, dry_run: T::Boolean).void } + def initialize(only_core: false, only_cask: false, dry_run: false) + @generate_formula_api = T.let(!only_cask, T::Boolean) + @generate_cask_api = T.let(!only_core, T::Boolean) + @dry_run = T.let(dry_run, T::Boolean) + @first_letter = T.let(nil, T.nilable(String)) + end + + sig { void } + def generate! + generate_api!(type: :formula) if generate_formula_api? + generate_api!(type: :cask) if generate_cask_api? + generate_packages_api! if generate_formula_api? && generate_cask_api? + end + + private + + sig { returns(T::Boolean) } + def generate_formula_api? = @generate_formula_api + + sig { returns(T::Boolean) } + def generate_cask_api? = @generate_cask_api + + sig { returns(T::Boolean) } + def dry_run? = @dry_run + + sig { params(type: Symbol).void } + def generate_api!(type:) + ohai "Generating #{type} API data..." + + tap = if type == :formula + CoreTap.instance + else + CoreCaskTap.instance + end + raise TapUnavailableError, tap.name unless tap.installed? + + unless dry_run? + directories = ["_data/#{type}", "api/#{type}", type.to_s] + directories << "api/cask_source" if type == :cask + + FileUtils.rm_rf "_data/formula_canonical.json" if type == :formula + FileUtils.rm_rf directories + FileUtils.mkdir_p directories + end + + Homebrew.with_no_api_env do + tap_migrations_json = JSON.dump(tap.tap_migrations) + File.write("api/#{type}_tap_migrations.json", tap_migrations_json) unless dry_run? + + if type == :formula + Formulary.enable_factory_cache! + ::Formula.generating_hash! + else + ::Cask::Cask.generating_hash! + end + + # TODO: double check that this is fine for formulae, since they used -1 before for some reason + latest_macos = MacOSVersion.new(HOMEBREW_MACOS_NEWEST_SUPPORTED).to_sym + Homebrew::SimulateSystem.with(os: latest_macos, arch: :arm) do + all_packages = if type == :formula + formulae(tap) + else + casks(tap) + end + + return if dry_run? + + all_packages.each do |name, hash| + json = JSON.pretty_generate(hash) + + # TODO: add cask-source + File.write("_data/#{type}/#{name.tr("+", "_")}.json", "#{json}\n") + File.write("api/#{type}/#{name}.json", json_template(type: type)) + File.write("#{type}/#{name}.html", html_template(name, type: type)) + end + end + + renames = if type == :formula + tap.formula_renames.merge(tap.alias_table) + else + tap.cask_renames + end + + canonical_json = JSON.pretty_generate(renames) + File.write("_data/#{type}_canonical.json", "#{canonical_json}\n") unless dry_run? + end + end + + sig { void } + def generate_packages_api! + ohai "Generating packages API data..." + + core_tap = CoreTap.instance + cask_tap = CoreCaskTap.instance + + OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag| + formulae = formulae(core_tap).transform_values do |hash| + FormulaHash.from_hash(hash, bottle_tag:) + end + + casks = casks(cask_tap).transform_values do |hash| + # InternalCaskHash.from_hash(hash, bottle_tag:) + Homebrew::API.merge_variations(hash, bottle_tag: bottle_tag) + end + + next if dry_run? + + packages_hash = { + formulae: formulae, + casks: casks, + core_aliases: core_tap.alias_table, + core_renames: core_tap.formula_renames, + core_tap_migrations: core_tap.tap_migrations, + cask_renames: cask_tap.cask_renames, + cask_tap_migrations: cask_tap.tap_migrations, + } + + FileUtils.mkdir_p "api/internal" + File.write("api/internal/packages.#{bottle_tag}.json", JSON.generate(packages_hash)) + end + end + + sig { params(tap: Tap).returns(T::Hash[String, T.untyped]) } + def formulae(tap) + reset_debugging! + # @formulae ||= T.let(T.must(tap.formula_names.slice(0, 2)).to_h do |name| + @formulae ||= T.let(tap.formula_names.to_h do |name| + debug_load!(name, type: :formula) + formula = Formulary.factory(name) + [formula.name, formula.to_hash_with_variations] + rescue + onoe "Error while generating data for formula '#{name}'." + raise + end, T.nilable(T::Hash[String, T.untyped])) + end + + sig { params(tap: Tap).returns(T::Hash[String, T.untyped]) } + def casks(tap) + reset_debugging! + # @casks ||= T.let(T.must(tap.cask_files.slice(0, 2)).to_h do |path| + @casks ||= T.let(tap.cask_files.to_h do |path| + debug_load!(path.stem, type: :cask) + cask = ::Cask::CaskLoader.load(path) + [cask.token, cask.to_hash_with_variations] + rescue + onoe "Error while generating data for cask '#{path.stem}'." + raise + end, T.nilable(T::Hash[String, T.untyped])) + end + + sig { params(title: String, type: Symbol).returns(String) } + def html_template(title, type:) + redirect_from_string = ("redirect_from: /formula-linux/#{title}\n" if type == :formula) + + <<~EOS + --- + title: '#{title}' + layout: #{type} + #{redirect_from_string}--- + {{ content }} + EOS + end + + sig { params(type: Symbol).returns(String) } + def json_template(type:) + <<~EOS + --- + layout: #{type}_json + --- + {{ content }} + EOS + end + + sig { void } + def reset_debugging! + @first_letter = nil + end + + sig { params(name: String, type: Symbol).void } + def debug_load!(name, type:) + return if name[0] == @first_letter + return unless Context.current.verbose? + + @first_letter = name[0] + puts "Loading #{type} starting with letter #{@first_letter}" + end + end + + module CompactSerializable + extend T::Helpers + + requires_ancestor { T::Struct } + + sig { params(args: T.untyped).returns(String) } + def to_json(*args) + # TODO: this should recursively remove nils from nested hashes/arrays too + serialize.compact.to_json(*args) + end + end + + class InternalCaskHash < T::Struct + include CompactSerializable + + # TODO: simplify these types when possible + PROPERTIES = T.let({ + artifacts: T::Array[T.untyped], + auto_updates: T::Boolean, + caveats: T::Array[String], + conflicts_with: T::Array[String], + container: T::Hash[String, T.untyped], + depends_on: ::Cask::DSL::DependsOn, + deprecation_date: String, + deprecation_reason: String, + desc: String, + disable_date: String, + disable_reason: String, + homepage: String, + name: T::Array[String], + rename: T::Array[String], + sha256: Checksum, + url: ::Cask::URL, + url_specs: T::Hash[Symbol, T.untyped], + version: String, + }.freeze, T::Hash[Symbol, T.untyped]) + + PROPERTIES.each do |property, type| + const property, T.nilable(type) + end + + sig { params(hash: T::Hash[String, T.untyped], bottle_tag: ::Utils::Bottles::Tag).returns(InternalCaskHash) } + def self.from_hash(hash, bottle_tag:) + hash = Homebrew::API.merge_variations(hash, bottle_tag: bottle_tag).transform_keys(&:to_sym) + new(**hash.slice(*PROPERTIES.keys)) + end + end + end +end diff --git a/Library/Homebrew/dev-cmd/generate-cask-api.rb b/Library/Homebrew/dev-cmd/generate-cask-api.rb index 973f9953bb056..3aaf10d044595 100644 --- a/Library/Homebrew/dev-cmd/generate-cask-api.rb +++ b/Library/Homebrew/dev-cmd/generate-cask-api.rb @@ -2,20 +2,11 @@ # frozen_string_literal: true require "abstract_command" -require "cask/cask" -require "fileutils" -require "formula" +require "api/generator" module Homebrew module DevCmd class GenerateCaskApi < AbstractCommand - CASK_JSON_TEMPLATE = <<~EOS - --- - layout: cask_json - --- - {{ content }} - EOS - cmd_args do description <<~EOS Generate `homebrew/cask` API data files for <#{HOMEBREW_API_WWW}>. @@ -29,81 +20,12 @@ class GenerateCaskApi < AbstractCommand sig { override.void } def run - tap = CoreCaskTap.instance - raise TapUnavailableError, tap.name unless tap.installed? - - unless args.dry_run? - directories = ["_data/cask", "api/cask", "api/cask-source", "cask", "api/internal"].freeze - FileUtils.rm_rf directories - FileUtils.mkdir_p directories - end - - Homebrew.with_no_api_env do - tap_migrations_json = JSON.dump(tap.tap_migrations) - File.write("api/cask_tap_migrations.json", tap_migrations_json) unless args.dry_run? - - Cask::Cask.generating_hash! - - all_casks = {} - latest_macos = MacOSVersion.new(HOMEBREW_MACOS_NEWEST_SUPPORTED).to_sym - Homebrew::SimulateSystem.with(os: latest_macos, arch: :arm) do - tap.cask_files.each do |path| - cask = Cask::CaskLoader.load(path) - name = cask.token - all_casks[name] = cask.to_hash_with_variations - json = JSON.pretty_generate(all_casks[name]) - cask_source = path.read - html_template_name = html_template(name) - - unless args.dry_run? - File.write("_data/cask/#{name.tr("+", "_")}.json", "#{json}\n") - File.write("api/cask/#{name}.json", CASK_JSON_TEMPLATE) - File.write("api/cask-source/#{name}.rb", cask_source) - File.write("cask/#{name}.html", html_template_name) - end - rescue - onoe "Error while generating data for cask '#{path.stem}'." - raise - end - end - - canonical_json = JSON.pretty_generate(tap.cask_renames) - File.write("_data/cask_canonical.json", "#{canonical_json}\n") unless args.dry_run? - - OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag| - renames = {} - variation_casks = all_casks.to_h do |token, cask| - cask = Homebrew::API.merge_variations(cask, bottle_tag:) + # odeprecated "brew generate-cask-api", "brew generate-package-api --only-cask" - cask["old_tokens"]&.each do |old_token| - renames[old_token] = token - end - - [token, cask] - end - - json_contents = { - casks: variation_casks, - renames: renames, - tap_migrations: CoreCaskTap.instance.tap_migrations, - } - - File.write("api/internal/cask.#{bottle_tag}.json", JSON.generate(json_contents)) unless args.dry_run? - end - end - end - - private - - sig { params(title: String).returns(String) } - def html_template(title) - <<~EOS - --- - title: '#{title}' - layout: cask - --- - {{ content }} - EOS + Homebrew::API::Generator.new( + only_cask: true, + dry_run: args.dry_run?, + ).generate! end end end diff --git a/Library/Homebrew/dev-cmd/generate-formula-api.rb b/Library/Homebrew/dev-cmd/generate-formula-api.rb index cc10e327fcb07..e3d1559800b16 100644 --- a/Library/Homebrew/dev-cmd/generate-formula-api.rb +++ b/Library/Homebrew/dev-cmd/generate-formula-api.rb @@ -2,19 +2,11 @@ # frozen_string_literal: true require "abstract_command" -require "fileutils" -require "formula" +require "api/generator" module Homebrew module DevCmd class GenerateFormulaApi < AbstractCommand - FORMULA_JSON_TEMPLATE = <<~EOS - --- - layout: formula_json - --- - {{ content }} - EOS - cmd_args do description <<~EOS Generate `homebrew/core` API data files for <#{HOMEBREW_API_WWW}>. @@ -28,122 +20,12 @@ class GenerateFormulaApi < AbstractCommand sig { override.void } def run - tap = CoreTap.instance - raise TapUnavailableError, tap.name unless tap.installed? - - unless args.dry_run? - directories = ["_data/formula", "api/formula", "formula", "api/internal"] - FileUtils.rm_rf directories + ["_data/formula_canonical.json"] - FileUtils.mkdir_p directories - end - - Homebrew.with_no_api_env do - tap_migrations_json = JSON.dump(tap.tap_migrations) - File.write("api/formula_tap_migrations.json", tap_migrations_json) unless args.dry_run? - - Formulary.enable_factory_cache! - Formula.generating_hash! - - all_formulae = {} - latest_macos = MacOSVersion.new((HOMEBREW_MACOS_NEWEST_UNSUPPORTED.to_i - 1).to_s).to_sym - Homebrew::SimulateSystem.with(os: latest_macos, arch: :arm) do - tap.formula_names.each do |name| - formula = Formulary.factory(name) - name = formula.name - all_formulae[name] = formula.to_hash_with_variations - json = JSON.pretty_generate(all_formulae[name]) - html_template_name = html_template(name) - - unless args.dry_run? - File.write("_data/formula/#{name.tr("+", "_")}.json", "#{json}\n") - File.write("api/formula/#{name}.json", FORMULA_JSON_TEMPLATE) - File.write("formula/#{name}.html", html_template_name) - end - rescue - onoe "Error while generating data for formula '#{name}'." - raise - end - end - - canonical_json = JSON.pretty_generate(tap.formula_renames.merge(tap.alias_table)) - File.write("_data/formula_canonical.json", "#{canonical_json}\n") unless args.dry_run? - - OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag| - macos_version = bottle_tag.to_macos_version if bottle_tag.macos? - - aliases = {} - renames = {} - variation_formulae = all_formulae.to_h do |name, formula| - formula = Homebrew::API.merge_variations(formula, bottle_tag:) - - formula["aliases"]&.each do |alias_name| - aliases[alias_name] = name - end - - formula["oldnames"]&.each do |oldname| - renames[oldname] = name - end - - version = Version.new(formula.dig("versions", "stable")) - pkg_version = PkgVersion.new(version, formula["revision"]) - version_scheme = formula.fetch("version_scheme", 0) - rebuild = formula.dig("bottle", "stable", "rebuild") || 0 - - bottle_collector = Utils::Bottles::Collector.new - formula.dig("bottle", "stable", "files")&.each do |tag, data| - tag = Utils::Bottles::Tag.from_symbol(tag) - bottle_collector.add tag, checksum: Checksum.new(data["sha256"]), cellar: :any - end + # odeprecated "brew generate-formula-api", "brew generate-package-api --only-core" - sha256 = bottle_collector.specification_for(bottle_tag)&.checksum&.to_s - - dependencies = Set.new(formula["dependencies"]) - - if macos_version - uses_from_macos = formula["uses_from_macos"].zip(formula["uses_from_macos_bounds"]) - dependencies += uses_from_macos.filter_map do |dep, bounds| - next if bounds.blank? - - since = bounds[:since] - next if since.blank? - - since_macos_version = MacOSVersion.from_symbol(since) - next if since_macos_version <= macos_version - - dep - end - else - dependencies += formula["uses_from_macos"] - end - - [name, [pkg_version.to_s, version_scheme, rebuild, sha256, dependencies.to_a]] - end - - json_contents = { - formulae: variation_formulae, - casks: [], - aliases: aliases, - renames: renames, - tap_migrations: CoreTap.instance.tap_migrations, - } - - File.write("api/internal/formula.#{bottle_tag}.json", JSON.generate(json_contents)) unless args.dry_run? - end - end - end - - private - - sig { params(title: String).returns(String) } - def html_template(title) - <<~EOS - --- - title: '#{title}' - layout: formula - redirect_from: /formula-linux/#{title} - --- - {{ content }} - EOS + Homebrew::API::Generator.new( + only_core: true, + dry_run: args.dry_run?, + ).generate! end end end diff --git a/Library/Homebrew/dev-cmd/generate-package-api.rb b/Library/Homebrew/dev-cmd/generate-package-api.rb new file mode 100644 index 0000000000000..110e54b802063 --- /dev/null +++ b/Library/Homebrew/dev-cmd/generate-package-api.rb @@ -0,0 +1,37 @@ +# typed: strict +# frozen_string_literal: true + +require "abstract_command" +require "api/generator" + +module Homebrew + module DevCmd + class GeneratePackageApi < AbstractCommand + cmd_args do + description <<~EOS + Generate API data files for <#{HOMEBREW_API_WWW}>. + The generated files are written to the current directory. + EOS + switch "--only-core", + description: "Only generate API data for packages in `homebrew/core`." + switch "--only-cask", + description: "Only generate API data for packages in `homebrew/cask`." + switch "-n", "--dry-run", + description: "Generate API data without writing it to files." + + conflicts "--only-core", "--only-cask" + + named_args :none + end + + sig { override.void } + def run + Homebrew::API::Generator.new( + only_core: args.only_core?, + only_cask: args.only_cask?, + dry_run: args.dry_run?, + ).generate! + end + end + end +end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/generate_package_api.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/generate_package_api.rbi new file mode 100644 index 0000000000000..5ff03d7c37d99 --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/generate_package_api.rbi @@ -0,0 +1,25 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Homebrew::DevCmd::GeneratePackageApi`. +# Please instead update this file by running `bin/tapioca dsl Homebrew::DevCmd::GeneratePackageApi`. + + +class Homebrew::DevCmd::GeneratePackageApi + sig { returns(Homebrew::DevCmd::GeneratePackageApi::Args) } + def args; end +end + +class Homebrew::DevCmd::GeneratePackageApi::Args < Homebrew::CLI::Args + sig { returns(T::Boolean) } + def dry_run?; end + + sig { returns(T::Boolean) } + def n?; end + + sig { returns(T::Boolean) } + def only_cask?; end + + sig { returns(T::Boolean) } + def only_core?; end +end