2024-07-06 10:00:47 -04:00
|
|
|
# typed: strict
|
2020-08-08 07:16:06 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2024-07-14 08:49:39 -04:00
|
|
|
require "utils/curl"
|
|
|
|
|
2020-08-08 07:16:06 +05:30
|
|
|
module Homebrew
|
|
|
|
module Livecheck
|
|
|
|
# The `Livecheck::Strategy` module contains the various strategies as well
|
|
|
|
# as some general-purpose methods for working with them. Within the context
|
|
|
|
# of the `brew livecheck` command, strategies are established procedures
|
|
|
|
# for finding new software versions at a given source.
|
|
|
|
module Strategy
|
2023-05-02 23:58:51 +02:00
|
|
|
extend Utils::Curl
|
|
|
|
|
2020-08-08 07:16:06 +05:30
|
|
|
module_function
|
|
|
|
|
2021-08-10 18:38:21 -04:00
|
|
|
# {Strategy} priorities informally range from 1 to 10, where 10 is the
|
2020-08-08 07:16:06 +05:30
|
|
|
# highest priority. 5 is the default priority because it's roughly in
|
|
|
|
# the middle of this range. Strategies with a priority of 0 (or lower)
|
|
|
|
# are ignored.
|
|
|
|
DEFAULT_PRIORITY = 5
|
2021-06-04 13:12:01 -04:00
|
|
|
|
2021-06-04 13:16:02 -04:00
|
|
|
# cURL's default `--connect-timeout` value can be up to two minutes, so
|
|
|
|
# we need to use a more reasonable duration (in seconds) to avoid a
|
|
|
|
# lengthy wait when a connection can't be established.
|
|
|
|
CURL_CONNECT_TIMEOUT = 10
|
|
|
|
|
|
|
|
# cURL does not set a default `--max-time` value, so we provide a value
|
|
|
|
# to ensure cURL will time out in a reasonable amount of time.
|
2024-07-04 20:09:36 -04:00
|
|
|
CURL_MAX_TIME = T.let(CURL_CONNECT_TIMEOUT + 5, Integer)
|
2021-06-04 13:16:02 -04:00
|
|
|
|
|
|
|
# The `curl` process will sometimes hang indefinitely (despite setting
|
|
|
|
# the `--max-time` argument) and it needs to be quit for livecheck to
|
|
|
|
# continue. This value is used to set the `timeout` argument on
|
2021-08-10 18:38:21 -04:00
|
|
|
# `Utils::Curl` method calls in {Strategy}.
|
2024-07-04 20:09:36 -04:00
|
|
|
CURL_PROCESS_TIMEOUT = T.let(CURL_MAX_TIME + 5, Integer)
|
2021-06-04 13:16:02 -04:00
|
|
|
|
2022-04-22 13:02:13 -04:00
|
|
|
# The maximum number of redirections that `curl` should allow.
|
|
|
|
MAX_REDIRECTIONS = 5
|
|
|
|
|
|
|
|
# This value is passed to `#parse_curl_output` to ensure that the limit
|
|
|
|
# for the number of responses it will parse corresponds to the maximum
|
|
|
|
# number of responses in this context. The `+ 1` here accounts for the
|
|
|
|
# situation where there are exactly `MAX_REDIRECTIONS` number of
|
|
|
|
# redirections, followed by a final `200 OK` response.
|
2024-07-04 20:09:36 -04:00
|
|
|
MAX_PARSE_ITERATIONS = T.let(MAX_REDIRECTIONS + 1, Integer)
|
2022-04-22 13:02:13 -04:00
|
|
|
|
2021-08-10 18:38:21 -04:00
|
|
|
# Baseline `curl` arguments used in {Strategy} methods.
|
2024-07-04 20:09:36 -04:00
|
|
|
DEFAULT_CURL_ARGS = T.let([
|
2021-06-04 13:16:02 -04:00
|
|
|
# Follow redirections to handle mirrors, relocations, etc.
|
|
|
|
"--location",
|
2022-04-22 13:02:13 -04:00
|
|
|
"--max-redirs", MAX_REDIRECTIONS.to_s,
|
2021-12-30 16:07:29 -05:00
|
|
|
# Avoid progress bar text, so we can reliably identify `curl` error
|
|
|
|
# messages in output
|
2022-04-22 13:02:13 -04:00
|
|
|
"--silent"
|
2024-07-04 20:09:36 -04:00
|
|
|
].freeze, T::Array[String])
|
2021-06-04 13:16:02 -04:00
|
|
|
|
|
|
|
# `curl` arguments used in `Strategy#page_content` method.
|
2024-07-04 20:09:36 -04:00
|
|
|
PAGE_CONTENT_CURL_ARGS = T.let(([
|
2021-06-04 13:16:02 -04:00
|
|
|
"--compressed",
|
2024-02-25 02:30:33 +05:30
|
|
|
# Return an error when the HTTP response code is 400 or greater but
|
|
|
|
# continue to return body content
|
2024-02-24 00:19:24 +05:30
|
|
|
"--fail-with-body",
|
2021-06-04 13:16:02 -04:00
|
|
|
# Include HTTP response headers in output, so we can identify the
|
|
|
|
# final URL after any redirections
|
|
|
|
"--include",
|
2024-07-04 20:09:36 -04:00
|
|
|
] + DEFAULT_CURL_ARGS).freeze, T::Array[String])
|
2021-06-04 13:16:02 -04:00
|
|
|
|
2021-08-10 18:38:21 -04:00
|
|
|
# Baseline `curl` options used in {Strategy} methods.
|
2024-07-04 20:09:36 -04:00
|
|
|
DEFAULT_CURL_OPTIONS = T.let({
|
2021-09-06 22:56:25 -04:00
|
|
|
print_stdout: false,
|
|
|
|
print_stderr: false,
|
|
|
|
debug: false,
|
|
|
|
verbose: false,
|
|
|
|
timeout: CURL_PROCESS_TIMEOUT,
|
|
|
|
connect_timeout: CURL_CONNECT_TIMEOUT,
|
|
|
|
max_time: CURL_MAX_TIME,
|
|
|
|
retries: 0,
|
2024-07-04 20:09:36 -04:00
|
|
|
}.freeze, T::Hash[Symbol, T.untyped])
|
2021-06-04 13:16:02 -04:00
|
|
|
|
2021-07-28 13:20:12 -04:00
|
|
|
# A regex used to identify a tarball extension at the end of a string.
|
2021-08-17 19:15:07 -04:00
|
|
|
TARBALL_EXTENSION_REGEX = /
|
|
|
|
\.t
|
|
|
|
(?:ar(?:\.(?:bz2|gz|lz|lzma|lzo|xz|Z|zst))?|
|
|
|
|
b2|bz2?|z2|az|gz|lz|lzma|xz|Z|aZ|zst)
|
|
|
|
$
|
2024-01-18 22:18:42 +00:00
|
|
|
/ix
|
2021-07-28 13:20:12 -04:00
|
|
|
|
2021-08-10 11:09:55 -04:00
|
|
|
# An error message to use when a `strategy` block returns a value of
|
|
|
|
# an inappropriate type.
|
|
|
|
INVALID_BLOCK_RETURN_VALUE_MSG = "Return value of a strategy block must be a string or array of strings."
|
|
|
|
|
2020-11-05 17:17:03 -05:00
|
|
|
# Creates and/or returns a `@strategies` `Hash`, which maps a snake
|
|
|
|
# case strategy name symbol (e.g. `:page_match`) to the associated
|
2021-08-10 18:38:21 -04:00
|
|
|
# strategy.
|
2020-08-08 07:16:06 +05:30
|
|
|
#
|
|
|
|
# At present, this should only be called after tap strategies have been
|
|
|
|
# loaded, otherwise livecheck won't be able to use them.
|
|
|
|
# @return [Hash]
|
2021-08-10 18:24:51 -04:00
|
|
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
2020-08-08 07:16:06 +05:30
|
|
|
def strategies
|
2024-07-06 10:00:47 -04:00
|
|
|
@strategies ||= T.let(Strategy.constants.sort.each_with_object({}) do |const_symbol, hash|
|
2021-08-09 18:18:12 -04:00
|
|
|
constant = Strategy.const_get(const_symbol)
|
|
|
|
next unless constant.is_a?(Class)
|
|
|
|
|
2023-03-07 10:11:59 -08:00
|
|
|
key = Utils.underscore(const_symbol).to_sym
|
2024-07-06 10:00:47 -04:00
|
|
|
hash[key] = constant
|
|
|
|
end, T.nilable(T::Hash[Symbol, T.untyped]))
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
|
|
|
private_class_method :strategies
|
|
|
|
|
2021-08-10 18:38:21 -04:00
|
|
|
# Returns the strategy that corresponds to the provided `Symbol` (or
|
|
|
|
# `nil` if there is no matching strategy).
|
2020-11-05 17:17:03 -05:00
|
|
|
#
|
2021-08-10 18:38:21 -04:00
|
|
|
# @param symbol [Symbol, nil] the strategy name in snake case as a
|
|
|
|
# `Symbol` (e.g. `:page_match`)
|
|
|
|
# @return [Class, nil]
|
2022-01-27 12:23:40 -05:00
|
|
|
sig { params(symbol: T.nilable(Symbol)).returns(T.untyped) }
|
2020-08-08 07:16:06 +05:30
|
|
|
def from_symbol(symbol)
|
2021-08-10 18:24:51 -04:00
|
|
|
strategies[symbol] if symbol.present?
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
# Returns an array of strategies that apply to the provided URL.
|
2020-11-05 17:17:03 -05:00
|
|
|
#
|
2020-08-08 07:16:06 +05:30
|
|
|
# @param url [String] the URL to check for matching strategies
|
2021-08-10 18:38:21 -04:00
|
|
|
# @param livecheck_strategy [Symbol] a strategy symbol from the
|
|
|
|
# `livecheck` block
|
2020-12-05 11:49:47 -05:00
|
|
|
# @param regex_provided [Boolean] whether a regex is provided in the
|
2020-08-08 07:16:06 +05:30
|
|
|
# `livecheck` block
|
2021-08-10 18:38:21 -04:00
|
|
|
# @param block_provided [Boolean] whether a `strategy` block is provided
|
|
|
|
# in the `livecheck` block
|
2020-08-08 07:16:06 +05:30
|
|
|
# @return [Array]
|
2021-08-10 18:24:51 -04:00
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
url: String,
|
|
|
|
livecheck_strategy: T.nilable(Symbol),
|
|
|
|
regex_provided: T::Boolean,
|
|
|
|
block_provided: T::Boolean,
|
|
|
|
).returns(T::Array[T.untyped])
|
|
|
|
}
|
2024-07-25 13:13:36 -04:00
|
|
|
def from_url(url, livecheck_strategy: nil, regex_provided: false, block_provided: false)
|
2023-02-27 17:50:13 -05:00
|
|
|
usable_strategies = strategies.select do |strategy_symbol, strategy|
|
2020-12-05 11:49:47 -05:00
|
|
|
if strategy == PageMatch
|
2023-02-23 12:40:07 -05:00
|
|
|
# Only treat the strategy as usable if the `livecheck` block
|
|
|
|
# contains a regex and/or `strategy` block
|
2021-01-07 13:49:05 -08:00
|
|
|
next if !regex_provided && !block_provided
|
2023-03-02 15:19:00 -05:00
|
|
|
elsif [Json, Xml, Yaml].include?(strategy)
|
2023-02-23 12:40:07 -05:00
|
|
|
# Only treat the strategy as usable if the `livecheck` block
|
2023-02-27 17:50:13 -05:00
|
|
|
# specifies the strategy and contains a `strategy` block
|
|
|
|
next if (livecheck_strategy != strategy_symbol) || !block_provided
|
2020-12-05 11:49:47 -05:00
|
|
|
elsif strategy.const_defined?(:PRIORITY) &&
|
2023-04-01 18:56:42 -07:00
|
|
|
!strategy.const_get(:PRIORITY).positive? &&
|
2023-02-27 18:45:49 -05:00
|
|
|
livecheck_strategy != strategy_symbol
|
2020-12-05 11:49:47 -05:00
|
|
|
# Ignore strategies with a priority of 0 or lower, unless the
|
|
|
|
# strategy is specified in the `livecheck` block
|
|
|
|
next
|
|
|
|
end
|
2020-08-08 07:16:06 +05:30
|
|
|
|
|
|
|
strategy.respond_to?(:match?) && strategy.match?(url)
|
2023-02-27 17:50:13 -05:00
|
|
|
end.values
|
2020-08-08 07:16:06 +05:30
|
|
|
|
|
|
|
# Sort usable strategies in descending order by priority, using the
|
|
|
|
# DEFAULT_PRIORITY when a strategy doesn't contain a PRIORITY constant
|
|
|
|
usable_strategies.sort_by do |strategy|
|
2023-04-01 18:56:42 -07:00
|
|
|
(strategy.const_defined?(:PRIORITY) ? -strategy.const_get(:PRIORITY) : -DEFAULT_PRIORITY)
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
|
|
|
end
|
2020-12-12 21:56:07 +01:00
|
|
|
|
2025-02-04 10:30:16 -05:00
|
|
|
# Creates `curl` `--data` or `--json` arguments (for `POST` requests`)
|
|
|
|
# from related `livecheck` block `url` options.
|
|
|
|
#
|
|
|
|
# @param post_form [Hash, nil] data to encode using `URI::encode_www_form`
|
|
|
|
# @param post_json [Hash, nil] data to encode using `JSON::generate`
|
|
|
|
# @return [Array]
|
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
post_form: T.nilable(T::Hash[T.any(String, Symbol), String]),
|
|
|
|
post_json: T.nilable(T::Hash[T.any(String, Symbol), String]),
|
|
|
|
).returns(T::Array[String])
|
|
|
|
}
|
|
|
|
def post_args(post_form: nil, post_json: nil)
|
|
|
|
if post_form.present?
|
|
|
|
require "uri"
|
|
|
|
["--data", URI.encode_www_form(post_form)]
|
|
|
|
elsif post_json.present?
|
|
|
|
require "json"
|
|
|
|
["--json", JSON.generate(post_json)]
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-08-10 18:38:21 -04:00
|
|
|
# Collects HTTP response headers, starting with the provided URL.
|
|
|
|
# Redirections will be followed and all the response headers are
|
|
|
|
# collected into an array of hashes.
|
|
|
|
#
|
|
|
|
# @param url [String] the URL to fetch
|
2025-02-04 10:30:16 -05:00
|
|
|
# @param url_options [Hash] options to modify curl behavior
|
2021-11-23 23:22:41 -05:00
|
|
|
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
2021-08-10 18:38:21 -04:00
|
|
|
# @return [Array]
|
2025-02-04 10:30:16 -05:00
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
url: String,
|
|
|
|
url_options: T::Hash[Symbol, T.untyped],
|
|
|
|
homebrew_curl: T::Boolean,
|
|
|
|
).returns(T::Array[T::Hash[String, String]])
|
|
|
|
}
|
|
|
|
def self.page_headers(url, url_options: {}, homebrew_curl: false)
|
2020-12-19 00:46:18 -05:00
|
|
|
headers = []
|
2020-12-13 12:21:59 +01:00
|
|
|
|
2025-02-04 10:30:16 -05:00
|
|
|
if url_options[:post_form].present? || url_options[:post_json].present?
|
|
|
|
curl_post_args = ["--request", "POST", *post_args(
|
|
|
|
post_form: url_options[:post_form],
|
|
|
|
post_json: url_options[:post_json],
|
|
|
|
)]
|
|
|
|
end
|
|
|
|
|
2020-12-14 02:10:38 +01:00
|
|
|
[:default, :browser].each do |user_agent|
|
2023-05-09 07:25:00 +02:00
|
|
|
begin
|
|
|
|
parsed_output = curl_headers(
|
2025-02-04 10:30:16 -05:00
|
|
|
*curl_post_args,
|
2023-05-15 13:14:13 -04:00
|
|
|
"--max-redirs",
|
|
|
|
MAX_REDIRECTIONS.to_s,
|
2023-05-09 07:25:00 +02:00
|
|
|
url,
|
|
|
|
wanted_headers: ["location", "content-disposition"],
|
|
|
|
use_homebrew_curl: homebrew_curl,
|
2024-03-07 16:20:20 +00:00
|
|
|
user_agent:,
|
2023-05-09 07:25:00 +02:00
|
|
|
**DEFAULT_CURL_OPTIONS,
|
|
|
|
)
|
|
|
|
rescue ErrorDuringExecution
|
|
|
|
next
|
|
|
|
end
|
2020-12-14 02:10:38 +01:00
|
|
|
|
2021-05-04 16:35:21 -04:00
|
|
|
parsed_output[:responses].each { |response| headers << response[:headers] }
|
|
|
|
break if headers.present?
|
2020-12-13 12:21:59 +01:00
|
|
|
end
|
|
|
|
|
2020-12-14 02:10:38 +01:00
|
|
|
headers
|
2020-12-12 21:56:07 +01:00
|
|
|
end
|
|
|
|
|
2020-12-22 22:46:52 -05:00
|
|
|
# Fetches the content at the URL and returns a hash containing the
|
|
|
|
# content and, if there are any redirections, the final URL.
|
2020-12-14 13:03:10 -05:00
|
|
|
# If `curl` encounters an error, the hash will contain a `:messages`
|
|
|
|
# array with the error message instead.
|
2020-12-22 22:46:52 -05:00
|
|
|
#
|
|
|
|
# @param url [String] the URL of the content to check
|
2025-02-04 10:30:16 -05:00
|
|
|
# @param url_options [Hash] options to modify curl behavior
|
2021-11-23 23:22:41 -05:00
|
|
|
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
2020-12-22 22:46:52 -05:00
|
|
|
# @return [Hash]
|
2025-02-04 10:30:16 -05:00
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
url: String,
|
|
|
|
url_options: T::Hash[Symbol, T.untyped],
|
|
|
|
homebrew_curl: T::Boolean,
|
|
|
|
).returns(T::Hash[Symbol, T.untyped])
|
|
|
|
}
|
|
|
|
def self.page_content(url, url_options: {}, homebrew_curl: false)
|
|
|
|
if url_options[:post_form].present? || url_options[:post_json].present?
|
|
|
|
curl_post_args = ["--request", "POST", *post_args(
|
|
|
|
post_form: url_options[:post_form],
|
|
|
|
post_json: url_options[:post_json],
|
|
|
|
)]
|
|
|
|
end
|
|
|
|
|
2023-04-01 18:56:42 -07:00
|
|
|
stderr = T.let(nil, T.nilable(String))
|
2021-06-09 23:54:56 +02:00
|
|
|
[:default, :browser].each do |user_agent|
|
2023-05-02 23:58:51 +02:00
|
|
|
stdout, stderr, status = curl_output(
|
2025-02-04 10:30:16 -05:00
|
|
|
*curl_post_args,
|
2021-06-09 23:54:56 +02:00
|
|
|
*PAGE_CONTENT_CURL_ARGS, url,
|
|
|
|
**DEFAULT_CURL_OPTIONS,
|
2024-05-23 13:08:25 -04:00
|
|
|
use_homebrew_curl: homebrew_curl || !curl_supports_fail_with_body?,
|
2024-03-07 16:20:20 +00:00
|
|
|
user_agent:
|
2021-06-09 23:54:56 +02:00
|
|
|
)
|
|
|
|
next unless status.success?
|
2020-12-14 13:03:10 -05:00
|
|
|
|
2021-06-09 23:54:56 +02:00
|
|
|
# stdout contains the header information followed by the page content.
|
|
|
|
# We use #scrub here to avoid "invalid byte sequence in UTF-8" errors.
|
|
|
|
output = stdout.scrub
|
2020-12-14 13:03:10 -05:00
|
|
|
|
2021-06-09 23:54:56 +02:00
|
|
|
# Separate the head(s)/body and identify the final URL (after any
|
|
|
|
# redirections)
|
2022-04-22 13:02:13 -04:00
|
|
|
parsed_output = parse_curl_output(output, max_iterations: MAX_PARSE_ITERATIONS)
|
2021-05-04 16:35:21 -04:00
|
|
|
final_url = curl_response_last_location(parsed_output[:responses], absolutize: true, base_url: url)
|
2020-12-14 13:03:10 -05:00
|
|
|
|
2021-05-04 16:35:21 -04:00
|
|
|
data = { content: parsed_output[:body] }
|
|
|
|
data[:final_url] = final_url if final_url.present? && final_url != url
|
2021-06-09 23:54:56 +02:00
|
|
|
return data
|
2020-12-14 13:03:10 -05:00
|
|
|
end
|
|
|
|
|
2021-08-25 11:58:28 -04:00
|
|
|
error_msgs = stderr&.scan(/^curl:.+$/)
|
|
|
|
{ messages: error_msgs.presence || ["cURL failed without a detectable error"] }
|
2020-12-12 21:56:07 +01:00
|
|
|
end
|
2021-08-10 11:09:55 -04:00
|
|
|
|
|
|
|
# Handles the return value from a `strategy` block in a `livecheck`
|
|
|
|
# block.
|
|
|
|
#
|
|
|
|
# @param value [] the return value from a `strategy` block
|
|
|
|
# @return [Array]
|
|
|
|
sig { params(value: T.untyped).returns(T::Array[String]) }
|
|
|
|
def self.handle_block_return(value)
|
|
|
|
case value
|
|
|
|
when String
|
|
|
|
[value]
|
|
|
|
when Array
|
|
|
|
value.compact.uniq
|
|
|
|
when nil
|
|
|
|
[]
|
|
|
|
else
|
|
|
|
raise TypeError, INVALID_BLOCK_RETURN_VALUE_MSG
|
|
|
|
end
|
|
|
|
end
|
2020-08-08 07:16:06 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
require_relative "strategy/apache"
|
|
|
|
require_relative "strategy/bitbucket"
|
2020-12-11 04:31:14 +01:00
|
|
|
require_relative "strategy/cpan"
|
2024-02-08 09:59:21 -05:00
|
|
|
require_relative "strategy/crate"
|
2021-03-17 01:58:31 +05:30
|
|
|
require_relative "strategy/electron_builder"
|
2021-04-04 03:00:34 +02:00
|
|
|
require_relative "strategy/extract_plist"
|
2020-08-08 07:16:06 +05:30
|
|
|
require_relative "strategy/git"
|
2020-12-02 18:04:22 +05:30
|
|
|
require_relative "strategy/github_latest"
|
2023-05-16 14:22:10 -04:00
|
|
|
require_relative "strategy/github_releases"
|
2020-08-08 07:16:06 +05:30
|
|
|
require_relative "strategy/gnome"
|
|
|
|
require_relative "strategy/gnu"
|
|
|
|
require_relative "strategy/hackage"
|
2020-12-14 02:49:32 +01:00
|
|
|
require_relative "strategy/header_match"
|
2023-02-23 12:40:07 -05:00
|
|
|
require_relative "strategy/json"
|
2020-08-08 07:16:06 +05:30
|
|
|
require_relative "strategy/launchpad"
|
|
|
|
require_relative "strategy/npm"
|
|
|
|
require_relative "strategy/page_match"
|
|
|
|
require_relative "strategy/pypi"
|
|
|
|
require_relative "strategy/sourceforge"
|
2020-12-12 21:59:04 +01:00
|
|
|
require_relative "strategy/sparkle"
|
2023-02-27 17:03:32 -05:00
|
|
|
require_relative "strategy/xml"
|
2020-08-08 07:16:06 +05:30
|
|
|
require_relative "strategy/xorg"
|
2023-03-02 15:19:00 -05:00
|
|
|
require_relative "strategy/yaml"
|