Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

livecheck: add Options class #19293

Merged
merged 7 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions Library/Homebrew/livecheck.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require "livecheck/constants"
require "livecheck/options"
require "cask/cask"

# The {Livecheck} class implements the DSL methods used in a formula's, cask's
Expand All @@ -15,6 +16,10 @@
class Livecheck
extend Forwardable

# Options to modify livecheck's behavior.
sig { returns(Homebrew::Livecheck::Options) }
attr_reader :options

# A very brief description of why the formula/cask/resource is skipped (e.g.
# `No longer developed or maintained`).
sig { returns(T.nilable(String)) }
Expand All @@ -24,13 +29,10 @@ class Livecheck
sig { returns(T.nilable(Proc)) }
attr_reader :strategy_block

# Options used by `Strategy` methods to modify `curl` behavior.
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
attr_reader :url_options

sig { params(package_or_resource: T.any(Cask::Cask, T.class_of(Formula), Resource)).void }
def initialize(package_or_resource)
@package_or_resource = package_or_resource
@options = T.let(Homebrew::Livecheck::Options.new, Homebrew::Livecheck::Options)
@referenced_cask_name = T.let(nil, T.nilable(String))
@referenced_formula_name = T.let(nil, T.nilable(String))
@regex = T.let(nil, T.nilable(Regexp))
Expand All @@ -40,7 +42,6 @@ def initialize(package_or_resource)
@strategy_block = T.let(nil, T.nilable(Proc))
@throttle = T.let(nil, T.nilable(Integer))
@url = T.let(nil, T.any(NilClass, String, Symbol))
@url_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped]))
end

# Sets the `@referenced_cask_name` instance variable to the provided `String`
Expand Down Expand Up @@ -169,16 +170,18 @@ def throttle(rate = T.unsafe(nil))
sig {
params(
# URL to check for version information.
url: T.any(String, Symbol),
post_form: T.nilable(T::Hash[Symbol, String]),
post_json: T.nilable(T::Hash[Symbol, String]),
url: T.any(String, Symbol),
homebrew_curl: T.nilable(T::Boolean),
post_form: T.nilable(T::Hash[Symbol, String]),
post_json: T.nilable(T::Hash[Symbol, String]),
).returns(T.nilable(T.any(String, Symbol)))
}
def url(url = T.unsafe(nil), post_form: nil, post_json: nil)
def url(url = T.unsafe(nil), homebrew_curl: nil, post_form: nil, post_json: nil)
raise ArgumentError, "Only use `post_form` or `post_json`, not both" if post_form && post_json

options = { post_form:, post_json: }.compact
@url_options = options if options.present?
@options.homebrew_curl = homebrew_curl unless homebrew_curl.nil?
@options.post_form = post_form unless post_form.nil?
@options.post_json = post_json unless post_json.nil?

case url
when nil
Expand All @@ -190,6 +193,7 @@ def url(url = T.unsafe(nil), post_form: nil, post_json: nil)
end
end

delegate url_options: :@options
delegate version: :@package_or_resource
delegate arch: :@package_or_resource
private :version, :arch
Expand All @@ -198,15 +202,15 @@ def url(url = T.unsafe(nil), post_form: nil, post_json: nil)
sig { returns(T::Hash[String, T.untyped]) }
def to_hash
{
"cask" => @referenced_cask_name,
"formula" => @referenced_formula_name,
"regex" => @regex,
"skip" => @skip,
"skip_msg" => @skip_msg,
"strategy" => @strategy,
"throttle" => @throttle,
"url" => @url,
"url_options" => @url_options,
"options" => @options.to_hash,
"cask" => @referenced_cask_name,
"formula" => @referenced_formula_name,
"regex" => @regex,
"skip" => @skip,
"skip_msg" => @skip_msg,
"strategy" => @strategy,
"throttle" => @throttle,
"url" => @url,
}
end
end
70 changes: 43 additions & 27 deletions Library/Homebrew/livecheck/livecheck.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
@livecheck_strategy_names[strategy_class] ||= Utils.demodulize(strategy_class.name)
end

