168 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			168 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # typed: true
 | |
| # frozen_string_literal: true
 | |
| 
 | |
| 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
 | |
| 
 | |
|     sig { params(info_plist_path: Pathname).returns(T.nilable(String)) }
 | |
|     def self.version_from_info_plist(info_plist_path)
 | |
|       plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist
 | |
| 
 | |
|       short_version = plist["CFBundleShortVersionString"].presence
 | |
|       version = plist["CFBundleVersion"].presence
 | |
| 
 | |
|       return decide_between_versions(short_version, version) if short_version && version
 | |
|     end
 | |
| 
 | |
|     sig { params(package_info_path: Pathname).returns(T.nilable(String)) }
 | |
|     def self.version_from_package_info(package_info_path)
 | |
|       contents = package_info_path.read
 | |
| 
 | |
|       short_version = contents[/CFBundleShortVersionString="([^"]*)"/, 1].presence
 | |
|       version = contents[/CFBundleVersion="([^"]*)"/, 1].presence
 | |
| 
 | |
|       return decide_between_versions(short_version, version) if short_version && version
 | |
|     end
 | |
| 
 | |
|     sig do
 | |
|       params(short_version: T.nilable(String), version: T.nilable(String))
 | |
|         .returns(T.nilable(String))
 | |
|     end
 | |
|     def self.decide_between_versions(short_version, version)
 | |
|       return short_version if short_version == version
 | |
| 
 | |
|       if short_version && version
 | |
|         return version if version.match?(/\A\d+(\.\d+)+\Z/) && version.start_with?("#{short_version}.")
 | |
|         return short_version if short_version.match?(/\A\d+(\.\d+)+\Z/) && short_version.start_with?("#{version}.")
 | |
| 
 | |
|         if short_version.match?(/\A\d+(\.\d+)*\Z/) && version.match?(/\A\d+\Z/)
 | |
|           return short_version if short_version.start_with?("#{version}.") || short_version.end_with?(".#{version}")
 | |
| 
 | |
|           return "#{short_version},#{version}"
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       short_version || version
 | |
|     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|
 | |
|           Pathname.glob(dir/"**"/app.source.basename/"Contents"/"Info.plist").reject do |info_plist_path|
 | |
|             # Ignore nested apps.
 | |
|             info_plist_path.parent.parent.parent.ascend.any? { |p| p.extname == ".app" }
 | |
|           end.sort
 | |
|         end
 | |
| 
 | |
|         info_plist_paths.each do |info_plist_path|
 | |
|           if (version = self.class.version_from_info_plist(info_plist_path))
 | |
|             return 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") }
 | |
|             .uniq
 | |
| 
 | |
|           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
 | |
| 
 | |
|             package_info_path = extract_dir/"PackageInfo"
 | |
|             if package_info_path.exist?
 | |
|               if (version = self.class.version_from_package_info(package_info_path))
 | |
|                 return version
 | |
|               end
 | |
|             elsif packages.count == 1
 | |
|               onoe "#{pkg_path.basename} does not contain a `PackageInfo` file."
 | |
|             end
 | |
| 
 | |
|             opoo "#{pkg_path.basename} contains multiple packages: (#{packages.join(", ")})" 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
 | 
