
- If a HEAD branch name isn't specified at all, then the user probably wants to shortcut adding one by being told what the default branch for the repo is. Otherwise they have to click the URL, look at the GitHub UI, then type the branch name into `branch: "foo"` syntax.
204 lines
6.9 KiB
Ruby
204 lines
6.9 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]
|
|
@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
|
|
|
|
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
|
|
|
|
message = "Git `head` URL must specify a branch name"
|
|
message += " - try `branch: \"#{detected_branch}\"`" if detected_branch.present?
|
|
|
|
problem message if specs[:branch].blank? || detected_branch != specs[:branch]
|
|
end
|
|
|
|
def problem(text)
|
|
@problems << text
|
|
end
|
|
end
|
|
end
|