diff --git a/Library/Homebrew/bundle_version.rb b/Library/Homebrew/bundle_version.rb index 16854fc40d..8bf70d76ea 100644 --- a/Library/Homebrew/bundle_version.rb +++ b/Library/Homebrew/bundle_version.rb @@ -17,7 +17,11 @@ module Homebrew sig { params(info_plist_path: Pathname).returns(T.nilable(T.attached_class)) } def self.from_info_plist(info_plist_path) 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 version = plist["CFBundleVersion"].presence diff --git a/Library/Homebrew/livecheck/livecheck.rb b/Library/Homebrew/livecheck/livecheck.rb index 92097bb711..cdb85e265c 100644 --- a/Library/Homebrew/livecheck/livecheck.rb +++ b/Library/Homebrew/livecheck/livecheck.rb @@ -501,8 +501,7 @@ module Homebrew regex_provided: livecheck_regex.present?, block_provided: livecheck.strategy_block.present?, ) - strategy = Strategy.from_symbol(livecheck_strategy) - strategy ||= strategies.first + strategy = Strategy.from_symbol(livecheck_strategy) || strategies.first strategy_name = livecheck_strategy_names[strategy] if debug @@ -514,24 +513,29 @@ module Homebrew puts "Regex: #{livecheck_regex.inspect}" if livecheck_regex.present? end - if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck.strategy_block.blank?) - odebug "#{strategy_name} strategy requires a regex or block" - next - end - - if livecheck_strategy.present? && livecheck_url.blank? - odebug "#{strategy_name} strategy requires a URL" - next - end - - if livecheck_strategy.present? && strategies.exclude?(strategy) - odebug "#{strategy_name} strategy does not apply to this URL" - next + if livecheck_strategy.present? + if livecheck_strategy == :page_match && (livecheck_regex.blank? && livecheck.strategy_block.blank?) + odebug "#{strategy_name} strategy requires a regex or block" + next + elsif livecheck_url.blank? + odebug "#{strategy_name} strategy requires a URL" + next + elsif strategies.exclude?(strategy) + odebug "#{strategy_name} strategy does not apply to this URL" + next + end end 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] regex = strategy_data[:regex] messages = strategy_data[:messages] @@ -559,7 +563,9 @@ module Homebrew end end - if debug && match_version_map.present? + next if match_version_map.blank? + + if debug puts puts "Matched Versions:" @@ -572,8 +578,6 @@ module Homebrew end end - next if match_version_map.blank? - version_info = { latest: Version.new(match_version_map.values.max_by { |v| LivecheckVersion.create(formula_or_cask, v) }), } diff --git a/Library/Homebrew/livecheck/strategy.rb b/Library/Homebrew/livecheck/strategy.rb index 69a139e161..76b1edfd79 100644 --- a/Library/Homebrew/livecheck/strategy.rb +++ b/Library/Homebrew/livecheck/strategy.rb @@ -148,6 +148,7 @@ require_relative "strategy/apache" require_relative "strategy/bitbucket" require_relative "strategy/cpan" require_relative "strategy/electron_builder" +require_relative "strategy/extract_plist" require_relative "strategy/git" require_relative "strategy/github_latest" require_relative "strategy/gnome" diff --git a/Library/Homebrew/livecheck/strategy/apache.rb b/Library/Homebrew/livecheck/strategy/apache.rb index c96f0b903c..2fdbbcb1fe 100644 --- a/Library/Homebrew/livecheck/strategy/apache.rb +++ b/Library/Homebrew/livecheck/strategy/apache.rb @@ -21,6 +21,8 @@ module Homebrew # # @api public class Apache + extend T::Sig + # The `Regexp` used to determine if the strategy applies to the URL. URL_MATCH_REGEX = %r{ ^https?://www\.apache\.org @@ -45,7 +47,15 @@ module Homebrew # @param url [String] the URL of the content to check # @param regex [Regexp] a regex used for matching versions in content # @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) # 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` 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 diff --git a/Library/Homebrew/livecheck/strategy/bitbucket.rb b/Library/Homebrew/livecheck/strategy/bitbucket.rb index dfa3539836..754d2c5e37 100644 --- a/Library/Homebrew/livecheck/strategy/bitbucket.rb +++ b/Library/Homebrew/livecheck/strategy/bitbucket.rb @@ -28,6 +28,8 @@ module Homebrew # # @api public class Bitbucket + extend T::Sig + # The `Regexp` used to determine if the strategy applies to the URL. URL_MATCH_REGEX = %r{ ^https?://bitbucket\.org @@ -52,7 +54,15 @@ module Homebrew # @param url [String] the URL of the content to check # @param regex [Regexp] a regex used for matching versions in content # @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) # Use `\.t` instead of specific tarball extensions (e.g. .tar.gz) @@ -71,7 +81,7 @@ module Homebrew # * `/href=.*?example-v?(\d+(?:\.\d+)+)\.t/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 diff --git a/Library/Homebrew/livecheck/strategy/cpan.rb b/Library/Homebrew/livecheck/strategy/cpan.rb index 235254254c..f642f200a0 100644 --- a/Library/Homebrew/livecheck/strategy/cpan.rb +++ b/Library/Homebrew/livecheck/strategy/cpan.rb @@ -18,6 +18,8 @@ module Homebrew # # @api public class Cpan + extend T::Sig + NICE_NAME = "CPAN" # 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 regex [Regexp] a regex used for matching versions in content # @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) # 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` 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 diff --git a/Library/Homebrew/livecheck/strategy/electron_builder.rb b/Library/Homebrew/livecheck/strategy/electron_builder.rb index b03fdb81a6..9c59b9114a 100644 --- a/Library/Homebrew/livecheck/strategy/electron_builder.rb +++ b/Library/Homebrew/livecheck/strategy/electron_builder.rb @@ -64,10 +64,11 @@ module Homebrew params( url: String, regex: T.nilable(Regexp), + cask: T.nilable(Cask::Cask), block: T.nilable(T.proc.params(arg0: Hash).returns(String)), ).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 match_data = { matches: {}, regex: regex, url: url } diff --git a/Library/Homebrew/livecheck/strategy/extract_plist.rb b/Library/Homebrew/livecheck/strategy/extract_plist.rb new file mode 100644 index 0000000000..7b24e3bafb --- /dev/null +++ b/Library/Homebrew/livecheck/strategy/extract_plist.rb @@ -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 diff --git a/Library/Homebrew/livecheck/strategy/git.rb b/Library/Homebrew/livecheck/strategy/git.rb index f57e047666..c681d5282a 100644 --- a/Library/Homebrew/livecheck/strategy/git.rb +++ b/Library/Homebrew/livecheck/strategy/git.rb @@ -24,6 +24,8 @@ module Homebrew # # @api public class Git + extend T::Sig + # The priority of the strategy on an informal scale of 1 to 10 (from # lowest to highest). PRIORITY = 8 @@ -74,7 +76,16 @@ module Homebrew # @param url [String] the URL of the Git repository to check # @param regex [Regexp] the regex to use for matching versions # @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 } tags_data = tag_info(url, regex) diff --git a/Library/Homebrew/livecheck/strategy/github_latest.rb b/Library/Homebrew/livecheck/strategy/github_latest.rb index 8197e635ad..d29888f329 100644 --- a/Library/Homebrew/livecheck/strategy/github_latest.rb +++ b/Library/Homebrew/livecheck/strategy/github_latest.rb @@ -32,6 +32,8 @@ module Homebrew # # @api public class GithubLatest + extend T::Sig + NICE_NAME = "GitHub - Latest" # 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 regex [Regexp] a regex used for matching versions in content # @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) # 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 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 diff --git a/Library/Homebrew/livecheck/strategy/gnome.rb b/Library/Homebrew/livecheck/strategy/gnome.rb index 19ea4ed70c..3eb6442cf9 100644 --- a/Library/Homebrew/livecheck/strategy/gnome.rb +++ b/Library/Homebrew/livecheck/strategy/gnome.rb @@ -17,6 +17,8 @@ module Homebrew # # @api public class Gnome + extend T::Sig + NICE_NAME = "GNOME" # 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 regex [Regexp] a regex used for matching versions in content # @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) 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` 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 diff --git a/Library/Homebrew/livecheck/strategy/gnu.rb b/Library/Homebrew/livecheck/strategy/gnu.rb index 8f2a5bcdca..a00e41ea9e 100644 --- a/Library/Homebrew/livecheck/strategy/gnu.rb +++ b/Library/Homebrew/livecheck/strategy/gnu.rb @@ -29,6 +29,8 @@ module Homebrew # # @api public class Gnu + extend T::Sig + NICE_NAME = "GNU" # 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 regex [Regexp] a regex used for matching versions in content # @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) # 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` 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 diff --git a/Library/Homebrew/livecheck/strategy/hackage.rb b/Library/Homebrew/livecheck/strategy/hackage.rb index cb986e34ba..a780c848a8 100644 --- a/Library/Homebrew/livecheck/strategy/hackage.rb +++ b/Library/Homebrew/livecheck/strategy/hackage.rb @@ -17,6 +17,8 @@ module Homebrew # # @api public class Hackage + extend T::Sig + # 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. PACKAGE_NAME_REGEX = /(?.+?)-\d+/i.freeze @@ -45,7 +47,15 @@ module Homebrew # @param url [String] the URL of the content to check # @param regex [Regexp] a regex used for matching versions in content # @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) # A page containing a directory listing of the latest source tarball @@ -54,7 +64,7 @@ module Homebrew # Example regex: `%r{

