brew/Library/Homebrew/resource_auditor.rb
Sam Ford 46d4af6031
Temporarily skip audit_download_strategy for lsr
This is a temporary workaround to allow us to merge support for
tangled.sh Git URLs in `DownloadStrategyDetector`, as it makes the
`using: :git` argument in the `lsr` formula redundant and causes brew
CI to fail. We can't remove that argument from the formula until the
brew change is merged, so this allows us to do so. This should be
removed after the brew change is available in a release.

Co-authored-by: Carlo Cabrera <github@carlo.cab>
2025-09-01 08:22:40 -04:00

213 lines
7.2 KiB
Ruby

# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "utils/svn"
module Homebrew
# Auditor for checking common violations in {Resource}s.
class ResourceAuditor
include Utils::Curl
attr_reader :name, :version, :checksum, :url, :mirrors, :using, :specs, :owner, :spec_name, :problems
def initialize(resource, spec_name, options = {})
@name = resource.name
@version = resource.version
@checksum = resource.checksum
@url = resource.url
@mirrors = resource.mirrors
@using = resource.using
@specs = resource.specs
@owner = resource.owner
@spec_name = spec_name
@online = options[:online]
@strict = options[:strict]
@only = options[:only]
@except = options[:except]
@core_tap = options[:core_tap]
@use_homebrew_curl = options[:use_homebrew_curl]
@problems = []
end
def audit
only_audits = @only
except_audits = @except
methods.map(&:to_s).grep(/^audit_/).each do |audit_method_name|
name = audit_method_name.delete_prefix("audit_")
next if only_audits&.exclude?(name)
next if except_audits&.include?(name)
send(audit_method_name)
end
self
end
def audit_version
if version.nil?
problem "Missing version"
elsif owner.is_a?(Formula) && !version.to_s.match?(GitHubPackages::VALID_OCI_TAG_REGEX) &&
(owner.core_formula? ||
(owner.bottle_defined? && GitHubPackages::URL_REGEX.match?(owner.bottle_specification.root_url)))
problem "`version #{version}` does not match #{GitHubPackages::VALID_OCI_TAG_REGEX.source}"
elsif !version.detected_from_url?
version_text = version
version_url = Version.detect(url, **specs)
if version_url.to_s == version_text.to_s && version.instance_of?(Version)
problem "`version #{version_text}` is redundant with version scanned from URL"
end
end
end
def audit_download_strategy
url_strategy = DownloadStrategyDetector.detect(url)
if (using == :git || url_strategy == GitDownloadStrategy) && specs[:tag] && !specs[:revision]
problem "Git should specify `revision:` when a `tag:` is specified."
end
return unless using
if using == :cvs
mod = specs[:module]
problem "Redundant `module:` value in URL" if mod == name
if url.match?(%r{:[^/]+$})
mod = url.split(":").last
if mod == name
problem "Redundant CVS module appended to URL"
else
problem "Specify CVS module as `module: \"#{mod}\"` instead of appending it to the URL"
end
end
end
# TODO: Remove this exception for `lsr` after support for tangled.sh
# Git URLs is available in a brew release.
return if name == "lsr"
return if url_strategy != DownloadStrategyDetector.detect("", using)
problem "Redundant `using:` value in URL"
end
def audit_checksum
return if spec_name == :head
# This condition is non-invertible.
# rubocop:disable Style/InvertibleUnlessCondition
return unless DownloadStrategyDetector.detect(url, using) <= CurlDownloadStrategy
# rubocop:enable Style/InvertibleUnlessCondition
problem "Checksum is missing" if checksum.blank?
end
def self.curl_deps
@curl_deps ||= begin
["curl"] + Formula["curl"].recursive_dependencies.map(&:name).uniq
rescue FormulaUnavailableError
[]
end
end
def audit_resource_name_matches_pypi_package_name_in_url
return unless url.match?(%r{^https?://files\.pythonhosted\.org/packages/})
return if name == owner.name # Skip the top-level package name as we only care about `resource "foo"` blocks.
if url.end_with? ".whl"
path = URI(url).path
return unless path.present?
pypi_package_name, = File.basename(path).split("-", 2)
else
url =~ %r{/(?<package_name>[^/]+)-}
pypi_package_name = Regexp.last_match(:package_name).to_s
end
T.must(pypi_package_name).gsub!(/[_.]/, "-")
return if name.casecmp(pypi_package_name).zero?
problem "`resource` name should be '#{pypi_package_name}' to match the PyPI package name"
end
def audit_urls
urls = [url] + mirrors
curl_dep = self.class.curl_deps.include?(owner.name)
# Ideally `ca-certificates` would not be excluded here, but sourcing a HTTP mirror was tricky.
# Instead, we have logic elsewhere to pass `--insecure` to curl when downloading the certs.
# TODO: try remove the OS/env conditional
if Homebrew::SimulateSystem.simulating_or_running_on_macos? && spec_name == :stable &&
owner.name != "ca-certificates" && curl_dep && !urls.find { |u| u.start_with?("http://") }
problem "Should always include at least one HTTP mirror"
end
return unless @online
urls.each do |url|
next if !@strict && mirrors.include?(url)
strategy = DownloadStrategyDetector.detect(url, using)
if strategy <= CurlDownloadStrategy && !url.start_with?("file")
raise HomebrewCurlDownloadStrategyError, url if
strategy <= HomebrewCurlDownloadStrategy && !Formula["curl"].any_version_installed?
# Skip https audit for curl dependencies
if !curl_dep && (http_content_problem = curl_check_http_content(
url,
"source URL",
specs:,
use_homebrew_curl: @use_homebrew_curl,
))
problem http_content_problem
end
elsif strategy <= GitDownloadStrategy
attempts = 0
remote_exists = T.let(false, T::Boolean)
while !remote_exists && attempts < Homebrew::EnvConfig.curl_retries.to_i
remote_exists = Utils::Git.remote_exists?(url)
attempts += 1
end
problem "The URL #{url} is not a valid Git URL" unless remote_exists
elsif strategy <= SubversionDownloadStrategy
next unless DevelopmentTools.subversion_handles_most_https_certificates?
next unless Utils::Svn.available?
problem "The URL #{url} is not a valid SVN URL" unless Utils::Svn.remote_exists? url
end
end
end
def audit_head_branch
return unless @online
return if spec_name != :head
return if specs[:tag].present?
return if specs[:revision].present?
# Skip `resource` URLs as they use SHAs instead of branch specifiers.
return if name != owner.name
return unless url.end_with?(".git")
return unless Utils::Git.remote_exists?(url)
detected_branch = Utils.popen_read("git", "ls-remote", "--symref", url, "HEAD")
.match(%r{ref: refs/heads/(.*?)\s+HEAD})&.to_a&.second
if specs[:branch].blank?
problem "Git `head` URL must specify a branch name"
return
end
return unless @core_tap
return if specs[:branch] == detected_branch
problem "To use a non-default HEAD branch, add the formula to `head_non_default_branch_allowlist.json`."
end
def problem(text)
@problems << text
end
end
end