brew/Library/Homebrew/dev-cmd/bump-cask-pr.rb
apainintheneck c033a4673c dev-cmd/bump-cask-pr: replace url before downloading files
Previously it was possible for both the version and url to be changed.
This would cause the old url to be used to download the cask and check
the checksum since the version and checksum were replaced in the cask
first and then the url would be replaced. This meant that the resulting
checksum was wrong if a url was provided and might be completely
invalid if it was interpolated.
2023-10-23 19:22:46 -07:00

313 lines
12 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "bump_version_parser"
require "cask"
require "cask/download"
require "cli/parser"
require "utils/tar"
module Homebrew
module_function
sig { returns(CLI::Parser) }
def bump_cask_pr_args
Homebrew::CLI::Parser.new do
description <<~EOS
Create a pull request to update <cask> with a new version.
A best effort to determine the <SHA-256> will be made if the value is not
supplied by the user.
EOS
switch "-n", "--dry-run",
description: "Print what would be done rather than doing it."
switch "--write-only",
description: "Make the expected file modifications without taking any Git actions."
switch "--commit",
depends_on: "--write-only",
description: "When passed with `--write-only`, generate a new commit after writing changes " \
"to the cask file."
switch "--no-audit",
description: "Don't run `brew audit` before opening the PR."
switch "--online",
hidden: true
switch "--no-style",
description: "Don't run `brew style --fix` before opening the PR."
switch "--no-browse",
description: "Print the pull request URL instead of opening in a browser."
switch "--no-fork",
description: "Don't try to fork the repository."
flag "--version=",
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=",
description: "Prepend <message> to the default pull request message."
flag "--url=",
description: "Specify the <URL> for the new download."
flag "--sha256=",
description: "Specify the <SHA-256> checksum of the new download."
flag "--fork-org=",
description: "Use the specified GitHub organization for forking."
switch "-f", "--force",
description: "Ignore duplicate open PRs."
conflicts "--dry-run", "--write"
conflicts "--no-audit", "--online"
conflicts "--version=", "--version-arm="
conflicts "--version=", "--version-intel="
named_args :cask, number: 1, without_api: true
end
end
sig { void }
def bump_cask_pr
args = bump_cask_pr_args.parse
# odeprecated "brew bump-cask-pr --online" if args.online?
# This will be run by `brew audit` or `brew style` later so run it first to
# not start spamming during normal output.
Homebrew.install_bundler_gems!(groups: ["style"]) if !args.no_audit? || !args.no_style?
# As this command is simplifying user-run commands then let's just use a
# user path, too.
ENV["PATH"] = PATH.new(ORIGINAL_PATHS).to_s
# Use the user's browser, too.
ENV["BROWSER"] = EnvConfig.browser
cask = args.named.to_casks.first
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?
new_version = BumpVersionParser.new(
general: args.version,
intel: args.version_intel,
arm: args.version_arm,
)
new_hash = unless (new_hash = args.sha256).nil?
raise UsageError, "`--sha256` must not be empty." if new_hash.blank?
["no_check", ":no_check"].include?(new_hash) ? :no_check : new_hash
end
new_base_url = unless (new_base_url = args.url).nil?
raise UsageError, "`--url` must not be empty." if new_base_url.blank?
begin
URI(new_base_url)
rescue URI::InvalidURIError
raise UsageError, "`--url` is not valid."
end
end
if new_version.blank? && new_base_url.nil? && new_hash.nil?
raise UsageError, "No `--version`, `--url` or `--sha256` argument specified!"
end
check_pull_requests(cask, args: args, new_version: new_version)
replacement_pairs ||= []
branch_name = "bump-#{cask.token}"
commit_message = nil
old_contents = File.read(cask.sourcefile_path)
if new_base_url
commit_message ||= "#{cask.token}: update URL"
m = /^ +url "(.+?)"\n/m.match(old_contents)
odie "Could not find old URL in cask!" if m.nil?
old_base_url = m.captures.fetch(0)
replacement_pairs << [
/#{Regexp.escape(old_base_url)}/,
new_base_url.to_s,
]
end
if new_version.present?
# For simplicity, our naming defers to the arm version if we multiple architectures are specified
branch_version = new_version.arm || new_version.general
if branch_version.is_a?(Cask::DSL::Version)
commit_version = if branch_version.before_comma == cask.version.before_comma
branch_version
else
branch_version.before_comma
end
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
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,
replacement_pairs,
read_only_run: args.dry_run?,
silent: args.quiet?)
run_cask_audit(cask, old_contents, args: args)
run_cask_style(cask, old_contents, args: args)
pr_info = {
branch_name: branch_name,
commit_message: commit_message,
old_contents: old_contents,
pr_message: "Created with `brew bump-cask-pr`.",
sourcefile_path: cask.sourcefile_path,
tap: cask.tap,
}
GitHub.create_bump_pr(pr_info, args: args)
end
sig {
params(
cask: Cask::Cask,
new_hash: T.nilable(String),
new_version: BumpVersionParser,
replacement_pairs: T::Array[[T.any(Regexp, String), T.any(Regexp, String)]],
).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
sig { params(cask: Cask::Cask, args: CLI::Args, new_version: BumpVersionParser).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:)
if args.dry_run?
if args.no_audit?
ohai "Skipping `brew audit`"
else
ohai "brew audit --cask --online #{cask.full_name}"
end
return
end
failed_audit = false
if args.no_audit?
ohai "Skipping `brew audit`"
else
system HOMEBREW_BREW_FILE, "audit", "--cask", "--online", cask.full_name
failed_audit = !$CHILD_STATUS.success?
end
return unless failed_audit
cask.sourcefile_path.atomic_write(old_contents)
odie "`brew audit` failed!"
end
sig { params(cask: Cask::Cask, old_contents: String, args: T.untyped).void }
def run_cask_style(cask, old_contents, args:)
if args.dry_run?
if args.no_style?
ohai "Skipping `brew style --fix`"
else
ohai "brew style --fix #{cask.sourcefile_path.basename}"
end
return
end
failed_style = false
if args.no_style?
ohai "Skipping `brew style --fix`"
else
system HOMEBREW_BREW_FILE, "style", "--fix", cask.sourcefile_path
failed_style = !$CHILD_STATUS.success?
end
return unless failed_style
cask.sourcefile_path.atomic_write(old_contents)
odie "`brew style --fix` failed!"
end
end