Merge pull request #13613 from mohammadzainabbas/mohammad

Augment `brew livecheck` with a `--resources` option to check resources
This commit is contained in:
Nanda H Krishna 2022-10-02 13:31:17 -04:00 committed by GitHub
commit 639e8eb237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 441 additions and 114 deletions

View File

@ -30,6 +30,9 @@ module Homebrew
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def newer_only?; end def newer_only?; end
sig { returns(T::Boolean) }
def resources?; end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def full_name?; end def full_name?; end

View File

@ -72,7 +72,8 @@ module Homebrew
ambiguous_casks = [] ambiguous_casks = []
if !args.formula? && !args.cask? if !args.formula? && !args.cask?
ambiguous_casks = formulae_and_casks.group_by { |item| Livecheck.formula_or_cask_name(item, full_name: true) } ambiguous_casks = formulae_and_casks \
.group_by { |item| Livecheck.package_or_resource_name(item, full_name: true) }
.values .values
.select { |items| items.length > 1 } .select { |items| items.length > 1 }
.flatten .flatten
@ -82,7 +83,7 @@ module Homebrew
ambiguous_names = [] ambiguous_names = []
unless args.full_name? unless args.full_name?
ambiguous_names = ambiguous_names =
(formulae_and_casks - ambiguous_casks).group_by { |item| Livecheck.formula_or_cask_name(item) } (formulae_and_casks - ambiguous_casks).group_by { |item| Livecheck.package_or_resource_name(item) }
.values .values
.select { |items| items.length > 1 } .select { |items| items.length > 1 }
.flatten .flatten
@ -92,7 +93,7 @@ module Homebrew
puts if i.positive? puts if i.positive?
use_full_name = args.full_name? || ambiguous_names.include?(formula_or_cask) use_full_name = args.full_name? || ambiguous_names.include?(formula_or_cask)
name = Livecheck.formula_or_cask_name(formula_or_cask, full_name: use_full_name) name = Livecheck.package_or_resource_name(formula_or_cask, full_name: use_full_name)
repository = if formula_or_cask.is_a?(Formula) repository = if formula_or_cask.is_a?(Formula)
if formula_or_cask.head_only? if formula_or_cask.head_only?
ohai name ohai name
@ -157,7 +158,7 @@ module Homebrew
rescue rescue
next next
end end
name = Livecheck.formula_or_cask_name(formula_or_cask) name = Livecheck.package_or_resource_name(formula_or_cask)
ambiguous_cask = begin ambiguous_cask = begin
formula_or_cask.is_a?(Cask::Cask) && !args.cask? && Formula[name] formula_or_cask.is_a?(Cask::Cask) && !args.cask? && Formula[name]
rescue FormulaUnavailableError rescue FormulaUnavailableError
@ -178,7 +179,7 @@ module Homebrew
end end
def livecheck_result(formula_or_cask) def livecheck_result(formula_or_cask)
name = Livecheck.formula_or_cask_name(formula_or_cask) name = Livecheck.package_or_resource_name(formula_or_cask)
referenced_formula_or_cask, = referenced_formula_or_cask, =
Livecheck.resolve_livecheck_reference(formula_or_cask, full_name: false, debug: false) Livecheck.resolve_livecheck_reference(formula_or_cask, full_name: false, debug: false)

View File

@ -37,6 +37,8 @@ module Homebrew
description: "Show the latest version only if it's newer than the formula/cask." description: "Show the latest version only if it's newer than the formula/cask."
switch "--json", switch "--json",
description: "Output information in JSON format." description: "Output information in JSON format."
switch "-r", "--resources",
description: "Also check resources for formulae."
switch "-q", "--quiet", switch "-q", "--quiet",
description: "Suppress warnings, don't print a progress bar for JSON output." description: "Suppress warnings, don't print a progress bar for JSON output."
switch "--formula", "--formulae", switch "--formula", "--formulae",
@ -101,6 +103,7 @@ module Homebrew
else else
raise UsageError, "A watchlist file is required when no arguments are given." raise UsageError, "A watchlist file is required when no arguments are given."
end end
formulae_and_casks_to_check = formulae_and_casks_to_check.sort_by do |formula_or_cask| formulae_and_casks_to_check = formulae_and_casks_to_check.sort_by do |formula_or_cask|
formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
end end
@ -111,6 +114,7 @@ module Homebrew
json: args.json?, json: args.json?,
full_name: args.full_name?, full_name: args.full_name?,
handle_name_conflict: !args.formula? && !args.cask?, handle_name_conflict: !args.formula? && !args.cask?,
check_resources: args.resources?,
newer_only: args.newer_only?, newer_only: args.newer_only?,
quiet: args.quiet?, quiet: args.quiet?,
debug: args.debug?, debug: args.debug?,

View File

