# 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