brew/Library/Homebrew/unversioned_cask_checker.rb

265 lines
8.5 KiB
Ruby
Raw Permalink Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
2020-12-13 12:19:15 +01:00
require "bundle_version"
require "cask/cask"
require "cask/installer"
require "system_command"
module Homebrew
# Check unversioned casks for updates by extracting their
# contents and guessing the version from contained files.
class UnversionedCaskChecker
include SystemCommand::Mixin
2023-04-24 19:14:04 -07:00
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::KeyboardLayout]) }
def keyboard_layouts
@keyboard_layouts ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::KeyboardLayout) }
end
sig { returns(T::Array[Cask::Artifact::Qlplugin]) }
def qlplugins
@qlplugins ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Qlplugin) }
end
sig { returns(T::Array[Cask::Artifact::Dictionary]) }
def dictionaries
@dictionaries ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Dictionary) }
end
sig { returns(T::Array[Cask::Artifact::ScreenSaver]) }
def screen_savers
@screen_savers ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::ScreenSaver) }
end
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
sig { returns(T::Array[Cask::Artifact::Installer]) }
def installers
@installers ||= @cask.artifacts.select { |a| a.is_a?(Cask::Artifact::Installer) }
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_qlplugin_cask?
qlplugins.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
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)
info_plist_paths = [
*apps,
*keyboard_layouts,
*mdimporters,
*colorpickers,
*dictionaries,
*qlplugins,
*installers,
*screen_savers,
].flat_map do |artifact|
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" }
.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)
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
sig { returns(T.nilable(String)) }
def guess_cask_version
if apps.empty? && pkgs.empty? && qlplugins.empty?
opoo "Cask #{cask} does not contain any apps, qlplugins or PKG installers."
return
end
2024-02-26 16:58:39 +00:00
Dir.mktmpdir("cask-checker", HOMEBREW_TEMP) do |dir|
dir = Pathname(dir)
2022-04-23 01:47:37 +01:00
installer.then 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|
2020-12-10 19:09:46 +01:00
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") }
2024-02-26 16:58:39 +00:00
Dir.mktmpdir("cask-checker", HOMEBREW_TEMP) 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.filter_map { |i| BundleVersion.from_info_plist(i)&.nice_version }
.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)
2023-11-05 08:55:58 -08:00
Pathname(extract_dir).mkpath
end
end
nil
end
end
end
end