diff --git a/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb b/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb index f734db5840..15faafce13 100644 --- a/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb +++ b/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "timeout" @@ -11,167 +11,168 @@ require "tap" require "unversioned_cask_checker" module Homebrew - extend SystemCommand::Mixin + module DevCmd + class BumpUnversionedCask < AbstractCommand + include SystemCommand::Mixin - sig { returns(CLI::Parser) } - def self.bump_unversioned_casks_args - Homebrew::CLI::Parser.new do - description <<~EOS - Check all casks with unversioned URLs in a given 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." + cmd_args do + description <<~EOS + Check all casks with unversioned URLs in a given 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 - end - - sig { void } - def self.bump_unversioned_casks - args = bump_unversioned_casks_args.parse - - Homebrew.install_bundler_gems!(groups: ["bump_unversioned_casks"]) - - state_file = if args.state_file.present? - Pathname(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.livecheckable? && !cask.discontinued? - 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, {}), dry_run: args.dry_run?) - - next unless new_state - - state[key] = new_state - - state_file.atomic_write JSON.pretty_generate(state) unless args.dry_run? - end - end - - sig { - params(cask: Cask::Cask, state: T::Hash[String, T.untyped], dry_run: T.nilable(T::Boolean)) - .returns(T.nilable(T::Hash[String, T.untyped])) - } - def self.bump_unversioned_cask(cask, state:, dry_run:) - 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 + named_args [:cask, :tap], min: 1, without_api: true 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}'." + 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.livecheckable? && !cask.discontinued? 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 - ] + ohai "Unversioned Casks: #{unversioned_casks.count} (#{state.size} cached)" - if 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}" + 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 + 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}'." end - begin - system_command! HOMEBREW_BREW_FILE, args: bump_cask_pr_args - rescue ErrorDuringExecution => e - onoe e + 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 - - { - "sha256" => sha256, - "check_time" => check_time.iso8601, - "time" => time&.iso8601, - "file_size" => file_size, - } end end diff --git a/Library/Homebrew/test/dev-cmd/bump-unversioned-casks_spec.rb b/Library/Homebrew/test/dev-cmd/bump-unversioned-casks_spec.rb index 578deae573..a7ab01f2c2 100644 --- a/Library/Homebrew/test/dev-cmd/bump-unversioned-casks_spec.rb +++ b/Library/Homebrew/test/dev-cmd/bump-unversioned-casks_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true require "cmd/shared_examples/args_parse" +require "dev-cmd/bump-unversioned-casks" -RSpec.describe "brew bump-unversioned-casks" do - it_behaves_like "parseable arguments" +RSpec.describe Homebrew::DevCmd::BumpUnversionedCask do + it_behaves_like "parseable arguments", argv: ["foo"] end