mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
feat: add update-perl-resources
dev-cmd
This commit is contained in:
parent
114d66082d
commit
f89ead08c3
36
Library/Homebrew/dev-cmd/update-perl-resources.rb
Normal file
36
Library/Homebrew/dev-cmd/update-perl-resources.rb
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# typed: strict
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "abstract_command"
|
||||||
|
require "utils/cpan"
|
||||||
|
|
||||||
|
module Homebrew
|
||||||
|
module DevCmd
|
||||||
|
class UpdatePerlResources < AbstractCommand
|
||||||
|
cmd_args do
|
||||||
|
description <<~EOS
|
||||||
|
Update versions for CPAN resource blocks in <formula>.
|
||||||
|
EOS
|
||||||
|
switch "-p", "--print-only",
|
||||||
|
description: "Print the updated resource blocks instead of changing <formula>."
|
||||||
|
switch "-s", "--silent",
|
||||||
|
description: "Suppress any output."
|
||||||
|
switch "--ignore-errors",
|
||||||
|
description: "Continue processing even if some resources can't be resolved."
|
||||||
|
|
||||||
|
named_args :formula, min: 1, without_api: true
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { override.void }
|
||||||
|
def run
|
||||||
|
args.named.to_formulae.each do |formula|
|
||||||
|
CPAN.update_perl_resources! formula,
|
||||||
|
print_only: args.print_only?,
|
||||||
|
silent: args.silent?,
|
||||||
|
verbose: args.verbose?,
|
||||||
|
ignore_errors: args.ignore_errors?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/update_perl_resources.rbi
generated
Normal file
28
Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/update_perl_resources.rbi
generated
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# typed: true
|
||||||
|
|
||||||
|
# DO NOT EDIT MANUALLY
|
||||||
|
# This is an autogenerated file for dynamic methods in `Homebrew::DevCmd::UpdatePerlResources`.
|
||||||
|
# Please instead update this file by running `bin/tapioca dsl Homebrew::DevCmd::UpdatePerlResources`.
|
||||||
|
|
||||||
|
|
||||||
|
class Homebrew::DevCmd::UpdatePerlResources
|
||||||
|
sig { returns(Homebrew::DevCmd::UpdatePerlResources::Args) }
|
||||||
|
def args; end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Homebrew::DevCmd::UpdatePerlResources::Args < Homebrew::CLI::Args
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def ignore_errors?; end
|
||||||
|
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def p?; end
|
||||||
|
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def print_only?; end
|
||||||
|
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def s?; end
|
||||||
|
|
||||||
|
sig { returns(T::Boolean) }
|
||||||
|
def silent?; end
|
||||||
|
end
|
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "cmd/shared_examples/args_parse"
|
||||||
|
require "dev-cmd/update-perl-resources"
|
||||||
|
|
||||||
|
RSpec.describe Homebrew::DevCmd::UpdatePerlResources do
|
||||||
|
it_behaves_like "parseable arguments"
|
||||||
|
end
|
47
Library/Homebrew/test/utils/cpan_spec.rb
Normal file
47
Library/Homebrew/test/utils/cpan_spec.rb
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "utils/cpan"
|
||||||
|
|
||||||
|
RSpec.describe CPAN do
|
||||||
|
let(:cpan_package_url) do
|
||||||
|
"https://cpan.metacpan.org/authors/id/P/PE/PEVANS/Scalar-List-Utils-1.68.tar.gz"
|
||||||
|
end
|
||||||
|
let(:non_cpan_package_url) do
|
||||||
|
"https://github.com/example/package/archive/v1.0.0.tar.gz"
|
||||||
|
end
|
||||||
|
|
||||||
|
describe CPAN::Package do
|
||||||
|
let(:package_from_cpan_url) { described_class.new("Scalar::Util", cpan_package_url) }
|
||||||
|
let(:package_from_non_cpan_url) { described_class.new("SomePackage", non_cpan_package_url) }
|
||||||
|
|
||||||
|
describe "initialize" do
|
||||||
|
it "initializes resource name" do
|
||||||
|
expect(package_from_cpan_url.name).to eq "Scalar::Util"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "extracts package name from CPAN url" do
|
||||||
|
expect(package_from_cpan_url.package_name).to eq "Scalar-List-Utils"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "extracts version from CPAN url" do
|
||||||
|
expect(package_from_cpan_url.current_version).to eq "1.68"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".valid_cpan_package?" do
|
||||||
|
it "is true for CPAN URLs" do
|
||||||
|
expect(package_from_cpan_url.valid_cpan_package?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is false for non-CPAN URLs" do
|
||||||
|
expect(package_from_non_cpan_url.valid_cpan_package?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".to_s" do
|
||||||
|
it "returns resource name" do
|
||||||
|
expect(package_from_cpan_url.to_s).to eq "Scalar::Util"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
235
Library/Homebrew/utils/cpan.rb
Normal file
235
Library/Homebrew/utils/cpan.rb
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# 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
|
Loading…
x
Reference in New Issue
Block a user