# typed: true # frozen_string_literal: true require "bundle_version" require "cask/cask" require "cask/installer" module Homebrew # Check unversioned casks for updates by extracting their # contents and guessing the version from contained files. # # @api private class UnversionedCaskChecker extend T::Sig sig { returns(Cask::Cask) } attr_reader :cask sig { params(cask: Cask::Cask).void } def initialize(cask) @cask = cask end sig { returns(Cask::Installer) } def installer @installer ||= Cask::Installer.new(cask, verify_download_integrity: false) end sig { returns(T::Array[Cask::Artifact::App]) } def apps @apps ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::App) } end sig { returns(T::Array[Cask::Artifact::Pkg]) } def pkgs @pkgs ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Pkg) } end sig { returns(T::Boolean) } def single_app_cask? apps.count == 1 end sig { returns(T::Boolean) } def single_pkg_cask? pkgs.count == 1 end # Filter paths to `Info.plist` files so that ones belonging # to e.g. nested `.app`s are ignored. sig { params(paths: T::Array[Pathname]).returns(T::Array[Pathname]) } def top_level_info_plists(paths) # Go from `./Contents/Info.plist` to `./`. top_level_paths = paths.map { |path| path.parent.parent } paths.reject do |path| top_level_paths.any? do |_other_top_level_path| path.ascend.drop(3).any? { |parent_path| top_level_paths.include?(parent_path) } 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)) } def guess_cask_version if apps.empty? && pkgs.empty? opoo "Cask #{cask} does not contain any apps or PKG installers." return end Dir.mktmpdir do |dir| dir = Pathname(dir) installer.yield_self do |i| i.extract_primary_container(to: dir) rescue ErrorDuringExecution => e onoe e return nil end 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 do |info_plist_path| if (version = BundleVersion.from_info_plist(info_plist_path)) return version.nice_version end end pkg_paths = pkgs.flat_map do |pkg| Pathname.glob(dir/"**"/pkg.path.basename).sort end pkg_paths.each do |pkg_path| packages = system_command!("installer", args: ["-plist", "-pkginfo", "-pkg", pkg_path]) .plist .map { |package| package.fetch("Package") } Dir.mktmpdir do |extract_dir| extract_dir = Pathname(extract_dir) FileUtils.rmdir extract_dir begin system_command! "pkgutil", args: ["--expand-full", pkg_path, extract_dir] rescue ErrorDuringExecution => e onoe "Failed to extract #{pkg_path.basename}: #{e}" next end top_level_info_plist_paths = top_level_info_plists(Pathname.glob(extract_dir/"**/Contents/Info.plist")) unique_info_plist_versions = top_level_info_plist_paths.map { |i| BundleVersion.from_info_plist(i)&.nice_version } .compact.uniq return unique_info_plist_versions.first if unique_info_plist_versions.count == 1 package_info_path = extract_dir/"PackageInfo" if package_info_path.exist? if (version = BundleVersion.from_package_info(package_info_path)) return version.nice_version end elsif packages.count == 1 onoe "#{pkg_path.basename} does not contain a `PackageInfo` file." end distribution_path = extract_dir/"Distribution" if distribution_path.exist? require "rexml/document" xml = REXML::Document.new(distribution_path.read) product = xml.get_elements("//installer-gui-script//product").first product_version = product["version"] if product return product_version if product_version.present? end opoo "#{pkg_path.basename} contains multiple packages: #{packages}" if packages.count != 1 $stderr.puts Pathname.glob(extract_dir/"**/*") .map { |path| regex = %r{\A(.*?\.(app|qlgenerator|saver|plugin|kext|bundle|osax))/.*\Z} path.to_s.sub(regex, '\1') }.uniq ensure Cask::Utils.gain_permissions_remove(extract_dir) extract_dir.mkpath end end nil end end end end