@ -126,11 +126,11 @@ module Homebrew
if debug if debug
# Print the chain of references for debugging # Print the chain of references for debugging
puts "Reference Chain:" puts "Reference Chain:"
puts formula_or_cask_name(first_formula_or_cask, full_name: full_name) puts package_or_resource_name(first_formula_or_cask, full_name: full_name)
references << referenced_formula_or_cask references << referenced_formula_or_cask
references.each do |ref_formula_or_cask| references.each do |ref_formula_or_cask|
puts formula_or_cask_name(ref_formula_or_cask, full_name: full_name) puts package_or_resource_name(ref_formula_or_cask, full_name: full_name)
end end
end end
@ -157,11 +157,14 @@ module Homebrew
# Executes the livecheck logic for each formula/cask in the # Executes the livecheck logic for each formula/cask in the
# `formulae_and_casks_to_check` array and prints the results. # `formulae_and_casks_to_check` array and prints the results.
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
sig { sig {
params( params(
formulae_and_casks_to_check: T::Array[T.any(Formula, Cask::Cask)], formulae_and_casks_to_check: T::Array[T.any(Formula, Cask::Cask)],
full_name: T::Boolean, full_name: T::Boolean,
handle_name_conflict: T::Boolean, handle_name_conflict: T::Boolean,
check_resources: T::Boolean,
json: T::Boolean, json: T::Boolean,
newer_only: T::Boolean, newer_only: T::Boolean,
debug: T::Boolean, debug: T::Boolean,
@ -171,14 +174,15 @@ module Homebrew
} }
def run_checks( def run_checks(
formulae_and_casks_to_check, formulae_and_casks_to_check,
full_name: false, handle_name_conflict: false, json: false, newer_only: false, full_name: false, handle_name_conflict: false, check_resources: false, json: false, newer_only: false,
debug: false, quiet: false, verbose: false debug: false, quiet: false, verbose: false
) )
load_other_tap_strategies(formulae_and_casks_to_check) load_other_tap_strategies(formulae_and_casks_to_check)
ambiguous_casks = [] ambiguous_casks = []
if handle_name_conflict if handle_name_conflict
ambiguous_casks = formulae_and_casks_to_check.group_by { |item| formula_or_cask_name(item, full_name: true) } ambiguous_casks = formulae_and_casks_to_check \
.group_by { |item| package_or_resource_name(item, full_name: true) }
.values .values
.select { |items| items.length > 1 } .select { |items| items.length > 1 }
.flatten .flatten
@ -188,7 +192,7 @@ module Homebrew
ambiguous_names = [] ambiguous_names = []
unless full_name unless full_name
ambiguous_names = ambiguous_names =
(formulae_and_casks_to_check - ambiguous_casks).group_by { |item| formula_or_cask_name(item) } (formulae_and_casks_to_check - ambiguous_casks).group_by { |item| package_or_resource_name(item) }
.values .values
.select { |items| items.length > 1 } .select { |items| items.length > 1 }
.flatten .flatten
@ -218,7 +222,7 @@ module Homebrew
cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask) cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask)
use_full_name = full_name || ambiguous_names.include?(formula_or_cask) use_full_name = full_name || ambiguous_names.include?(formula_or_cask)
name = formula_or_cask_name(formula_or_cask, full_name: use_full_name) name = package_or_resource_name(formula_or_cask, full_name: use_full_name)
referenced_formula_or_cask, livecheck_references = referenced_formula_or_cask, livecheck_references =
resolve_livecheck_reference(formula_or_cask, full_name: use_full_name, debug: debug) resolve_livecheck_reference(formula_or_cask, full_name: use_full_name, debug: debug)
@ -282,6 +286,30 @@ module Homebrew
version_info[:latest] if version_info.present? version_info[:latest] if version_info.present?
end end
check_for_resources = check_resources && formula_or_cask.is_a?(Formula) && formula_or_cask.resources.present?
if check_for_resources
resource_version_info = formula_or_cask.resources.map do |resource|
res_skip_info ||= SkipConditions.skip_information(resource, verbose: verbose)
if res_skip_info.present?
res_skip_info
else
res_version_info = resource_version(
resource,
json: json,
debug: debug,
quiet: quiet,
verbose: verbose,
)
if res_version_info.empty?
status_hash(resource, "error", ["Unable to get versions"], verbose: verbose)
else
res_version_info
end
end
end.compact_blank
Homebrew.failed = true if resource_version_info.any? { |info| info[:status] == "error" }
end
if latest.blank? if latest.blank?
no_versions_msg = "Unable to get versions" no_versions_msg = "Unable to get versions"
raise Livecheck::Error, no_versions_msg unless json raise Livecheck::Error, no_versions_msg unless json
@ -289,7 +317,14 @@ module Homebrew
next version_info if version_info.is_a?(Hash) && version_info[:status] && version_info[:messages] 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], full_name: use_full_name, verbose: verbose) latest_info = status_hash(formula_or_cask, "error", [no_versions_msg], full_name: use_full_name,
verbose: verbose)
if check_for_resources
resource_version_info.map! { |r| r.except!(:meta) } unless verbose
latest_info[:resources] = resource_version_info
end
next latest_info
end end
if (m = latest.to_s.match(/(.*)-release$/)) && !current.to_s.match(/.*-release$/) if (m = latest.to_s.match(/(.*)-release$/)) && !current.to_s.match(/.*-release$/)
@ -324,6 +359,8 @@ module Homebrew
info[:meta][:head_only] = true if formula&.head_only? info[:meta][:head_only] = true if formula&.head_only?
info[:meta].merge!(version_info[:meta]) if version_info.present? && version_info.key?(:meta) info[:meta].merge!(version_info[:meta]) if version_info.present? && version_info.key?(:meta)
info[:resources] = resource_version_info if check_for_resources
next if newer_only && !info[:version][:outdated] next if newer_only && !info[:version][:outdated]
has_a_newer_upstream_version ||= true has_a_newer_upstream_version ||= true
@ -331,10 +368,12 @@ module Homebrew
if json if json
progress&.increment progress&.increment
info.except!(:meta) unless verbose info.except!(:meta) unless verbose
resource_version_info.map! { |r| r.except!(:meta) } if check_for_resources && !verbose
next info next info
end end
puts if debug
print_latest_version(info, verbose: verbose, ambiguous_cask: ambiguous_casks.include?(formula_or_cask)) print_latest_version(info, verbose: verbose, ambiguous_cask: ambiguous_casks.include?(formula_or_cask))
print_resources_info(resource_version_info, verbose: verbose) if check_for_resources
nil nil
rescue => e rescue => e
Homebrew.failed = true Homebrew.failed = true
@ -344,11 +383,12 @@ module Homebrew
progress&.increment progress&.increment
status_hash(formula_or_cask, "error", [e.to_s], full_name: use_full_name, verbose: verbose) unless quiet status_hash(formula_or_cask, "error", [e.to_s], full_name: use_full_name, verbose: verbose) unless quiet
elsif !quiet elsif !quiet
name = formula_or_cask_name(formula_or_cask, full_name: use_full_name) name = package_or_resource_name(formula_or_cask, full_name: use_full_name)
name += " (cask)" if ambiguous_casks.include?(formula_or_cask) name += " (cask)" if ambiguous_casks.include?(formula_or_cask)
onoe "#{Tty.blue}#{name}#{Tty.reset}: #{e}" onoe "#{Tty.blue}#{name}#{Tty.reset}: #{e}"
$stderr.puts e.backtrace if debug && !e.is_a?(Livecheck::Error) $stderr.puts e.backtrace if debug && !e.is_a?(Livecheck::Error)
print_resources_info(resource_version_info, verbose: verbose) if check_for_resources
nil nil
end end
end end
@ -367,16 +407,20 @@ module Homebrew
puts JSON.pretty_generate(formulae_checked.compact) puts JSON.pretty_generate(formulae_checked.compact)
end end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
sig { params(formula_or_cask: T.any(Formula, Cask::Cask), full_name: T::Boolean).returns(String) } sig { params(package_or_resource: T.any(Formula, Cask::Cask, Resource), full_name: T::Boolean).returns(String) }
def formula_or_cask_name(formula_or_cask, full_name: false) def package_or_resource_name(package_or_resource, full_name: false)
case formula_or_cask case package_or_resource
when Formula when Formula
formula_name(formula_or_cask, full_name: full_name) formula_name(package_or_resource, full_name: full_name)
when Cask::Cask when Cask::Cask
cask_name(formula_or_cask, full_name: full_name) cask_name(package_or_resource, full_name: full_name)
when Resource
package_or_resource.name
else else
T.absurd(formula_or_cask) T.absurd(package_or_resource)
end end
end end
@ -396,40 +440,44 @@ module Homebrew
sig { sig {
params( params(
formula_or_cask: T.any(Formula, Cask::Cask), package_or_resource: T.any(Formula, Cask::Cask, Resource),
status_str: String, status_str: String,
messages: T.nilable(T::Array[String]), messages: T.nilable(T::Array[String]),
full_name: T::Boolean, full_name: T::Boolean,
verbose: T::Boolean, verbose: T::Boolean,
).returns(Hash) ).returns(Hash)
} }
def status_hash(formula_or_cask, status_str, messages = nil, full_name: false, verbose: false) def status_hash(package_or_resource, status_str, messages = nil, full_name: false, verbose: false)
formula = formula_or_cask if formula_or_cask.is_a?(Formula) formula = package_or_resource if package_or_resource.is_a?(Formula)
cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask) cask = package_or_resource if package_or_resource.is_a?(Cask::Cask)
resource = package_or_resource if package_or_resource.is_a?(Resource)
status_hash = {} status_hash = {}
if formula if formula
status_hash[:formula] = formula_name(formula, full_name: full_name) status_hash[:formula] = formula_name(formula, full_name: full_name)
elsif cask elsif cask
status_hash[:cask] = cask_name(formula_or_cask, full_name: full_name) status_hash[:cask] = cask_name(cask, full_name: full_name)
elsif resource
status_hash[:resource] = resource.name
end end
status_hash[:status] = status_str status_hash[:status] = status_str
status_hash[:messages] = messages if messages.is_a?(Array) status_hash[:messages] = messages if messages.is_a?(Array)
status_hash[:meta] = { status_hash[:meta] = {
livecheckable: formula_or_cask.livecheckable?, livecheckable: package_or_resource.livecheckable?,
} }
status_hash[:meta][:head_only] = true if formula&.head_only? status_hash[:meta][:head_only] = true if formula&.head_only?
status_hash status_hash
end end
# Formats and prints the livecheck result for a formula. # Formats and prints the livecheck result for a formula/cask/resource.
sig { params(info: Hash, verbose: T::Boolean, ambiguous_cask: T::Boolean).void } sig { params(info: Hash, verbose: T::Boolean, ambiguous_cask: T::Boolean).void }
def print_latest_version(info, verbose:, ambiguous_cask: false) def print_latest_version(info, verbose: false, ambiguous_cask: false)
formula_or_cask_s = "#{Tty.blue}#{info[:formula] || info[:cask]}#{Tty.reset}" package_or_resource_s = info[:resource].present? ? " " : ""
formula_or_cask_s += " (cask)" if ambiguous_cask package_or_resource_s += "#{Tty.blue}#{info[:formula] || info[:cask] || info[:resource]}#{Tty.reset}"
formula_or_cask_s += " (guessed)" if !info[:meta][:livecheckable] && verbose package_or_resource_s += " (cask)" if ambiguous_cask
package_or_resource_s += " (guessed)" if verbose && !info[:meta][:livecheckable]
current_s = if info[:version][:newer_than_upstream] current_s = if info[:version][:newer_than_upstream]
"#{Tty.red}#{info[:version][:current]}#{Tty.reset}" "#{Tty.red}#{info[:version][:current]}#{Tty.reset}"
@ -443,47 +491,61 @@ module Homebrew
info[:version][:latest] info[:version][:latest]
end end
puts "#{formula_or_cask_s}: #{current_s} ==> #{latest_s}" puts "#{package_or_resource_s}: #{current_s} ==> #{latest_s}"
end
# Prints the livecheck result for the resources of a given Formula.
sig { params(info: T::Array[Hash], verbose: T::Boolean).void }
def print_resources_info(info, verbose: false)
info.each do |r_info|
if r_info[:status] && r_info[:messages]
SkipConditions.print_skip_information(r_info)
else
print_latest_version(r_info, verbose: verbose)
end
end
end end
sig { sig {
params( params(
livecheck_url: T.any(String, Symbol), livecheck_url: T.any(String, Symbol),
formula_or_cask: T.any(Formula, Cask::Cask), package_or_resource: T.any(Formula, Cask::Cask, Resource),
).returns(T.nilable(String)) ).returns(T.nilable(String))
} }
def livecheck_url_to_string(livecheck_url, formula_or_cask) def livecheck_url_to_string(livecheck_url, package_or_resource)
case livecheck_url case livecheck_url
when String when String
livecheck_url livecheck_url
when :url when :url
formula_or_cask.url&.to_s if formula_or_cask.is_a?(Cask::Cask) package_or_resource.url&.to_s if package_or_resource.is_a?(Cask::Cask) || package_or_resource.is_a?(Resource)
when :head, :stable when :head, :stable
formula_or_cask.send(livecheck_url)&.url if formula_or_cask.is_a?(Formula) package_or_resource.send(livecheck_url)&.url if package_or_resource.is_a?(Formula)
when :homepage when :homepage
formula_or_cask.homepage package_or_resource.homepage unless package_or_resource.is_a?(Resource)
end end
end end
# Returns an Array containing the formula/cask URLs that can be used by livecheck. # Returns an Array containing the formula/cask/resource URLs that can be used by livecheck.
sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(T::Array[String]) } sig { params(package_or_resource: T.any(Formula, Cask::Cask, Resource)).returns(T::Array[String]) }
def checkable_urls(formula_or_cask) def checkable_urls(package_or_resource)
urls = [] urls = []
case formula_or_cask case package_or_resource
when Formula when Formula
if formula_or_cask.stable if package_or_resource.stable
urls << formula_or_cask.stable.url urls << package_or_resource.stable.url
urls.concat(formula_or_cask.stable.mirrors) urls.concat(package_or_resource.stable.mirrors)
end end
urls << formula_or_cask.head.url if formula_or_cask.head urls << package_or_resource.head.url if package_or_resource.head
urls << formula_or_cask.homepage if formula_or_cask.homepage urls << package_or_resource.homepage if package_or_resource.homepage
when Cask::Cask when Cask::Cask
urls << formula_or_cask.appcast.to_s if formula_or_cask.appcast urls << package_or_resource.appcast.to_s if package_or_resource.appcast
urls << formula_or_cask.url.to_s if formula_or_cask.url urls << package_or_resource.url.to_s if package_or_resource.url
urls << formula_or_cask.homepage if formula_or_cask.homepage urls << package_or_resource.homepage if package_or_resource.homepage
when Resource
urls << package_or_resource.url
else else
T.absurd(formula_or_cask) T.absurd(package_or_resource)
end end
urls.compact.uniq urls.compact.uniq
@ -561,7 +623,7 @@ module Homebrew
homebrew_curl_root_domains.include?(url_root_domain) homebrew_curl_root_domains.include?(url_root_domain)
end end
# Identifies the latest version of the formula and returns a Hash containing # Identifies the latest version of the formula/cask and returns a Hash containing
# the version information. Returns nil if a latest version couldn't be found. # the version information. Returns nil if a latest version couldn't be found.
# rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/CyclomaticComplexity
sig { sig {
@ -713,7 +775,6 @@ module Homebrew
version.to_s.include?(rejection) version.to_s.include?(rejection)
end end
end end
next if match_version_map.blank? next if match_version_map.blank?
if debug if debug
@ -770,6 +831,195 @@ module Homebrew
nil nil
end end
# rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/CyclomaticComplexity
# Identifies the latest version of a resource and returns a Hash containing the
# version information. Returns nil if a latest version couldn't be found.
sig {
params(
resource: Resource,
json: T::Boolean,
debug: T::Boolean,
quiet: T::Boolean,
verbose: T::Boolean,
).returns(Hash)
}
def resource_version(
resource,
json: false,
debug: false,
quiet: false,
verbose: false
)
has_livecheckable = resource.livecheckable?
if debug
puts "\n\n"
puts "Resource: #{resource.name}"
puts "Livecheckable?: #{has_livecheckable ? "Yes" : "No"}"
end
resource_version_info = {}
livecheck = resource.livecheck
livecheck_url = livecheck.url
livecheck_regex = livecheck.regex
livecheck_strategy = livecheck.strategy
livecheck_strategy_block = livecheck.strategy_block
livecheck_url_string = livecheck_url_to_string(livecheck_url, resource)
urls = [livecheck_url_string] if livecheck_url_string
urls ||= checkable_urls(resource)
checked_urls = []
# rubocop:disable Metrics/BlockLength
urls.each_with_index do |original_url, i|
# Only preprocess the URL when it's appropriate
url = if STRATEGY_SYMBOLS_TO_SKIP_PREPROCESS_URL.include?(livecheck_strategy)
original_url
else
preprocess_url(original_url)
end
next if checked_urls.include?(url)
strategies = Strategy.from_url(
url,
livecheck_strategy: livecheck_strategy,
url_provided: livecheck_url.present?,
regex_provided: livecheck_regex.present?,
block_provided: livecheck_strategy_block.present?,
)
strategy = Strategy.from_symbol(livecheck_strategy) || strategies.first
strategy_name = livecheck_strategy_names[strategy]
if debug
puts
if livecheck_url.is_a?(Symbol)
# This assumes the URL symbol will fit within the available space
puts "URL (#{livecheck_url}):".ljust(18, " ") + original_url
else
puts "URL: #{original_url}"
end
puts "URL (processed): #{url}" if url != original_url
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?
end
if livecheck_strategy.present?
if livecheck_url.blank? && strategy.method(:find_versions).parameters.include?([:keyreq, :url])
odebug "#{strategy_name} strategy requires a URL"
next
elsif livecheck_strategy != :page_match && strategies.exclude?(strategy)
odebug "#{strategy_name} strategy does not apply to this URL"
next
end
end
puts if debug && strategy.blank?
next if strategy.blank?
strategy_data = strategy.find_versions(
url: url,
regex: livecheck_regex,
homebrew_curl: false,
&livecheck_strategy_block
)
match_version_map = strategy_data[:matches]
regex = strategy_data[:regex]
messages = strategy_data[:messages]
checked_urls << url
if messages.is_a?(Array) && match_version_map.blank?
puts messages unless json
next if i + 1 < urls.length
return status_hash(resource, "error", messages, verbose: verbose)
end
if debug
if strategy_data[:url].present? && strategy_data[:url] != url
puts "URL (strategy): #{strategy_data[:url]}"
end
puts "URL (final): #{strategy_data[:final_url]}" if strategy_data[:final_url].present?
if strategy_data[:regex].present? && strategy_data[:regex] != livecheck_regex
puts "Regex (strategy): #{strategy_data[:regex].inspect}"
end
puts "Cached?: Yes" if strategy_data[:cached] == true
end
match_version_map.delete_if do |_match, version|
next true if version.blank?
next false if has_livecheckable
UNSTABLE_VERSION_KEYWORDS.any? do |rejection|
version.to_s.include?(rejection)
end
end
next if match_version_map.blank?
if debug
puts
puts "Matched Versions:"
if verbose
match_version_map.each do |match, version|
puts "#{match} => #{version.inspect}"
end
else
puts match_version_map.values.join(", ")
end
end
res_current = resource.version
res_latest = Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(resource, v) })
return status_hash(resource, "error", ["Unable to get versions"], verbose: verbose) if res_latest.blank?
is_outdated = res_current < res_latest
is_newer_than_upstream = res_current > res_latest
resource_version_info = {
resource: resource.name,
version: {
current: res_current.to_s,
latest: res_latest.to_s,
outdated: is_outdated,
newer_than_upstream: is_newer_than_upstream,
},
}
resource_version_info[:meta] = { livecheckable: has_livecheckable, url: {} }
if livecheck_url.is_a?(Symbol) && livecheck_url_string
resource_version_info[:meta][:url][:symbol] = livecheck_url
end
resource_version_info[:meta][:url][:original] = original_url
resource_version_info[:meta][:url][:processed] = url if url != original_url
if strategy_data[:url].present? && strategy_data[:url] != url
resource_version_info[:meta][:url][:strategy] = strategy_data[:url]
end
resource_version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
resource_version_info[:meta][:strategy] = strategy.present? ? strategy_name : nil
if strategies.present?
resource_version_info[:meta][:strategies] = strategies.map { |s| livecheck_strategy_names[s] }
end
resource_version_info[:meta][:regex] = regex.inspect if regex.present?
resource_version_info[:meta][:cached] = true if strategy_data[:cached] == true
rescue => e
Homebrew.failed = true
if json
status_hash(resource, "error", [e.to_s], verbose: verbose)
elsif !quiet
onoe "#{Tty.blue}#{resource.name}#{Tty.reset}: #{e}"
$stderr.puts e.backtrace if debug && !e.is_a?(Livecheck::Error)
nil
end
end
# rubocop:enable Metrics/BlockLength
resource_version_info
end
end end
# rubocop:enable Metrics/ModuleLength # rubocop:enable Metrics/ModuleLength
end end

