diff --git a/Library/Homebrew/Gemfile.lock b/Library/Homebrew/Gemfile.lock index 3d09c128b4..1c0c3fd0c0 100644 --- a/Library/Homebrew/Gemfile.lock +++ b/Library/Homebrew/Gemfile.lock @@ -67,7 +67,7 @@ GEM mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) - parallel_tests (3.12.0) + parallel_tests (3.12.1) parallel parlour (8.0.0) commander (~> 4.5) @@ -185,7 +185,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.2.0) + unicode-display_width (2.3.0) unparser (0.6.4) diff-lcs (~> 1.3) parser (>= 3.1.0) diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index 4011403a32..52f3d7018b 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -26,7 +26,7 @@ module Homebrew Homebrew::API.fetch "#{formula_api_path}/#{name}.json" end - sig { returns(Array) } + sig { returns(Hash) } def all_formulae @all_formulae ||= begin curl_args = %w[--compressed --silent https://formulae.brew.sh/api/formula.json] @@ -37,11 +37,23 @@ module Homebrew json_formulae = JSON.parse(cached_formula_json_file.read) + @all_aliases = {} json_formulae.to_h do |json_formula| + json_formula["aliases"].each do |alias_name| + @all_aliases[alias_name] = json_formula["name"] + end + [json_formula["name"], json_formula.except("name")] end end end + + sig { returns(Hash) } + def all_aliases + all_formulae if @all_aliases.blank? + + @all_aliases + end end end end diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index 7fd0c55d91..dc7ad2ad83 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -24,7 +24,7 @@ module Cask def initialize(cask, appcast: nil, download: nil, quarantine: nil, token_conflicts: nil, online: nil, strict: nil, signing: nil, - new_cask: nil) + new_cask: nil, only: [], except: []) # `new_cask` implies `online`, `token_conflicts`, `strict` and `signing` online = new_cask if online.nil? @@ -47,44 +47,22 @@ module Cask @signing = signing @new_cask = new_cask @token_conflicts = token_conflicts + @only = only + @except = except end def run! - check_denylist - check_reverse_migration - check_required_stanzas - check_version - check_sha256 - check_desc - check_url - check_unnecessary_verified - check_missing_verified - check_no_match - check_generic_artifacts - check_token_valid - check_token_bad_words - check_token_conflicts - check_languages - check_download - check_https_availability - check_single_pre_postflight - check_single_uninstall_zap - check_untrusted_pkg - livecheck_result = check_livecheck_version - check_hosting_with_livecheck(livecheck_result: livecheck_result) - check_appcast_and_livecheck - check_latest_with_appcast_or_livecheck - check_latest_with_auto_updates - check_stanza_requires_uninstall - check_appcast_contains_version - check_gitlab_repository - check_gitlab_repository_archived - check_gitlab_prerelease_version - check_github_repository - check_github_repository_archived - check_github_prerelease_version - check_bitbucket_repository - check_signing + only_audits = @only + except_audits = @except + + private_methods.map(&:to_s).grep(/^check_/).each do |audit_method_name| + name = audit_method_name.delete_prefix("check_") + next if !only_audits.empty? && only_audits&.exclude?(name) + next if except_audits&.include?(name) + + send(audit_method_name) + end + self rescue => e odebug e, e.backtrace @@ -100,10 +78,27 @@ module Cask @warnings ||= [] end + sig { returns(T::Boolean) } + def errors? + errors.any? + end + + sig { returns(T::Boolean) } + def warnings? + warnings.any? + end + + sig { returns(T::Boolean) } + def success? + !(errors? || warnings?) + end + + sig { params(message: T.nilable(String), location: T.nilable(String)).void } def add_error(message, location: nil) errors << ({ message: message, location: location }) end + sig { params(message: T.nilable(String), location: T.nilable(String)).void } def add_warning(message, location: nil) if strict? add_error message, location: location @@ -112,14 +107,6 @@ module Cask end end - def errors? - errors.any? - end - - def warnings? - warnings.any? - end - def result if errors? Formatter.error("failed") @@ -150,12 +137,9 @@ module Cask summary.join("\n") end - def success? - !(errors? || warnings?) - end - private + sig { void } def check_untrusted_pkg odebug "Auditing pkg stanza: allow_untrusted" @@ -170,6 +154,7 @@ module Cask add_error "allow_untrusted is not permitted in official Homebrew Cask taps" end + sig { void } def check_stanza_requires_uninstall odebug "Auditing stanzas which require an uninstall" @@ -179,6 +164,7 @@ module Cask add_error "installer and pkg stanzas require an uninstall stanza" end + sig { void } def check_single_pre_postflight odebug "Auditing preflight and postflight stanzas" @@ -195,6 +181,7 @@ module Cask add_error "only a single postflight stanza is allowed" end + sig { void } def check_single_uninstall_zap odebug "Auditing single uninstall_* and zap stanzas" @@ -221,6 +208,7 @@ module Cask add_error "only a single zap stanza is allowed" end + sig { void } def check_required_stanzas odebug "Auditing required stanzas" [:version, :sha256, :url, :homepage].each do |sym| @@ -233,58 +221,66 @@ module Cask add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty? end - def check_version - return unless cask.version + sig { void } + def check_description_present + # Fonts seldom benefit from descriptions and requiring them disproportionately increases the maintenance burden + return if cask.tap == "homebrew/cask-fonts" - check_no_string_version_latest + add_warning "Cask should have a description. Please add a `desc` stanza." if cask.desc.blank? end + sig { void } def check_no_string_version_latest - odebug "Verifying version :latest does not appear as a string ('latest')" + return unless cask.version + + odebug "Auditing version :latest does not appear as a string ('latest')" return unless cask.version.raw_version == "latest" add_error "you should use version :latest instead of version 'latest'" end - def check_sha256 + sig { void } + def check_sha256_no_check_if_latest return unless cask.sha256 - check_sha256_no_check_if_latest - check_sha256_no_check_if_unversioned - check_sha256_actually_256 - check_sha256_invalid - end - - def check_sha256_no_check_if_latest - odebug "Verifying sha256 :no_check with version :latest" + odebug "Auditing sha256 :no_check with version :latest" return unless cask.version.latest? return if cask.sha256 == :no_check add_error "you should use sha256 :no_check when version is :latest" end + sig { void } def check_sha256_no_check_if_unversioned + return unless cask.sha256 return if cask.sha256 == :no_check add_error "Use `sha256 :no_check` when URL is unversioned." if cask.url&.unversioned? end + sig { void } def check_sha256_actually_256 - odebug "Verifying sha256 string is a legal SHA-256 digest" + return unless cask.sha256 + + odebug "Auditing sha256 string is a legal SHA-256 digest" return unless cask.sha256.is_a?(Checksum) return if cask.sha256.length == 64 && cask.sha256[/^[0-9a-f]+$/i] add_error "sha256 string must be of 64 hexadecimal characters" end + sig { void } def check_sha256_invalid - odebug "Verifying sha256 is not a known invalid value" + return unless cask.sha256 + + odebug "Auditing sha256 is not a known invalid value" empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" return unless cask.sha256 == empty_sha256 add_error "cannot use the sha256 for an empty string: #{empty_sha256}" end + sig { void } def check_appcast_and_livecheck return unless cask.appcast @@ -295,6 +291,7 @@ module Cask end end + sig { void } def check_latest_with_appcast_or_livecheck return unless cask.version.latest? @@ -302,6 +299,7 @@ module Cask add_error "Casks with a `livecheck` should not use `version :latest`." if cask.livecheckable? end + sig { void } def check_latest_with_auto_updates return unless cask.version.latest? return unless cask.auto_updates @@ -311,7 +309,8 @@ module Cask LIVECHECK_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#stanza-livecheck" - def check_hosting_with_livecheck(livecheck_result:) + sig { params(livecheck_result: T::Boolean).void } + def check_hosting_with_livecheck(livecheck_result: check_livecheck_version) return if cask.discontinued? || cask.version.latest? return if block_url_offline? || cask.appcast || cask.livecheckable? return if livecheck_result == :auto_detected @@ -330,24 +329,12 @@ module Cask end end - def check_desc - return if cask.desc.present? - - # Fonts seldom benefit from descriptions and requiring them disproportionately increases the maintenance burden - return if cask.tap == "homebrew/cask-fonts" - - add_warning "Cask should have a description. Please add a `desc` stanza." - end - - def check_url - return unless cask.url - - check_download_url_format - end - SOURCEFORGE_OSDN_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#sourceforgeosdn-urls" + sig { void } def check_download_url_format + return unless cask.url + odebug "Auditing URL format" if bad_sourceforge_url? add_error "SourceForge URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}" @@ -356,82 +343,9 @@ module Cask end end - def bad_url_format?(regex, valid_formats_array) - return false unless cask.url.to_s.match?(regex) - - valid_formats_array.none? { |format| cask.url.to_s =~ format } - end - - def bad_sourceforge_url? - bad_url_format?(/sourceforge/, - [ - %r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z}, - %r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)/)}, - ]) - end - - def bad_osdn_url? - bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}]) - end - - def homepage - URI(cask.homepage.to_s).host - end - - def domain - URI(cask.url.to_s).host - end - - def url_match_homepage? - host = cask.url.to_s - host_uri = URI(host) - host = if host.match?(/:\d/) && host_uri.port != 80 - "#{host_uri.host}:#{host_uri.port}" - else - host_uri.host - end - home = homepage.downcase - if (split_host = host.split(".")).length >= 3 - host = split_host[-2..].join(".") - end - if (split_home = homepage.split(".")).length >= 3 - home = split_home[-2..].join(".") - end - host == home - end - - def strip_url_scheme(url) - url.sub(%r{^[^:/]+://(www\.)?}, "") - end - - def url_from_verified - strip_url_scheme(cask.url.verified) - end - - def verified_matches_url? - url_domain, url_path = strip_url_scheme(cask.url.to_s).split("/", 2) - verified_domain, verified_path = url_from_verified.split("/", 2) - - (url_domain == verified_domain || (verified_domain && url_domain&.end_with?(".#{verified_domain}"))) && - (!verified_path || url_path&.start_with?(verified_path)) - end - - def verified_present? - cask.url.verified.present? - end - - def file_url? - URI(cask.url.to_s).scheme == "file" - end - - def block_url_offline? - return false if online? - - cask.url.from_block? - end - VERIFIED_URL_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#when-url-and-homepage-domains-differ-add-verified" + sig { void } def check_unnecessary_verified return if block_url_offline? return unless verified_present? @@ -443,6 +357,7 @@ module Cask "See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}" end + sig { void } def check_missing_verified return if block_url_offline? return if file_url? @@ -454,6 +369,7 @@ module Cask "See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}" end + sig { void } def check_no_match return if block_url_offline? return unless verified_present? @@ -464,6 +380,7 @@ module Cask "See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}" end + sig { void } def check_generic_artifacts cask.artifacts.select { |a| a.is_a?(Artifact::Artifact) }.each do |artifact| unless artifact.target.absolute? @@ -472,6 +389,7 @@ module Cask end end + sig { void } def check_languages @cask.languages.each do |language| Locale.parse(language) @@ -480,6 +398,7 @@ module Cask end end + sig { void } def check_token_conflicts return unless token_conflicts? return unless core_formula_names.include?(cask.token) @@ -488,6 +407,7 @@ module Cask "#{Formatter.url(core_formula_url)}" end + sig { void } def check_token_valid add_error "cask token contains non-ascii characters" unless cask.token.ascii_only? add_error "cask token + should be replaced by -plus-" if cask.token.include? "+" @@ -505,6 +425,7 @@ module Cask add_error "cask token should not have leading or trailing hyphens" end + sig { void } def check_token_bad_words return unless new_cask? @@ -532,19 +453,7 @@ module Cask add_warning "cask token mentions framework" end - def core_tap - @core_tap ||= CoreTap.instance - end - - def core_formula_names - core_tap.formula_names - end - - sig { returns(String) } - def core_formula_url - "#{core_tap.default_remote}/blob/HEAD/Formula/#{cask.token}.rb" - end - + sig { void } def check_download return if download.blank? || cask.url.blank? @@ -554,11 +463,11 @@ module Cask add_error "download not possible: #{e}" end + sig { void } def check_signing return if !signing? || download.blank? || cask.url.blank? odebug "Auditing signing" - odebug cask.artifacts artifacts = cask.artifacts.select { |k| k.is_a?(Artifact::Pkg) || k.is_a?(Artifact::App) } return if artifacts.empty? @@ -598,6 +507,7 @@ module Cask end end + sig { returns(T.nilable(T.any(T::Boolean, Symbol))) } def check_livecheck_version return unless appcast? @@ -635,6 +545,7 @@ module Cask false end + sig { void } def check_appcast_contains_version return unless appcast? return if cask.appcast.to_s.empty? @@ -661,6 +572,7 @@ module Cask EOS end + sig { void } def check_github_prerelease_version return if cask.tap == "homebrew/cask-versions" @@ -674,6 +586,7 @@ module Cask add_error error if error end + sig { void } def check_gitlab_prerelease_version return if cask.tap == "homebrew/cask-versions" @@ -688,6 +601,7 @@ module Cask add_error error if error end + sig { void } def check_github_repository_archived user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if online? return if user.nil? @@ -708,6 +622,7 @@ module Cask end end + sig { void } def check_gitlab_repository_archived user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if online? return if user.nil? @@ -728,6 +643,7 @@ module Cask end end + sig { void } def check_github_repository return unless new_cask? @@ -740,6 +656,7 @@ module Cask add_error error if error end + sig { void } def check_gitlab_repository return unless new_cask? @@ -752,6 +669,7 @@ module Cask add_error error if error end + sig { void } def check_bitbucket_repository return unless new_cask? @@ -764,6 +682,62 @@ module Cask add_error error if error end + sig { void } + def check_denylist + return unless cask.tap + return unless cask.tap.official? + return unless (reason = Denylist.reason(cask.token)) + + add_error "#{cask.token} is not allowed: #{reason}" + end + + sig { void } + def check_reverse_migration + return unless new_cask? + return unless cask.tap + return unless cask.tap.official? + return unless cask.tap.tap_migrations.key?(cask.token) + + add_error "#{cask.token} is listed in tap_migrations.json" + end + + sig { void } + def check_https_availability + return unless download + + if cask.url && !cask.url.using + validate_url_for_https_availability(cask.url, "binary URL", cask.token, cask.tap, + user_agents: [cask.url.user_agent]) + end + + if cask.appcast && appcast? + validate_url_for_https_availability(cask.appcast, "appcast URL", cask.token, cask.tap, check_content: true) + end + + return unless cask.homepage + + validate_url_for_https_availability(cask.homepage, "homepage URL", cask.token, cask.tap, + user_agents: [:browser, :default], + check_content: true, + strict: strict?) + end + + # sig { + # params(url_to_check: T.any(String, URL), url_type: String, cask_token: String, tap: Tap, + # options: T.untyped).void + # } + def validate_url_for_https_availability(url_to_check, url_type, cask_token, tap, **options) + problem = curl_check_http_content(url_to_check.to_s, url_type, **options) + exception = tap&.audit_exception(:secure_connection_audit_skiplist, cask_token, url_to_check.to_s) + + if problem + add_error problem unless exception + elsif exception + add_error "#{url_to_check} is in the secure connection audit skiplist but does not need to be skipped" + end + end + + sig { params(regex: T.any(String, Regexp)).returns(T.nilable(T::Array[String])) } def get_repo_data(regex) return unless online? @@ -777,52 +751,107 @@ module Cask [user, repo] end - def check_denylist - return unless cask.tap - return unless cask.tap.official? - return unless (reason = Denylist.reason(cask.token)) + sig { + params(regex: T.any(String, Regexp), valid_formats_array: T::Array[T.any(String, Regexp)]).returns(T::Boolean) + } + def bad_url_format?(regex, valid_formats_array) + return false unless cask.url.to_s.match?(regex) - add_error "#{cask.token} is not allowed: #{reason}" + valid_formats_array.none? { |format| cask.url.to_s =~ format } end - def check_reverse_migration - return unless new_cask? - return unless cask.tap - return unless cask.tap.official? - return unless cask.tap.tap_migrations.key?(cask.token) - - add_error "#{cask.token} is listed in tap_migrations.json" + sig { returns(T::Boolean) } + def bad_sourceforge_url? + bad_url_format?(/sourceforge/, + [ + %r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z}, + %r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)/)}, + ]) end - def check_https_availability - return unless download - - if cask.url && !cask.url.using - check_url_for_https_availability(cask.url, "binary URL", cask.token, cask.tap, - user_agents: [cask.url.user_agent]) - end - - if cask.appcast && appcast? - check_url_for_https_availability(cask.appcast, "appcast URL", cask.token, cask.tap, check_content: true) - end - - return unless cask.homepage - - check_url_for_https_availability(cask.homepage, "homepage URL", cask.token, cask.tap, - user_agents: [:browser, :default], - check_content: true, - strict: strict?) + sig { returns(T::Boolean) } + def bad_osdn_url? + bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}]) end - def check_url_for_https_availability(url_to_check, url_type, cask_token, tap, **options) - problem = curl_check_http_content(url_to_check.to_s, url_type, **options) - exception = tap&.audit_exception(:secure_connection_audit_skiplist, cask_token, url_to_check.to_s) + # sig { returns(String) } + def homepage + URI(cask.homepage.to_s).host + end - if problem - add_error problem unless exception - elsif exception - add_error "#{url_to_check} is in the secure connection audit skiplist but does not need to be skipped" + # sig { returns(String) } + def domain + URI(cask.url.to_s).host + end + + sig { returns(T::Boolean) } + def url_match_homepage? + host = cask.url.to_s + host_uri = URI(host) + host = if host.match?(/:\d/) && host_uri.port != 80 + "#{host_uri.host}:#{host_uri.port}" + else + host_uri.host end + home = homepage.downcase + if (split_host = host.split(".")).length >= 3 + host = split_host[-2..].join(".") + end + if (split_home = homepage.split(".")).length >= 3 + home = split_home[-2..].join(".") + end + host == home + end + + # sig { params(url: String).returns(String) } + def strip_url_scheme(url) + url.sub(%r{^[^:/]+://(www\.)?}, "") + end + + # sig { returns(String) } + def url_from_verified + strip_url_scheme(cask.url.verified) + end + + sig { returns(T::Boolean) } + def verified_matches_url? + url_domain, url_path = strip_url_scheme(cask.url.to_s).split("/", 2) + verified_domain, verified_path = url_from_verified.split("/", 2) + + (url_domain == verified_domain || (verified_domain && url_domain&.end_with?(".#{verified_domain}"))) && + (!verified_path || url_path&.start_with?(verified_path)) + end + + sig { returns(T::Boolean) } + def verified_present? + cask.url.verified.present? + end + + sig { returns(T::Boolean) } + def file_url? + URI(cask.url.to_s).scheme == "file" + end + + sig { returns(T::Boolean) } + def block_url_offline? + return false if online? + + cask.url.from_block? + end + + sig { returns(Tap) } + def core_tap + @core_tap ||= CoreTap.instance + end + + # sig { returns(T::Array[String]) } + def core_formula_names + core_tap.formula_names + end + + sig { returns(String) } + def core_formula_url + "#{core_tap.default_remote}/blob/HEAD/Formula/#{cask.token}.rb" end end end diff --git a/Library/Homebrew/cask/auditor.rb b/Library/Homebrew/cask/auditor.rb index 2b80340860..daae35b9b4 100644 --- a/Library/Homebrew/cask/auditor.rb +++ b/Library/Homebrew/cask/auditor.rb @@ -144,7 +144,6 @@ module Cask quarantine: @quarantine, ) audit.run! - audit end def language_blocks diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index deadc6bd53..24d32b0cfa 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -579,6 +579,7 @@ module Homebrew end def self.autoremove(dry_run: false) + require "utils/autoremove" require "cask/caskroom" # If this runs after install, uninstall, reinstall or upgrade, @@ -593,7 +594,7 @@ module Homebrew end casks = Cask::Caskroom.casks - removable_formulae = Formula.unused_formulae_with_no_dependents(formulae, casks) + removable_formulae = Utils::Autoremove.removable_formulae(formulae, casks) return if removable_formulae.blank? diff --git a/Library/Homebrew/cmd/leaves.rb b/Library/Homebrew/cmd/leaves.rb index ec134de4a1..621d64ae84 100644 --- a/Library/Homebrew/cmd/leaves.rb +++ b/Library/Homebrew/cmd/leaves.rb @@ -37,7 +37,7 @@ module Homebrew def leaves args = leaves_args.parse - leaves_list = Formula.formulae_with_no_formula_dependents(Formula.installed) + leaves_list = Formula.installed - Formula.installed.flat_map(&:runtime_formula_dependencies) leaves_list.select!(&method(:installed_on_request?)) if args.installed_on_request? leaves_list.select!(&method(:installed_as_dependency?)) if args.installed_as_dependency? diff --git a/Library/Homebrew/cmd/shellenv.sh b/Library/Homebrew/cmd/shellenv.sh index 97c630e470..a19a2223d5 100644 --- a/Library/Homebrew/cmd/shellenv.sh +++ b/Library/Homebrew/cmd/shellenv.sh @@ -9,6 +9,8 @@ # HOMEBREW_CELLAR and HOMEBREW_PREFIX are set by extend/ENV/super.rb # HOMEBREW_REPOSITORY is set by bin/brew +# Trailing colon in MANPATH adds default man dirs to search path in Linux, does no harm in macOS. +# Please do not submit PRs to remove it! # shellcheck disable=SC2154 homebrew-shellenv() { if [[ "${HOMEBREW_PATH%%:"${HOMEBREW_PREFIX}"/sbin*}" == "${HOMEBREW_PREFIX}/bin" ]] diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index 178b346167..11149ca2b9 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -1049,11 +1049,11 @@ module Homebrew when :quarantine_available nil when :xattr_broken - "There's no working version of `xattr` on this system." + "No Cask quarantine support available: there's no working version of `xattr` on this system." when :no_swift - "Swift is not available on this system." + "No Cask quarantine support available: there's no available version of `swift` on this system." else - "Unknown support status" + "No Cask quarantine support available: unknown reason." end end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 269e689c76..10d7fd3ca4 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1844,46 +1844,6 @@ class Formula end.uniq(&:name) end - # An array of all installed {Formula} with {Cask} dependents. - # @private - def self.formulae_with_cask_dependents(casks) - casks.flat_map { |cask| cask.depends_on[:formula] } - .compact - .map { |f| Formula[f] } - .flat_map { |f| [f, *f.runtime_formula_dependencies].compact } - end - - # An array of all installed {Formula} without {Formula} dependents - # @private - def self.formulae_with_no_formula_dependents(formulae) - return [] if formulae.blank? - - formulae - formulae.flat_map(&:runtime_formula_dependencies) - end - - # Recursive function that returns an array of {Formula} without - # {Formula} dependents that weren't installed on request. - # @private - def self.unused_formulae_with_no_formula_dependents(formulae) - unused_formulae = formulae_with_no_formula_dependents(formulae).reject do |f| - Tab.for_keg(f.any_installed_keg).installed_on_request - end - - if unused_formulae.present? - unused_formulae += unused_formulae_with_no_formula_dependents(formulae - unused_formulae) - end - - unused_formulae - end - - # An array of {Formula} without {Formula} or {Cask} - # dependents that weren't installed on request. - # @private - def self.unused_formulae_with_no_dependents(formulae, casks) - unused_formulae = unused_formulae_with_no_formula_dependents(formulae) - unused_formulae - formulae_with_cask_dependents(casks) - end - def self.installed_with_alias_path(alias_path) return [] if alias_path.nil? @@ -2744,6 +2704,10 @@ class Formula super end + # Whether this formula was loaded using the formulae.brew.sh API + # @private + attr_accessor :loaded_from_api + # Whether this formula contains OS/arch-specific blocks # (e.g. `on_macos`, `on_arm`, `on_monterey :or_older`, `on_system :linux, macos: :big_sur_or_newer`). # @private diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index e0a696fd0e..b375f8dfc0 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -1165,7 +1165,7 @@ class FormulaInstaller if pour_bottle?(output_warning: true) formula.fetch_bottle_tab - elsif Homebrew::EnvConfig.install_from_api? + elsif formula.core_formula? && Homebrew::EnvConfig.install_from_api? odie "Unable to build #{formula.name} from source with HOMEBREW_INSTALL_FROM_API." else formula.fetch_patches @@ -1202,6 +1202,7 @@ class FormulaInstaller tab.unused_options = [] tab.built_as_bottle = true tab.poured_from_bottle = true + tab.loaded_from_api = formula.class.loaded_from_api tab.installed_as_dependency = installed_as_dependency? tab.installed_on_request = installed_on_request? tab.time = Time.now.to_i diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 658b211c62..a043518bb0 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -217,6 +217,7 @@ module Formulary end end + klass.loaded_from_api = true mod.const_set(class_s, klass) cache[:api] ||= {} @@ -529,6 +530,14 @@ module Formulary end end + # Load aliases from the API. + class AliasAPILoader < FormulaAPILoader + def initialize(alias_name) + super Homebrew::API::Formula.all_aliases[alias_name] + @alias_path = Formulary.core_alias_path(alias_name).to_s + end + end + # Return a {Formula} instance for the given reference. # `ref` is a string containing: # @@ -655,6 +664,7 @@ module Formulary if ref.start_with?("homebrew/core/") && Homebrew::EnvConfig.install_from_api? name = ref.split("/", 3).last return FormulaAPILoader.new(name) if Homebrew::API::Formula.all_formulae.key?(name) + return AliasAPILoader.new(name) if Homebrew::API::Formula.all_aliases.key?(name) end return TapLoader.new(ref, from: from) @@ -662,14 +672,15 @@ module Formulary return FromPathLoader.new(ref) if File.extname(ref) == ".rb" && Pathname.new(ref).expand_path.exist? - if Homebrew::EnvConfig.install_from_api? && Homebrew::API::Formula.all_formulae.key?(ref) - return FormulaAPILoader.new(ref) + if Homebrew::EnvConfig.install_from_api? + return FormulaAPILoader.new(ref) if Homebrew::API::Formula.all_formulae.key?(ref) + return AliasAPILoader.new(ref) if Homebrew::API::Formula.all_aliases.key?(ref) end formula_with_that_name = core_path(ref) return FormulaLoader.new(ref, formula_with_that_name) if formula_with_that_name.file? - possible_alias = CoreTap.instance.alias_dir/ref + possible_alias = core_alias_path(ref) return AliasLoader.new(possible_alias) if possible_alias.file? possible_tap_formulae = tap_paths(ref) @@ -705,6 +716,10 @@ module Formulary CoreTap.instance.formula_dir/"#{name.to_s.downcase}.rb" end + def self.core_alias_path(name) + CoreTap.instance.alias_dir/name.to_s.downcase + end + def self.tap_paths(name, taps = Dir[HOMEBREW_LIBRARY/"Taps/*/*/"]) name = name.to_s.downcase taps.map do |tap| diff --git a/Library/Homebrew/sorbet/rbi/gems/parallel_tests@3.12.0.rbi b/Library/Homebrew/sorbet/rbi/gems/parallel_tests@3.12.1.rbi similarity index 100% rename from Library/Homebrew/sorbet/rbi/gems/parallel_tests@3.12.0.rbi rename to Library/Homebrew/sorbet/rbi/gems/parallel_tests@3.12.1.rbi diff --git a/Library/Homebrew/sorbet/rbi/gems/unicode-display_width@2.2.0.rbi b/Library/Homebrew/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi similarity index 100% rename from Library/Homebrew/sorbet/rbi/gems/unicode-display_width@2.2.0.rbi rename to Library/Homebrew/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 8dba8022f6..06a0152de5 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -31,6 +31,7 @@ class Tab < OpenStruct "installed_as_dependency" => false, "installed_on_request" => false, "poured_from_bottle" => false, + "loaded_from_api" => false, "time" => Time.now.to_i, "source_modified_time" => formula.source_modified_time.to_i, "compiler" => compiler, @@ -189,6 +190,7 @@ class Tab < OpenStruct "installed_as_dependency" => false, "installed_on_request" => false, "poured_from_bottle" => false, + "loaded_from_api" => false, "time" => nil, "source_modified_time" => 0, "stdlib" => nil, @@ -332,6 +334,7 @@ class Tab < OpenStruct "unused_options" => unused_options.as_flags, "built_as_bottle" => built_as_bottle, "poured_from_bottle" => poured_from_bottle, + "loaded_from_api" => loaded_from_api, "installed_as_dependency" => installed_as_dependency, "installed_on_request" => installed_on_request, "changed_files" => changed_files&.map(&:to_s), @@ -384,6 +387,7 @@ class Tab < OpenStruct "Built from source" end + s << "using the formulae.brew.sh API" if loaded_from_api s << Time.at(time).strftime("on %Y-%m-%d at %H:%M:%S") if time unless used_options.empty? diff --git a/Library/Homebrew/test/cask/audit_spec.rb b/Library/Homebrew/test/cask/audit_spec.rb index 020b8dfe33..e01de0e554 100644 --- a/Library/Homebrew/test/cask/audit_spec.rb +++ b/Library/Homebrew/test/cask/audit_spec.rb @@ -33,13 +33,15 @@ describe Cask::Audit, :cask do let(:cask) { instance_double(Cask::Cask) } let(:new_cask) { nil } let(:online) { nil } + let(:only) { [] } let(:strict) { nil } let(:token_conflicts) { nil } let(:audit) { described_class.new(cask, online: online, strict: strict, new_cask: new_cask, - token_conflicts: token_conflicts) + token_conflicts: token_conflicts, + only: only) } describe "#new" do @@ -121,6 +123,8 @@ describe Cask::Audit, :cask do let(:cask) { Cask::CaskLoader.load(cask_token) } describe "required stanzas" do + let(:only) { ["required_stanzas"] } + %w[version sha256 url name homepage].each do |stanza| context "when missing #{stanza}" do let(:cask_token) { "missing-#{stanza}" } @@ -132,6 +136,7 @@ describe Cask::Audit, :cask do describe "token validation" do let(:strict) { true } + let(:only) { ["token_valid"] } let(:cask) do tmp_cask cask_token.to_s, <<~RUBY cask '#{cask_token}' do @@ -228,6 +233,7 @@ describe Cask::Audit, :cask do describe "token bad words" do let(:new_cask) { true } + let(:only) { ["token_bad_words", "reverse_migration"] } let(:online) { false } let(:cask) do tmp_cask cask_token.to_s, <<~RUBY @@ -343,6 +349,7 @@ describe Cask::Audit, :cask do end describe "locale validation" do + let(:only) { ["languages"] } let(:cask) do tmp_cask "locale-cask-test", <<~RUBY cask 'locale-cask-test' do @@ -390,6 +397,7 @@ describe Cask::Audit, :cask do end describe "pkg allow_untrusted checks" do + let(:only) { ["untrusted_pkg"] } let(:message) { "allow_untrusted is not permitted in official Homebrew Cask taps" } context "when the Cask has no pkg stanza" do @@ -412,6 +420,7 @@ describe Cask::Audit, :cask do end describe "signing checks" do + let(:only) { ["signing"] } let(:download_double) { instance_double(Cask::Download) } let(:unpack_double) { instance_double(UnpackStrategy::Zip) } @@ -459,6 +468,7 @@ describe Cask::Audit, :cask do end describe "livecheck should be skipped" do + let(:only) { ["livecheck_version"] } let(:online) { true } let(:message) { /Version '[^']*' differs from '[^']*' retrieved by livecheck\./ } @@ -512,6 +522,7 @@ describe Cask::Audit, :cask do end describe "when the Cask stanza requires uninstall" do + let(:only) { ["stanza_requires_uninstall"] } let(:message) { "installer and pkg stanzas require an uninstall stanza" } context "when the Cask does not require an uninstall" do @@ -681,12 +692,14 @@ describe Cask::Audit, :cask do let(:message) { "you should use version :latest instead of version 'latest'" } context "when version is 'latest'" do + let(:only) { ["no_string_version_latest"] } let(:cask_token) { "version-latest-string" } it { is_expected.to fail_with(message) } end context "when version is :latest" do + let(:only) { ["sha256_no_check_if_latest"] } let(:cask_token) { "version-latest-with-checksum" } it { is_expected.not_to fail_with(message) } @@ -695,18 +708,21 @@ describe Cask::Audit, :cask do describe "sha256 checks" do context "when version is :latest and sha256 is not :no_check" do + let(:only) { ["sha256_no_check_if_latest"] } let(:cask_token) { "version-latest-with-checksum" } it { is_expected.to fail_with("you should use sha256 :no_check when version is :latest") } end context "when sha256 is not a legal SHA-256 digest" do + let(:only) { ["sha256_actually_256"] } let(:cask_token) { "invalid-sha256" } it { is_expected.to fail_with("sha256 string must be of 64 hexadecimal characters") } end context "when sha256 is sha256 for empty string" do + let(:only) { ["sha256_invalid"] } let(:cask_token) { "sha256-for-empty-string" } it { is_expected.to fail_with(/cannot use the sha256 for an empty string/) } @@ -714,6 +730,7 @@ describe Cask::Audit, :cask do end describe "hosting with livecheck checks" do + let(:only) { ["hosting_with_livecheck"] } let(:message) { /please add a livecheck/ } context "when the download does not use hosting with a livecheck" do @@ -761,6 +778,7 @@ describe Cask::Audit, :cask do end describe "latest with appcast checks" do + let(:only) { ["latest_with_appcast_or_livecheck"] } let(:message) { "Casks with an `appcast` should not use `version :latest`." } context "when the Cask is :latest and does not have an appcast" do @@ -783,6 +801,8 @@ describe Cask::Audit, :cask do end describe "denylist checks" do + let(:only) { ["denylist"] } + context "when the Cask is not on the denylist" do let(:cask_token) { "adobe-air" } @@ -805,6 +825,7 @@ describe Cask::Audit, :cask do end describe "latest with auto_updates checks" do + let(:only) { ["latest_with_auto_updates"] } let(:message) { "Casks with `version :latest` should not use `auto_updates`." } context "when the Cask is :latest and does not have auto_updates" do @@ -833,6 +854,7 @@ describe Cask::Audit, :cask do end describe "preferred download URL formats" do + let(:only) { ["download_url_format"] } let(:message) { /URL format incorrect/ } context "with incorrect SourceForge URL format" do @@ -867,6 +889,8 @@ describe Cask::Audit, :cask do end describe "generic artifact checks" do + let(:only) { ["generic_artifacts"] } + context "with relative target" do let(:cask_token) { "generic-artifact-relative-target" } @@ -887,6 +911,8 @@ describe Cask::Audit, :cask do end describe "url checks" do + let(:only) { %w[unnecessary_verified missing_verified no_match] } + context "with a block" do let(:cask_token) { "booby-trap" } @@ -900,7 +926,7 @@ describe Cask::Audit, :cask do let(:online) { false } it "does not evaluate the block" do - expect(run).not_to pass + expect(run).not_to fail_with(/Boom/) end end @@ -915,6 +941,7 @@ describe Cask::Audit, :cask do end describe "token conflicts" do + let(:only) { ["token_conflicts"] } let(:cask_token) { "with-binary" } let(:token_conflicts) { true } @@ -935,6 +962,7 @@ describe Cask::Audit, :cask do end describe "audit of downloads" do + let(:only) { ["download"] } let(:cask_token) { "basic-cask" } let(:cask) { Cask::CaskLoader.load(cask_token) } let(:download_double) { instance_double(Cask::Download) } @@ -959,6 +987,7 @@ describe Cask::Audit, :cask do context "when an exception is raised" do let(:cask) { instance_double(Cask::Cask) } + let(:only) { ["description_present"] } it "fails the audit" do expect(cask).to receive(:tap).and_raise(StandardError.new) @@ -966,7 +995,8 @@ describe Cask::Audit, :cask do end end - describe "without description" do + describe "checking description" do + let(:only) { ["description_present"] } let(:cask_token) { "without-description" } let(:cask) do tmp_cask cask_token.to_s, <<~RUBY @@ -996,131 +1026,132 @@ describe Cask::Audit, :cask do expect(run).to warn_with(/should have a description/) end end - end - context "with description" do - let(:cask_token) { "with-description" } - let(:cask) do - tmp_cask cask_token.to_s, <<~RUBY - cask "#{cask_token}" do - version "1.0" - sha256 "8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a" - url "https://brew.sh/\#{version}.zip" - name "Audit" - desc "Cask Auditor" - homepage "https://brew.sh/" - app "Audit.app" - end - RUBY - end - - it "passes" do - expect(run).to pass - end - end - - context "when the url matches the homepage" do - let(:cask_token) { "foo" } - let(:cask) do - tmp_cask cask_token.to_s, <<~RUBY - cask '#{cask_token}' do - version '1.0' - sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' - url 'https://foo.brew.sh/foo.zip' - name 'Audit' - desc 'Audit Description' - homepage 'https://foo.brew.sh' - app 'Audit.app' - end - RUBY - end - - it { is_expected.to pass } - end - - context "when the url does not match the homepage" do - let(:cask_token) { "foo" } - let(:cask) do - tmp_cask cask_token.to_s, <<~RUBY - cask '#{cask_token}' do - version "1.8.0_72,8.13.0.5" - sha256 "8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a" - url "https://brew.sh/foo-\#{version.after_comma}.zip" - name "Audit" - desc "Audit Description" - homepage "https://foo.example.org" - app "Audit.app" - end - RUBY - end - - it { is_expected.to fail_with(/a 'verified' parameter has to be added/) } - end - - context "when the url does not match the homepage with verified" do - let(:cask_token) { "foo" } - let(:cask) do - tmp_cask cask_token.to_s, <<~RUBY - cask "#{cask_token}" do - version "1.8.0_72,8.13.0.5" - sha256 "8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a" - url "https://brew.sh/foo-\#{version.after_comma}.zip", verified: "brew.sh" - name "Audit" - desc "Audit Description" - homepage "https://foo.example.org" - app "Audit.app" - end - RUBY - end - - it { is_expected.to pass } - end - - context "when there is no homepage" do - let(:cask_token) { "foo" } - let(:cask) do - tmp_cask cask_token.to_s, <<~RUBY - cask '#{cask_token}' do - version '1.8.0_72,8.13.0.5' - sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' - url 'https://brew.sh/foo.zip' - name 'Audit' - desc 'Audit Description' - app 'Audit.app' - end - RUBY - end - - it { is_expected.to fail_with(/a homepage stanza is required/) } - end - - context "when url is lazy" do - let(:strict) { true } - let(:cask_token) { "with-lazy" } - let(:cask) do - tmp_cask cask_token.to_s, <<~RUBY - cask '#{cask_token}' do - version '1.8.0_72,8.13.0.5' - sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' - url do - ['https://brew.sh/foo.zip', {referer: 'https://example.com', cookies: {'foo' => 'bar'}}] + context "with description" do + let(:cask_token) { "with-description" } + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask "#{cask_token}" do + version "1.0" + sha256 "8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a" + url "https://brew.sh/\#{version}.zip" + name "Audit" + desc "Cask Auditor" + homepage "https://brew.sh/" + app "Audit.app" end - name 'Audit' - desc 'Audit Description' - homepage 'https://brew.sh' - app 'Audit.app' - end - RUBY + RUBY + end + + it "passes" do + expect(run).to pass + end + end + end + + describe "checking verified" do + let(:only) { %w[unnecessary_verified missing_verified no_match required_stanzas] } + let(:cask_token) { "foo" } + + context "when the url matches the homepage" do + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.0' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url 'https://foo.brew.sh/foo.zip' + name 'Audit' + desc 'Audit Description' + homepage 'https://foo.brew.sh' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to pass } end - it { is_expected.to pass } + context "when the url does not match the homepage" do + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version "1.8.0_72,8.13.0.5" + sha256 "8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a" + url "https://brew.sh/foo-\#{version.after_comma}.zip" + name "Audit" + desc "Audit Description" + homepage "https://foo.example.org" + app "Audit.app" + end + RUBY + end - it "receives a referer" do - expect(audit.cask.url.referer).to eq "https://example.com" + it { is_expected.to fail_with(/a 'verified' parameter has to be added/) } end - it "receives cookies" do - expect(audit.cask.url.cookies).to eq "foo" => "bar" + context "when the url does not match the homepage with verified" do + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask "#{cask_token}" do + version "1.8.0_72,8.13.0.5" + sha256 "8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a" + url "https://brew.sh/foo-\#{version.after_comma}.zip", verified: "brew.sh" + name "Audit" + desc "Audit Description" + homepage "https://foo.example.org" + app "Audit.app" + end + RUBY + end + + it { is_expected.to pass } + end + + context "when there is no homepage" do + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.8.0_72,8.13.0.5' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url 'https://brew.sh/foo.zip' + name 'Audit' + desc 'Audit Description' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to fail_with(/a homepage stanza is required/) } + end + + context "when url is lazy" do + let(:strict) { true } + let(:cask_token) { "with-lazy" } + let(:cask) do + tmp_cask cask_token.to_s, <<~RUBY + cask '#{cask_token}' do + version '1.8.0_72,8.13.0.5' + sha256 '8dd95daa037ac02455435446ec7bc737b34567afe9156af7d20b2a83805c1d8a' + url do + ['https://brew.sh/foo.zip', {referer: 'https://example.com', cookies: {'foo' => 'bar'}}] + end + name 'Audit' + desc 'Audit Description' + homepage 'https://brew.sh' + app 'Audit.app' + end + RUBY + end + + it { is_expected.to pass } + + it "receives a referer" do + expect(audit.cask.url.referer).to eq "https://example.com" + end + + it "receives cookies" do + expect(audit.cask.url.cookies).to eq "foo" => "bar" + end end end end diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index a125919c33..6aeb223eff 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -450,136 +450,6 @@ describe Formula do end end - shared_context "with formulae for dependency testing" do - let(:formula_with_deps) do - formula "zero" do - url "zero-1.0" - end - end - - let(:formula_is_dep1) do - formula "one" do - url "one-1.1" - end - end - - let(:formula_is_dep2) do - formula "two" do - url "two-1.1" - end - end - - let(:formulae) do - [ - formula_with_deps, - formula_is_dep1, - formula_is_dep2, - ] - end - - before do - allow(formula_with_deps).to receive(:runtime_formula_dependencies).and_return([formula_is_dep1, - formula_is_dep2]) - allow(formula_is_dep1).to receive(:runtime_formula_dependencies).and_return([formula_is_dep2]) - end - end - - describe "::formulae_with_no_formula_dependents" do - include_context "with formulae for dependency testing" - - it "filters out dependencies" do - expect(described_class.formulae_with_no_formula_dependents(formulae)) - .to eq([formula_with_deps]) - end - end - - describe "::unused_formulae_with_no_formula_dependents" do - include_context "with formulae for dependency testing" - - let(:tab_from_keg) { double } - - before do - allow(Tab).to receive(:for_keg).and_return(tab_from_keg) - end - - specify "installed on request" do - allow(tab_from_keg).to receive(:installed_on_request).and_return(true) - expect(described_class.unused_formulae_with_no_formula_dependents(formulae)) - .to eq([]) - end - - specify "not installed on request" do - allow(tab_from_keg).to receive(:installed_on_request).and_return(false) - expect(described_class.unused_formulae_with_no_formula_dependents(formulae)) - .to eq(formulae) - end - end - - shared_context "with formulae and casks for dependency testing" do - include_context "with formulae for dependency testing" - - require "cask/cask_loader" - - let(:cask_one_dep) do - Cask::CaskLoader.load(+<<-RUBY) - cask "red" do - depends_on formula: "two" - end - RUBY - end - - let(:cask_multiple_deps) do - Cask::CaskLoader.load(+<<-RUBY) - cask "blue" do - depends_on formula: "zero" - end - RUBY - end - - let(:cask_no_deps1) do - Cask::CaskLoader.load(+<<-RUBY) - cask "green" do - end - RUBY - end - - let(:cask_no_deps2) do - Cask::CaskLoader.load(+<<-RUBY) - cask "purple" do - end - RUBY - end - - let(:casks_no_deps) { [cask_no_deps1, cask_no_deps2] } - let(:casks_one_dep) { [cask_no_deps1, cask_no_deps2, cask_one_dep] } - let(:casks_multiple_deps) { [cask_no_deps1, cask_no_deps2, cask_multiple_deps] } - - before do - allow(described_class).to receive("[]").with("zero").and_return(formula_with_deps) - allow(described_class).to receive("[]").with("one").and_return(formula_is_dep1) - allow(described_class).to receive("[]").with("two").and_return(formula_is_dep2) - end - end - - describe "::formulae_with_cask_dependents" do - include_context "with formulae and casks for dependency testing" - - specify "no dependents" do - expect(described_class.formulae_with_cask_dependents(casks_no_deps)) - .to eq([]) - end - - specify "one dependent" do - expect(described_class.formulae_with_cask_dependents(casks_one_dep)) - .to eq([formula_is_dep2]) - end - - specify "multiple dependents" do - expect(described_class.formulae_with_cask_dependents(casks_multiple_deps)) - .to eq(formulae) - end - end - describe "::inreplace" do specify "raises build error on failure" do f = formula do diff --git a/Library/Homebrew/test/utils/autoremove_spec.rb b/Library/Homebrew/test/utils/autoremove_spec.rb new file mode 100644 index 0000000000..d7fe8fc9d6 --- /dev/null +++ b/Library/Homebrew/test/utils/autoremove_spec.rb @@ -0,0 +1,162 @@ +# typed: false +# frozen_string_literal: true + +require "utils/autoremove" + +describe Utils::Autoremove do + shared_context "with formulae for dependency testing" do + let(:formula_with_deps) do + formula "zero" do + url "zero-1.0" + + depends_on "three" => :build + end + end + + let(:formula_is_dep1) do + formula "one" do + url "one-1.1" + end + end + + let(:formula_is_dep2) do + formula "two" do + url "two-1.1" + end + end + + let(:formula_is_build_dep) do + formula "three" do + url "three-1.1" + end + end + + let(:formulae) do + [ + formula_with_deps, + formula_is_dep1, + formula_is_dep2, + formula_is_build_dep, + ] + end + + let(:tab_from_keg) { double } + + before do + allow(formula_with_deps).to receive(:runtime_formula_dependencies).and_return([formula_is_dep1, + formula_is_dep2]) + allow(formula_is_dep1).to receive(:runtime_formula_dependencies).and_return([formula_is_dep2]) + + allow(Tab).to receive(:for_keg).and_return(tab_from_keg) + end + end + + describe "::formulae_with_no_formula_dependents" do + include_context "with formulae for dependency testing" + + before do + allow(Formulary).to receive(:factory).with("three").and_return(formula_is_build_dep) + end + + context "when formulae are bottles" do + it "filters out runtime dependencies" do + allow(tab_from_keg).to receive(:poured_from_bottle).and_return(true) + expect(described_class.send(:formulae_with_no_formula_dependents, formulae)) + .to eq([formula_with_deps, formula_is_build_dep]) + end + end + + context "when formulae are built from source" do + it "filters out runtime and build dependencies" do + allow(tab_from_keg).to receive(:poured_from_bottle).and_return(false) + expect(described_class.send(:formulae_with_no_formula_dependents, formulae)) + .to eq([formula_with_deps]) + end + end + end + + describe "::unused_formulae_with_no_formula_dependents" do + include_context "with formulae for dependency testing" + + before do + allow(tab_from_keg).to receive(:poured_from_bottle).and_return(true) + end + + specify "installed on request" do + allow(tab_from_keg).to receive(:installed_on_request).and_return(true) + expect(described_class.send(:unused_formulae_with_no_formula_dependents, formulae)) + .to eq([]) + end + + specify "not installed on request" do + allow(tab_from_keg).to receive(:installed_on_request).and_return(false) + expect(described_class.send(:unused_formulae_with_no_formula_dependents, formulae)) + .to match_array(formulae) + end + end + + shared_context "with formulae and casks for dependency testing" do + include_context "with formulae for dependency testing" + + require "cask/cask_loader" + + let(:cask_one_dep) do + Cask::CaskLoader.load(+<<-RUBY) + cask "red" do + depends_on formula: "two" + end + RUBY + end + + let(:cask_multiple_deps) do + Cask::CaskLoader.load(+<<-RUBY) + cask "blue" do + depends_on formula: "zero" + end + RUBY + end + + let(:cask_no_deps1) do + Cask::CaskLoader.load(+<<-RUBY) + cask "green" do + end + RUBY + end + + let(:cask_no_deps2) do + Cask::CaskLoader.load(+<<-RUBY) + cask "purple" do + end + RUBY + end + + let(:casks_no_deps) { [cask_no_deps1, cask_no_deps2] } + let(:casks_one_dep) { [cask_no_deps1, cask_no_deps2, cask_one_dep] } + let(:casks_multiple_deps) { [cask_no_deps1, cask_no_deps2, cask_multiple_deps] } + + before do + allow(Formula).to receive("[]").with("zero").and_return(formula_with_deps) + allow(Formula).to receive("[]").with("one").and_return(formula_is_dep1) + allow(Formula).to receive("[]").with("two").and_return(formula_is_dep2) + end + end + + describe "::formulae_with_cask_dependents" do + include_context "with formulae and casks for dependency testing" + + specify "no dependents" do + expect(described_class.send(:formulae_with_cask_dependents, casks_no_deps)) + .to eq([]) + end + + specify "one dependent" do + expect(described_class.send(:formulae_with_cask_dependents, casks_one_dep)) + .to eq([formula_is_dep2]) + end + + specify "multiple dependents" do + expect(described_class.send(:formulae_with_cask_dependents, casks_multiple_deps)) + .to match_array([formula_with_deps, formula_is_dep1, formula_is_dep2]) + end + end +end diff --git a/Library/Homebrew/utils/autoremove.rb b/Library/Homebrew/utils/autoremove.rb new file mode 100644 index 0000000000..cb34473286 --- /dev/null +++ b/Library/Homebrew/utils/autoremove.rb @@ -0,0 +1,68 @@ +# typed: false +# frozen_string_literal: true + +module Utils + # Helper function for finding autoremovable formulae. + # + # @private + module Autoremove + module_function + + # An array of all installed {Formula} with {Cask} dependents. + # @private + def formulae_with_cask_dependents(casks) + casks.flat_map { |cask| cask.depends_on[:formula] } + .compact + .map { |f| Formula[f] } + .flat_map { |f| [f, *f.runtime_formula_dependencies].compact } + end + private_class_method :formulae_with_cask_dependents + + # An array of all installed {Formula} without runtime {Formula} + # dependents for bottles and without build {Formula} dependents + # for those built from source. + # @private + def formulae_with_no_formula_dependents(formulae) + return [] if formulae.blank? + + dependents = [] + formulae.each do |formula| + dependents += formula.runtime_formula_dependencies + + # Ignore build dependencies when the formula is a bottle + next if Tab.for_keg(formula.any_installed_keg).poured_from_bottle + + formula.deps.select(&:build?).each do |dep| + suppress(FormulaUnavailableError) { dependents << dep.to_formula } + end + end + formulae - dependents + end + private_class_method :formulae_with_no_formula_dependents + + # Recursive function that returns an array of {Formula} without + # {Formula} dependents that weren't installed on request. + # @private + def unused_formulae_with_no_formula_dependents(formulae) + unused_formulae = formulae_with_no_formula_dependents(formulae).reject do |f| + Tab.for_keg(f.any_installed_keg).installed_on_request + end + + if unused_formulae.present? + unused_formulae += unused_formulae_with_no_formula_dependents(formulae - unused_formulae) + end + + unused_formulae + end + private_class_method :unused_formulae_with_no_formula_dependents + + # An array of {Formula} without {Formula} or {Cask} + # dependents that weren't installed on request and without + # build dependencies for {Formula} installed from source. + # @private + def removable_formulae(formulae, casks) + unused_formulae = unused_formulae_with_no_formula_dependents(formulae) + unused_formulae - formulae_with_cask_dependents(casks) + end + end +end diff --git a/Library/Homebrew/vendor/bundle/bundler/setup.rb b/Library/Homebrew/vendor/bundle/bundler/setup.rb index 0576bd78c8..91ec4112a1 100644 --- a/Library/Homebrew/vendor/bundle/bundler/setup.rb +++ b/Library/Homebrew/vendor/bundle/bundler/setup.rb @@ -57,7 +57,7 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mechanize-2.8.5/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/method_source-1.0.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mustache-1.1.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parallel-1.22.1/lib" -$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parallel_tests-3.12.0/lib" +$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parallel_tests-3.12.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parser-3.1.2.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rainbow-3.1.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-runtime-0.5.10175/lib" @@ -85,7 +85,7 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rspec-wait-0.0.9/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rspec_junit_formatter-0.5.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-ast-1.21.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ruby-progressbar-1.11.0/lib" -$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unicode-display_width-2.2.0/lib" +$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unicode-display_width-2.3.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-1.35.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-performance-1.15.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-rails-2.16.0/lib" diff --git a/docs/Manpage.md b/docs/Manpage.md index 1d82739b95..19350e83d0 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -1866,6 +1866,8 @@ Only supports GitHub Actions as a CI provider. This is because Homebrew uses Git Don't pass `--online` to `brew audit` and skip `brew livecheck`. * `--skip-dependents`: Don't test any dependents. +* `--skip-livecheck`: + Don't test livecheck. * `--skip-recursive-dependents`: Only test the direct dependents. * `--only-cleanup-before`: diff --git a/manpages/brew.1 b/manpages/brew.1 index 796baa506a..bec8a19d20 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -2672,6 +2672,10 @@ Don\'t pass \fB\-\-online\fR to \fBbrew audit\fR and skip \fBbrew livecheck\fR\. Don\'t test any dependents\. . .TP +\fB\-\-skip\-livecheck\fR +Don\'t test livecheck\. +. +.TP \fB\-\-skip\-recursive\-dependents\fR Only test the direct dependents\. .