mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
- Align casks with formulae: platform support comes from `depends_on :macos`/`:linux`/`macos:` data rather than generation heuristics guessing intent from `os` stanzas, Linux checksums or `on_linux` blocks. - `Cask#to_hash_with_variations` now emits variations for every valid OS/arch tag whenever `on_system` blocks exist, matching `Formula#to_hash_with_variations`; the Linux-specific gate and its `sha256_set_for_linux?` and `on_linux_blocks_exist?` tracking are removed. macOS-only casks publish truthful Linux variations, e.g. a `null` `sha256`, instead of omitting them. - `Cask::Installer` gains a first-class unsupported-system error: API-loaded casks with no activatable artifact for the running system fail with "This cask is not available on macOS/Linux." instead of installing nothing. Audited casks always declare an activatable artifact for the systems they support, so missing artifacts in API data mean the system is unsupported. Source loads keep working for unaudited casks, e.g. naked containers. - A sweep of the full homebrew/cask generation pipeline (all casks, both Linux tags, including internal per-tag payloads) confirmed no cask needs new `depends_on` annotations and nothing regresses.
842 lines
28 KiB
Ruby
842 lines
28 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
require "bundle_version"
|
|
require "cask/cask_loader"
|
|
require "cask/config"
|
|
require "cask/dsl"
|
|
require "cask/metadata"
|
|
require "cask/tab"
|
|
require "utils/output"
|
|
require "api_hashable"
|
|
require "trust"
|
|
|
|
module Cask
|
|
# An instance of a cask.
|
|
class Cask
|
|
extend Forwardable
|
|
extend APIHashable
|
|
extend ::Utils::Output::Mixin
|
|
include Metadata
|
|
|
|
# The unique identifier for this {Cask}, used to refer to it in commands
|
|
# and tap paths.
|
|
# e.g. `firefox`
|
|
#
|
|
# @api public
|
|
sig { returns(String) }
|
|
attr_reader :token
|
|
|
|
# The configuration of this {Cask}.
|
|
#
|
|
# @api internal
|
|
sig { returns(Config) }
|
|
attr_reader :config
|
|
|
|
sig { returns(T.nilable(Pathname)) }
|
|
attr_reader :sourcefile_path
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
attr_reader :source
|
|
|
|
sig { returns(Config) }
|
|
attr_reader :default_config
|
|
|
|
sig { returns(T.nilable(CaskLoader::ILoader)) }
|
|
attr_reader :loader
|
|
|
|
sig { returns(T.nilable(Pathname)) }
|
|
attr_accessor :download
|
|
|
|
sig { returns(T::Boolean) }
|
|
attr_accessor :allow_reassignment
|
|
|
|
sig { params(eval_all: T::Boolean).returns(T::Array[Cask]) }
|
|
def self.all(eval_all: false)
|
|
if !eval_all && !Homebrew::EnvConfig.tap_trust_configured?
|
|
raise ArgumentError,
|
|
"Cask::Cask#all cannot be used without `HOMEBREW_REQUIRE_TAP_TRUST=1` or " \
|
|
"`HOMEBREW_NO_REQUIRE_TAP_TRUST=1`"
|
|
end
|
|
|
|
# Load core casks from tokens so they load from the API when the core cask is not tapped.
|
|
tokens_and_files = CoreCaskTap.instance.cask_tokens
|
|
tokens_and_files += Tap.reject(&:core_cask_tap?).flat_map(&:cask_files)
|
|
.then { |files| Homebrew::Trust.trusted_cask_files(files) }
|
|
tokens_and_files.filter_map do |token_or_file|
|
|
CaskLoader.load(token_or_file)
|
|
rescue CaskUnreadableError, CaskInvalidError => e
|
|
# Don't let one broken cask break commands. But do complain.
|
|
opoo e.message
|
|
|
|
nil
|
|
end
|
|
end
|
|
|
|
# This collides with Kernel#tap, complicating the type signature.
|
|
# Overload sigs are not supported by Sorbet, otherwise we would use:
|
|
# sig { params(blk: T.proc.params(arg0: Cask).void).returns(T.self_type) }
|
|
# sig { params(blk: NilClass).returns(T.nilable(Tap)) }
|
|
# Using a union type would require casts or type guards at call sites,
|
|
# so T.untyped is used as the return type instead.
|
|
sig { params(blk: T.nilable(T.proc.params(arg0: Cask).void)).returns(T.untyped) }
|
|
def tap(&blk)
|
|
return super if block_given? # Kernel#tap
|
|
|
|
@tap
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
token: String,
|
|
sourcefile_path: T.nilable(Pathname),
|
|
source: T.nilable(String),
|
|
tap: T.nilable(Tap),
|
|
loaded_from_api: T::Boolean,
|
|
loaded_from_internal_api: T::Boolean,
|
|
api_source: T.nilable(T::Hash[String, T.untyped]),
|
|
config: T.nilable(Config),
|
|
allow_reassignment: T::Boolean,
|
|
loader: T.nilable(CaskLoader::ILoader),
|
|
block: T.nilable(T.proc.bind(DSL).void),
|
|
).void
|
|
}
|
|
def initialize(token, sourcefile_path: nil, source: nil, tap: nil, loaded_from_api: false,
|
|
loaded_from_internal_api: false, api_source: nil, config: nil, allow_reassignment: false,
|
|
loader: nil, &block)
|
|
@token = token
|
|
@sourcefile_path = sourcefile_path
|
|
@source = source
|
|
@tap = tap
|
|
@allow_reassignment = allow_reassignment
|
|
@loaded_from_api = loaded_from_api
|
|
@loaded_from_internal_api = loaded_from_internal_api
|
|
@api_source = api_source
|
|
@language_variations_available = T.let(false, T::Boolean)
|
|
@language_evaluator = T.let(nil, T.nilable(T.proc.params(languages: T::Array[String]).returns(T.nilable(String))))
|
|
@loader = loader
|
|
# Sorbet has trouble with bound procs assigned to instance variables:
|
|
# https://github.com/sorbet/sorbet/issues/6843
|
|
@block = T.let(block, T.untyped)
|
|
|
|
@default_config = T.let(config || Config.new, Config)
|
|
|
|
@config = T.let(
|
|
if config_path.exist?
|
|
Config.from_json(File.read(config_path), ignore_invalid_keys: true)
|
|
else
|
|
@default_config
|
|
end,
|
|
Config,
|
|
)
|
|
refresh
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def loaded_from_api? = @loaded_from_api
|
|
|
|
sig { returns(T::Boolean) }
|
|
def loaded_from_internal_api? = @loaded_from_internal_api
|
|
|
|
sig { returns(T.any(String, Pathname)) }
|
|
def reloadable_ref
|
|
return full_name if loaded_from_api?
|
|
|
|
sourcefile_path || raise("unexpected nil cask sourcefile_path")
|
|
end
|
|
|
|
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
attr_reader :api_source
|
|
|
|
# An old name for the cask.
|
|
sig { returns(T::Array[String]) }
|
|
def old_tokens
|
|
@old_tokens ||= T.let(
|
|
if (t = tap)
|
|
Tap.tap_migration_oldnames(t, token) +
|
|
t.cask_reverse_renames.fetch(token, [])
|
|
else
|
|
[]
|
|
end,
|
|
T.nilable(T::Array[String]),
|
|
)
|
|
end
|
|
|
|
sig { params(config: Config).void }
|
|
def config=(config)
|
|
@config = config
|
|
|
|
refresh
|
|
end
|
|
|
|
sig { void }
|
|
def refresh
|
|
@dsl = T.let(DSL.new(self), T.nilable(DSL))
|
|
return unless @block
|
|
|
|
dsl!.instance_eval(&@block)
|
|
dsl!.language_eval
|
|
rescue NoMethodError => e
|
|
raise CaskInvalidError.new(token, e.message), e.backtrace
|
|
end
|
|
|
|
# Refresh the cask as evaluated on `tag` and yield. Returns `nil` instead of
|
|
# raising when the cask has `on_system` blocks that omit the tag.
|
|
sig {
|
|
type_parameters(:U)
|
|
.params(tag: ::Utils::Bottles::Tag, _block: T.proc.returns(T.type_parameter(:U)))
|
|
.returns(T.nilable(T.type_parameter(:U)))
|
|
}
|
|
def refresh_for_tag(tag, &_block)
|
|
Homebrew::SimulateSystem.with(os: tag.system, arch: tag.arch) do
|
|
refresh
|
|
yield
|
|
end
|
|
rescue CaskInvalidError, CaskUnreadableError
|
|
raise unless on_system_blocks_exist?
|
|
|
|
nil
|
|
end
|
|
|
|
def_delegators :@dsl, *(::Cask::DSL::DSL_METHODS - [:language])
|
|
|
|
sig {
|
|
params(
|
|
args: String,
|
|
default: T::Boolean,
|
|
block: T.nilable(T.proc.returns(String)),
|
|
).returns(T.nilable(String))
|
|
}
|
|
def language(*args, default: false, &block)
|
|
return @language_evaluator.call(config.languages) if args.empty? && block.nil? && @language_evaluator
|
|
|
|
dsl!.language(*args, default:, &block)
|
|
end
|
|
|
|
sig { returns(DSL::Caveats) }
|
|
def caveats_object = dsl!.caveats_object
|
|
|
|
sig { params(caskroom_path: Pathname).returns(T::Array[[String, String]]) }
|
|
def timestamped_versions(caskroom_path: self.caskroom_path)
|
|
pattern = metadata_timestamped_path(version: "*", timestamp: "*", caskroom_path:).to_s
|
|
relative_paths = Pathname.glob(pattern)
|
|
.map { |p| p.relative_path_from(p.parent.parent) }
|
|
# Sorbet is unaware that Pathname is sortable: https://github.com/sorbet/sorbet/issues/6844
|
|
T.unsafe(relative_paths).sort_by(&:basename) # sort by timestamp
|
|
.map { |p| p.split.map(&:to_s) }
|
|
end
|
|
|
|
# The fully-qualified token of this {Cask}.
|
|
#
|
|
# @api internal
|
|
sig { returns(String) }
|
|
def full_token
|
|
return token if (t = tap).nil?
|
|
return token if t.core_cask_tap?
|
|
|
|
"#{t.name}/#{token}"
|
|
end
|
|
|
|
# Alias for {#full_token}.
|
|
#
|
|
# @api internal
|
|
sig { returns(String) }
|
|
def full_name = full_token
|
|
|
|
sig { returns(T::Boolean) }
|
|
def installed?
|
|
installed_caskfile&.exist? || false
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def any_version_installed? = installed?
|
|
|
|
sig { returns(T::Boolean) }
|
|
def font?
|
|
artifacts.all?(Artifact::Font)
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def supports_linux?
|
|
return true if depends_on.requires_linux?
|
|
|
|
!depends_on.requires_macos?
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def supports_macos?
|
|
!depends_on.requires_linux?
|
|
end
|
|
|
|
# The caskfile is needed during installation when there are legacy
|
|
# `*flight` blocks or language variations missing from API data.
|
|
sig { returns(T::Boolean) }
|
|
def caskfile_only?
|
|
(languages.any? && !@language_variations_available) || artifacts.any?(Artifact::AbstractFlightBlock)
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def uninstall_flight_blocks?
|
|
artifacts.any? do |artifact|
|
|
case artifact
|
|
when Artifact::PreflightBlock
|
|
artifact.directives.key?(:uninstall_preflight)
|
|
when Artifact::PostflightBlock
|
|
artifact.directives.key?(:uninstall_postflight)
|
|
end
|
|
end
|
|
end
|
|
|
|
sig { returns(T.nilable(Time)) }
|
|
def install_time
|
|
# <caskroom_path>/.metadata/<version>/<timestamp>/Casks/<token>.{rb,json} -> <timestamp>
|
|
caskfile = installed_caskfile
|
|
Time.strptime(caskfile.dirname.dirname.basename.to_s, Metadata::TIMESTAMP_FORMAT) if caskfile
|
|
end
|
|
|
|
sig { returns(T.nilable(Pathname)) }
|
|
def installed_caskfile
|
|
Caskroom.cask_installed_caskfile(token, old_tokens:)
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def installed_version
|
|
# <caskroom_path>/.metadata/<version>/<timestamp>/Casks/<token>.{rb,json} -> <version>
|
|
Caskroom.cask_installed_version(token, old_tokens:)
|
|
end
|
|
|
|
sig { void }
|
|
def pin
|
|
return unless (installed_version = self.installed_version)
|
|
|
|
versioned_path = caskroom_path/installed_version
|
|
return unless versioned_path.exist?
|
|
|
|
HOMEBREW_PINNED_CASKS.mkpath
|
|
return if pinned?
|
|
|
|
pin_path.unlink if pin_path.file? || pin_path.symlink?
|
|
pin_path.make_relative_symlink(versioned_path)
|
|
end
|
|
|
|
sig { void }
|
|
def unpin
|
|
pin_path.unlink if pin_path.symlink?
|
|
HOMEBREW_PINNED_CASKS.rmdir_if_possible
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def pinned?
|
|
pin_path.symlink? && pin_path.exist?
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def pinnable?
|
|
return false unless (installed_version = self.installed_version)
|
|
|
|
(caskroom_path/installed_version).exist?
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def pinned_version
|
|
pin_path.resolved_path.basename.to_s if pinned?
|
|
end
|
|
|
|
sig { returns(Pathname) }
|
|
def pin_path
|
|
HOMEBREW_PINNED_CASKS/token
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def bundle_short_version
|
|
bundle_version&.short_version
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def bundle_long_version
|
|
bundle_version&.version
|
|
end
|
|
|
|
sig { returns(Tab) }
|
|
def tab
|
|
Tab.for_cask(self)
|
|
end
|
|
|
|
sig { returns(Pathname) }
|
|
def config_path
|
|
metadata_main_container_path/"config.json"
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def checksumable?
|
|
return false if (url = self.url).nil? || url.to_s.blank?
|
|
|
|
DownloadStrategyDetector.detect(url.to_s, url.using) <= AbstractFileDownloadStrategy || false
|
|
end
|
|
|
|
sig { returns(Pathname) }
|
|
def download_sha_path
|
|
metadata_main_container_path/"LATEST_DOWNLOAD_SHA256"
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def new_download_sha
|
|
require "cask/installer"
|
|
|
|
# Call checksumable? before hashing
|
|
@new_download_sha ||= T.let(
|
|
Installer.new(self, verify_download_integrity: false)
|
|
.download(quiet: true)
|
|
.instance_eval { |x| Digest::SHA256.file(x).hexdigest },
|
|
T.nilable(String),
|
|
)
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def outdated_download_sha?
|
|
return true unless checksumable?
|
|
|
|
current_download_sha = download_sha_path.read if download_sha_path.exist?
|
|
current_download_sha.blank? || current_download_sha != new_download_sha
|
|
end
|
|
|
|
sig { returns(Pathname) }
|
|
def caskroom_path
|
|
@caskroom_path ||= T.let(Caskroom.path.join(token), T.nilable(Pathname))
|
|
end
|
|
|
|
# Check if the installed cask is outdated.
|
|
#
|
|
# @api internal
|
|
sig {
|
|
params(greedy: T::Boolean, greedy_latest: T.nilable(T::Boolean), greedy_auto_updates: T.nilable(T::Boolean))
|
|
.returns(T::Boolean)
|
|
}
|
|
def outdated?(greedy: false, greedy_latest: false, greedy_auto_updates: false)
|
|
!outdated_version(greedy:, greedy_latest:,
|
|
greedy_auto_updates:).nil?
|
|
end
|
|
|
|
sig {
|
|
params(greedy: T::Boolean, greedy_latest: T.nilable(T::Boolean), greedy_auto_updates: T.nilable(T::Boolean))
|
|
.returns(T.nilable(String))
|
|
}
|
|
def outdated_version(greedy: false, greedy_latest: false, greedy_auto_updates: false)
|
|
# special case: tap version is not available
|
|
return if version.nil?
|
|
|
|
if version.latest?
|
|
return installed_version if (greedy || greedy_latest) && outdated_download_sha?
|
|
|
|
return
|
|
end
|
|
|
|
return if installed_version == version
|
|
|
|
if auto_updates && !greedy && !greedy_auto_updates
|
|
return unless Homebrew::EnvConfig.upgrade_auto_updates_casks?
|
|
|
|
return installed_version if auto_updates_bundle_outdated?
|
|
|
|
return
|
|
end
|
|
|
|
installed_version
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
greedy: T::Boolean,
|
|
verbose: T::Boolean,
|
|
json: T::Boolean,
|
|
greedy_latest: T::Boolean,
|
|
greedy_auto_updates: T::Boolean,
|
|
).returns(T.any(String, T::Hash[Symbol, T.untyped]))
|
|
}
|
|
def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates)
|
|
return token if !verbose && !json
|
|
|
|
installed_version = outdated_version(greedy:, greedy_latest:,
|
|
greedy_auto_updates:).to_s
|
|
|
|
if json
|
|
{
|
|
name: token,
|
|
installed_versions: [installed_version],
|
|
current_version: version,
|
|
pinned: pinned?,
|
|
pinned_version:,
|
|
}
|
|
else
|
|
pinned = " [pinned at #{pinned_version}]" if pinned?
|
|
|
|
"#{token} (#{installed_version}) != #{version}#{pinned}"
|
|
end
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def ruby_source_path
|
|
return @ruby_source_path if defined?(@ruby_source_path)
|
|
|
|
return unless (sfp = sourcefile_path)
|
|
return unless (t = tap)
|
|
|
|
@ruby_source_path = T.let(sfp.relative_path_from(t.path).to_s, T.nilable(String))
|
|
end
|
|
|
|
sig { returns(T::Hash[Symbol, T.nilable(String)]) }
|
|
def ruby_source_checksum
|
|
@ruby_source_checksum ||= T.let(
|
|
begin
|
|
sfp = sourcefile_path
|
|
{
|
|
sha256: sfp ? Digest::SHA256.file(sfp).hexdigest : nil,
|
|
}.freeze
|
|
end,
|
|
T.nilable(T::Hash[Symbol, T.nilable(String)]),
|
|
)
|
|
end
|
|
|
|
sig { returns(T::Array[String]) }
|
|
def languages
|
|
@languages ||= T.let(dsl!.languages, T.nilable(T::Array[String]))
|
|
end
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def tap_git_head
|
|
@tap_git_head ||= T.let(tap&.git_head, T.nilable(String))
|
|
rescue TapUnavailableError
|
|
nil
|
|
end
|
|
|
|
sig { params(cask_struct: Homebrew::API::CaskStruct, tap_git_head: T.nilable(String)).void }
|
|
def populate_from_api!(cask_struct, tap_git_head:)
|
|
raise ArgumentError, "Expected cask to be loaded from the API" unless loaded_from_api?
|
|
|
|
@languages = cask_struct.languages
|
|
@language_variations_available = cask_struct.language_variations.any?
|
|
@language_evaluator = ->(languages) { cask_struct.language(languages) }
|
|
@tap_git_head = tap_git_head
|
|
@ruby_source_path = cask_struct.ruby_source_path
|
|
@ruby_source_checksum = cask_struct.ruby_source_checksum
|
|
end
|
|
|
|
# The string representation of this {Cask}, returning its {#token}.
|
|
#
|
|
# @api public
|
|
sig { returns(String) }
|
|
def to_s = token
|
|
|
|
sig { returns(String) }
|
|
def inspect
|
|
"#<Cask #{token}#{sourcefile_path&.to_s&.prepend(" ")}>"
|
|
end
|
|
|
|
sig { returns(Integer) }
|
|
def hash
|
|
token.hash
|
|
end
|
|
|
|
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
def eql?(other)
|
|
instance_of?(other.class) && token == other.token
|
|
end
|
|
alias == eql?
|
|
|
|
sig { returns(T::Hash[String, T.untyped]) }
|
|
def to_h
|
|
{
|
|
"token" => token,
|
|
"full_token" => full_name,
|
|
"old_tokens" => old_tokens,
|
|
"tap" => tap&.name,
|
|
"name" => name,
|
|
"desc" => desc,
|
|
"homepage" => homepage,
|
|
"url" => url,
|
|
"url_specs" => url_specs,
|
|
"version" => version,
|
|
"autobump" => autobump?,
|
|
"no_autobump_message" => no_autobump_message,
|
|
"skip_livecheck" => livecheck.skip?,
|
|
"installed" => installed_version,
|
|
"installed_time" => install_time&.to_i,
|
|
"bundle_version" => bundle_long_version,
|
|
"bundle_short_version" => bundle_short_version,
|
|
"pinned" => pinned?,
|
|
"pinned_version" => pinned_version,
|
|
"outdated" => outdated?,
|
|
"sha256" => sha256,
|
|
"artifacts" => artifacts_list,
|
|
"caveats" => caveats_for_api,
|
|
"caveats_rosetta" => caveats_object.invoked?(:requires_rosetta) || nil,
|
|
"depends_on" => depends_on,
|
|
"conflicts_with" => conflicts_with,
|
|
"container" => container&.pairs,
|
|
"rename" => rename_list,
|
|
"auto_updates" => auto_updates,
|
|
"deprecated" => deprecated?,
|
|
"deprecation_date" => deprecation_date,
|
|
"deprecation_reason" => deprecation_reason,
|
|
"deprecation_replacement_formula" => deprecation_replacement_formula,
|
|
"deprecation_replacement_cask" => deprecation_replacement_cask,
|
|
"deprecate_args" => deprecate_args,
|
|
"disabled" => disabled?,
|
|
"disable_date" => disable_date,
|
|
"disable_reason" => disable_reason,
|
|
"disable_replacement_formula" => disable_replacement_formula,
|
|
"disable_replacement_cask" => disable_replacement_cask,
|
|
"disable_args" => disable_args,
|
|
"tap_git_head" => tap_git_head,
|
|
"languages" => languages,
|
|
"ruby_source_path" => ruby_source_path,
|
|
"ruby_source_checksum" => ruby_source_checksum,
|
|
}
|
|
end
|
|
|
|
HASH_KEYS_TO_SKIP = %w[outdated installed pinned pinned_version versions].freeze
|
|
private_constant :HASH_KEYS_TO_SKIP
|
|
|
|
AUTO_UPDATES_BAD_BUNDLE_VERSIONS = %w[0 0.0].freeze
|
|
private_constant :AUTO_UPDATES_BAD_BUNDLE_VERSIONS
|
|
|
|
sig { returns(T::Hash[String, T.untyped]) }
|
|
def to_hash_with_variations
|
|
if loaded_from_internal_api?
|
|
raise UsageError, "Cannot call #to_hash_with_variations on casks loaded from the internal API"
|
|
end
|
|
|
|
if loaded_from_api? && (json_cask = api_source) && !Homebrew::EnvConfig.no_install_from_api?
|
|
return api_to_local_hash(json_cask.dup)
|
|
end
|
|
|
|
hash = to_h_with_language_variations
|
|
variations = {}
|
|
|
|
if dsl!.on_system_blocks_exist?
|
|
begin
|
|
OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag|
|
|
macos_requirements = [depends_on.macos, depends_on.maximum_macos].compact
|
|
next if bottle_tag.macos? &&
|
|
macos_requirements.present? &&
|
|
!dsl!.depends_on_set_in_block? &&
|
|
macos_requirements.any? { |requirement| !requirement.allows?(bottle_tag.to_macos_version) }
|
|
|
|
refresh_for_tag(bottle_tag) do
|
|
to_h_with_language_variations.each do |key, value|
|
|
next if HASH_KEYS_TO_SKIP.include? key
|
|
next if value.to_s == hash[key].to_s
|
|
|
|
variations[bottle_tag.to_sym] ||= {}
|
|
variations[bottle_tag.to_sym][key] = value
|
|
end
|
|
end
|
|
end
|
|
ensure
|
|
refresh
|
|
end
|
|
end
|
|
|
|
hash["variations"] = variations
|
|
hash
|
|
end
|
|
|
|
sig { returns(T::Hash[String, T.untyped]) }
|
|
def to_h_with_language_variations
|
|
language_groups = dsl!.language_groups
|
|
return to_h if language_groups.empty?
|
|
|
|
default_language_group = dsl!.default_language_group
|
|
raise CaskInvalidError.new(self, "No default language specified.") if default_language_group.nil?
|
|
|
|
original_config = config
|
|
language_hashes = language_groups.to_h do |languages|
|
|
localised_config = original_config.dup
|
|
localised_config.explicit = original_config.explicit.dup
|
|
localised_config.languages = [languages.fetch(0)]
|
|
self.config = localised_config
|
|
[languages, [to_h, dsl!.language_eval]]
|
|
end
|
|
hash = language_hashes.fetch(default_language_group).first
|
|
hash["language_variations"] = language_hashes.map do |languages, (language_hash, value)|
|
|
variation = {
|
|
"languages" => languages,
|
|
"default" => languages == default_language_group,
|
|
"value" => value,
|
|
}
|
|
language_hash.each do |key, language_value|
|
|
next if HASH_KEYS_TO_SKIP.include? key
|
|
next if language_value.to_s == hash[key].to_s
|
|
|
|
variation[key] = language_value
|
|
end
|
|
variation
|
|
end
|
|
hash
|
|
ensure
|
|
self.config = original_config if original_config
|
|
end
|
|
|
|
sig { returns(T::Hash[String, T.untyped]) }
|
|
def to_installed_json_hash
|
|
cask_url = url
|
|
return {} if cask_url.nil? || cask_url.only_path.blank?
|
|
|
|
{ "url_specs" => { "only_path" => cask_url.only_path } }
|
|
end
|
|
|
|
sig { params(uninstall_only: T::Boolean).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
def artifacts_list(uninstall_only: false)
|
|
artifacts.filter_map do |artifact|
|
|
case artifact
|
|
when Artifact::AbstractFlightBlock
|
|
uninstall_flight_block = artifact.directives.key?(:uninstall_preflight) ||
|
|
artifact.directives.key?(:uninstall_postflight)
|
|
next if uninstall_only && !uninstall_flight_block
|
|
|
|
# Only indicate whether this block is used as we don't load it from the API
|
|
{ artifact.summarize.to_sym => nil }
|
|
else
|
|
zap_artifact = artifact.is_a?(Artifact::Zap)
|
|
uninstall_artifact = artifact.respond_to?(:uninstall_phase) || artifact.respond_to?(:post_uninstall_phase)
|
|
next if uninstall_only && !zap_artifact && !uninstall_artifact
|
|
|
|
entry = T.let(
|
|
{ artifact.class.dsl_key => artifact.to_args },
|
|
T::Hash[Symbol, T.any(String, T::Array[T.anything])],
|
|
)
|
|
entry[:target] = artifact.target.to_s if !uninstall_only && artifact.is_a?(Artifact::Relocated)
|
|
entry
|
|
end
|
|
end
|
|
end
|
|
|
|
sig { params(uninstall_only: T::Boolean).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
def rename_list(uninstall_only: false)
|
|
rename.filter_map do |rename|
|
|
{ from: rename.from, to: rename.to }
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Returns caveats text for API serialization, excluding conditional
|
|
# built-in caveats that depend on the current machine's state.
|
|
# These are stored as separate boolean fields (e.g. caveats_rosetta)
|
|
# and evaluated at install time instead.
|
|
sig { returns(T.nilable(String)) }
|
|
def caveats_for_api
|
|
Tty.strip_ansi(caveats_object.to_s_without_conditional)
|
|
.presence
|
|
end
|
|
|
|
sig { returns(T.nilable(Homebrew::BundleVersion)) }
|
|
def bundle_version
|
|
@bundle_version ||= T.let(
|
|
if (bundle = artifacts.find { |a| a.is_a?(Artifact::App) }&.target) &&
|
|
(plist = Pathname("#{bundle}/Contents/Info.plist")) && plist.exist? && plist.readable?
|
|
Homebrew::BundleVersion.from_info_plist(plist)
|
|
end,
|
|
T.nilable(Homebrew::BundleVersion),
|
|
)
|
|
end
|
|
|
|
sig { returns(DSL) }
|
|
def dsl!
|
|
@dsl || raise("unexpected nil @dsl")
|
|
end
|
|
|
|
sig { returns(T.nilable(Artifact::App)) }
|
|
def single_app_artifact
|
|
app_artifacts = artifacts.grep(Artifact::App)
|
|
return unless app_artifacts.one?
|
|
|
|
app_artifacts.first
|
|
end
|
|
|
|
sig { returns(T.nilable(Pathname)) }
|
|
def installed_app_info_plist
|
|
return unless (app_artifact = single_app_artifact)
|
|
|
|
info_plist = app_artifact.target/"Contents/Info.plist"
|
|
info_plist if info_plist.exist? && info_plist.readable?
|
|
end
|
|
|
|
sig { params(first: T.nilable(String), second: T.nilable(String)).returns(T.nilable(Integer)) }
|
|
def compare_version_strings(first, second)
|
|
return if first.blank? || second.blank?
|
|
return if first.split(".").size != second.split(".").size
|
|
|
|
Version.new(first) <=> Version.new(second)
|
|
rescue
|
|
nil
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def auto_updates_bundle_outdated?
|
|
return false if !auto_updates || version.latest?
|
|
return false unless installed_app_info_plist
|
|
|
|
tap_short_version = version.csv.first.to_s.presence || version.to_s
|
|
|
|
begin
|
|
installed_short_version = bundle_short_version
|
|
installed_bundle_version = bundle_long_version
|
|
rescue ErrorDuringExecution
|
|
return false
|
|
end
|
|
installed_bundle_version = nil if AUTO_UPDATES_BAD_BUNDLE_VERSIONS.include?(installed_bundle_version)
|
|
installed_short_version = nil if AUTO_UPDATES_BAD_BUNDLE_VERSIONS.include?(installed_short_version)
|
|
return false if installed_short_version.nil? && installed_bundle_version.nil?
|
|
|
|
# Some apps split a cask version like 2.61-2057 across the short
|
|
# version and bundle version fields.
|
|
if installed_short_version && installed_bundle_version
|
|
combined_version_comparisons = version.csv.filter_map do |candidate|
|
|
compare_version_strings("#{installed_short_version}-#{installed_bundle_version}", candidate.to_s)
|
|
end
|
|
return false if combined_version_comparisons.include?(0)
|
|
return false if combined_version_comparisons.present? && combined_version_comparisons.exclude?(-1)
|
|
end
|
|
|
|
return false if [installed_short_version, installed_bundle_version].any? do |installed_plist_version|
|
|
compare_version_strings(installed_plist_version, tap_short_version)&.zero?
|
|
end
|
|
|
|
short_comparison = compare_version_strings(installed_short_version, tap_short_version)
|
|
return true if short_comparison == -1
|
|
return false if short_comparison == 1
|
|
|
|
build_comparisons = version.csv.filter_map do |candidate|
|
|
compare_version_strings(installed_bundle_version, candidate.to_s)
|
|
end
|
|
return false if build_comparisons.empty?
|
|
return false if build_comparisons.include?(0)
|
|
|
|
build_comparisons.include?(-1)
|
|
end
|
|
|
|
sig { params(hash: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
|
def api_to_local_hash(hash)
|
|
hash["token"] = token
|
|
hash["installed"] = installed_version
|
|
hash["pinned"] = pinned?
|
|
hash["pinned_version"] = pinned_version
|
|
hash["outdated"] = outdated?
|
|
hash
|
|
end
|
|
|
|
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
def url_specs
|
|
url&.specs.dup.tap do |url_specs|
|
|
case url_specs&.dig(:user_agent)
|
|
when :default
|
|
url_specs.delete(:user_agent)
|
|
when Symbol
|
|
url_specs[:user_agent] = ":#{url_specs[:user_agent]}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|