sig { params(strategy_class: T::Class[T.anything]).returns(T::Array[Symbol]) }
private_class_method def self.livecheck_find_versions_parameters(strategy_class)
@livecheck_find_versions_parameters ||= T.let({}, T.nilable(T::Hash[T::Class[T.anything], T::Array[Symbol]]))
@livecheck_find_versions_parameters[strategy_class] ||=
T::Utils.signature_for_method(strategy_class.method(:find_versions)).parameters.map(&:second)
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 }
Expand Down Expand Up @@ -613,8 +620,9 @@
livecheck = formula_or_cask.livecheck
referenced_livecheck = referenced_formula_or_cask&.livecheck

livecheck_options = livecheck.options || referenced_livecheck&.options
livecheck_url_options = livecheck_options.url_options.compact
livecheck_url = livecheck.url || referenced_livecheck&.url
livecheck_url_options = livecheck.url_options || referenced_livecheck&.url_options
livecheck_regex = livecheck.regex || referenced_livecheck&.regex
livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
Expand Down Expand Up @@ -676,8 +684,8 @@
elsif original_url.present? && original_url != "None"
puts "URL: #{original_url}"
end
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
puts "URL (processed): #{url}" if url != original_url
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
if strategies.present? && verbose
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names(s) }.join(", ")}"
end
Expand All @@ -697,23 +705,25 @@

next if strategy.blank?

homebrew_curl = case strategy_name
when "PageMatch", "HeaderMatch"
use_homebrew_curl?(referenced_package, url)
if (livecheck_homebrew_curl = livecheck_options.homebrew_curl).nil?
case strategy_name
when "PageMatch", "HeaderMatch"
if (homebrew_curl = use_homebrew_curl?(referenced_package, url))
livecheck_options = livecheck_options.merge({ homebrew_curl: })
livecheck_homebrew_curl = homebrew_curl

Check warning on line 713 in Library/Homebrew/livecheck/livecheck.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/livecheck/livecheck.rb#L713

Added line #L713 was not covered by tests
end
end
end
puts "Homebrew curl?: Yes" if debug && homebrew_curl.present?

strategy_args = {
regex: livecheck_regex,
url_options: livecheck_url_options,
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.
strategy_args[:cask] = cask if strategy_name == "ExtractPlist" && cask.present?
strategy_args[:url] = url
puts "Homebrew curl?: #{livecheck_homebrew_curl ? "Yes" : "No"}" if debug && !livecheck_homebrew_curl.nil?

# Only use arguments that the strategy's `#find_versions` method
# supports
find_versions_parameters = livecheck_find_versions_parameters(strategy)
strategy_args = {}
strategy_args[:cask] = cask if find_versions_parameters.include?(:cask)
strategy_args[:url] = url if find_versions_parameters.include?(:url)
strategy_args[:regex] = livecheck_regex if find_versions_parameters.include?(:regex)
strategy_args[:options] = livecheck_options if find_versions_parameters.include?(:options)
strategy_args.compact!

strategy_data = strategy.find_versions(**strategy_args, &livecheck_strategy_block)
Expand Down Expand Up @@ -813,7 +823,6 @@
end
version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
version_info[:meta][:url][:homebrew_curl] = homebrew_curl if homebrew_curl.present?
end
version_info[:meta][:strategy] = strategy_name if strategy.present?
version_info[:meta][:strategies] = strategies.map { |s| livecheck_strategy_names(s) } if strategies.present?
Expand Down Expand Up @@ -860,9 +869,10 @@
resource_version_info = {}

livecheck = resource.livecheck
livecheck_options = livecheck.options
livecheck_url_options = livecheck_options.url_options.compact

Check warning on line 873 in Library/Homebrew/livecheck/livecheck.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/livecheck/livecheck.rb#L872-L873

Added lines #L872 - L873 were not covered by tests
livecheck_reference = livecheck.formula
livecheck_url = livecheck.url
livecheck_url_options = livecheck.url_options
livecheck_regex = livecheck.regex
livecheck_strategy = livecheck.strategy
livecheck_strategy_block = livecheck.strategy_block
Expand Down Expand Up @@ -902,8 +912,8 @@
elsif original_url.present? && original_url != "None"
puts "URL: #{original_url}"
end
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
puts "URL (processed): #{url}" if url != original_url
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
if strategies.present? && verbose
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names(s) }.join(", ")}"
end
Expand All @@ -926,16 +936,22 @@
puts if debug && strategy.blank? && livecheck_reference != :parent
next if strategy.blank? && livecheck_reference != :parent

if debug && !(livecheck_homebrew_curl = livecheck_options.homebrew_curl).nil?
puts "Homebrew curl?: #{livecheck_homebrew_curl ? "Yes" : "No"}"
end

if livecheck_reference == :parent
match_version_map = { formula_latest => Version.new(formula_latest) }
cached = true
else
strategy_args = {
url:,
regex: livecheck_regex,
url_options: livecheck_url_options,
homebrew_curl: false,
}.compact
# Only use arguments that the strategy's `#find_versions` method
# supports
find_versions_parameters = livecheck_find_versions_parameters(strategy)
strategy_args = {}

Check warning on line 950 in Library/Homebrew/livecheck/livecheck.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/livecheck/livecheck.rb#L950

Added line #L950 was not covered by tests
strategy_args[:url] = url if find_versions_parameters.include?(:url)
strategy_args[:regex] = livecheck_regex if find_versions_parameters.include?(:regex)
strategy_args[:options] = livecheck_options if find_versions_parameters.include?(:options)
strategy_args.compact!

Check warning on line 954 in Library/Homebrew/livecheck/livecheck.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/livecheck/livecheck.rb#L954

Added line #L954 was not covered by tests

strategy_data = strategy.find_versions(**strategy_args, &livecheck_strategy_block)
match_version_map = strategy_data[:matches]
Expand Down
105 changes: 105 additions & 0 deletions Library/Homebrew/livecheck/options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# typed: strong
# frozen_string_literal: true

module Homebrew
module Livecheck
# Options to modify livecheck's behavior. These primarily come from
# `livecheck` blocks but they can also be set by livecheck at runtime.
#
# Option values use a `nil` default to indicate that the value has not been
# set.
class Options < T::Struct
# Whether to use brewed curl.
prop :homebrew_curl, T.nilable(T::Boolean)

# Form data to use when making a `POST` request.
prop :post_form, T.nilable(T::Hash[Symbol, String])

# JSON data to use when making a `POST` request.
prop :post_json, T.nilable(T::Hash[Symbol, String])

# Returns a `Hash` of options that are provided as arguments to `url`.
sig { returns(T::Hash[Symbol, T.untyped]) }
def url_options
{
homebrew_curl:,
post_form:,
post_json:,
}
end

# Returns a `Hash` of all instance variables, using `String` keys.
sig { returns(T::Hash[String, T.untyped]) }
def to_hash
T.let(serialize, T::Hash[String, T.untyped])
end

# Returns a `Hash` of all instance variables, using `Symbol` keys.
sig { returns(T::Hash[Symbol, T.untyped]) }
def to_h = to_hash.transform_keys(&:to_sym)

# Returns a new object formed by merging `other` values with a copy of
# `self`.
#
# `nil` values are removed from `other` before merging if it is an
# `Options` object, as these are unitiailized values. This ensures that
# existing values in `self` aren't unexpectedly overwritten with defaults.
sig { params(other: T.any(Options, T::Hash[Symbol, T.untyped])).returns(Options) }
def merge(other)
return dup if other.empty?

this_hash = to_h
other_hash = other.is_a?(Options) ? other.to_h : other
return dup if this_hash == other_hash

new_options = this_hash.merge(other_hash)
Options.new(**new_options)
end

# Merges values from `other` into `self` and returns `self`.
#
# `nil` values are removed from `other` before merging if it is an
# `Options` object, as these are unitiailized values. This ensures that
# existing values in `self` aren't unexpectedly overwritten with defaults.
sig { params(other: T.any(Options, T::Hash[Symbol, T.untyped])).returns(Options) }
def merge!(other)
return self if other.empty?

if other.is_a?(Options)
return self if self == other

other.instance_variables.each do |ivar|
next if (v = T.let(other.instance_variable_get(ivar), Object)).nil?

instance_variable_set(ivar, v)
end
else
other.each do |k, v|
cmd = :"#{k}="
send(cmd, v) if respond_to?(cmd)
end
end

self
end

sig { params(other: Object).returns(T::Boolean) }
def ==(other)
return false unless other.is_a?(Options)

@homebrew_curl == other.homebrew_curl &&
@post_form == other.post_form &&
@post_json == other.post_json
end
alias eql? ==

# Whether the object has only default values.
sig { returns(T::Boolean) }
def empty? = to_hash.empty?

# Whether the object has any non-default values.
sig { returns(T::Boolean) }
def present? = !empty?
end
end
end
Loading
Loading