brew/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb
Sam Ford 01cb74e525
livecheck: clarify livecheckable language
Formulae, casks, and resources have a `#livecheckable?` method that
indicates whether they contain a `livecheck` block. This is intended
to be read as "has a livecheckable?", not "is livecheckable?" (as
livecheck can find versions for some packages/resources without a
`livecheck` block). Unfortunately, correct understanding of this
method's behavior [outside of documentation] relies on historical
knowledge that few people possess, so this is often confusing to
anyone who hasn't been working on livecheck since 2020.

In the olden days, a "livecheckable" was a Ruby file containing a
`livecheck` block (originally a hash) with a filename that
corresponded to a related formula. The `livecheck` blocks in
livecheckable files were integrated into their respective formulae in
August 2020, so [first-party] livecheckables ceased to exist at that
time. From that point forward, we simply referred to these as
`livecheck` blocks.

With that in mind, this clarifies the situation by replacing
"livecheckable" language. This includes renaming `#livecheckable?` to
`#livecheck_defined?`, replacing usage of "livecheckable" as a noun
with "`livecheck` block", replacing "livecheckable" as a boolean with
"livecheck_defined", and replacing incorrect usage of "livecheckable"
as an adjective with "checkable".
2024-12-02 10:13:03 -05:00

182 lines
5.3 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "timeout"
require "cask/download"
require "cask/installer"
require "cask/cask_loader"
require "system_command"
require "tap"
require "unversioned_cask_checker"
module Homebrew
module DevCmd
class BumpUnversionedCasks < AbstractCommand
include SystemCommand::Mixin
cmd_args do
description <<~EOS
Check all casks with unversioned URLs in a given <tap> for updates.
EOS
switch "-n", "--dry-run",
description: "Do everything except caching state and opening pull requests."
flag "--limit=",
description: "Maximum runtime in minutes."
flag "--state-file=",
description: "File for caching state."
named_args [:cask, :tap], min: 1, without_api: true
end
sig { override.void }
def run
Homebrew.install_bundler_gems!(groups: ["bump_unversioned_casks"])
state_file = if args.state_file.present?
Pathname(T.must(args.state_file)).expand_path
else
HOMEBREW_CACHE/"bump_unversioned_casks.json"
end
state_file.dirname.mkpath
state = state_file.exist? ? JSON.parse(state_file.read) : {}
casks = args.named.to_paths(only: :cask, recurse_tap: true).map { |path| Cask::CaskLoader.load(path) }
unversioned_casks = casks.select do |cask|
cask.url&.unversioned? && !cask.livecheck_defined?
end
ohai "Unversioned Casks: #{unversioned_casks.count} (#{state.size} cached)"
checked, unchecked = unversioned_casks.partition { |c| state.key?(c.full_name) }
queue = Queue.new
# Start with random casks which have not been checked.
unchecked.shuffle.each do |c|
queue.enq c
end
# Continue with previously checked casks, ordered by when they were last checked.
checked.sort_by { |c| state.dig(c.full_name, "check_time") }.each do |c|
queue.enq c
end
limit = args.limit.presence&.to_i
end_time = Time.now + (limit * 60) if limit
until queue.empty? || (end_time && end_time < Time.now)
cask = queue.deq
key = cask.full_name
new_state = bump_unversioned_cask(cask, state: state.fetch(key, {}))
next unless new_state
state[key] = new_state
state_file.atomic_write JSON.pretty_generate(state) unless args.dry_run?
end
end
private
sig {
params(cask: Cask::Cask, state: T::Hash[String, T.untyped])
.returns(T.nilable(T::Hash[String, T.untyped]))
}
def bump_unversioned_cask(cask, state:)
ohai "Checking #{cask.full_name}"
unversioned_cask_checker = UnversionedCaskChecker.new(cask)
if !unversioned_cask_checker.single_app_cask? &&
!unversioned_cask_checker.single_pkg_cask? &&
!unversioned_cask_checker.single_qlplugin_cask?
opoo "Skipping, not a single-app or PKG cask."
return
end
last_check_time = state["check_time"]&.then { |t| Time.parse(t) }
check_time = Time.now
if last_check_time && (check_time - last_check_time) / 3600 < 24
opoo "Skipping, already checked within the last 24 hours."
return
end
last_sha256 = state["sha256"]
last_time = state["time"]&.then { |t| Time.parse(t) }
last_file_size = state["file_size"]
download = Cask::Download.new(cask)
time, file_size = begin
download.time_file_size
rescue
[nil, nil]
end
if last_time != time || last_file_size != file_size
sha256 = begin
Timeout.timeout(5 * 60) do
unversioned_cask_checker.installer.download.sha256
end
rescue => e
onoe e
nil
end
if sha256.present? && last_sha256 != sha256
version = begin
Timeout.timeout(60) do
unversioned_cask_checker.guess_cask_version
end
rescue Timeout::Error
onoe "Timed out guessing version for cask '#{cask}'."
nil
end
if version
if cask.version == version
oh1 "Cask #{cask} is up-to-date at #{version}"
else
bump_cask_pr_args = [
"bump-cask-pr",
"--version", version.to_s,
"--sha256", ":no_check",
"--message", "Automatic update via `brew bump-unversioned-casks`.",
cask.sourcefile_path
]
if args.dry_run?
bump_cask_pr_args << "--dry-run"
oh1 "Would bump #{cask} from #{cask.version} to #{version}"
else
oh1 "Bumping #{cask} from #{cask.version} to #{version}"
end
begin
system_command! HOMEBREW_BREW_FILE, args: bump_cask_pr_args
rescue ErrorDuringExecution => e
onoe e
end
end
end
end
end
{
"sha256" => sha256,
"check_time" => check_time.iso8601,
"time" => time&.iso8601,
"file_size" => file_size,
}
end
end
end
end