
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>
213 lines
7.2 KiB
Ruby
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
|