diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index df9d6aae2f1150b71447694c97d8ca221403cd2d..f40c0bd411a1484b48f5e0b24d01c2416c5949d1 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -4,6 +4,7 @@ require "cask/denylist" require "cask/download" require "digest" +require "livecheck/livecheck" require "utils/curl" require "utils/git" require "utils/shared_audits" @@ -66,10 +67,11 @@ module Cask check_single_uninstall_zap check_untrusted_pkg check_hosting_with_appcast - check_latest_with_appcast + check_latest_with_appcast_or_livecheck check_latest_with_auto_updates check_stanza_requires_uninstall check_appcast_contains_version + check_livecheck_version check_gitlab_repository check_gitlab_repository_archived check_gitlab_prerelease_version @@ -274,11 +276,11 @@ module Cask add_error "cannot use the sha256 for an empty string: #{empty_sha256}" end - def check_latest_with_appcast + def check_latest_with_appcast_or_livecheck return unless cask.version.latest? - return unless cask.appcast - add_error "Casks with an appcast should not use version :latest" + add_error "Casks with an appcast should not use version :latest" if cask.appcast + add_error "Casks with a livecheck should not use version :latest" if cask.livecheckable? end def check_latest_with_auto_updates @@ -511,6 +513,18 @@ module Cask add_error "download not possible: #{e}" end + def check_livecheck_version + return unless appcast? + return unless cask.livecheckable? + return if cask.livecheck.skip? + return if cask.version.latest? + + latest_version = Homebrew::Livecheck.latest_version(cask)&.fetch(:latest) + return if cask.version.to_s == latest_version.to_s + + add_error "Version '#{cask.version}' differs from '#{latest_version}' retrieved by livecheck." + end + def check_appcast_contains_version return unless appcast? return if cask.appcast.to_s.empty? diff --git a/Library/Homebrew/cli/args.rbi b/Library/Homebrew/cli/args.rbi index ccf30d30ebff83ef2f16b062b22259a876e6b7f0..9d4f99b8b5ab03351133fb6c434e07c61f968efb 100644 --- a/Library/Homebrew/cli/args.rbi +++ b/Library/Homebrew/cli/args.rbi @@ -24,6 +24,15 @@ module Homebrew sig { returns(T.nilable(T::Boolean)) } def force_bottle?; end + sig { returns(T.nilable(T::Boolean)) } + def newer_only?; end + + sig { returns(T.nilable(T::Boolean)) } + def full_name?; end + + sig { returns(T.nilable(T::Boolean)) } + def json?; end + sig { returns(T.nilable(T::Boolean)) } def debug?; end diff --git a/Library/Homebrew/dev-cmd/livecheck.rb b/Library/Homebrew/dev-cmd/livecheck.rb index b2929339a1082384eefd8290a2b176b96732a05a..ae616463fc6438e9b3e72b51fa351bfc06b9f799 100644 --- a/Library/Homebrew/dev-cmd/livecheck.rb +++ b/Library/Homebrew/dev-cmd/livecheck.rb @@ -101,6 +101,15 @@ module Homebrew raise UsageError, "No formulae or casks to check." if formulae_and_casks_to_check.blank? - Livecheck.run_checks(formulae_and_casks_to_check, args) + options = { + json: args.json?, + full_name: args.full_name?, + newer_only: args.newer_only?, + quiet: args.quiet?, + debug: args.debug?, + verbose: args.verbose?, + }.compact + + Livecheck.run_checks(formulae_and_casks_to_check, **options) end end diff --git a/Library/Homebrew/livecheck/livecheck.rb b/Library/Homebrew/livecheck/livecheck.rb index 854ebae4fb91c084465ca33eab0dbf96417d559b..9025a1fb1ef65a12cc82226d483da9e25b495f58 100644 --- a/Library/Homebrew/livecheck/livecheck.rb +++ b/Library/Homebrew/livecheck/livecheck.rb @@ -12,6 +12,8 @@ module Homebrew # # @api private module Livecheck + extend T::Sig + module_function GITEA_INSTANCES = %w[ @@ -41,10 +43,25 @@ module Homebrew rc ].freeze + def livecheck_strategy_names + return @livecheck_strategy_names if defined?(@livecheck_strategy_names) + + # Cache demodulized strategy names, to avoid repeating this work + @livecheck_strategy_names = {} + Strategy.constants.sort.each do |strategy_symbol| + strategy = Strategy.const_get(strategy_symbol) + @livecheck_strategy_names[strategy] = strategy.name.demodulize + end + @livecheck_strategy_names.freeze + end + # Executes the livecheck logic for each formula/cask in the # `formulae_and_casks_to_check` array and prints the results. # @return [nil] - def run_checks(formulae_and_casks_to_check, args) + def run_checks( + formulae_and_casks_to_check, + full_name: false, json: false, newer_only: false, debug: false, quiet: false, verbose: false + ) # Identify any non-homebrew/core taps in use for current formulae non_core_taps = {} formulae_and_casks_to_check.each do |formula_or_cask| @@ -62,17 +79,9 @@ module Homebrew Dir["#{tap_strategy_path}/*.rb"].sort.each(&method(:require)) if Dir.exist?(tap_strategy_path) end - # Cache demodulized strategy names, to avoid repeating this work - @livecheck_strategy_names = {} - Strategy.constants.sort.each do |strategy_symbol| - strategy = Strategy.const_get(strategy_symbol) - @livecheck_strategy_names[strategy] = strategy.name.demodulize - end - @livecheck_strategy_names.freeze - has_a_newer_upstream_version = false - if args.json? && !args.quiet? && $stderr.tty? + if json && !quiet && $stderr.tty? formulae_and_casks_total = if formulae_and_casks_to_check == Formula formulae_and_casks_to_check.count else @@ -96,7 +105,7 @@ module Homebrew formula = formula_or_cask if formula_or_cask.is_a?(Formula) cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask) - if args.debug? && i.positive? + if debug && i.positive? puts <<~EOS ---------- @@ -104,7 +113,7 @@ module Homebrew EOS end - skip_result = skip_conditions(formula_or_cask, args: args) + skip_result = skip_conditions(formula_or_cask, json: json, full_name: full_name, quiet: quiet) next skip_result if skip_result != false formula&.head&.downloader&.shutup! @@ -126,17 +135,20 @@ module Homebrew latest = if formula&.head_only? formula.head.downloader.fetch_last_commit else - version_info = latest_version(formula_or_cask, args: args) + version_info = latest_version( + formula_or_cask, + json: json, full_name: full_name, verbose: verbose, debug: debug, + ) version_info[:latest] if version_info.present? end if latest.blank? no_versions_msg = "Unable to get versions" - raise TypeError, no_versions_msg unless args.json? + raise TypeError, no_versions_msg unless json next version_info if version_info.is_a?(Hash) && version_info[:status] && version_info[:messages] - next status_hash(formula_or_cask, "error", [no_versions_msg], args: args) + next status_hash(formula_or_cask, "error", [no_versions_msg], full_name: full_name, verbose: verbose) end if (m = latest.to_s.match(/(.*)-release$/)) && !current.to_s.match(/.*-release$/) @@ -154,8 +166,8 @@ module Homebrew is_newer_than_upstream = (formula&.stable? || cask) && (current > latest) info = {} - info[:formula] = formula_name(formula, args: args) if formula - info[:cask] = cask_name(cask, args: args) if cask + info[:formula] = formula_name(formula, full_name: full_name) if formula + info[:cask] = cask_name(cask, full_name: full_name) if cask info[:version] = { current: current.to_s, latest: latest.to_s, @@ -168,35 +180,33 @@ module Homebrew info[:meta][:head_only] = true if formula&.head_only? info[:meta].merge!(version_info[:meta]) if version_info.present? && version_info.key?(:meta) - next if args.newer_only? && !info[:version][:outdated] + next if newer_only && !info[:version][:outdated] has_a_newer_upstream_version ||= true - if args.json? + if json progress&.increment - info.except!(:meta) unless args.verbose? + info.except!(:meta) unless verbose next info end - print_latest_version(info, args: args) + print_latest_version(info, verbose: verbose) nil rescue => e Homebrew.failed = true - if args.json? + if json progress&.increment - status_hash(formula_or_cask, "error", [e.to_s], args: args) - elsif !args.quiet? - onoe "#{Tty.blue}#{formula_or_cask_name(formula_or_cask, args: args)}#{Tty.reset}: #{e}" + status_hash(formula_or_cask, "error", [e.to_s], full_name: full_name, verbose: verbose) + elsif !quiet + onoe "#{Tty.blue}#{formula_or_cask_name(formula_or_cask, full_name: full_name)}#{Tty.reset}: #{e}" nil end end - if args.newer_only? && !has_a_newer_upstream_version && !args.debug? && !args.json? - puts "No newer upstream versions." - end + puts "No newer upstream versions." if newer_only && !has_a_newer_upstream_version && !debug && !json - return unless args.json? + return unless json if progress progress.finish @@ -208,45 +218,47 @@ module Homebrew puts JSON.generate(formulae_checked.compact) end - def formula_or_cask_name(formula_or_cask, args:) + sig { params(formula_or_cask: T.any(Formula, Cask::Cask), full_name: T::Boolean).returns(String) } + def formula_or_cask_name(formula_or_cask, full_name: false) case formula_or_cask when Formula - formula_name(formula_or_cask, args: args) + formula_name(formula_or_cask, full_name: full_name) when Cask::Cask - cask_name(formula_or_cask, args: args) + cask_name(formula_or_cask, full_name: full_name) end end - def cask_name(cask, args:) - args.full_name? ? cask.full_name : cask.token + # Returns the fully-qualified name of a cask if the `full_name` argument is + # provided; returns the name otherwise. + sig { params(cask: Cask::Cask, full_name: T::Boolean).returns(String) } + def cask_name(cask, full_name: false) + full_name ? cask.full_name : cask.token end # Returns the fully-qualified name of a formula if the `full_name` argument is # provided; returns the name otherwise. - # @return [String] - def formula_name(formula, args:) - args.full_name? ? formula.full_name : formula.name + sig { params(formula: Formula, full_name: T::Boolean).returns(String) } + def formula_name(formula, full_name: false) + full_name ? formula.full_name : formula.name end - def status_hash(formula_or_cask, status_str, messages = nil, args:) + def status_hash(formula_or_cask, status_str, messages = nil, full_name: false, verbose: false) formula = formula_or_cask if formula_or_cask.is_a?(Formula) cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask) status_hash = {} if formula - status_hash[:formula] = formula_name(formula, args: args) + status_hash[:formula] = formula_name(formula, full_name: full_name) elsif cask - status_hash[:cask] = cask_name(formula_or_cask, args: args) + status_hash[:cask] = cask_name(formula_or_cask, full_name: full_name) end status_hash[:status] = status_str status_hash[:messages] = messages if messages.is_a?(Array) - if args.verbose? - status_hash[:meta] = { - livecheckable: formula_or_cask.livecheckable?, - } - status_hash[:meta][:head_only] = true if formula&.head_only? - end + status_hash[:meta] = { + livecheckable: formula_or_cask.livecheckable?, + } + status_hash[:meta][:head_only] = true if formula&.head_only? status_hash end @@ -254,54 +266,55 @@ module Homebrew # If a formula has to be skipped, it prints or returns a Hash contaning the reason # for doing so; returns false otherwise. # @return [Hash, nil, Boolean] - def skip_conditions(formula_or_cask, args:) + def skip_conditions(formula_or_cask, json: false, full_name: false, quiet: false, verbose: false) formula = formula_or_cask if formula_or_cask.is_a?(Formula) if formula&.deprecated? && !formula.livecheckable? - return status_hash(formula, "deprecated", args: args) if args.json? + return status_hash(formula, "deprecated", full_name: full_name, verbose: verbose) if json - puts "#{Tty.red}#{formula_name(formula, args: args)}#{Tty.reset} : deprecated" unless args.quiet? + puts "#{Tty.red}#{formula_name(formula, full_name: full_name)}#{Tty.reset} : deprecated" unless quiet return end if formula&.disabled? && !formula.livecheckable? - return status_hash(formula, "disabled", args: args) if args.json? + return status_hash(formula, "disabled", full_name: full_name, verbose: verbose) if json - puts "#{Tty.red}#{formula_name(formula, args: args)}#{Tty.reset} : disabled" unless args.quiet? + puts "#{Tty.red}#{formula_name(formula, full_name: full_name)}#{Tty.reset} : disabled" unless quiet return end if formula&.versioned_formula? && !formula.livecheckable? - return status_hash(formula, "versioned", args: args) if args.json? + return status_hash(formula, "versioned", full_name: full_name, verbose: verbose) if json - puts "#{Tty.red}#{formula_name(formula, args: args)}#{Tty.reset} : versioned" unless args.quiet? + puts "#{Tty.red}#{formula_name(formula, full_name: full_name)}#{Tty.reset} : versioned" unless quiet return end if formula&.head_only? && !formula.any_version_installed? head_only_msg = "HEAD only formula must be installed to be livecheckable" - return status_hash(formula, "error", [head_only_msg], args: args) if args.json? + return status_hash(formula, "error", [head_only_msg], full_name: full_name, verbose: verbose) if json - puts "#{Tty.red}#{formula_name(formula, args: args)}#{Tty.reset} : #{head_only_msg}" unless args.quiet? + puts "#{Tty.red}#{formula_name(formula, full_name: full_name)}#{Tty.reset} : #{head_only_msg}" unless quiet return end is_gist = formula&.stable&.url&.include?("gist.github.com") if formula_or_cask.livecheck.skip? || is_gist - skip_msg = if formula_or_cask.livecheck.skip_msg.is_a?(String) && - formula_or_cask.livecheck.skip_msg.present? - formula_or_cask.livecheck.skip_msg.to_s + skip_message = if formula_or_cask.livecheck.skip_msg.is_a?(String) && + formula_or_cask.livecheck.skip_msg.present? + formula_or_cask.livecheck.skip_msg.to_s.presence elsif is_gist "Stable URL is a GitHub Gist" - else - "" end - return status_hash(formula_or_cask, "skipped", (skip_msg.blank? ? nil : [skip_msg]), args: args) if args.json? + if json + skip_messages = skip_message ? [skip_message] : nil + return status_hash(formula_or_cask, "skipped", skip_messages, full_name: full_name, verbose: verbose) + end - unless args.quiet? - puts "#{Tty.red}#{formula_or_cask_name(formula_or_cask, args: args)}#{Tty.reset} : skipped" \ - "#{" - #{skip_msg}" if skip_msg.present?}" + unless quiet + puts "#{Tty.red}#{formula_or_cask_name(formula_or_cask, full_name: full_name)}#{Tty.reset} : skipped" \ + "#{" - #{skip_message}" if skip_message}" end return end @@ -311,9 +324,9 @@ module Homebrew # Formats and prints the livecheck result for a formula. # @return [nil] - def print_latest_version(info, args:) + def print_latest_version(info, verbose:) formula_or_cask_s = "#{Tty.blue}#{info[:formula] || info[:cask]}#{Tty.reset}" - formula_or_cask_s += " (guessed)" if !info[:meta][:livecheckable] && args.verbose? + formula_or_cask_s += " (guessed)" if !info[:meta][:livecheckable] && verbose current_s = if info[:version][:newer_than_upstream] "#{Tty.red}#{info[:version][:current]}#{Tty.reset}" @@ -393,7 +406,7 @@ module Homebrew # Identifies the latest version of the formula and returns a Hash containing # the version information. Returns nil if a latest version couldn't be found. # @return [Hash, nil] - def latest_version(formula_or_cask, args:) + def latest_version(formula_or_cask, json: false, full_name: false, verbose: false, debug: false) formula = formula_or_cask if formula_or_cask.is_a?(Formula) cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask) @@ -406,19 +419,19 @@ module Homebrew urls = [livecheck_url] if livecheck_url.present? urls ||= checkable_urls(formula_or_cask) - if args.debug? + if debug puts if formula - puts "Formula: #{formula_name(formula, args: args)}" + puts "Formula: #{formula_name(formula, full_name: full_name)}" puts "Head only?: true" if formula.head_only? elsif cask - puts "Cask: #{cask_name(formula_or_cask, args: args)}" + puts "Cask: #{cask_name(formula_or_cask, full_name: full_name)}" end puts "Livecheckable?: #{has_livecheckable ? "Yes" : "No"}" end urls.each_with_index do |original_url, i| - if args.debug? + if debug puts puts "URL: #{original_url}" end @@ -443,12 +456,12 @@ module Homebrew ) strategy = Strategy.from_symbol(livecheck_strategy) strategy ||= strategies.first - strategy_name = @livecheck_strategy_names[strategy] + strategy_name = livecheck_strategy_names[strategy] - if args.debug? + if debug puts "URL (processed): #{url}" if url != original_url - if strategies.present? && args.verbose? - puts "Strategies: #{strategies.map { |s| @livecheck_strategy_names[s] }.join(", ")}" + if strategies.present? && verbose + puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}" end puts "Strategy: #{strategy.blank? ? "None" : strategy_name}" puts "Regex: #{livecheck_regex.inspect}" if livecheck_regex.present? @@ -471,13 +484,13 @@ module Homebrew regex = strategy_data[:regex] if strategy_data[:messages].is_a?(Array) && match_version_map.blank? - puts strategy_data[:messages] unless args.json? + puts strategy_data[:messages] unless json next if i + 1 < urls.length - return status_hash(formula, "error", strategy_data[:messages], args: args) + return status_hash(formula, "error", strategy_data[:messages], full_name: full_name, verbose: verbose) end - if args.debug? + if debug puts "URL (strategy): #{strategy_data[:url]}" if strategy_data[:url] != url puts "Regex (strategy): #{strategy_data[:regex].inspect}" if strategy_data[:regex] != livecheck_regex end @@ -491,11 +504,11 @@ module Homebrew end end - if args.debug? && match_version_map.present? + if debug && match_version_map.present? puts puts "Matched Versions:" - if args.verbose? + if verbose match_version_map.each do |match, version| puts "#{match} => #{version.inspect}" end @@ -510,7 +523,7 @@ module Homebrew latest: Version.new(match_version_map.values.max), } - if args.json? && args.verbose? + if json && verbose version_info[:meta] = { url: { original: original_url, @@ -519,9 +532,7 @@ module Homebrew } version_info[:meta][:url][:processed] = url if url != original_url version_info[:meta][:url][:strategy] = strategy_data[:url] if strategy_data[:url] != url - if strategies.present? - version_info[:meta][:strategies] = strategies.map { |s| @livecheck_strategy_names[s] } - end + version_info[:meta][:strategies] = strategies.map { |s| livecheck_strategy_names[s] } if strategies.present? version_info[:meta][:regex] = regex.inspect if regex.present? end diff --git a/Library/Homebrew/test/livecheck/livecheck_spec.rb b/Library/Homebrew/test/livecheck/livecheck_spec.rb index 635b829196e45a319d092c17ad9341459f55c8d4..f742c9843a4d865272d94a3f71c53227a96cc07d 100644 --- a/Library/Homebrew/test/livecheck/livecheck_spec.rb +++ b/Library/Homebrew/test/livecheck/livecheck_spec.rb @@ -91,35 +91,29 @@ describe Homebrew::Livecheck do RUBY end - let(:args) { double("livecheck_args", full_name?: false, json?: false, quiet?: false, verbose?: true) } - describe "::formula_name" do it "returns the name of the formula" do - expect(livecheck.formula_name(f, args: args)).to eq("test") + expect(livecheck.formula_name(f)).to eq("test") end it "returns the full name" do - allow(args).to receive(:full_name?).and_return(true) - - expect(livecheck.formula_name(f, args: args)).to eq("test") + expect(livecheck.formula_name(f, full_name: true)).to eq("test") end end describe "::cask_name" do it "returns the token of the cask" do - expect(livecheck.cask_name(c, args: args)).to eq("test") + expect(livecheck.cask_name(c)).to eq("test") end it "returns the full name of the cask" do - allow(args).to receive(:full_name?).and_return(true) - - expect(livecheck.cask_name(c, args: args)).to eq("test") + expect(livecheck.cask_name(c, full_name: true)).to eq("test") end end describe "::status_hash" do it "returns a hash containing the livecheck status" do - expect(livecheck.status_hash(f, "error", ["Unable to get versions"], args: args)) + expect(livecheck.status_hash(f, "error", ["Unable to get versions"])) .to eq({ formula: "test", status: "error", @@ -133,47 +127,47 @@ describe Homebrew::Livecheck do describe "::skip_conditions" do it "skips a deprecated formula without a livecheckable" do - expect { livecheck.skip_conditions(f_deprecated, args: args) } + expect { livecheck.skip_conditions(f_deprecated) } .to output("test_deprecated : deprecated\n").to_stdout .and not_to_output.to_stderr end it "skips a disabled formula without a livecheckable" do - expect { livecheck.skip_conditions(f_disabled, args: args) } + expect { livecheck.skip_conditions(f_disabled) } .to output("test_disabled : disabled\n").to_stdout .and not_to_output.to_stderr end it "skips a versioned formula without a livecheckable" do - expect { livecheck.skip_conditions(f_versioned, args: args) } + expect { livecheck.skip_conditions(f_versioned) } .to output("test@0.0.1 : versioned\n").to_stdout .and not_to_output.to_stderr end it "skips a HEAD-only formula if not installed" do - expect { livecheck.skip_conditions(f_head_only, args: args) } + expect { livecheck.skip_conditions(f_head_only) } .to output("test_head_only : HEAD only formula must be installed to be livecheckable\n").to_stdout .and not_to_output.to_stderr end it "skips a formula with a GitHub Gist stable URL" do - expect { livecheck.skip_conditions(f_gist, args: args) } + expect { livecheck.skip_conditions(f_gist) } .to output("test_gist : skipped - Stable URL is a GitHub Gist\n").to_stdout .and not_to_output.to_stderr end it "skips a formula with a skip livecheckable" do - expect { livecheck.skip_conditions(f_skip, args: args) } + expect { livecheck.skip_conditions(f_skip) } .to output("test_skip : skipped - Not maintained\n").to_stdout .and not_to_output.to_stderr end it "returns false for a non-skippable formula" do - expect(livecheck.skip_conditions(f, args: args)).to eq(false) + expect(livecheck.skip_conditions(f)).to eq(false) end it "returns false for a non-skippable cask" do - expect(livecheck.skip_conditions(c, args: args)).to eq(false) + expect(livecheck.skip_conditions(c)).to eq(false) end end