2025-07-06 16:23:31 +09:00

236 lines
7.0 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "utils/inreplace"
# Helper functions for updating CPAN resources.
module CPAN
METACPAN_URL_PREFIX = "https://cpan.metacpan.org/authors/id/"
private_constant :METACPAN_URL_PREFIX
# Represents a Perl package from an existing resource.
class Package
sig { params(resource_name: String, resource_url: String).void }
def initialize(resource_name, resource_url)
@cpan_info = T.let(nil, T.nilable(T::Array[String]))
@resource_name = resource_name
@resource_url = resource_url
@is_cpan_url = T.let(resource_url.start_with?(METACPAN_URL_PREFIX), T::Boolean)
end
sig { returns(String) }
def name
@resource_name
end
sig { returns(T.nilable(String)) }
def current_version
extract_version_from_url if @current_version.blank?
@current_version
end
sig { returns(T.nilable(String)) }
def package_name
extract_package_name_from_url if @package_name.blank?
@package_name
end
sig { returns(T::Boolean) }
def valid_cpan_package?
@is_cpan_url
end
# Get latest release information from MetaCPAN API.
sig { returns(T.nilable(T::Array[String])) }
def latest_cpan_info
return @cpan_info if @cpan_info.present?
return unless valid_cpan_package?
pname = package_name
return unless pname
metadata_url = "https://fastapi.metacpan.org/v1/release/#{pname}"
result = Utils::Curl.curl_output(metadata_url, "--location", "--fail")
return unless result.status.success?
begin
json = JSON.parse(result.stdout)
rescue JSON::ParserError
return
end
download_url = json["download_url"]
return unless download_url
checksum = get_checksum_from_cpan(download_url)
return unless checksum
@cpan_info = [@resource_name, download_url, checksum, json["version"]]
end
sig { returns(String) }
def to_s
@resource_name
end
private
sig { returns(T.nilable(String)) }
def extract_version_from_url
return unless @is_cpan_url
match = File.basename(@resource_url).match(/^(.+)-([0-9.v]+)\.tar\.gz$/)
return unless match
@current_version = T.let(match[2], T.nilable(String))
end
sig { returns(T.nilable(String)) }
def extract_package_name_from_url
return unless @is_cpan_url
match = File.basename(@resource_url).match(/^(.+)-([0-9.v]+)\.tar\.gz$/)
return unless match
@package_name = T.let(match[1], T.nilable(String))
end
# Get SHA256 checksum from CPAN CHECKSUMS file.
sig { params(download_url: String).returns(T.nilable(String)) }
def get_checksum_from_cpan(download_url)
filename = File.basename(download_url)
dir_url = File.dirname(download_url)
checksums_url = "#{dir_url}/CHECKSUMS"
checksums_result = Utils::Curl.curl_output(checksums_url, "--location", "--fail")
return unless checksums_result.status.success?
checksums_content = checksums_result.stdout
file_block_pattern = /'#{Regexp.escape(filename)}'\s*=>\s*\{[^}]*'sha256'\s*=>\s*'([a-f0-9]{64})'/mi
sha256_match = checksums_content.match(file_block_pattern)
return sha256_match[1] if sha256_match
alt_pattern = /#{Regexp.escape(filename)}.*?sha256.*?([a-f0-9]{64})/mi
alt_match = checksums_content.match(alt_pattern)
return alt_match[1] if alt_match
nil
end
end
# Update CPAN resources in a formula.
sig {
params(
formula: Formula,
print_only: T.nilable(T::Boolean),
silent: T.nilable(T::Boolean),
verbose: T.nilable(T::Boolean),
ignore_errors: T.nilable(T::Boolean),
).returns(T.nilable(T::Boolean))
}
def self.update_perl_resources!(formula, print_only: false, silent: false, verbose: false, ignore_errors: false)
cpan_resources = formula.resources.select { |resource| resource.url.start_with?(METACPAN_URL_PREFIX) }
odie "\"#{formula.name}\" has no CPAN resources to update." if cpan_resources.empty?
show_info = !print_only && !silent
non_cpan_resources = formula.resources.reject { |resource| resource.url.start_with?(METACPAN_URL_PREFIX) }
ohai "Skipping #{non_cpan_resources.length} non-CPAN resources" if non_cpan_resources.any? && show_info
ohai "Found #{cpan_resources.length} CPAN resources to update" if show_info
new_resource_blocks = ""
package_errors = ""
updated_count = 0
cpan_resources.each do |resource|
package = Package.new(resource.name, resource.url)
unless package.valid_cpan_package?
if ignore_errors
package_errors += " # RESOURCE-ERROR: \"#{resource.name}\" is not a valid CPAN resource\n"
next
else
odie "\"#{resource.name}\" is not a valid CPAN resource"
end
end
ohai "Checking \"#{resource.name}\" for updates..." if show_info
info = package.latest_cpan_info
unless info
if ignore_errors
package_errors += " # RESOURCE-ERROR: Unable to resolve \"#{resource.name}\"\n"
next
else
odie "Unable to resolve \"#{resource.name}\""
end
end
name, url, checksum, new_version = info
current_version = package.current_version
if current_version && new_version && current_version != new_version
ohai "\"#{resource.name}\": #{current_version} -> #{new_version}" if show_info
updated_count += 1
elsif show_info
ohai "\"#{resource.name}\": already up to date (#{current_version})" if current_version
end
new_resource_blocks += <<-EOS
resource "#{name}" do
url "#{url}"
sha256 "#{checksum}"
end
EOS
end
package_errors += "\n" if package_errors.present?
resource_section = "#{package_errors}#{new_resource_blocks}"
if print_only
puts resource_section.chomp
return true
end
if formula.resources.all? { |resource| resource.name.start_with?("homebrew-") }
inreplace_regex = / def install/
resource_section += " def install"
else
inreplace_regex = /
\ \ (
(\#\ RESOURCE-ERROR:\ .*\s+)*
resource\ .*\ do\s+
url\ .*\s+
sha256\ .*\s+
((\#.*\s+)*
patch\ (.*\ )?do\s+
url\ .*\s+
sha256\ .*\s+
end\s+)*
end\s+)+
/x
resource_section += " "
end
ohai "Updating resource blocks" unless silent
Utils::Inreplace.inreplace formula.path do |s|
if T.must(s.inreplace_string.split(/^ test do\b/, 2).first).scan(inreplace_regex).length > 1
odie "Unable to update resource blocks for \"#{formula.name}\" automatically. Please update them manually."
end
s.sub! inreplace_regex, resource_section
end
if package_errors.present?
ofail "Unable to resolve some dependencies. Please check #{formula.path} for RESOURCE-ERROR comments."
elsif updated_count.positive?
ohai "Updated #{updated_count} CPAN resource#{"s" if updated_count != 1}" unless silent
end
true
end
end