example-(.*?)/?

}i` regex ||= %r{

#{Regexp.escape(match[:package_name])}-(.*?)/?

}i - PageMatch.find_versions(page_url, regex, &block) + PageMatch.find_versions(page_url, regex, cask: cask, &block) end end end diff --git a/Library/Homebrew/livecheck/strategy/header_match.rb b/Library/Homebrew/livecheck/strategy/header_match.rb index b413d0a1ba..1e10286d6a 100644 --- a/Library/Homebrew/livecheck/strategy/header_match.rb +++ b/Library/Homebrew/livecheck/strategy/header_match.rb @@ -35,8 +35,16 @@ module Homebrew # Checks the final URL for new versions after following all redirections, # using the provided regex for matching. - sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) } - def self.find_versions(url, regex, &block) + sig { + 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 } headers = Strategy.page_headers(url) @@ -45,7 +53,7 @@ module Homebrew merged_headers = headers.reduce(&:merge) if block - match = block.call(merged_headers, regex) + match = yield merged_headers, regex else match = nil diff --git a/Library/Homebrew/livecheck/strategy/launchpad.rb b/Library/Homebrew/livecheck/strategy/launchpad.rb index a871973b03..1257383a3e 100644 --- a/Library/Homebrew/livecheck/strategy/launchpad.rb +++ b/Library/Homebrew/livecheck/strategy/launchpad.rb @@ -23,6 +23,8 @@ module Homebrew # # @api public class Launchpad + extend T::Sig + # The `Regexp` used to determine if the strategy applies to the URL. URL_MATCH_REGEX = %r{ ^https?://(?:[^/]+?\.)*launchpad\.net @@ -43,7 +45,15 @@ module Homebrew # @param url [String] the URL of the content to check # @param regex [Regexp] a regex used for matching versions in content # @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) # 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 regex ||= %r{class="[^"]*version[^"]*"[^>]*>\s*Latest version is (.+)\s* @@ -65,10 +52,10 @@ describe Homebrew::Livecheck::Strategy::Sparkle 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.title).to eq(appcast_item.title) - expect(item_from_appcast_xml.url).to eq(appcast_item.url) - expect(item_from_appcast_xml.bundle_version.short_version).to eq(appcast_item.bundle_version.short_version) - expect(item_from_appcast_xml.bundle_version.version).to eq(appcast_item.bundle_version.version) + expect(item_from_appcast_xml.title).to eq(appcast_data[:title]) + expect(item_from_appcast_xml.url).to eq(appcast_data[:url]) + expect(item_from_appcast_xml.short_version).to eq(appcast_data[:short_version]) + expect(item_from_appcast_xml.version).to eq(appcast_data[:version]) end end end diff --git a/Library/Homebrew/unversioned_cask_checker.rb b/Library/Homebrew/unversioned_cask_checker.rb index 1fef545e0c..01c7538f21 100644 --- a/Library/Homebrew/unversioned_cask_checker.rb +++ b/Library/Homebrew/unversioned_cask_checker.rb @@ -60,6 +60,56 @@ module Homebrew 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)) } def guess_cask_version if apps.empty? && pkgs.empty?