View File

@ -11,15 +11,17 @@ module Homebrew
include Comparable include Comparable
sig { params(formula_or_cask: T.any(Formula, Cask::Cask), version: Version).returns(LivecheckVersion) } sig {
def self.create(formula_or_cask, version) params(package_or_resource: T.any(Formula, Cask::Cask, Resource), version: Version).returns(LivecheckVersion)
versions = case formula_or_cask }
when Formula def self.create(package_or_resource, version)
versions = case package_or_resource
when Formula, Resource
[version] [version]
when Cask::Cask when Cask::Cask
version.to_s.split(/[,:]/).map { |s| Version.new(s) } version.to_s.split(/[,:]/).map { |s| Version.new(s) }
else else
T.absurd(formula_or_cask) T.absurd(package_or_resource)
end end
new(versions) new(versions)
end end

View File

@ -6,7 +6,7 @@ require "livecheck/livecheck"
module Homebrew module Homebrew
module Livecheck module Livecheck
# The `Livecheck::SkipConditions` module primarily contains methods that # The `Livecheck::SkipConditions` module primarily contains methods that
# check for various formula/cask conditions where a check should be skipped. # check for various formula/cask/resource conditions where a check should be skipped.
# #
# @api private # @api private
module SkipConditions module SkipConditions
@ -16,14 +16,14 @@ module Homebrew
sig { sig {
params( params(
formula_or_cask: T.any(Formula, Cask::Cask), package_or_resource: T.any(Formula, Cask::Cask, Resource),
livecheckable: T::Boolean, livecheckable: T::Boolean,
full_name: T::Boolean, full_name: T::Boolean,
verbose: T::Boolean, verbose: T::Boolean,
).returns(Hash) ).returns(Hash)
} }
def formula_or_cask_skip(formula_or_cask, livecheckable, full_name: false, verbose: false) def package_or_resource_skip(package_or_resource, livecheckable, full_name: false, verbose: false)
formula = formula_or_cask if formula_or_cask.is_a?(Formula) formula = package_or_resource if package_or_resource.is_a?(Formula)
if (stable_url = formula&.stable&.url) if (stable_url = formula&.stable&.url)
stable_is_gist = stable_url.match?(%r{https?://gist\.github(?:usercontent)?\.com/}i) stable_is_gist = stable_url.match?(%r{https?://gist\.github(?:usercontent)?\.com/}i)
@ -33,8 +33,8 @@ module Homebrew
stable_from_internet_archive = stable_url.match?(%r{https?://web\.archive\.org/}i) stable_from_internet_archive = stable_url.match?(%r{https?://web\.archive\.org/}i)
end end
skip_message = if formula_or_cask.livecheck.skip_msg.present? skip_message = if package_or_resource.livecheck.skip_msg.present?
formula_or_cask.livecheck.skip_msg package_or_resource.livecheck.skip_msg
elsif !livecheckable elsif !livecheckable
if stable_from_google_code_archive if stable_from_google_code_archive
"Stable URL is from Google Code Archive" "Stable URL is from Google Code Archive"
@ -45,10 +45,10 @@ module Homebrew
end end
end end
return {} if !formula_or_cask.livecheck.skip? && skip_message.blank? return {} if !package_or_resource.livecheck.skip? && skip_message.blank?
skip_messages = skip_message ? [skip_message] : nil skip_messages = skip_message ? [skip_message] : nil
Livecheck.status_hash(formula_or_cask, "skipped", skip_messages, full_name: full_name, verbose: verbose) Livecheck.status_hash(package_or_resource, "skipped", skip_messages, full_name: full_name, verbose: verbose)
end end
sig { sig {
@ -157,7 +157,7 @@ module Homebrew
# Skip conditions for formulae. # Skip conditions for formulae.
FORMULA_CHECKS = [ FORMULA_CHECKS = [
:formula_or_cask_skip, :package_or_resource_skip,
:formula_head_only, :formula_head_only,
:formula_deprecated, :formula_deprecated,
:formula_disabled, :formula_disabled,
@ -166,76 +166,85 @@ module Homebrew
# Skip conditions for casks. # Skip conditions for casks.
CASK_CHECKS = [ CASK_CHECKS = [
:formula_or_cask_skip, :package_or_resource_skip,
:cask_discontinued, :cask_discontinued,
:cask_version_latest, :cask_version_latest,
:cask_url_unversioned, :cask_url_unversioned,
].freeze ].freeze
# If a formula/cask should be skipped, we return a hash from # Skip conditions for resources.
RESOURCE_CHECKS = [
:package_or_resource_skip,
].freeze
# If a formula/cask/resource should be skipped, we return a hash from
# `Livecheck#status_hash`, which contains a `status` type and sometimes # `Livecheck#status_hash`, which contains a `status` type and sometimes
# error `messages`. # error `messages`.
sig { sig {
params( params(
formula_or_cask: T.any(Formula, Cask::Cask), package_or_resource: T.any(Formula, Cask::Cask, Resource),
full_name: T::Boolean, full_name: T::Boolean,
verbose: T::Boolean, verbose: T::Boolean,
).returns(Hash) ).returns(Hash)
} }
def skip_information(formula_or_cask, full_name: false, verbose: false) def skip_information(package_or_resource, full_name: false, verbose: false)
livecheckable = formula_or_cask.livecheckable? livecheckable = package_or_resource.livecheckable?
checks = case formula_or_cask checks = case package_or_resource
when Formula when Formula
FORMULA_CHECKS FORMULA_CHECKS
when Cask::Cask when Cask::Cask
CASK_CHECKS CASK_CHECKS
when Resource
RESOURCE_CHECKS
end end
return {} unless checks return {} unless checks
checks.each do |method_name| checks.each do |method_name|
skip_hash = send(method_name, formula_or_cask, livecheckable, full_name: full_name, verbose: verbose) skip_hash = send(method_name, package_or_resource, livecheckable, full_name: full_name, verbose: verbose)
return skip_hash if skip_hash.present? return skip_hash if skip_hash.present?
end end
{} {}
end end
# Skip conditions for formulae/casks referenced in a `livecheck` block # Skip conditions for formulae/casks/resources referenced in a `livecheck` block
# are treated differently than normal. We only respect certain skip # are treated differently than normal. We only respect certain skip
# conditions (returning the related hash) and others are treated as # conditions (returning the related hash) and others are treated as
# errors. # errors.
sig { sig {
params( params(
livecheck_formula_or_cask: T.any(Formula, Cask::Cask), livecheck_package_or_resource: T.any(Formula, Cask::Cask, Resource),
original_formula_or_cask_name: String, original_package_or_resource_name: String,
full_name: T::Boolean, full_name: T::Boolean,
verbose: T::Boolean, verbose: T::Boolean,
).returns(T.nilable(Hash)) ).returns(T.nilable(Hash))
} }
def referenced_skip_information( def referenced_skip_information(
livecheck_formula_or_cask, livecheck_package_or_resource,
original_formula_or_cask_name, original_package_or_resource_name,
full_name: false, full_name: false,
verbose: false verbose: false
) )
skip_info = SkipConditions.skip_information( skip_info = SkipConditions.skip_information(
livecheck_formula_or_cask, livecheck_package_or_resource,
full_name: full_name, full_name: full_name,
verbose: verbose, verbose: verbose,
) )
return if skip_info.blank? return if skip_info.blank?
referenced_name = Livecheck.formula_or_cask_name(livecheck_formula_or_cask, full_name: full_name) referenced_name = Livecheck.package_or_resource_name(livecheck_package_or_resource, full_name: full_name)
referenced_type = case livecheck_formula_or_cask referenced_type = case livecheck_package_or_resource
when Formula when Formula
:formula :formula
when Cask::Cask when Cask::Cask
:cask :cask
when Resource
:resource
end end
if skip_info[:status] != "error" && if skip_info[:status] != "error" &&
!(skip_info[:status] == "skipped" && livecheck_formula_or_cask.livecheck.skip?) !(skip_info[:status] == "skipped" && livecheck_package_or_resource.livecheck.skip?)
error_msg_end = if skip_info[:status] == "skipped" error_msg_end = if skip_info[:status] == "skipped"
"automatically skipped" "automatically skipped"
else else
@ -245,7 +254,7 @@ module Homebrew
raise "Referenced #{referenced_type} (#{referenced_name}) is #{error_msg_end}" raise "Referenced #{referenced_type} (#{referenced_name}) is #{error_msg_end}"
end end
skip_info[referenced_type] = original_formula_or_cask_name skip_info[referenced_type] = original_package_or_resource_name
skip_info skip_info
end end
@ -258,6 +267,8 @@ module Homebrew
skip_hash[:formula] skip_hash[:formula]
elsif skip_hash[:cask].is_a?(String) elsif skip_hash[:cask].is_a?(String)
skip_hash[:cask] skip_hash[:cask]
elsif skip_hash[:resource].is_a?(String)
" #{skip_hash[:resource]}"
end end
return unless name return unless name

View File

@ -11,6 +11,7 @@ describe Homebrew::Livecheck do
let(:homepage_url) { "https://brew.sh" } let(:homepage_url) { "https://brew.sh" }
let(:livecheck_url) { "https://formulae.brew.sh/api/formula/ruby.json" } let(:livecheck_url) { "https://formulae.brew.sh/api/formula/ruby.json" }
let(:stable_url) { "https://brew.sh/test-0.0.1.tgz" } let(:stable_url) { "https://brew.sh/test-0.0.1.tgz" }
let(:resource_url) { "https://brew.sh/foo-1.0.tar.gz" }
let(:f) do let(:f) do
formula("test") do formula("test") do
@ -23,8 +24,20 @@ describe Homebrew::Livecheck do
url "https://formulae.brew.sh/api/formula/ruby.json" url "https://formulae.brew.sh/api/formula/ruby.json"
regex(/"stable":"(\d+(?:\.\d+)+)"/i) regex(/"stable":"(\d+(?:\.\d+)+)"/i)
end end
resource "foo" do
url "https://brew.sh/foo-1.0.tar.gz"
sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
livecheck do
url "https://brew.sh/test/releases"
regex(/foo[._-]v?(\d+(?:\.\d+)+)\.t/i)
end end
end end
end
end
let(:r) { f.resources.first }
let(:c) do let(:c) do
Cask::CaskLoader.load(+<<-RUBY) Cask::CaskLoader.load(+<<-RUBY)
@ -44,15 +57,6 @@ describe Homebrew::Livecheck do
RUBY RUBY
end end
let(:f_duplicate_urls) do
formula("test_duplicate_urls") do
desc "Test formula with a duplicate URL"
homepage "https://github.com/Homebrew/brew.git"
url "https://brew.sh/test-0.0.1.tgz"
head "https://github.com/Homebrew/brew.git"
end
end
describe "::resolve_livecheck_reference" do describe "::resolve_livecheck_reference" do
context "when a formula/cask has a livecheck block without formula/cask methods" do context "when a formula/cask has a livecheck block without formula/cask methods" do
it "returns [nil, []]" do it "returns [nil, []]" do
@ -83,7 +87,7 @@ describe Homebrew::Livecheck do
end end
describe "::status_hash" do describe "::status_hash" do
it "returns a hash containing the livecheck status" do it "returns a hash containing the livecheck status for a formula" do
expect(livecheck.status_hash(f, "error", ["Unable to get versions"])) expect(livecheck.status_hash(f, "error", ["Unable to get versions"]))
.to eq({ .to eq({
formula: "test", formula: "test",
@ -94,6 +98,18 @@ describe Homebrew::Livecheck do
}, },
}) })
end end
it "returns a hash containing the livecheck status for a resource" do
expect(livecheck.status_hash(r, "error", ["Unable to get versions"]))
.to eq({
resource: "foo",
status: "error",
messages: ["Unable to get versions"],
meta: {
livecheckable: true,
},
})
end
end end
describe "::livecheck_url_to_string" do describe "::livecheck_url_to_string" do
@ -101,14 +117,27 @@ describe Homebrew::Livecheck do
homepage_url_s = homepage_url homepage_url_s = homepage_url
stable_url_s = stable_url stable_url_s = stable_url
head_url_s = head_url head_url_s = head_url
resource_url_s = resource_url
formula("test_livecheck_url") do formula("test_livecheck_url") do
desc "Test Livecheck URL formula" desc "Test Livecheck URL formula"
homepage homepage_url_s homepage homepage_url_s
url stable_url_s url stable_url_s
head head_url_s head head_url_s
resource "foo" do
url resource_url_s
sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
livecheck do
url "https://brew.sh/test/releases"
regex(/foo[._-]v?(\d+(?:\.\d+)+)\.t/i)
end end
end end
end
end
let(:r_livecheck_url) { f_livecheck_url.resources.first }
let(:c_livecheck_url) do let(:c_livecheck_url) do
Cask::CaskLoader.load(+<<-RUBY) Cask::CaskLoader.load(+<<-RUBY)
@ -123,30 +152,48 @@ describe Homebrew::Livecheck do
RUBY RUBY
end end
it "returns a URL string when given a livecheck_url string" do it "returns a URL string when given a livecheck_url string for a formula" do
expect(livecheck.livecheck_url_to_string(livecheck_url, f_livecheck_url)).to eq(livecheck_url) expect(livecheck.livecheck_url_to_string(livecheck_url, f_livecheck_url)).to eq(livecheck_url)
end end
it "returns a URL string when given a livecheck_url string for a resource" do
expect(livecheck.livecheck_url_to_string(livecheck_url, r_livecheck_url)).to eq(livecheck_url)
end
it "returns a URL symbol when given a valid livecheck_url symbol" do it "returns a URL symbol when given a valid livecheck_url symbol" do
expect(livecheck.livecheck_url_to_string(:head, f_livecheck_url)).to eq(head_url) expect(livecheck.livecheck_url_to_string(:head, f_livecheck_url)).to eq(head_url)
expect(livecheck.livecheck_url_to_string(:homepage, f_livecheck_url)).to eq(homepage_url) expect(livecheck.livecheck_url_to_string(:homepage, f_livecheck_url)).to eq(homepage_url)
expect(livecheck.livecheck_url_to_string(:homepage, c_livecheck_url)).to eq(homepage_url) expect(livecheck.livecheck_url_to_string(:homepage, c_livecheck_url)).to eq(homepage_url)
expect(livecheck.livecheck_url_to_string(:stable, f_livecheck_url)).to eq(stable_url) expect(livecheck.livecheck_url_to_string(:stable, f_livecheck_url)).to eq(stable_url)
expect(livecheck.livecheck_url_to_string(:url, c_livecheck_url)).to eq(cask_url) expect(livecheck.livecheck_url_to_string(:url, c_livecheck_url)).to eq(cask_url)
expect(livecheck.livecheck_url_to_string(:url, r_livecheck_url)).to eq(resource_url)
end end
it "returns nil when not given a string or valid symbol" do it "returns nil when not given a string or valid symbol" do
expect(livecheck.livecheck_url_to_string(nil, f_livecheck_url)).to be_nil expect(livecheck.livecheck_url_to_string(nil, f_livecheck_url)).to be_nil
expect(livecheck.livecheck_url_to_string(nil, c_livecheck_url)).to be_nil expect(livecheck.livecheck_url_to_string(nil, c_livecheck_url)).to be_nil
expect(livecheck.livecheck_url_to_string(nil, r_livecheck_url)).to be_nil
expect(livecheck.livecheck_url_to_string(:invalid_symbol, f_livecheck_url)).to be_nil expect(livecheck.livecheck_url_to_string(:invalid_symbol, f_livecheck_url)).to be_nil
expect(livecheck.livecheck_url_to_string(:invalid_symbol, c_livecheck_url)).to be_nil expect(livecheck.livecheck_url_to_string(:invalid_symbol, c_livecheck_url)).to be_nil
expect(livecheck.livecheck_url_to_string(:invalid_symbol, r_livecheck_url)).to be_nil
end end
end end
describe "::checkable_urls" do describe "::checkable_urls" do
let(:resource_url) { "https://brew.sh/foo-1.0.tar.gz" }
let(:f_duplicate_urls) do
formula("test_duplicate_urls") do
desc "Test formula with a duplicate URL"
homepage "https://github.com/Homebrew/brew.git"
url "https://brew.sh/test-0.0.1.tgz"
head "https://github.com/Homebrew/brew.git"
end
end
it "returns the list of URLs to check" do it "returns the list of URLs to check" do
expect(livecheck.checkable_urls(f)).to eq([stable_url, head_url, homepage_url]) expect(livecheck.checkable_urls(f)).to eq([stable_url, head_url, homepage_url])
expect(livecheck.checkable_urls(c)).to eq([cask_url, homepage_url]) expect(livecheck.checkable_urls(c)).to eq([cask_url, homepage_url])
expect(livecheck.checkable_urls(r)).to eq([resource_url])
expect(livecheck.checkable_urls(f_duplicate_urls)).to eq([stable_url, head_url]) expect(livecheck.checkable_urls(f_duplicate_urls)).to eq([stable_url, head_url])
end end
end end

View File

@ -6,6 +6,7 @@ require "livecheck/livecheck_version"
describe Homebrew::Livecheck::LivecheckVersion do describe Homebrew::Livecheck::LivecheckVersion do
let(:formula) { instance_double(Formula) } let(:formula) { instance_double(Formula) }
let(:cask) { instance_double(Cask::Cask) } let(:cask) { instance_double(Cask::Cask) }
let(:resource) { instance_double(Resource) }
before do before do
# Case statements use #=== for case equality purposes # Case statements use #=== for case equality purposes
@ -13,6 +14,8 @@ describe Homebrew::Livecheck::LivecheckVersion do
allow(Formula).to receive(:===).with(formula).and_return(true) allow(Formula).to receive(:===).with(formula).and_return(true)
allow(Cask::Cask).to receive(:===).and_call_original allow(Cask::Cask).to receive(:===).and_call_original
allow(Cask::Cask).to receive(:===).with(cask).and_return(true) allow(Cask::Cask).to receive(:===).with(cask).and_return(true)
allow(Resource).to receive(:===).and_call_original
allow(Resource).to receive(:===).with(resource).and_return(true)
end end
specify "::create" do specify "::create" do
@ -28,5 +31,11 @@ describe Homebrew::Livecheck::LivecheckVersion do
.to eq ["1.0", "100", "1426778671"] .to eq ["1.0", "100", "1426778671"]
expect(described_class.create(cask, Version.new("0.17.0,20210111183933,226")).versions) expect(described_class.create(cask, Version.new("0.17.0,20210111183933,226")).versions)
.to eq ["0.17.0", "20210111183933", "226"] .to eq ["0.17.0", "20210111183933", "226"]
expect(described_class.create(resource, Version.new("1.1.6")).versions).to eq ["1.1.6"]
expect(described_class.create(resource, Version.new("2.19.0,1.8.0")).versions).to eq ["2.19.0,1.8.0"]
expect(described_class.create(resource, Version.new("1.0,100:1426778671")).versions).to eq ["1.0,100:1426778671"]
expect(described_class.create(resource, Version.new("0.17.0,20210111183933,226")).versions)
.to eq ["0.17.0,20210111183933,226"]
end end
end end