| 
									
										
										
										
											2024-08-12 10:30:59 +01:00
										 |  |  | # typed: true # rubocop:todo Sorbet/StrictSigil | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-13 12:19:15 +01:00
										 |  |  | require "bundle_version" | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  | require "cask/cask" | 
					
						
							|  |  |  | require "cask/installer" | 
					
						
							| 
									
										
										
										
											2024-01-26 17:33:55 -08:00
										 |  |  | require "system_command" | 
					
						
							| 
									
										
										
										
											2025-08-20 19:20:19 +01:00
										 |  |  | require "utils/output" | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | module Homebrew | 
					
						
							|  |  |  |   # Check unversioned casks for updates by extracting their | 
					
						
							|  |  |  |   # contents and guessing the version from contained files. | 
					
						
							|  |  |  |   class UnversionedCaskChecker | 
					
						
							| 
									
										
										
										
											2024-01-26 17:33:55 -08:00
										 |  |  |     include SystemCommand::Mixin | 
					
						
							| 
									
										
										
										
											2025-08-20 19:20:19 +01:00
										 |  |  |     include Utils::Output::Mixin | 
					
						
							| 
									
										
										
										
											2024-01-26 17:33:55 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-24 19:14:04 -07:00
										 |  |  |     sig { returns(Cask::Cask) } | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-06 21:30:45 +02:00
										 |  |  |     sig { returns(T::Array[Cask::Artifact::KeyboardLayout]) } | 
					
						
							| 
									
										
										
										
											2023-03-26 08:10:54 +02:00
										 |  |  |     def keyboard_layouts | 
					
						
							|  |  |  |       @keyboard_layouts ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::KeyboardLayout) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-07 17:38:47 +10:00
										 |  |  |     sig { returns(T::Array[Cask::Artifact::Qlplugin]) } | 
					
						
							|  |  |  |     def qlplugins | 
					
						
							|  |  |  |       @qlplugins ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Qlplugin) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-04 05:06:58 +02:00
										 |  |  |     sig { returns(T::Array[Cask::Artifact::Dictionary]) } | 
					
						
							|  |  |  |     def dictionaries | 
					
						
							|  |  |  |       @dictionaries ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Dictionary) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-03 03:56:04 +02:00
										 |  |  |     sig { returns(T::Array[Cask::Artifact::ScreenSaver]) } | 
					
						
							|  |  |  |     def screen_savers | 
					
						
							|  |  |  |       @screen_savers ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::ScreenSaver) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-29 13:15:07 +02:00
										 |  |  |     sig { returns(T::Array[Cask::Artifact::Colorpicker]) } | 
					
						
							|  |  |  |     def colorpickers | 
					
						
							|  |  |  |       @colorpickers ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Colorpicker) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(T::Array[Cask::Artifact::Mdimporter]) } | 
					
						
							|  |  |  |     def mdimporters | 
					
						
							|  |  |  |       @mdimporters ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Mdimporter) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-05 14:26:38 +11:00
										 |  |  |     sig { returns(T::Array[Cask::Artifact::Installer]) } | 
					
						
							|  |  |  |     def installers | 
					
						
							|  |  |  |       @installers ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Installer) } | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |     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? | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |       apps.one? | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-21 16:09:09 +10:00
										 |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     def single_qlplugin_cask? | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |       qlplugins.one? | 
					
						
							| 
									
										
										
										
											2021-07-21 16:09:09 +10:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     def single_pkg_cask? | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |       pkgs.one? | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |     # 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-26 16:58:39 +00:00
										 |  |  |       Dir.mktmpdir("cask-checker", HOMEBREW_TEMP) do |dir| | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  |         dir = Pathname(dir) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         installer.extract_primary_container(to: dir) | 
					
						
							| 
									
										
										
										
											2025-08-26 21:23:07 +08:00
										 |  |  |         installer.process_rename_operations(target_dir: dir) | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-29 13:15:07 +02:00
										 |  |  |         info_plist_paths = [ | 
					
						
							|  |  |  |           *apps, | 
					
						
							|  |  |  |           *keyboard_layouts, | 
					
						
							|  |  |  |           *mdimporters, | 
					
						
							|  |  |  |           *colorpickers, | 
					
						
							| 
									
										
										
										
											2023-04-04 05:06:58 +02:00
										 |  |  |           *dictionaries, | 
					
						
							| 
									
										
										
										
											2023-03-29 13:15:07 +02:00
										 |  |  |           *qlplugins, | 
					
						
							|  |  |  |           *installers, | 
					
						
							| 
									
										
										
										
											2023-04-03 03:56:04 +02:00
										 |  |  |           *screen_savers, | 
					
						
							| 
									
										
										
										
											2023-03-29 13:15:07 +02:00
										 |  |  |         ].flat_map do |artifact| | 
					
						
							| 
									
										
										
										
											2023-04-03 04:53:18 +02:00
										 |  |  |           sources = if artifact.is_a?(Cask::Artifact::Installer) | 
					
						
							|  |  |  |             # Installers are sometimes contained within an `.app`, so try both. | 
					
						
							|  |  |  |             installer_path = artifact.path | 
					
						
							|  |  |  |             installer_path.ascend | 
					
						
							| 
									
										
										
										
											2023-04-03 20:47:15 +02:00
										 |  |  |                           .select { |path| path == installer_path || path.extname == ".app" } | 
					
						
							| 
									
										
										
										
											2023-04-03 04:53:18 +02:00
										 |  |  |                           .sort | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             [artifact.source.basename] | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           sources.flat_map do |source| | 
					
						
							|  |  |  |             top_level_info_plists(Pathname.glob(dir/"**"/source/"Contents"/"Info.plist")).sort | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         info_plist_paths.each(&parse_info_plist) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-21 14:06:33 +01:00
										 |  |  |         pkg_paths = pkgs.flat_map { |pkg| Pathname.glob(dir/"**"/pkg.path.basename).sort } | 
					
						
							|  |  |  |         pkg_paths = Pathname.glob(dir/"**"/"*.pkg").sort if pkg_paths.empty? | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         pkg_paths.each do |pkg_path| | 
					
						
							| 
									
										
										
										
											2024-02-26 16:58:39 +00:00
										 |  |  |           Dir.mktmpdir("cask-checker", HOMEBREW_TEMP) do |extract_dir| | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  |             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) | 
					
						
							| 
									
										
										
										
											2023-11-05 08:55:58 -08:00
										 |  |  |             Pathname(extract_dir).mkpath | 
					
						
							| 
									
										
										
										
											2021-04-04 03:00:34 +02:00
										 |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         nil | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       versions | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |     sig { returns(T.nilable(String)) } | 
					
						
							|  |  |  |     def guess_cask_version | 
					
						
							| 
									
										
										
										
											2021-07-22 19:36:50 +10:00
										 |  |  |       if apps.empty? && pkgs.empty? && qlplugins.empty? | 
					
						
							|  |  |  |         opoo "Cask #{cask} does not contain any apps, qlplugins or PKG installers." | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |         return | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-26 16:58:39 +00:00
										 |  |  |       Dir.mktmpdir("cask-checker", HOMEBREW_TEMP) do |dir| | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |         dir = Pathname(dir) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-23 01:47:37 +01:00
										 |  |  |         installer.then do |i| | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |           i.extract_primary_container(to: dir) | 
					
						
							|  |  |  |         rescue ErrorDuringExecution => e | 
					
						
							|  |  |  |           onoe e | 
					
						
							|  |  |  |           return nil | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         info_plist_paths = apps.flat_map do |app| | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |           top_level_info_plists(Pathname.glob(dir/"**"/app.source.basename/"Contents"/"Info.plist")).sort | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         info_plist_paths.each do |info_plist_path| | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |           if (version = BundleVersion.from_info_plist(info_plist_path)) | 
					
						
							|  |  |  |             return version.nice_version | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         pkg_paths = pkgs.flat_map do |pkg| | 
					
						
							| 
									
										
										
										
											2020-12-10 19:09:46 +01:00
										 |  |  |           Pathname.glob(dir/"**"/pkg.path.basename).sort | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         pkg_paths.each do |pkg_path| | 
					
						
							|  |  |  |           packages = | 
					
						
							|  |  |  |             system_command!("installer", args: ["-plist", "-pkginfo", "-pkg", pkg_path]) | 
					
						
							|  |  |  |             .plist | 
					
						
							|  |  |  |             .map { |package| package.fetch("Package") } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-26 16:58:39 +00:00
										 |  |  |           Dir.mktmpdir("cask-checker", HOMEBREW_TEMP) do |extract_dir| | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |             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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |             top_level_info_plist_paths = top_level_info_plists(Pathname.glob(extract_dir/"**/Contents/Info.plist")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             unique_info_plist_versions = | 
					
						
							| 
									
										
										
										
											2024-02-22 23:29:55 +00:00
										 |  |  |               top_level_info_plist_paths.filter_map { |i| BundleVersion.from_info_plist(i)&.nice_version } | 
					
						
							|  |  |  |                                         .uniq | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |             return unique_info_plist_versions.first if unique_info_plist_versions.one? | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |             package_info_path = extract_dir/"PackageInfo" | 
					
						
							|  |  |  |             if package_info_path.exist? | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |               if (version = BundleVersion.from_package_info(package_info_path)) | 
					
						
							|  |  |  |                 return version.nice_version | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |               end | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |             elsif packages.one? | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |               onoe "#{pkg_path.basename} does not contain a `PackageInfo` file." | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |             distribution_path = extract_dir/"Distribution" | 
					
						
							|  |  |  |             if distribution_path.exist? | 
					
						
							| 
									
										
										
										
											2021-06-25 18:46:12 -04:00
										 |  |  |               require "rexml/document" | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-25 18:46:12 -04:00
										 |  |  |               xml = REXML::Document.new(distribution_path.read) | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-25 18:46:12 -04:00
										 |  |  |               product = xml.get_elements("//installer-gui-script//product").first | 
					
						
							|  |  |  |               product_version = product["version"] if product | 
					
						
							|  |  |  |               return product_version if product_version.present? | 
					
						
							| 
									
										
										
										
											2020-12-14 18:43:31 +01:00
										 |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             opoo "#{pkg_path.basename} contains multiple packages: #{packages}" if packages.count != 1
 | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             $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) | 
					
						
							| 
									
										
										
										
											2023-11-05 08:55:58 -08:00
										 |  |  |             Pathname(extract_dir).mkpath | 
					
						
							| 
									
										
										
										
											2020-12-07 16:31:45 +01:00
										 |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         nil | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |