Add extract_plist strategy.

This commit is contained in:
Markus Reiter 2021-04-04 03:00:34 +02:00
parent 6d0275ff57
commit a210b1a04e
No known key found for this signature in database
GPG Key ID: 245293B51702655B
24 changed files with 379 additions and 77 deletions

View File

@ -17,7 +17,11 @@ module Homebrew
sig { params(info_plist_path: Pathname).returns(T.nilable(T.attached_class)) } sig { params(info_plist_path: Pathname).returns(T.nilable(T.attached_class)) }
def self.from_info_plist(info_plist_path) def self.from_info_plist(info_plist_path)
plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist
from_info_plist_content(plist)
end
sig { params(plist: T::Hash[String, T.untyped]).returns(T.nilable(T.attached_class)) }
def self.from_info_plist_content(plist)
short_version = plist["CFBundleShortVersionString"].presence short_version = plist["CFBundleShortVersionString"].presence
version = plist["CFBundleVersion"].presence version = plist["CFBundleVersion"].presence

View File

@ -501,8 +501,7 @@ module Homebrew
regex_provided: livecheck_regex.present?, regex_provided: livecheck_regex.present?,
block_provided: livecheck.strategy_block.present?, block_provided: livecheck.strategy_block.present?,
) )
strategy = Strategy.from_symbol(livecheck_strategy) strategy = Strategy.from_symbol(livecheck_strategy) || strategies.first
strategy ||= strategies.first
strategy_name = livecheck_strategy_names[strategy] strategy_name = livecheck_strategy_names[strategy]
if debug if debug
@ -514,24 +513,29 @@ module Homebrew
puts "Regex: #{livecheck_regex.inspect}" if livecheck_regex.present? puts "Regex: #{livecheck_regex.inspect}" if livecheck_regex.present?
end end
if livecheck_strategy.present?
if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck.strategy_block.blank?) if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck.strategy_block.blank?)
odebug "#{strategy_name} strategy requires a regex or block" odebug "#{strategy_name} strategy requires a regex or block"
next next
end elsif livecheck_url.blank?
if livecheck_strategy.present? && livecheck_url.blank?
odebug "#{strategy_name} strategy requires a URL" odebug "#{strategy_name} strategy requires a URL"
next next
end elsif strategies.exclude?(strategy)
if livecheck_strategy.present? && strategies.exclude?(strategy)
odebug "#{strategy_name} strategy does not apply to this URL" odebug "#{strategy_name} strategy does not apply to this URL"
next next
end end
end
next if strategy.blank? next if strategy.blank?
strategy_data = strategy.find_versions(url, livecheck_regex, &livecheck.strategy_block) strategy_data = begin
strategy.find_versions(url, livecheck_regex, cask: cask, &livecheck.strategy_block)
rescue ArgumentError => e
raise unless e.message.include?("unknown keyword: cask")
odeprecated "`def self.find_versions` in `#{strategy}` without a `cask` parameter"
strategy.find_versions(url, livecheck_regex, &livecheck.strategy_block)
end
match_version_map = strategy_data[:matches] match_version_map = strategy_data[:matches]
regex = strategy_data[:regex] regex = strategy_data[:regex]
messages = strategy_data[:messages] messages = strategy_data[:messages]
@ -559,7 +563,9 @@ module Homebrew
end end
end end
if debug && match_version_map.present? next if match_version_map.blank?
if debug
puts puts
puts "Matched Versions:" puts "Matched Versions:"
@ -572,8 +578,6 @@ module Homebrew
end end
end end
next if match_version_map.blank?
version_info = { version_info = {
latest: Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(formula_or_cask, v) }), latest: Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(formula_or_cask, v) }),
} }

View File

@ -148,6 +148,7 @@ require_relative "strategy/apache"
require_relative "strategy/bitbucket" require_relative "strategy/bitbucket"
require_relative "strategy/cpan" require_relative "strategy/cpan"
require_relative "strategy/electron_builder" require_relative "strategy/electron_builder"
require_relative "strategy/extract_plist"
require_relative "strategy/git" require_relative "strategy/git"
require_relative "strategy/github_latest" require_relative "strategy/github_latest"
require_relative "strategy/gnome" require_relative "strategy/gnome"

View File

@ -21,6 +21,8 @@ module Homebrew
# #
# @api public # @api public
class Apache class Apache
extend T::Sig
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
URL_MATCH_REGEX = %r{ URL_MATCH_REGEX = %r{
^https?://www\.apache\.org ^https?://www\.apache\.org
@ -45,7 +47,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
# Use `\.t` instead of specific tarball extensions (e.g. .tar.gz) # Use `\.t` instead of specific tarball extensions (e.g. .tar.gz)
@ -60,7 +70,7 @@ module Homebrew
# * `/href=["']?example-v?(\d+(?:\.\d+)+)-bin\.zip/i` # * `/href=["']?example-v?(\d+(?:\.\d+)+)-bin\.zip/i`
regex ||= /href=["']?#{Regexp.escape(match[:prefix])}v?(\d+(?:\.\d+)+)#{Regexp.escape(suffix)}/i regex ||= /href=["']?#{Regexp.escape(match[:prefix])}v?(\d+(?:\.\d+)+)#{Regexp.escape(suffix)}/i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -28,6 +28,8 @@ module Homebrew
# #
# @api public # @api public
class Bitbucket class Bitbucket
extend T::Sig
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
URL_MATCH_REGEX = %r{ URL_MATCH_REGEX = %r{
^https?://bitbucket\.org ^https?://bitbucket\.org
@ -52,7 +54,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
# Use `\.t` instead of specific tarball extensions (e.g. .tar.gz) # Use `\.t` instead of specific tarball extensions (e.g. .tar.gz)
@ -71,7 +81,7 @@ module Homebrew
# * `/href=.*?example-v?(\d+(?:\.\d+)+)\.t/i` # * `/href=.*?example-v?(\d+(?:\.\d+)+)\.t/i`
regex ||= /href=.*?#{Regexp.escape(match[:prefix])}v?(\d+(?:\.\d+)+)#{Regexp.escape(suffix)}/i regex ||= /href=.*?#{Regexp.escape(match[:prefix])}v?(\d+(?:\.\d+)+)#{Regexp.escape(suffix)}/i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -18,6 +18,8 @@ module Homebrew
# #
# @api public # @api public
class Cpan class Cpan
extend T::Sig
NICE_NAME = "CPAN" NICE_NAME = "CPAN"
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
@ -43,7 +45,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
# Use `\.t` instead of specific tarball extensions (e.g. .tar.gz) # Use `\.t` instead of specific tarball extensions (e.g. .tar.gz)
@ -55,7 +65,7 @@ module Homebrew
# Example regex: `/href=.*?Brew[._-]v?(\d+(?:\.\d+)*)\.t/i` # Example regex: `/href=.*?Brew[._-]v?(\d+(?:\.\d+)*)\.t/i`
regex ||= /href=.*?#{match[:prefix]}[._-]v?(\d+(?:\.\d+)*)#{Regexp.escape(suffix)}/i regex ||= /href=.*?#{match[:prefix]}[._-]v?(\d+(?:\.\d+)*)#{Regexp.escape(suffix)}/i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -64,10 +64,11 @@ module Homebrew
params( params(
url: String, url: String,
regex: T.nilable(Regexp), regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: Hash).returns(String)), block: T.nilable(T.proc.params(arg0: Hash).returns(String)),
).returns(T::Hash[Symbol, T.untyped]) ).returns(T::Hash[Symbol, T.untyped])
} }
def self.find_versions(url, regex = nil, &block) def self.find_versions(url, regex, cask: nil, &block)
raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex
match_data = { matches: {}, regex: regex, url: url } match_data = { matches: {}, regex: regex, url: url }

View File

@ -0,0 +1,91 @@
# typed: true
# frozen_string_literal: true
require "bundle_version"
require "unversioned_cask_checker"
require_relative "page_match"
module Homebrew
module Livecheck
module Strategy
# The {ExtractPlist} strategy downloads the file at a URL and
# extracts versions from contained `.plist` files.
#
# @api private
class ExtractPlist
extend T::Sig
# A priority of zero causes livecheck to skip the strategy. We only
# apply {ExtractPlist} using `strategy :extract_plist` in a `livecheck` block,
# as we can't automatically determine when this can be successfully
# applied to a URL without fetching the content.
PRIORITY = 0
# The `Regexp` used to determine if the strategy applies to the URL.
URL_MATCH_REGEX = %r{^https?://}i.freeze
# Whether the strategy can be applied to the provided URL.
# The strategy will technically match any HTTP URL but is
# only usable with a `livecheck` block containing a regex
# or block.
sig { params(url: String).returns(T::Boolean) }
def self.match?(url)
URL_MATCH_REGEX.match?(url)
end
# @api private
Item = Struct.new(
# @api private
:bundle_version,
keyword_init: true,
) do
extend T::Sig
extend Forwardable
# @api public
delegate version: :bundle_version
# @api public
delegate short_version: :bundle_version
end
# Checks the content at the URL for new versions.
sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: Cask::Cask,
block: T.nilable(T.proc.params(arg0: T::Hash[String, Item]).returns(String)),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask:, &block)
raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex
raise ArgumentError, "The #{T.must(name).demodulize} strategy only supports casks." unless T.unsafe(cask)
match_data = { matches: {}, regex: regex, url: url }
unversioned_cask_checker = UnversionedCaskChecker.new(cask)
versions = unversioned_cask_checker.all_versions.transform_values { |v| Item.new(bundle_version: v) }
if block
match = block.call(versions)
unless T.unsafe(match).is_a?(String)
raise TypeError, "Return value of `strategy :extract_plist` block must be a string."
end
match_data[:matches][match] = Version.new(match) if match
elsif versions.any?
versions.each_value do |item|
version = item.bundle_version.nice_version
match_data[:matches][version] = Version.new(version)
end
end
match_data
end
end
end
end
end

View File

@ -24,6 +24,8 @@ module Homebrew
# #
# @api public # @api public
class Git class Git
extend T::Sig
# The priority of the strategy on an informal scale of 1 to 10 (from # The priority of the strategy on an informal scale of 1 to 10 (from
# lowest to highest). # lowest to highest).
PRIORITY = 8 PRIORITY = 8
@ -74,7 +76,16 @@ module Homebrew
# @param url [String] the URL of the Git repository to check # @param url [String] the URL of the Git repository to check
# @param regex [Regexp] the regex to use for matching versions # @param regex [Regexp] the regex to use for matching versions
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: T::Array[String])
.returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match_data = { matches: {}, regex: regex, url: url } match_data = { matches: {}, regex: regex, url: url }
tags_data = tag_info(url, regex) tags_data = tag_info(url, regex)

View File

@ -32,6 +32,8 @@ module Homebrew
# #
# @api public # @api public
class GithubLatest class GithubLatest
extend T::Sig
NICE_NAME = "GitHub - Latest" NICE_NAME = "GitHub - Latest"
# A priority of zero causes livecheck to skip the strategy. We do this # A priority of zero causes livecheck to skip the strategy. We do this
@ -60,7 +62,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.sub(/\.git$/i, "").match(URL_MATCH_REGEX) match = url.sub(/\.git$/i, "").match(URL_MATCH_REGEX)
# Example URL: `https://github.com/example/example/releases/latest` # Example URL: `https://github.com/example/example/releases/latest`
@ -69,7 +79,7 @@ module Homebrew
# The default regex is the same for all URLs using this strategy # The default regex is the same for all URLs using this strategy
regex ||= %r{href=.*?/tag/v?(\d+(?:\.\d+)+)["' >]}i regex ||= %r{href=.*?/tag/v?(\d+(?:\.\d+)+)["' >]}i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -17,6 +17,8 @@ module Homebrew
# #
# @api public # @api public
class Gnome class Gnome
extend T::Sig
NICE_NAME = "GNOME" NICE_NAME = "GNOME"
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
@ -40,7 +42,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
page_url = "https://download.gnome.org/sources/#{match[:package_name]}/cache.json" page_url = "https://download.gnome.org/sources/#{match[:package_name]}/cache.json"
@ -57,7 +67,7 @@ module Homebrew
# Example regex: `/example-(\d+\.([0-8]\d*?)?[02468](?:\.\d+)*?)\.t/i` # Example regex: `/example-(\d+\.([0-8]\d*?)?[02468](?:\.\d+)*?)\.t/i`
regex ||= /#{Regexp.escape(match[:package_name])}-(\d+\.([0-8]\d*?)?[02468](?:\.\d+)*?)\.t/i regex ||= /#{Regexp.escape(match[:package_name])}-(\d+\.([0-8]\d*?)?[02468](?:\.\d+)*?)\.t/i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -29,6 +29,8 @@ module Homebrew
# #
# @api public # @api public
class Gnu class Gnu
extend T::Sig
NICE_NAME = "GNU" NICE_NAME = "GNU"
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
@ -52,7 +54,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
# The directory listing page for the project's files # The directory listing page for the project's files
@ -68,7 +78,7 @@ module Homebrew
# Example regex: `%r{href=.*?example[._-]v?(\d+(?:\.\d+)*)(?:\.[a-z]+|/)}i` # Example regex: `%r{href=.*?example[._-]v?(\d+(?:\.\d+)*)(?:\.[a-z]+|/)}i`
regex ||= %r{href=.*?#{match[:project_name]}[._-]v?(\d+(?:\.\d+)*)(?:\.[a-z]+|/)}i regex ||= %r{href=.*?#{match[:project_name]}[._-]v?(\d+(?:\.\d+)*)(?:\.[a-z]+|/)}i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -17,6 +17,8 @@ module Homebrew
# #
# @api public # @api public
class Hackage class Hackage
extend T::Sig
# A `Regexp` used in determining if the strategy applies to the URL and # A `Regexp` used in determining if the strategy applies to the URL and
# also as part of extracting the package name from the URL basename. # also as part of extracting the package name from the URL basename.
PACKAGE_NAME_REGEX = /(?<package_name>.+?)-\d+/i.freeze PACKAGE_NAME_REGEX = /(?<package_name>.+?)-\d+/i.freeze
@ -45,7 +47,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = File.basename(url).match(FILENAME_REGEX) match = File.basename(url).match(FILENAME_REGEX)
# A page containing a directory listing of the latest source tarball # A page containing a directory listing of the latest source tarball
@ -54,7 +64,7 @@ module Homebrew
# Example regex: `%r{<h3>example-(.*?)/?</h3>}i` # Example regex: `%r{<h3>example-(.*?)/?</h3>}i`
regex ||= %r{<h3>#{Regexp.escape(match[:package_name])}-(.*?)/?</h3>}i regex ||= %r{<h3>#{Regexp.escape(match[:package_name])}-(.*?)/?</h3>}i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -35,8 +35,16 @@ module Homebrew
# Checks the final URL for new versions after following all redirections, # Checks the final URL for new versions after following all redirections,
# using the provided regex for matching. # using the provided regex for matching.
sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) } sig {
def self.find_versions(url, regex, &block) params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: T::Hash[String, String])
.returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match_data = { matches: {}, regex: regex, url: url } match_data = { matches: {}, regex: regex, url: url }
headers = Strategy.page_headers(url) headers = Strategy.page_headers(url)
@ -45,7 +53,7 @@ module Homebrew
merged_headers = headers.reduce(&:merge) merged_headers = headers.reduce(&:merge)
if block if block
match = block.call(merged_headers, regex) match = yield merged_headers, regex
else else
match = nil match = nil

View File

@ -23,6 +23,8 @@ module Homebrew
# #
# @api public # @api public
class Launchpad class Launchpad
extend T::Sig
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
URL_MATCH_REGEX = %r{ URL_MATCH_REGEX = %r{
^https?://(?:[^/]+?\.)*launchpad\.net ^https?://(?:[^/]+?\.)*launchpad\.net
@ -43,7 +45,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
# The main page for the project on Launchpad # The main page for the project on Launchpad
@ -52,7 +62,7 @@ module Homebrew
# The default regex is the same for all URLs using this strategy # The default regex is the same for all URLs using this strategy
regex ||= %r{class="[^"]*version[^"]*"[^>]*>\s*Latest version is (.+)\s*</} regex ||= %r{class="[^"]*version[^"]*"[^>]*>\s*Latest version is (.+)\s*</}
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -17,6 +17,8 @@ module Homebrew
# #
# @api public # @api public
class Npm class Npm
extend T::Sig
NICE_NAME = "npm" NICE_NAME = "npm"
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
@ -39,7 +41,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
page_url = "https://www.npmjs.com/package/#{match[:package_name]}?activeTab=versions" page_url = "https://www.npmjs.com/package/#{match[:package_name]}?activeTab=versions"
@ -49,7 +59,7 @@ module Homebrew
# * `%r{href=.*?/package/@example/example/v/(\d+(?:\.\d+)+)"}i` # * `%r{href=.*?/package/@example/example/v/(\d+(?:\.\d+)+)"}i`
regex ||= %r{href=.*?/package/#{Regexp.escape(match[:package_name])}/v/(\d+(?:\.\d+)+)"}i regex ||= %r{href=.*?/package/#{Regexp.escape(match[:package_name])}/v/(\d+(?:\.\d+)+)"}i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -81,11 +81,12 @@ module Homebrew
params( params(
url: String, url: String,
regex: T.nilable(Regexp), regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
provided_content: T.nilable(String), provided_content: T.nilable(String),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))), block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped]) ).returns(T::Hash[Symbol, T.untyped])
} }
def self.find_versions(url, regex, provided_content = nil, &block) def self.find_versions(url, regex, cask: nil, provided_content: nil, &block)
match_data = { matches: {}, regex: regex, url: url } match_data = { matches: {}, regex: regex, url: url }
content = if provided_content.is_a?(String) content = if provided_content.is_a?(String)

View File

@ -17,6 +17,8 @@ module Homebrew
# #
# @api public # @api public
class Pypi class Pypi
extend T::Sig
NICE_NAME = "PyPI" NICE_NAME = "PyPI"
# The `Regexp` used to extract the package name and suffix (e.g., file # The `Regexp` used to extract the package name and suffix (e.g., file
@ -49,7 +51,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = File.basename(url).match(FILENAME_REGEX) match = File.basename(url).match(FILENAME_REGEX)
# Use `\.t` instead of specific tarball extensions (e.g. .tar.gz) # Use `\.t` instead of specific tarball extensions (e.g. .tar.gz)
@ -64,7 +74,7 @@ module Homebrew
re_suffix = Regexp.escape(suffix) re_suffix = Regexp.escape(suffix)
regex ||= %r{href=.*?/packages.*?/#{re_package_name}[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)#{re_suffix}}i regex ||= %r{href=.*?/packages.*?/#{re_package_name}[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)#{re_suffix}}i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -31,6 +31,8 @@ module Homebrew
# #
# @api public # @api public
class Sourceforge class Sourceforge
extend T::Sig
NICE_NAME = "SourceForge" NICE_NAME = "SourceForge"
# The `Regexp` used to determine if the strategy applies to the URL. # The `Regexp` used to determine if the strategy applies to the URL.
@ -55,7 +57,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
match = url.match(URL_MATCH_REGEX) match = url.match(URL_MATCH_REGEX)
page_url = "https://sourceforge.net/projects/#{match[:project_name]}/rss" page_url = "https://sourceforge.net/projects/#{match[:project_name]}/rss"
@ -65,7 +75,7 @@ module Homebrew
# create something that works for most URLs. # create something that works for most URLs.
regex ||= %r{url=.*?/#{Regexp.escape(match[:project_name])}/files/.*?[-_/](\d+(?:[-.]\d+)+)[-_/%.]}i regex ||= %r{url=.*?/#{Regexp.escape(match[:project_name])}/files/.*?[-_/](\d+(?:[-.]\d+)+)[-_/%.]}i
PageMatch.find_versions(page_url, regex, &block) PageMatch.find_versions(page_url, regex, cask: cask, &block)
end end
end end
end end

View File

@ -32,12 +32,24 @@ module Homebrew
URL_MATCH_REGEX.match?(url) URL_MATCH_REGEX.match?(url)
end end
Item = Struct.new(:title, :url, :bundle_version, :short_version, :version, keyword_init: true) do # @api private
Item = Struct.new(
# @api public
:title,
# @api public
:url,
# @api private
:bundle_version,
keyword_init: true,
) do
extend T::Sig extend T::Sig
extend Forwardable extend Forwardable
# @api public
delegate version: :bundle_version delegate version: :bundle_version
# @api public
delegate short_version: :bundle_version delegate short_version: :bundle_version
end end
@ -74,8 +86,6 @@ module Homebrew
title: title, title: title,
url: url, url: url,
bundle_version: bundle_version, bundle_version: bundle_version,
short_version: bundle_version&.short_version,
version: bundle_version&.version,
}.compact }.compact
Item.new(**data) unless data.empty? Item.new(**data) unless data.empty?
@ -85,8 +95,15 @@ module Homebrew
end end
# Checks the content at the URL for new versions. # Checks the content at the URL for new versions.
sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) } sig {
def self.find_versions(url, regex, &block) params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: Item).returns(String)),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex
match_data = { matches: {}, regex: regex, url: url } match_data = { matches: {}, regex: regex, url: url }
@ -96,7 +113,13 @@ module Homebrew
if (item = item_from_content(content)) if (item = item_from_content(content))
match = if block match = if block
block.call(item)&.to_s value = block.call(item)
unless T.unsafe(value).is_a?(String)
raise TypeError, "Return value of `strategy :sparkle` block must be a string."
end
value
else else
item.bundle_version&.nice_version item.bundle_version&.nice_version
end end

View File

@ -38,6 +38,8 @@ module Homebrew
# #
# @api public # @api public
class Xorg class Xorg
extend T::Sig
NICE_NAME = "X.Org" NICE_NAME = "X.Org"
# A `Regexp` used in determining if the strategy applies to the URL and # A `Regexp` used in determining if the strategy applies to the URL and
@ -78,7 +80,15 @@ module Homebrew
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content
# @return [Hash] # @return [Hash]
def self.find_versions(url, regex = nil, &block) sig {
params(
url: String,
regex: T.nilable(Regexp),
cask: T.nilable(Cask::Cask),
block: T.nilable(T.proc.params(arg0: String).returns(T.any(T::Array[String], String))),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, &block)
file_name = File.basename(url) file_name = File.basename(url)
match = file_name.match(FILENAME_REGEX) match = file_name.match(FILENAME_REGEX)
@ -92,7 +102,7 @@ module Homebrew
# Use the cached page content to avoid duplicate fetches # Use the cached page content to avoid duplicate fetches
cached_content = @page_data[page_url] cached_content = @page_data[page_url]
match_data = PageMatch.find_versions(page_url, regex, cached_content, &block) match_data = PageMatch.find_versions(page_url, regex, provided_content: cached_content, cask: cask, &block)
# Cache any new page content # Cache any new page content
@page_data[page_url] = match_data[:content] if match_data[:content].present? @page_data[page_url] = match_data[:content] if match_data[:content].present?

View File

@ -79,7 +79,8 @@ describe Homebrew::Livecheck::Strategy::PageMatch do
describe "::find_versions?" do describe "::find_versions?" do
it "finds versions in provided_content" do it "finds versions in provided_content" do
expect(page_match.find_versions(url, regex, page_content)).to eq(find_versions_cached_return_hash) expect(page_match.find_versions(url, regex, provided_content: page_content))
.to eq(find_versions_cached_return_hash)
end end
end end
end end

View File

@ -12,24 +12,11 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
{ {
title: "Version 1.2.3", title: "Version 1.2.3",
url: "https://www.example.com/example/example.tar.gz", url: "https://www.example.com/example/example.tar.gz",
bundle_version: Homebrew::BundleVersion.new("1.2.3", "1234"),
short_version: "1.2.3", short_version: "1.2.3",
version: "1234", version: "1234",
} }
} }
let(:appcast_item) {
Homebrew::Livecheck::Strategy::Sparkle::Item.new(
{
title: appcast_data[:title],
url: appcast_data[:url],
bundle_version: appcast_data[:bundle_version],
short_version: appcast_data[:bundle_version]&.short_version,
version: appcast_data[:bundle_version]&.version,
},
)
}
let(:appcast_xml) { let(:appcast_xml) {
<<~EOS <<~EOS
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
@ -65,10 +52,10 @@ describe Homebrew::Livecheck::Strategy::Sparkle do
it "returns an Item when given XML data" do it "returns an Item when given XML data" do
expect(item_from_appcast_xml).to be_a(Homebrew::Livecheck::Strategy::Sparkle::Item) expect(item_from_appcast_xml).to be_a(Homebrew::Livecheck::Strategy::Sparkle::Item)
expect(item_from_appcast_xml.title).to eq(appcast_item.title) expect(item_from_appcast_xml.title).to eq(appcast_data[:title])
expect(item_from_appcast_xml.url).to eq(appcast_item.url) expect(item_from_appcast_xml.url).to eq(appcast_data[:url])
expect(item_from_appcast_xml.bundle_version.short_version).to eq(appcast_item.bundle_version.short_version) expect(item_from_appcast_xml.short_version).to eq(appcast_data[:short_version])
expect(item_from_appcast_xml.bundle_version.version).to eq(appcast_item.bundle_version.version) expect(item_from_appcast_xml.version).to eq(appcast_data[:version])
end end
end end
end end

View File

@ -60,6 +60,56 @@ module Homebrew
end end
end end
sig { returns(T::Hash[String, BundleVersion]) }
def all_versions
versions = {}
parse_info_plist = proc do |info_plist_path|
plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist
id = plist["CFBundleIdentifier"]
version = BundleVersion.from_info_plist_content(plist)
versions[id] = version if id && version
end
Dir.mktmpdir do |dir|
dir = Pathname(dir)
installer.extract_primary_container(to: dir)
info_plist_paths = apps.flat_map do |app|
top_level_info_plists(Pathname.glob(dir/"**"/app.source.basename/"Contents"/"Info.plist")).sort
end
info_plist_paths.each(&parse_info_plist)
pkg_paths = pkgs.flat_map do |pkg|
Pathname.glob(dir/"**"/pkg.path.basename).sort
end
pkg_paths.each do |pkg_path|
Dir.mktmpdir do |extract_dir|
extract_dir = Pathname(extract_dir)
FileUtils.rmdir extract_dir
system_command! "pkgutil", args: ["--expand-full", pkg_path, extract_dir]
top_level_info_plist_paths = top_level_info_plists(Pathname.glob(extract_dir/"**/Contents/Info.plist"))
top_level_info_plist_paths.each(&parse_info_plist)
ensure
Cask::Utils.gain_permissions_remove(extract_dir)
extract_dir.mkpath
end
end
nil
end
versions
end
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def guess_cask_version def guess_cask_version
if apps.empty? && pkgs.empty? if apps.empty? && pkgs.empty?