mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
1037 lines
38 KiB
Ruby
1037 lines
38 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
require "livecheck/constants"
|
|
require "livecheck/error"
|
|
require "livecheck/livecheck_version"
|
|
require "livecheck/skip_conditions"
|
|
require "livecheck/strategy"
|
|
require "addressable"
|
|
require "uri"
|
|
|
|
module Homebrew
|
|
# The {Livecheck} module consists of methods used by the `brew livecheck`
|
|
# command. These methods print the requested livecheck information
|
|
# for formulae.
|
|
#
|
|
# @api private
|
|
module Livecheck
|
|
module_function
|
|
|
|
GITEA_INSTANCES = T.let(%w[
|
|
codeberg.org
|
|
gitea.com
|
|
opendev.org
|
|
tildegit.org
|
|
].freeze, T::Array[String])
|
|
|
|
GOGS_INSTANCES = T.let(%w[
|
|
lolg.it
|
|
].freeze, T::Array[String])
|
|
|
|
STRATEGY_SYMBOLS_TO_SKIP_PREPROCESS_URL = T.let([
|
|
:extract_plist,
|
|
:github_latest,
|
|
:header_match,
|
|
:json,
|
|
:page_match,
|
|
:sparkle,
|
|
:xml,
|
|
:yaml,
|
|
].freeze, T::Array[Symbol])
|
|
|
|
UNSTABLE_VERSION_KEYWORDS = T.let(%w[
|
|
alpha
|
|
beta
|
|
bpo
|
|
dev
|
|
experimental
|
|
prerelease
|
|
preview
|
|
rc
|
|
].freeze, T::Array[String])
|
|
|
|
sig { returns(T::Hash[T::Class[T.anything], String]) }
|
|
def livecheck_strategy_names
|
|
return T.must(@livecheck_strategy_names) if defined?(@livecheck_strategy_names)
|
|
|
|
# Cache demodulized strategy names, to avoid repeating this work
|
|
@livecheck_strategy_names = T.let({}, T.nilable(T::Hash[T::Class[T.anything], String]))
|
|
Strategy.constants.sort.each do |const_symbol|
|
|
constant = Strategy.const_get(const_symbol)
|
|
next unless constant.is_a?(Class)
|
|
|
|
T.must(@livecheck_strategy_names)[constant] = Utils.demodulize(T.must(constant.name))
|
|
end
|
|
T.must(@livecheck_strategy_names).freeze
|
|
end
|
|
|
|
# Uses `formulae_and_casks_to_check` to identify taps in use other than
|
|
# homebrew/core and homebrew/cask and loads strategies from them.
|
|
sig { params(formulae_and_casks_to_check: T::Array[T.any(Formula, Cask::Cask)]).void }
|
|
def load_other_tap_strategies(formulae_and_casks_to_check)
|
|
other_taps = {}
|
|
formulae_and_casks_to_check.each do |formula_or_cask|
|
|
next if formula_or_cask.tap.blank?
|
|
next if formula_or_cask.tap.core_tap?
|
|
next if formula_or_cask.tap.core_cask_tap?
|
|
next if other_taps[formula_or_cask.tap.name]
|
|
|
|
other_taps[formula_or_cask.tap.name] = formula_or_cask.tap
|
|
end
|
|
other_taps = other_taps.sort.to_h
|
|
|
|
other_taps.each_value do |tap|
|
|
tap_strategy_path = "#{tap.path}/livecheck/strategy"
|
|
Dir["#{tap_strategy_path}/*.rb"].sort.each { require(_1) } if Dir.exist?(tap_strategy_path)
|
|
end
|
|
end
|
|
|
|
# Resolve formula/cask references in `livecheck` blocks to a final formula
|
|
# or cask.
|
|
sig {
|
|
params(
|
|
formula_or_cask: T.any(Formula, Cask::Cask),
|
|
first_formula_or_cask: T.any(Formula, Cask::Cask),
|
|
references: T::Array[T.any(Formula, Cask::Cask)],
|
|
full_name: T::Boolean,
|
|
debug: T::Boolean,
|
|
).returns(T.nilable(T::Array[T.untyped]))
|
|
}
|
|
def resolve_livecheck_reference(
|
|
formula_or_cask,
|
|
first_formula_or_cask = formula_or_cask,
|
|
references = [],
|
|
full_name: false,
|
|
debug: false
|
|
)
|
|
# Check the livecheck block for a formula or cask reference
|
|
livecheck = formula_or_cask.livecheck
|
|
livecheck_formula = livecheck.formula
|
|
livecheck_cask = livecheck.cask
|
|
return [nil, references] if livecheck_formula.blank? && livecheck_cask.blank?
|
|
|
|
# Load the referenced formula or cask
|
|
referenced_formula_or_cask = Homebrew.with_no_api_env do
|
|
if livecheck_formula
|
|
Formulary.factory(livecheck_formula)
|
|
elsif livecheck_cask
|
|
Cask::CaskLoader.load(livecheck_cask)
|
|
end
|
|
end
|
|
|
|
# Error if a `livecheck` block references a formula/cask that was already
|
|
# referenced (or itself)
|
|
if referenced_formula_or_cask == first_formula_or_cask ||
|
|
referenced_formula_or_cask == formula_or_cask ||
|
|
references.include?(referenced_formula_or_cask)
|
|
if debug
|
|
# Print the chain of references for debugging
|
|
puts "Reference Chain:"
|
|
puts package_or_resource_name(first_formula_or_cask, full_name: full_name)
|
|
|
|
references << referenced_formula_or_cask
|
|
references.each do |ref_formula_or_cask|
|
|
puts package_or_resource_name(ref_formula_or_cask, full_name: full_name)
|
|
end
|
|
end
|
|
|
|
raise "Circular formula/cask reference encountered"
|
|
end
|
|
references << referenced_formula_or_cask
|
|
|
|
# Check the referenced formula/cask for a reference
|
|
next_referenced_formula_or_cask, next_references = resolve_livecheck_reference(
|
|
referenced_formula_or_cask,
|
|
first_formula_or_cask,
|
|
references,
|
|
full_name: full_name,
|
|
debug: debug,
|
|
)
|
|
|
|
# Returning references along with the final referenced formula/cask
|
|
# allows us to print the chain of references in the debug output
|
|
[
|
|
next_referenced_formula_or_cask || referenced_formula_or_cask,
|
|
next_references,
|
|
]
|
|
end
|
|
|
|
# Executes the livecheck logic for each formula/cask in the
|
|
# `formulae_and_casks_to_check` array and prints the results.
|
|
sig {
|
|
params(
|
|
formulae_and_casks_to_check: T::Array[T.any(Formula, Cask::Cask)],
|
|
full_name: T::Boolean,
|
|
handle_name_conflict: T::Boolean,
|
|
check_resources: T::Boolean,
|
|
json: T::Boolean,
|
|
newer_only: T::Boolean,
|
|
debug: T::Boolean,
|
|
quiet: T::Boolean,
|
|
verbose: T::Boolean,
|
|
).void
|
|
}
|
|
def run_checks(
|
|
formulae_and_casks_to_check,
|
|
full_name: false, handle_name_conflict: false, check_resources: false, json: false, newer_only: false,
|
|
debug: false, quiet: false, verbose: false
|
|
)
|
|
load_other_tap_strategies(formulae_and_casks_to_check)
|
|
|
|
ambiguous_casks = []
|
|
if handle_name_conflict
|
|
ambiguous_casks = formulae_and_casks_to_check
|
|
.group_by { |item| package_or_resource_name(item, full_name: true) }
|
|
.values
|
|
.select { |items| items.length > 1 }
|
|
.flatten
|
|
.select { |item| item.is_a?(Cask::Cask) }
|
|
end
|
|
|
|
ambiguous_names = []
|
|
unless full_name
|
|
ambiguous_names =
|
|
(formulae_and_casks_to_check - ambiguous_casks).group_by { |item| package_or_resource_name(item) }
|
|
.values
|
|
.select { |items| items.length > 1 }
|
|
.flatten
|
|
end
|
|
|
|
has_a_newer_upstream_version = T.let(false, T::Boolean)
|
|
|
|
if json && !quiet && $stderr.tty?
|
|
formulae_and_casks_total = formulae_and_casks_to_check.count
|
|
|
|
Tty.with($stderr) do |stderr|
|
|
stderr.puts Formatter.headline("Running checks", color: :blue)
|
|
end
|
|
|
|
require "ruby-progressbar"
|
|
progress = ProgressBar.create(
|
|
total: formulae_and_casks_total,
|
|
progress_mark: "#",
|
|
remainder_mark: ".",
|
|
format: " %t: [%B] %c/%C ",
|
|
output: $stderr,
|
|
)
|
|
end
|
|
|
|
formulae_checked = formulae_and_casks_to_check.map.with_index do |formula_or_cask, i|
|
|
formula = formula_or_cask if formula_or_cask.is_a?(Formula)
|
|
cask = formula_or_cask if formula_or_cask.is_a?(Cask::Cask)
|
|
|
|
use_full_name = full_name || ambiguous_names.include?(formula_or_cask)
|
|
name = package_or_resource_name(formula_or_cask, full_name: use_full_name)
|
|
|
|
referenced_formula_or_cask, livecheck_references =
|
|
resolve_livecheck_reference(formula_or_cask, full_name: use_full_name, debug: debug)
|
|
|
|
if debug && i.positive?
|
|
puts <<~EOS
|
|
|
|
----------
|
|
|
|
EOS
|
|
elsif debug
|
|
puts
|
|
end
|
|
|
|
# Check skip conditions for a referenced formula/cask
|
|
if referenced_formula_or_cask
|
|
skip_info = SkipConditions.referenced_skip_information(
|
|
referenced_formula_or_cask,
|
|
name,
|
|
full_name: use_full_name,
|
|
verbose: verbose,
|
|
)
|
|
end
|
|
|
|
skip_info ||= SkipConditions.skip_information(formula_or_cask, full_name: use_full_name, verbose: verbose)
|
|
if skip_info.present?
|
|
next skip_info if json && !newer_only
|
|
|
|
SkipConditions.print_skip_information(skip_info) if !newer_only && !quiet
|
|
next
|
|
end
|
|
|
|
formula&.head&.downloader&.quiet!
|
|
|
|
# Use the `stable` version for comparison except for installed
|
|
# head-only formulae. A formula with `stable` and `head` that's
|
|
# installed using `--head` will still use the `stable` version for
|
|
# comparison.
|
|
current = if formula
|
|
if formula.head_only?
|
|
formula.any_installed_version.version.commit
|
|
else
|
|
T.must(formula.stable).version
|
|
end
|
|
else
|
|
Version.new(formula_or_cask.version)
|
|
end
|
|
|
|
current_str = current.to_s
|
|
current = LivecheckVersion.create(formula_or_cask, current)
|
|
|
|
latest = if formula&.head_only?
|
|
T.must(formula.head).downloader.fetch_last_commit
|
|
else
|
|
version_info = latest_version(
|
|
formula_or_cask,
|
|
referenced_formula_or_cask: referenced_formula_or_cask,
|
|
livecheck_references: livecheck_references,
|
|
json: json, full_name: use_full_name, verbose: verbose, debug: debug
|
|
)
|
|
version_info[:latest] if version_info.present?
|
|
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,
|
|
latest.to_s,
|
|
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?
|
|
no_versions_msg = "Unable to get versions"
|
|
raise Livecheck::Error, no_versions_msg unless json
|
|
next if quiet
|
|
|
|
next version_info if version_info.is_a?(Hash) && version_info[:status] && version_info[:messages]
|
|
|
|
latest_info = status_hash(formula_or_cask, "error", [no_versions_msg], full_name: use_full_name,
|
|
verbose: verbose)
|
|
if check_for_resources
|
|
unless verbose
|
|
resource_version_info.map! do |info|
|
|
info.delete(:meta)
|
|
info
|
|
end
|
|
end
|
|
latest_info[:resources] = resource_version_info
|
|
end
|
|
|
|
next latest_info
|
|
end
|
|
|
|
if (m = latest.to_s.match(/(.*)-release$/)) && !current.to_s.match(/.*-release$/)
|
|
latest = Version.new(m[1])
|
|
end
|
|
|
|
latest_str = latest.to_s
|
|
latest = LivecheckVersion.create(formula_or_cask, latest)
|
|
|
|
is_outdated = if formula&.head_only?
|
|
# A HEAD-only formula is considered outdated if the latest upstream
|
|
# commit hash is different than the installed version's commit hash
|
|
(current != latest)
|
|
else
|
|
(current < latest)
|
|
end
|
|
|
|
is_newer_than_upstream = (formula&.stable? || cask) && (current > latest)
|
|
|
|
info = {}
|
|
info[:formula] = name if formula
|
|
info[:cask] = name if cask
|
|
info[:version] = {
|
|
current: current_str,
|
|
latest: latest_str,
|
|
outdated: is_outdated,
|
|
newer_than_upstream: is_newer_than_upstream,
|
|
}
|
|
info[:meta] = {
|
|
livecheckable: formula_or_cask.livecheckable?,
|
|
}
|
|
info[:meta][:head_only] = true if formula&.head_only?
|
|
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]
|
|
|
|
has_a_newer_upstream_version ||= true
|
|
|
|
if json
|
|
progress&.increment
|
|
info.delete(:meta) unless verbose
|
|
if check_for_resources && !verbose
|
|
resource_version_info.map! do |info|
|
|
info.delete(:meta)
|
|
info
|
|
end
|
|
end
|
|
next info
|
|
end
|
|
puts if debug
|
|
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
|
|
rescue => e
|
|
Homebrew.failed = true
|
|
use_full_name = full_name || ambiguous_names.include?(formula_or_cask)
|
|
|
|
if json
|
|
progress&.increment
|
|
unless quiet
|
|
status_hash(formula_or_cask, "error", [e.to_s], full_name: use_full_name,
|
|
verbose: verbose)
|
|
end
|
|
elsif !quiet
|
|
name = package_or_resource_name(formula_or_cask, full_name: use_full_name)
|
|
name += " (cask)" if ambiguous_casks.include?(formula_or_cask)
|
|
|
|
onoe "#{Tty.blue}#{name}#{Tty.reset}: #{e}"
|
|
$stderr.puts Utils::Backtrace.clean(e) if debug && !e.is_a?(Livecheck::Error)
|
|
print_resources_info(resource_version_info, verbose: verbose) if check_for_resources
|
|
nil
|
|
end
|
|
end
|
|
|
|
puts "No newer upstream versions." if newer_only && !has_a_newer_upstream_version && !debug && !json && !quiet
|
|
|
|
return unless json
|
|
|
|
if progress
|
|
progress.finish
|
|
Tty.with($stderr) do |stderr|
|
|
stderr.print "#{Tty.up}#{Tty.erase_line}" * 2
|
|
end
|
|
end
|
|
|
|
puts JSON.pretty_generate(formulae_checked.compact)
|
|
end
|
|
|
|
sig { params(package_or_resource: T.any(Formula, Cask::Cask, Resource), full_name: T::Boolean).returns(String) }
|
|
def package_or_resource_name(package_or_resource, full_name: false)
|
|
case package_or_resource
|
|
when Formula
|
|
formula_name(package_or_resource, full_name: full_name)
|
|
when Cask::Cask
|
|
cask_name(package_or_resource, full_name: full_name)
|
|
when Resource
|
|
package_or_resource.name
|
|
else
|
|
T.absurd(package_or_resource)
|
|
end
|
|
end
|
|
|
|
# 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.
|
|
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
|
|
|
|
sig {
|
|
params(
|
|
package_or_resource: T.any(Formula, Cask::Cask, Resource),
|
|
status_str: String,
|
|
messages: T.nilable(T::Array[String]),
|
|
full_name: T::Boolean,
|
|
verbose: T::Boolean,
|
|
).returns(T::Hash[Symbol, T.untyped])
|
|
}
|
|
def status_hash(package_or_resource, status_str, messages = nil, full_name: false, verbose: false)
|
|
formula = package_or_resource if package_or_resource.is_a?(Formula)
|
|
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 = {}
|
|
if formula
|
|
status_hash[:formula] = formula_name(formula, full_name: full_name)
|
|
elsif cask
|
|
status_hash[:cask] = cask_name(cask, full_name: full_name)
|
|
elsif resource
|
|
status_hash[:resource] = resource.name
|
|
end
|
|
status_hash[:status] = status_str
|
|
status_hash[:messages] = messages if messages.is_a?(Array)
|
|
|
|
status_hash[:meta] = {
|
|
livecheckable: package_or_resource.livecheckable?,
|
|
}
|
|
status_hash[:meta][:head_only] = true if formula&.head_only?
|
|
|
|
status_hash
|
|
end
|
|
|
|
# Formats and prints the livecheck result for a formula/cask/resource.
|
|
sig { params(info: T::Hash[Symbol, T.untyped], verbose: T::Boolean, ambiguous_cask: T::Boolean).void }
|
|
def print_latest_version(info, verbose: false, ambiguous_cask: false)
|
|
package_or_resource_s = info[:resource].present? ? " " : ""
|
|
package_or_resource_s += "#{Tty.blue}#{info[:formula] || info[:cask] || info[:resource]}#{Tty.reset}"
|
|
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]
|
|
"#{Tty.red}#{info[:version][:current]}#{Tty.reset}"
|
|
else
|
|
info[:version][:current]
|
|
end
|
|
|
|
latest_s = if info[:version][:outdated]
|
|
"#{Tty.green}#{info[:version][:latest]}#{Tty.reset}"
|
|
else
|
|
info[:version][:latest]
|
|
end
|
|
|
|
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[T::Hash[Symbol, T.untyped]], 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
|
|
|
|
sig {
|
|
params(
|
|
livecheck_url: T.any(String, Symbol),
|
|
package_or_resource: T.any(Formula, Cask::Cask, Resource),
|
|
).returns(T.nilable(String))
|
|
}
|
|
def livecheck_url_to_string(livecheck_url, package_or_resource)
|
|
case livecheck_url
|
|
when String
|
|
livecheck_url
|
|
when :url
|
|
package_or_resource.url&.to_s if package_or_resource.is_a?(Cask::Cask) || package_or_resource.is_a?(Resource)
|
|
when :head, :stable
|
|
package_or_resource.send(livecheck_url)&.url if package_or_resource.is_a?(Formula)
|
|
when :homepage
|
|
package_or_resource.homepage unless package_or_resource.is_a?(Resource)
|
|
end
|
|
end
|
|
|
|
# Returns an Array containing the formula/cask/resource URLs that can be used by livecheck.
|
|
sig { params(package_or_resource: T.any(Formula, Cask::Cask, Resource)).returns(T::Array[String]) }
|
|
def checkable_urls(package_or_resource)
|
|
urls = []
|
|
|
|
case package_or_resource
|
|
when Formula
|
|
if package_or_resource.stable
|
|
urls << T.must(package_or_resource.stable).url
|
|
urls.concat(T.must(package_or_resource.stable).mirrors)
|
|
end
|
|
urls << T.must(package_or_resource.head).url if package_or_resource.head
|
|
urls << package_or_resource.homepage if package_or_resource.homepage
|
|
when Cask::Cask
|
|
urls << package_or_resource.url.to_s if package_or_resource.url
|
|
urls << package_or_resource.homepage if package_or_resource.homepage
|
|
when Resource
|
|
urls << package_or_resource.url
|
|
else
|
|
T.absurd(package_or_resource)
|
|
end
|
|
|
|
urls.compact.uniq
|
|
end
|
|
|
|
# Preprocesses and returns the URL used by livecheck.
|
|
sig { params(url: String).returns(String) }
|
|
def preprocess_url(url)
|
|
begin
|
|
uri = Addressable::URI.parse url
|
|
rescue Addressable::URI::InvalidURIError
|
|
return url
|
|
end
|
|
|
|
host = uri.host
|
|
path = uri.path
|
|
return url if host.nil? || path.nil?
|
|
|
|
host = "github.com" if host == "github.s3.amazonaws.com"
|
|
path = path.delete_prefix("/").delete_suffix(".git")
|
|
scheme = uri.scheme
|
|
|
|
if host == "github.com"
|
|
return url if path.match? %r{/releases/latest/?$}
|
|
|
|
owner, repo = path.delete_prefix("downloads/").split("/")
|
|
url = "#{scheme}://#{host}/#{owner}/#{repo}.git"
|
|
elsif GITEA_INSTANCES.include?(host)
|
|
return url if path.match? %r{/releases/latest/?$}
|
|
|
|
owner, repo = path.split("/")
|
|
url = "#{scheme}://#{host}/#{owner}/#{repo}.git"
|
|
elsif GOGS_INSTANCES.include?(host)
|
|
owner, repo = path.split("/")
|
|
url = "#{scheme}://#{host}/#{owner}/#{repo}.git"
|
|
# sourcehut
|
|
elsif host == "git.sr.ht"
|
|
owner, repo = path.split("/")
|
|
url = "#{scheme}://#{host}/#{owner}/#{repo}"
|
|
# GitLab (gitlab.com or self-hosted)
|
|
elsif path.include?("/-/archive/")
|
|
url = url.sub(%r{/-/archive/.*$}i, ".git")
|
|
end
|
|
|
|
url
|
|
end
|
|
|
|
# livecheck should fetch a URL using brewed curl if the formula/cask
|
|
# contains a `stable`/`url` or `head` URL `using: :homebrew_curl` that
|
|
# shares the same root domain.
|
|
sig { params(formula_or_cask: T.any(Formula, Cask::Cask), url: String).returns(T::Boolean) }
|
|
def use_homebrew_curl?(formula_or_cask, url)
|
|
url_root_domain = Addressable::URI.parse(url)&.domain
|
|
return false if url_root_domain.blank?
|
|
|
|
# Collect root domains of URLs with `using: :homebrew_curl`
|
|
homebrew_curl_root_domains = []
|
|
case formula_or_cask
|
|
when Formula
|
|
[:stable, :head].each do |spec_name|
|
|
next unless (spec = formula_or_cask.send(spec_name))
|
|
next if spec.using != :homebrew_curl
|
|
|
|
domain = Addressable::URI.parse(spec.url)&.domain
|
|
homebrew_curl_root_domains << domain if domain.present?
|
|
end
|
|
when Cask::Cask
|
|
return false if formula_or_cask.url.using != :homebrew_curl
|
|
|
|
domain = Addressable::URI.parse(formula_or_cask.url.to_s)&.domain
|
|
homebrew_curl_root_domains << domain if domain.present?
|
|
end
|
|
|
|
homebrew_curl_root_domains.include?(url_root_domain)
|
|
end
|
|
|
|
# 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.
|
|
sig {
|
|
params(
|
|
formula_or_cask: T.any(Formula, Cask::Cask),
|
|
referenced_formula_or_cask: T.nilable(T.any(Formula, Cask::Cask)),
|
|
livecheck_references: T::Array[T.any(Formula, Cask::Cask)],
|
|
json: T::Boolean,
|
|
full_name: T::Boolean,
|
|
verbose: T::Boolean,
|
|
debug: T::Boolean,
|
|
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
|
}
|
|
def latest_version(
|
|
formula_or_cask,
|
|
referenced_formula_or_cask: nil,
|
|
livecheck_references: [],
|
|
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)
|
|
|
|
has_livecheckable = formula_or_cask.livecheckable?
|
|
livecheck = formula_or_cask.livecheck
|
|
referenced_livecheck = referenced_formula_or_cask&.livecheck
|
|
|
|
livecheck_url = livecheck.url || referenced_livecheck&.url
|
|
livecheck_regex = livecheck.regex || referenced_livecheck&.regex
|
|
livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
|
|
livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
|
|
|
|
livecheck_url_string = livecheck_url_to_string(
|
|
livecheck_url,
|
|
referenced_formula_or_cask || formula_or_cask,
|
|
)
|
|
|
|
urls = [livecheck_url_string] if livecheck_url_string
|
|
urls ||= checkable_urls(referenced_formula_or_cask || formula_or_cask)
|
|
|
|
if debug
|
|
if formula
|
|
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, full_name: full_name)}"
|
|
end
|
|
puts "Livecheckable?: #{has_livecheckable ? "Yes" : "No"}"
|
|
|
|
livecheck_references.each do |ref_formula_or_cask|
|
|
case ref_formula_or_cask
|
|
when Formula
|
|
puts "Formula Ref: #{formula_name(ref_formula_or_cask, full_name: full_name)}"
|
|
when Cask::Cask
|
|
puts "Cask Ref: #{cask_name(ref_formula_or_cask, full_name: full_name)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
checked_urls = []
|
|
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
|
|
|
|
next if strategy.blank?
|
|
|
|
homebrew_curl = case strategy_name
|
|
when "PageMatch", "HeaderMatch"
|
|
use_homebrew_curl?((referenced_formula_or_cask || formula_or_cask), url)
|
|
end
|
|
puts "Homebrew curl?: Yes" if debug && homebrew_curl.present?
|
|
|
|
strategy_args = {
|
|
regex: livecheck_regex,
|
|
homebrew_curl: homebrew_curl,
|
|
}
|
|
# TODO: Set `cask`/`url` args based on the presence of the keyword arg
|
|
# in the strategy's `#find_versions` method once we figure out why
|
|
# `strategy.method(:find_versions).parameters` isn't working as
|
|
# expected.
|
|
if strategy_name == "ExtractPlist"
|
|
strategy_args[:cask] = cask if cask.present?
|
|
else
|
|
strategy_args[:url] = url
|
|
end
|
|
strategy_args.compact!
|
|
|
|
strategy_data = strategy.find_versions(**strategy_args, &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(formula_or_cask, "error", messages, full_name: full_name, 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
|
|
|
|
version_info = {
|
|
latest: Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(formula_or_cask, v) }),
|
|
}
|
|
|
|
if json && verbose
|
|
version_info[:meta] = {}
|
|
|
|
if livecheck_references.present?
|
|
version_info[:meta][:references] = livecheck_references.map do |ref_formula_or_cask|
|
|
case ref_formula_or_cask
|
|
when Formula
|
|
{ formula: formula_name(ref_formula_or_cask, full_name: full_name) }
|
|
when Cask::Cask
|
|
{ cask: cask_name(ref_formula_or_cask, full_name: full_name) }
|
|
end
|
|
end
|
|
end
|
|
|
|
version_info[:meta][:url] = {}
|
|
version_info[:meta][:url][:symbol] = livecheck_url if livecheck_url.is_a?(Symbol) && livecheck_url_string
|
|
version_info[:meta][:url][:original] = original_url
|
|
version_info[:meta][:url][:processed] = url if url != original_url
|
|
if strategy_data[:url].present? && strategy_data[:url] != url
|
|
version_info[:meta][:url][:strategy] = strategy_data[:url]
|
|
end
|
|
version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
|
|
version_info[:meta][:url][:homebrew_curl] = homebrew_curl if homebrew_curl.present?
|
|
|
|
version_info[:meta][:strategy] = strategy.present? ? strategy_name : nil
|
|
version_info[:meta][:strategies] = strategies.map { |s| livecheck_strategy_names[s] } if strategies.present?
|
|
version_info[:meta][:regex] = regex.inspect if regex.present?
|
|
version_info[:meta][:cached] = true if strategy_data[:cached] == true
|
|
end
|
|
|
|
return version_info
|
|
end
|
|
nil
|
|
end
|
|
|
|
# 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,
|
|
formula_latest: String,
|
|
json: T::Boolean,
|
|
debug: T::Boolean,
|
|
quiet: T::Boolean,
|
|
verbose: T::Boolean,
|
|
).returns(T::Hash[Symbol, T.untyped])
|
|
}
|
|
def resource_version(
|
|
resource,
|
|
formula_latest,
|
|
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 = []
|
|
urls.each_with_index do |original_url, i|
|
|
url = original_url.gsub(Constants::LATEST_VERSION, formula_latest)
|
|
|
|
# Only preprocess the URL when it's appropriate
|
|
url = preprocess_url(url) unless STRATEGY_SYMBOLS_TO_SKIP_PREPROCESS_URL.include?(livecheck_strategy)
|
|
|
|
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_args = {
|
|
url: url,
|
|
regex: livecheck_regex,
|
|
homebrew_curl: false,
|
|
}.compact
|
|
|
|
strategy_data = strategy.find_versions(**strategy_args, &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 = T.must(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 Utils::Backtrace.clean(e) if debug && !e.is_a?(Livecheck::Error)
|
|
nil
|
|
end
|
|
end
|
|
resource_version_info
|
|
end
|
|
end
|
|
end
|