
For casks with certain stanzas, *flight and language stanzas in this case, we need to use the caskfile to install them correctly. The JSON API is not an option. This delays loading from the source API until just before we try to install one of these casks. This reduces the number of requests we make to the source API.
402 lines
11 KiB
Ruby
402 lines
11 KiB
Ruby
# typed: false
|
|
# frozen_string_literal: true
|
|
|
|
require "cask/cask_loader"
|
|
require "cask/config"
|
|
require "cask/dsl"
|
|
require "cask/metadata"
|
|
require "searchable"
|
|
require "utils/bottles"
|
|
|
|
module Cask
|
|
# An instance of a cask.
|
|
#
|
|
# @api private
|
|
class Cask
|
|
extend T::Sig
|
|
|
|
extend Forwardable
|
|
extend Searchable
|
|
include Metadata
|
|
|
|
# Needs a leading slash to avoid `File.expand.path` complaining about non-absolute home.
|
|
HOME_PLACEHOLDER = "/$HOME"
|
|
HOMEBREW_PREFIX_PLACEHOLDER = "$HOMEBREW_PREFIX"
|
|
APPDIR_PLACEHOLDER = "$APPDIR"
|
|
|
|
# TODO: can be removed when API JSON is regenerated with HOMEBREW_PREFIX_PLACEHOLDER.
|
|
HOMEBREW_OLD_PREFIX_PLACEHOLDER = "$(brew --prefix)"
|
|
|
|
attr_reader :token, :sourcefile_path, :source, :config, :default_config, :loaded_from_api, :loader
|
|
|
|
attr_accessor :download, :allow_reassignment
|
|
|
|
class << self
|
|
def generating_hash!
|
|
return if generating_hash?
|
|
|
|
# Apply monkeypatches for API generation
|
|
@old_homebrew_prefix = HOMEBREW_PREFIX
|
|
@old_home = Dir.home
|
|
Object.send(:remove_const, :HOMEBREW_PREFIX)
|
|
Object.const_set(:HOMEBREW_PREFIX, Pathname(HOMEBREW_PREFIX_PLACEHOLDER))
|
|
ENV["HOME"] = HOME_PLACEHOLDER
|
|
|
|
@generating_hash = true
|
|
end
|
|
|
|
def generated_hash!
|
|
return unless generating_hash?
|
|
|
|
# Revert monkeypatches for API generation
|
|
Object.send(:remove_const, :HOMEBREW_PREFIX)
|
|
Object.const_set(:HOMEBREW_PREFIX, @old_homebrew_prefix)
|
|
ENV["HOME"] = @old_home
|
|
|
|
@generating_hash = false
|
|
end
|
|
|
|
def generating_hash?
|
|
@generating_hash ||= false
|
|
@generating_hash == true
|
|
end
|
|
end
|
|
|
|
def self.all
|
|
# TODO: ideally avoid using ARGV by moving to e.g. CLI::Parser
|
|
if ARGV.exclude?("--eval-all") && !Homebrew::EnvConfig.eval_all?
|
|
odeprecated "Cask::Cask#all without --all or HOMEBREW_EVAL_ALL"
|
|
end
|
|
|
|
Tap.flat_map(&:cask_files).map do |f|
|
|
CaskLoader::FromTapPathLoader.new(f).load(config: nil)
|
|
rescue CaskUnreadableError => e
|
|
opoo e.message
|
|
|
|
nil
|
|
end.compact
|
|
end
|
|
|
|
def tap
|
|
return super if block_given? # Object#tap
|
|
|
|
@tap
|
|
end
|
|
|
|
def initialize(token, sourcefile_path: nil, source: nil, tap: 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 = false
|
|
@loader = loader
|
|
@block = block
|
|
|
|
@default_config = config || Config.new
|
|
|
|
self.config = if config_path.exist?
|
|
Config.from_json(File.read(config_path), ignore_invalid_keys: true)
|
|
else
|
|
@default_config
|
|
end
|
|
end
|
|
|
|
def config=(config)
|
|
@config = config
|
|
|
|
refresh
|
|
end
|
|
|
|
def refresh
|
|
@dsl = DSL.new(self)
|
|
return unless @block
|
|
|
|
@dsl.instance_eval(&@block)
|
|
@dsl.language_eval
|
|
end
|
|
|
|
DSL::DSL_METHODS.each do |method_name|
|
|
define_method(method_name) { |&block| @dsl.send(method_name, &block) }
|
|
end
|
|
|
|
sig { returns(T::Array[[String, String]]) }
|
|
def timestamped_versions
|
|
Pathname.glob(metadata_timestamped_path(version: "*", timestamp: "*"))
|
|
.map { |p| p.relative_path_from(p.parent.parent) }
|
|
.sort_by(&:basename) # sort by timestamp
|
|
.map { |p| p.split.map(&:to_s) }
|
|
end
|
|
|
|
def versions
|
|
timestamped_versions.map(&:first)
|
|
.reverse
|
|
.uniq
|
|
.reverse
|
|
end
|
|
|
|
def os_versions
|
|
# TODO: use #to_hash_with_variations instead once all casks use on_system blocks
|
|
@os_versions ||= begin
|
|
version_os_hash = {}
|
|
actual_version = MacOS.full_version.to_s
|
|
|
|
MacOSVersions::SYMBOLS.each do |os_name, os_version|
|
|
MacOS.full_version = os_version
|
|
cask = CaskLoader.load(full_name)
|
|
version_os_hash[os_name] = cask.version if cask.version != version
|
|
end
|
|
|
|
version_os_hash
|
|
ensure
|
|
MacOS.full_version = actual_version
|
|
end
|
|
end
|
|
|
|
def full_name
|
|
return token if tap.nil?
|
|
return token if tap.user == "Homebrew"
|
|
|
|
"#{tap.name}/#{token}"
|
|
end
|
|
|
|
def installed?
|
|
!versions.empty?
|
|
end
|
|
|
|
# The caskfile is needed during installation when there are
|
|
# `*flight` blocks or the cask has multiple languages
|
|
def caskfile_only?
|
|
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
|
|
end
|
|
|
|
sig { returns(T.nilable(Time)) }
|
|
def install_time
|
|
_, time = timestamped_versions.last
|
|
return unless time
|
|
|
|
Time.strptime(time, Metadata::TIMESTAMP_FORMAT)
|
|
end
|
|
|
|
def installed_caskfile
|
|
installed_version = timestamped_versions.last
|
|
caskfile_dir = metadata_main_container_path.join(*installed_version, "Casks")
|
|
return caskfile_dir.join("#{token}.json") if caskfile_dir.join("#{token}.json").exist?
|
|
|
|
caskfile_dir.join("#{token}.rb")
|
|
end
|
|
|
|
def config_path
|
|
metadata_main_container_path/"config.json"
|
|
end
|
|
|
|
def checksumable?
|
|
DownloadStrategyDetector.detect(url.to_s, url.using) <= AbstractFileDownloadStrategy
|
|
end
|
|
|
|
def download_sha_path
|
|
metadata_main_container_path/"LATEST_DOWNLOAD_SHA256"
|
|
end
|
|
|
|
def new_download_sha
|
|
require "cask/installer"
|
|
|
|
# Call checksumable? before hashing
|
|
@new_download_sha ||= Installer.new(self, verify_download_integrity: false)
|
|
.download(quiet: true)
|
|
.instance_eval { |x| Digest::SHA256.file(x).hexdigest }
|
|
end
|
|
|
|
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
|
|
|
|
def caskroom_path
|
|
@caskroom_path ||= Caskroom.path.join(token)
|
|
end
|
|
|
|
def outdated?(greedy: false, greedy_latest: false, greedy_auto_updates: false)
|
|
!outdated_versions(greedy: greedy, greedy_latest: greedy_latest,
|
|
greedy_auto_updates: greedy_auto_updates).empty?
|
|
end
|
|
|
|
def outdated_versions(greedy: false, greedy_latest: false, greedy_auto_updates: false)
|
|
# special case: tap version is not available
|
|
return [] if version.nil?
|
|
|
|
if version.latest?
|
|
return versions if (greedy || greedy_latest) && outdated_download_sha?
|
|
|
|
return []
|
|
elsif auto_updates && !greedy && !greedy_auto_updates
|
|
return []
|
|
end
|
|
|
|
installed = versions
|
|
current = installed.last
|
|
|
|
# not outdated unless there is a different version on tap
|
|
return [] if current == version
|
|
|
|
# collect all installed versions that are different than tap version and return them
|
|
installed.reject { |v| v == version }
|
|
end
|
|
|
|
def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates)
|
|
return token if !verbose && !json
|
|
|
|
installed_versions = outdated_versions(greedy: greedy, greedy_latest: greedy_latest,
|
|
greedy_auto_updates: greedy_auto_updates).join(", ")
|
|
|
|
if json
|
|
{
|
|
name: token,
|
|
installed_versions: installed_versions,
|
|
current_version: version,
|
|
}
|
|
else
|
|
"#{token} (#{installed_versions}) != #{version}"
|
|
end
|
|
end
|
|
|
|
def ruby_source_checksum
|
|
@ruby_source_checksum ||= {
|
|
"sha256" => Digest::SHA256.file(sourcefile_path).hexdigest,
|
|
}.freeze
|
|
end
|
|
|
|
def languages
|
|
@languages ||= @dsl.languages
|
|
end
|
|
|
|
def tap_git_head
|
|
@tap_git_head ||= tap&.git_head
|
|
end
|
|
|
|
def populate_from_api!(json_cask)
|
|
@loaded_from_api = true
|
|
@languages = json_cask[:languages]
|
|
@tap_git_head = json_cask[:tap_git_head]
|
|
@ruby_source_checksum = json_cask[:ruby_source_checksum].freeze
|
|
end
|
|
|
|
def to_s
|
|
@token
|
|
end
|
|
|
|
def hash
|
|
token.hash
|
|
end
|
|
|
|
def eql?(other)
|
|
instance_of?(other.class) && token == other.token
|
|
end
|
|
alias == eql?
|
|
|
|
def to_h
|
|
if loaded_from_api && !Homebrew::EnvConfig.no_install_from_api?
|
|
json_cask = Homebrew::API::Cask.all_casks[token]
|
|
return api_to_local_hash(Homebrew::API.merge_variations(json_cask))
|
|
end
|
|
|
|
url_specs = url&.specs.dup
|
|
case url_specs&.dig(:user_agent)
|
|
when :default
|
|
url_specs.delete(:user_agent)
|
|
when Symbol
|
|
url_specs[:user_agent] = ":#{url_specs[:user_agent]}"
|
|
end
|
|
|
|
{
|
|
"token" => token,
|
|
"full_token" => full_name,
|
|
"tap" => tap&.name,
|
|
"name" => name,
|
|
"desc" => desc,
|
|
"homepage" => homepage,
|
|
"url" => url,
|
|
"url_specs" => url_specs,
|
|
"appcast" => appcast,
|
|
"version" => version,
|
|
"versions" => os_versions,
|
|
"installed" => versions.last,
|
|
"outdated" => outdated?,
|
|
"sha256" => sha256,
|
|
"artifacts" => artifacts_list,
|
|
"caveats" => (caveats unless caveats.empty?),
|
|
"depends_on" => depends_on,
|
|
"conflicts_with" => conflicts_with,
|
|
"container" => container&.pairs,
|
|
"auto_updates" => auto_updates,
|
|
"tap_git_head" => tap_git_head,
|
|
"languages" => languages,
|
|
"ruby_source_checksum" => ruby_source_checksum,
|
|
}
|
|
end
|
|
|
|
def to_hash_with_variations
|
|
if loaded_from_api && !Homebrew::EnvConfig.no_install_from_api?
|
|
return api_to_local_hash(Homebrew::API::Cask.all_casks[token])
|
|
end
|
|
|
|
hash = to_h
|
|
variations = {}
|
|
|
|
hash_keys_to_skip = %w[outdated installed versions]
|
|
|
|
if @dsl.on_system_blocks_exist?
|
|
[:arm, :intel].each do |arch|
|
|
MacOSVersions::SYMBOLS.each_key do |os_name|
|
|
bottle_tag = ::Utils::Bottles::Tag.new(system: os_name, arch: arch)
|
|
next unless bottle_tag.valid_combination?
|
|
|
|
Homebrew::SimulateSystem.os = os_name
|
|
Homebrew::SimulateSystem.arch = arch
|
|
|
|
refresh
|
|
|
|
to_h.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
|
|
end
|
|
|
|
Homebrew::SimulateSystem.clear
|
|
refresh
|
|
|
|
hash["variations"] = variations
|
|
hash
|
|
end
|
|
|
|
private
|
|
|
|
def api_to_local_hash(hash)
|
|
hash["token"] = token
|
|
hash["installed"] = versions.last
|
|
hash["outdated"] = outdated?
|
|
hash
|
|
end
|
|
|
|
def artifacts_list
|
|
artifacts.map do |artifact|
|
|
case artifact
|
|
when Artifact::AbstractFlightBlock
|
|
# Only indicate whether this block is used as we don't load it from the API
|
|
{ artifact.summarize => nil }
|
|
else
|
|
{ artifact.class.dsl_key => artifact.to_args }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|