bump-cask-pr: add arch-specific version support

This commit is contained in:
Razvan Azamfirei 2023-07-21 11:45:34 -04:00
parent 3fee251765
commit c64c834d05
No known key found for this signature in database
GPG Key ID: 62E97BFCB12046BD
9 changed files with 283 additions and 98 deletions

View File

@ -4,6 +4,8 @@ module Cask
class Cask class Cask
def appcast; end def appcast; end
def appdir; end
def artifacts; end def artifacts; end
def auto_updates; end def auto_updates; end
@ -14,9 +16,11 @@ module Cask
def container; end def container; end
def depends_on; end
def desc; end def desc; end
def depends_on; end def discontinued?; end
def homepage; end def homepage; end
@ -24,8 +28,14 @@ module Cask
def languages; end def languages; end
def livecheck; end
def livecheckable?; end
def name; end def name; end
def on_system_blocks_exist?; end
def sha256; end def sha256; end
def staged_path; end def staged_path; end
@ -33,13 +43,5 @@ module Cask
def url; end def url; end
def version; end def version; end
def appdir; end
def discontinued?; end
def livecheck; end
def livecheckable?; end
end end
end end

View File

@ -1,9 +1,10 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "cask" require "cask"
require "cask/download" require "cask/download"
require "cli/parser" require "cli/parser"
require "extend/version-parser"
require "utils/tar" require "utils/tar"
module Homebrew module Homebrew
@ -38,6 +39,10 @@ module Homebrew
description: "Don't try to fork the repository." description: "Don't try to fork the repository."
flag "--version=", flag "--version=",
description: "Specify the new <version> for the cask." description: "Specify the new <version> for the cask."
flag "--version-arm=",
description: "Specify the new cask <version> for the ARM architecture."
flag "--version-intel=",
description: "Specify the new cask <version> for the Intel architecture."
flag "--message=", flag "--message=",
description: "Prepend <message> to the default pull request message." description: "Prepend <message> to the default pull request message."
flag "--url=", flag "--url=",
@ -51,11 +56,14 @@ module Homebrew
conflicts "--dry-run", "--write" conflicts "--dry-run", "--write"
conflicts "--no-audit", "--online" conflicts "--no-audit", "--online"
conflicts "--version=", "--version-arm="
conflicts "--version=", "--version-intel="
named_args :cask, number: 1, without_api: true named_args :cask, number: 1, without_api: true
end end
end end
sig { void }
def bump_cask_pr def bump_cask_pr
args = bump_cask_pr_args.parse args = bump_cask_pr_args.parse
@ -68,19 +76,18 @@ module Homebrew
ENV["PATH"] = PATH.new(ORIGINAL_PATHS).to_s ENV["PATH"] = PATH.new(ORIGINAL_PATHS).to_s
# Use the user's browser, too. # Use the user's browser, too.
ENV["BROWSER"] = Homebrew::EnvConfig.browser ENV["BROWSER"] = EnvConfig.browser
cask = args.named.to_casks.first cask = args.named.to_casks.first
odie "This cask is not in a tap!" if cask.tap.blank? odie "This cask is not in a tap!" if cask.tap.blank?
odie "This cask's tap is not a Git repository!" unless cask.tap.git? odie "This cask's tap is not a Git repository!" unless cask.tap.git?
new_version = unless (new_version = args.version).nil? new_version = VersionParser.new(
raise UsageError, "`--version` must not be empty." if new_version.blank? general: args.version,
intel: args.version_intel,
new_version = :latest if ["latest", ":latest"].include?(new_version) arm: args.version_arm,
Cask::DSL::Version.new(new_version) )
end
new_hash = unless (new_hash = args.sha256).nil? new_hash = unless (new_hash = args.sha256).nil?
raise UsageError, "`--sha256` must not be empty." if new_hash.blank? raise UsageError, "`--sha256` must not be empty." if new_hash.blank?
@ -98,85 +105,33 @@ module Homebrew
end end
end end
if new_version.nil? && new_base_url.nil? && new_hash.nil? if new_version.blank? && new_base_url.nil? && new_hash.nil?
raise UsageError, "No `--version`, `--url` or `--sha256` argument specified!" raise UsageError, "No `--version`, `--url` or `--sha256` argument specified!"
end end
old_version = cask.version check_pull_requests(cask, args: args, new_version: new_version)
old_hash = cask.sha256
check_pull_requests(cask, state: "open", args: args) replacement_pairs ||= []
# if we haven't already found open requests, try for an exact match across closed requests
check_pull_requests(cask, state: "closed", args: args, version: new_version) if new_version.present?
old_contents = File.read(cask.sourcefile_path)
replacement_pairs = []
branch_name = "bump-#{cask.token}" branch_name = "bump-#{cask.token}"
commit_message = nil commit_message = nil
if new_version if new_version.present?
branch_name += "-#{new_version.tr(",:", "-")}" # For simplicity, our naming defers to the arm version if we multiple architectures are specified
commit_message_version = if new_version.before_comma == old_version.before_comma branch_version = new_version.arm || new_version.general
new_version if branch_version.is_a?(Cask::DSL::Version)
commit_version = if branch_version.before_comma == cask.version.before_comma
branch_version
else else
new_version.before_comma branch_version.before_comma
end end
commit_message ||= "#{cask.token} #{commit_message_version}" branch_name = "bump-#{cask.token}-#{branch_version.tr(",:", "-")}"
commit_message ||= "#{cask.token} #{commit_version}"
end
replacement_pairs = replace_version_and_checksum(cask, new_hash, new_version, replacement_pairs)
end
# Now that we have all replacement pairs, we will replace them further down
old_version_regex = old_version.latest? ? ":latest" : "[\"']#{Regexp.escape(old_version.to_s)}[\"']" old_contents = File.read(cask.sourcefile_path)
replacement_pairs << [
/version\s+#{old_version_regex}/m,
"version #{new_version.latest? ? ":latest" : "\"#{new_version}\""}",
]
if new_version.latest? || new_hash == :no_check
opoo "Ignoring specified `--sha256=` argument." if new_hash.is_a?(String)
replacement_pairs << [/"#{old_hash}"/, ":no_check"] if old_hash != :no_check
elsif old_hash == :no_check && new_hash != :no_check
replacement_pairs << [":no_check", "\"#{new_hash}\""] if new_hash.is_a?(String)
elsif old_hash != :no_check
if new_hash.nil? || cask.languages.present?
if new_hash && cask.languages.present?
opoo "Multiple checksum replacements required; ignoring specified `--sha256` argument."
end
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
replacement_pairs.uniq.compact,
read_only_run: true,
silent: true)
tmp_cask = Cask::CaskLoader.load(tmp_contents)
tmp_config = tmp_cask.config
OnSystem::ARCH_OPTIONS.each do |arch|
SimulateSystem.with arch: arch do
languages = cask.languages
languages = [nil] if languages.empty?
languages.each do |language|
new_hash_config = if language.blank?
tmp_config
else
tmp_config.merge(Cask::Config.new(explicit: { languages: [language] }))
end
new_hash_cask = Cask::CaskLoader.load(tmp_contents)
new_hash_cask.config = new_hash_config
old_hash = new_hash_cask.sha256.to_s
cask_download = Cask::Download.new(new_hash_cask, quarantine: true)
download = cask_download.fetch(verify_download_integrity: false)
Utils::Tar.validate_file(download)
replacement_pairs << [new_hash_cask.sha256.to_s, download.sha256]
end
end
end
elsif new_hash
opoo "Cask contains multiple hashes; only updating hash for current arch." if cask.on_system_blocks_exist?
replacement_pairs << [old_hash.to_s, new_hash]
end
end
end
if new_base_url if new_base_url
commit_message ||= "#{cask.token}: update URL" commit_message ||= "#{cask.token}: update URL"
@ -194,8 +149,10 @@ module Homebrew
commit_message ||= "#{cask.token}: update checksum" if new_hash commit_message ||= "#{cask.token}: update checksum" if new_hash
# Remove nested arrays where elements are identical
replacement_pairs = replacement_pairs.reject { |pair| pair[0] == pair[1] }.uniq.compact
Utils::Inreplace.inreplace_pairs(cask.sourcefile_path, Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
replacement_pairs.uniq.compact, replacement_pairs,
read_only_run: args.dry_run?, read_only_run: args.dry_run?,
silent: args.quiet?) silent: args.quiet?)
@ -203,25 +160,110 @@ module Homebrew
run_cask_style(cask, old_contents, args: args) run_cask_style(cask, old_contents, args: args)
pr_info = { pr_info = {
sourcefile_path: cask.sourcefile_path,
old_contents: old_contents,
branch_name: branch_name, branch_name: branch_name,
commit_message: commit_message, commit_message: commit_message,
tap: cask.tap, old_contents: old_contents,
pr_message: "Created with `brew bump-cask-pr`.", pr_message: "Created with `brew bump-cask-pr`.",
sourcefile_path: cask.sourcefile_path,
tap: cask.tap,
} }
GitHub.create_bump_pr(pr_info, args: args) GitHub.create_bump_pr(pr_info, args: args)
end end
def check_pull_requests(cask, state:, args:, version: nil) sig {
tap_remote_repo = cask.tap.full_name || cask.tap.remote_repo params(
GitHub.check_for_duplicate_pull_requests(cask.token, tap_remote_repo, cask: Cask::Cask,
state: state, new_hash: T.nilable(String),
version: version, new_version: VersionParser,
file: cask.sourcefile_path.relative_path_from(cask.tap.path).to_s, replacement_pairs: T::Array[[T.any(Regexp, String), T.any(Regexp, String)]],
args: args) ).returns(T::Array[[T.any(Regexp, String), T.any(Regexp, String)]])
}
def replace_version_and_checksum(cask, new_hash, new_version, replacement_pairs)
# When blocks are absent, arch is not relevant. For consistency, we simulate the arm architecture.
arch_options = cask.on_system_blocks_exist? ? OnSystem::ARCH_OPTIONS : [:arm]
arch_options.each do |arch|
SimulateSystem.with arch: arch do
old_cask = Cask::CaskLoader.load(cask.sourcefile_path)
old_version = old_cask.version
bump_version = new_version.send(arch) || new_version.general
old_version_regex = old_version.latest? ? ":latest" : %Q(["']#{Regexp.escape(old_version.to_s)}["'])
replacement_pairs << [/version\s+#{old_version_regex}/m,
"version #{bump_version.latest? ? ":latest" : %Q("#{bump_version}")}"]
# We are replacing our version here so we can get the new hash
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
replacement_pairs.uniq.compact,
read_only_run: true,
silent: true)
tmp_cask = Cask::CaskLoader.load(tmp_contents)
old_hash = tmp_cask.sha256
if tmp_cask.version.latest? || new_hash == :no_check
opoo "Ignoring specified `--sha256=` argument." if new_hash.is_a?(String)
replacement_pairs << [/"#{old_hash}"/, ":no_check"] if old_hash != :no_check
elsif old_hash == :no_check && new_hash != :no_check
replacement_pairs << [":no_check", "\"#{new_hash}\""] if new_hash.is_a?(String)
elsif old_hash != :no_check
if new_hash && cask.languages.present?
opoo "Multiple checksum replacements required; ignoring specified `--sha256` argument."
end
languages = if cask.languages.empty?
[nil]
else
cask.languages
end
languages.each do |language|
new_cask = Cask::CaskLoader.load(tmp_contents)
new_cask.config = if language.blank?
tmp_cask.config
else
tmp_cask.config.merge(Cask::Config.new(explicit: { languages: [language] }))
end
download = Cask::Download.new(new_cask, quarantine: true).fetch(verify_download_integrity: false)
Utils::Tar.validate_file(download)
if new_cask.sha256.to_s != download.sha256
replacement_pairs << [new_cask.sha256.to_s,
download.sha256]
end
end
elsif new_hash
opoo "Cask contains multiple hashes; only updating hash for current arch." if cask.on_system_blocks_exist?
replacement_pairs << [old_hash.to_s, new_hash]
end
end
end
replacement_pairs
end end
sig { params(cask: Cask::Cask, args: Homebrew::CLI::Args, new_version: VersionParser).void }
def check_pull_requests(cask, args:, new_version:)
tap_remote_repo = cask.tap.full_name || cask.tap.remote_repo
GitHub.check_for_duplicate_pull_requests(cask.token, tap_remote_repo,
state: "open",
version: nil,
file: cask.sourcefile_path.relative_path_from(cask.tap.path).to_s,
args: args)
# if we haven't already found open requests, try for an exact match across closed requests
new_version.instance_variables.each do |version_type|
version = new_version.instance_variable_get(version_type)
next if version.blank?
GitHub.check_for_duplicate_pull_requests(
cask.token,
tap_remote_repo,
state: "closed",
version: version,
file: cask.sourcefile_path.relative_path_from(cask.tap.path).to_s,
args: args,
)
end
end
sig { params(cask: Cask::Cask, old_contents: String, args: T.untyped).void }
def run_cask_audit(cask, old_contents, args:) def run_cask_audit(cask, old_contents, args:)
if args.dry_run? if args.dry_run?
if args.no_audit? if args.no_audit?
@ -249,6 +291,7 @@ module Homebrew
odie "`brew audit` failed!" odie "`brew audit` failed!"
end end
sig { params(cask: Cask::Cask, old_contents: String, args: T.untyped).void }
def run_cask_style(cask, old_contents, args:) def run_cask_style(cask, old_contents, args:)
if args.dry_run? if args.dry_run?
if args.no_style? if args.no_style?

View File

@ -0,0 +1,52 @@
# typed: strict
# frozen_string_literal: true
# Class handling architecture-specific version information.
#
# @api private
class VersionParser
sig { returns(T.nilable(T.any(Version, Cask::DSL::Version))) }
attr_reader :arm, :general, :intel
sig {
params(general: T.nilable(T.any(Version, String)),
arm: T.nilable(T.any(Version, String)),
intel: T.nilable(T.any(Version, String))).void
}
def initialize(general: nil, arm: nil, intel: nil)
@general = T.let(parse_version(general), T.nilable(T.any(Version, Cask::DSL::Version))) if general.present?
@arm = T.let(parse_version(arm), T.nilable(T.any(Version, Cask::DSL::Version))) if arm.present?
@intel = T.let(parse_version(intel), T.nilable(T.any(Version, Cask::DSL::Version))) if intel.present?
return if @general.present?
raise UsageError, "`--version` must not be empty." if arm.blank? && intel.blank?
raise UsageError, "`--version-arm` must not be empty." if arm.blank?
raise UsageError, "`--version-intel` must not be empty." if intel.blank?
end
sig {
params(version: T.any(Version, String))
.returns(T.nilable(T.any(Version, Cask::DSL::Version)))
}
def parse_version(version)
if version.is_a?(Version)
version
elsif version.is_a?(String)
parse_cask_version(version)
end
end
sig { params(version: String).returns(T.nilable(Cask::DSL::Version)) }
def parse_cask_version(version)
if version == "latest"
Cask::DSL::Version.new(:latest)
else
Cask::DSL::Version.new(version)
end
end
sig { returns(T::Boolean) }
def blank?
@general.blank? && @arm.blank? && @intel.blank?
end
end

View File

@ -1,7 +1,77 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/bump-cask-pr"
describe "brew bump-cask-pr" do describe "brew bump-cask-pr" do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
describe Homebrew::VersionParser do
let(:general_version) { "1.2.3" }
let(:intel_version) { "2.3.4" }
let(:arm_version) { "3.4.5" }
context "when initializing with no versions" do
it "raises a usage error" do
expect do
described_class.new
end.to raise_error(UsageError, "Invalid usage: `--version` must not be empty.")
end
end
context "when initializing with valid versions" do
let(:new_version) { described_class.new(general: general_version, arm: arm_version, intel: intel_version) }
it "correctly parses general version" do
expect(new_version.general).to eq(Cask::DSL::Version.new(general_version.to_s))
end
it "correctly parses arm version" do
expect(new_version.arm).to eq(Cask::DSL::Version.new(arm_version.to_s))
end
it "correctly parses intel version" do
expect(new_version.intel).to eq(Cask::DSL::Version.new(intel_version.to_s))
end
context "when only the intel version is provided" do
it "raises a UsageError" do
expect do
described_class.new(intel: intel_version)
end.to raise_error(UsageError,
"Invalid usage: `--version-arm` must not be empty.")
end
end
context "when only the arm version is provided" do
it "raises a UsageError" do
expect do
described_class.new(arm: arm_version)
end.to raise_error(UsageError,
"Invalid usage: `--version-intel` must not be empty.")
end
end
context "when the version is latest" do
it "returns a version object for latest" do
new_version = described_class.new(general: "latest")
expect(new_version.general.to_s).to eq("latest")
end
context "when the version is not latest" do
it "returns a version object for the given version" do
new_version = described_class.new(general: general_version)
expect(new_version.general.to_s).to eq(general_version)
end
end
end
context "when checking if VersionParser is blank" do
it "returns false if any version is present" do
new_version = described_class.new(general: general_version.to_s, arm: "", intel: "")
expect(new_version.blank?).to be(false)
end
end
end
end
end end

View File

@ -478,6 +478,8 @@ _brew_bump_cask_pr() {
--url --url
--verbose --verbose
--version --version
--version-arm
--version-intel
--write-only --write-only
" "
return return

View File

@ -412,6 +412,8 @@ __fish_brew_complete_arg 'bump-cask-pr' -l sha256 -d 'Specify the SHA-256 checks
__fish_brew_complete_arg 'bump-cask-pr' -l url -d 'Specify the URL for the new download' __fish_brew_complete_arg 'bump-cask-pr' -l url -d 'Specify the URL for the new download'
__fish_brew_complete_arg 'bump-cask-pr' -l verbose -d 'Make some output more verbose' __fish_brew_complete_arg 'bump-cask-pr' -l verbose -d 'Make some output more verbose'
__fish_brew_complete_arg 'bump-cask-pr' -l version -d 'Specify the new version for the cask' __fish_brew_complete_arg 'bump-cask-pr' -l version -d 'Specify the new version for the cask'
__fish_brew_complete_arg 'bump-cask-pr' -l version-arm -d 'Specify the new cask version for the ARM architecture'
__fish_brew_complete_arg 'bump-cask-pr' -l version-intel -d 'Specify the new cask version for the Intel architecture'
__fish_brew_complete_arg 'bump-cask-pr' -l write-only -d 'Make the expected file modifications without taking any Git actions' __fish_brew_complete_arg 'bump-cask-pr' -l write-only -d 'Make the expected file modifications without taking any Git actions'
__fish_brew_complete_arg 'bump-cask-pr' -a '(__fish_brew_suggest_casks_all)' __fish_brew_complete_arg 'bump-cask-pr' -a '(__fish_brew_suggest_casks_all)'

View File

@ -527,7 +527,9 @@ _brew_bump_cask_pr() {
'--sha256[Specify the SHA-256 checksum of the new download]' \ '--sha256[Specify the SHA-256 checksum of the new download]' \
'--url[Specify the URL for the new download]' \ '--url[Specify the URL for the new download]' \
'--verbose[Make some output more verbose]' \ '--verbose[Make some output more verbose]' \
'--version[Specify the new version for the cask]' \ '(--version-arm --version-intel)--version[Specify the new version for the cask]' \
'(--version)--version-arm[Specify the new cask version for the ARM architecture]' \
'(--version)--version-intel[Specify the new cask version for the Intel architecture]' \
'--write-only[Make the expected file modifications without taking any Git actions]' \ '--write-only[Make the expected file modifications without taking any Git actions]' \
- cask \ - cask \
'*::cask:__brew_casks' '*::cask:__brew_casks'

View File

@ -1048,6 +1048,10 @@ supplied by the user.
Don't try to fork the repository. Don't try to fork the repository.
* `--version`: * `--version`:
Specify the new *`version`* for the cask. Specify the new *`version`* for the cask.
* `--version-arm`:
Specify the new cask *`version`* for the ARM architecture.
* `--version-intel`:
Specify the new cask *`version`* for the Intel architecture.
* `--message`: * `--message`:
Prepend *`message`* to the default pull request message. Prepend *`message`* to the default pull request message.
* `--url`: * `--url`:

View File

@ -1500,6 +1500,14 @@ Don\'t try to fork the repository\.
Specify the new \fIversion\fR for the cask\. Specify the new \fIversion\fR for the cask\.
. .
.TP .TP
\fB\-\-version\-arm\fR
Specify the new cask \fIversion\fR for the ARM architecture\.
.
.TP
\fB\-\-version\-intel\fR
Specify the new cask \fIversion\fR for the Intel architecture\.
.
.TP
\fB\-\-message\fR \fB\-\-message\fR
Prepend \fImessage\fR to the default pull request message\. Prepend \fImessage\fR to the default pull request message\.
. .