brew/Library/Homebrew/cask/cask_loader.rb

402 lines
11 KiB
Ruby
Raw Normal View History

2020-10-10 14:16:11 +02:00
# typed: false
# frozen_string_literal: true
2020-09-17 14:01:37 +08:00
require "cask/cache"
require "cask/cask"
2018-03-07 16:14:55 +00:00
require "uri"
2018-09-06 08:29:14 +02:00
module Cask
2020-08-24 22:50:29 +02:00
# Loads a cask from various sources.
#
# @api private
module CaskLoader
2020-08-24 22:50:29 +02:00
# Loads a cask from a string.
class FromContentLoader
2017-06-28 09:25:14 +02:00
attr_reader :content
2017-10-08 15:20:58 +02:00
def self.can_load?(ref)
return false unless ref.respond_to?(:to_str)
2018-09-17 02:45:00 +02:00
2017-10-08 15:20:58 +02:00
content = ref.to_str
# Cache compiled regex
@regex ||= begin
token = /(?:"[^"]*"|'[^']*')/
curly = /\(\s*#{token.source}\s*\)\s*\{.*\}/
do_end = /\s+#{token.source}\s+do(?:\s*;\s*|\s+).*end/
/\A\s*cask(?:#{curly.source}|#{do_end.source})\s*\Z/m
end
2017-10-08 15:20:58 +02:00
content.match?(@regex)
2017-10-08 15:20:58 +02:00
end
def initialize(content)
@content = content.force_encoding("UTF-8")
end
2020-09-29 23:46:30 +02:00
def load(config:)
@config = config
instance_eval(content, __FILE__, __LINE__)
end
2016-10-08 13:25:38 +02:00
private
2016-10-08 13:25:38 +02:00
2017-07-29 16:27:54 +02:00
def cask(header_token, **options, &block)
Cask.new(header_token, source: content, **options, config: @config, &block)
end
2016-10-08 13:25:38 +02:00
end
2020-08-24 22:50:29 +02:00
# Loads a cask from a path.
class FromPathLoader < FromContentLoader
def self.can_load?(ref)
2017-06-28 09:25:14 +02:00
path = Pathname(ref)
path.extname == ".rb" && path.expand_path.exist?
end
attr_reader :token, :path
2020-08-19 17:12:32 +01:00
def initialize(path) # rubocop:disable Lint/MissingSuper
2017-06-28 09:25:14 +02:00
path = Pathname(path).expand_path
@token = path.basename(".rb").to_s
@path = path
end
2020-09-29 23:46:30 +02:00
def load(config:)
2017-06-28 09:25:14 +02:00
raise CaskUnavailableError.new(token, "'#{path}' does not exist.") unless path.exist?
raise CaskUnavailableError.new(token, "'#{path}' is not readable.") unless path.readable?
raise CaskUnavailableError.new(token, "'#{path}' is not a file.") unless path.file?
2020-09-29 23:46:30 +02:00
@content = path.read(encoding: "UTF-8")
@config = config
begin
instance_eval(content, path).tap do |cask|
raise CaskUnreadableError.new(token, "'#{path}' does not contain a cask.") unless cask.is_a?(Cask)
end
rescue NameError, ArgumentError, ScriptError => e
2020-09-29 23:46:30 +02:00
error = CaskUnreadableError.new(token, e.message)
error.set_backtrace e.backtrace
raise error
end
end
private
2017-07-29 16:27:54 +02:00
def cask(header_token, **options, &block)
raise CaskTokenMismatchError.new(token, header_token) if token != header_token
2017-07-29 16:27:54 +02:00
super(header_token, **options, sourcefile_path: path, &block)
end
2016-10-08 13:25:38 +02:00
end
2020-08-24 22:50:29 +02:00
# Loads a cask from a URI.
class FromURILoader < FromPathLoader
2020-10-20 12:03:48 +02:00
extend T::Sig
def self.can_load?(ref)
# Cache compiled regex
@uri_regex ||= begin
uri_regex = ::URI::DEFAULT_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end
return false unless ref.to_s.match?(@uri_regex)
uri = URI(ref)
return false unless uri
return false unless uri.path
true
end
2017-06-11 02:00:59 +02:00
attr_reader :url
2020-10-20 12:03:48 +02:00
sig { params(url: T.any(URI::Generic, String)).void }
def initialize(url)
2017-06-11 02:00:59 +02:00
@url = URI(url)
super Cache.path/File.basename(@url.path)
end
2020-09-29 23:46:30 +02:00
def load(config:)
2017-06-28 09:25:14 +02:00
path.dirname.mkpath
begin
2021-01-26 15:21:24 -05:00
ohai "Downloading #{url}"
2017-08-08 18:10:13 +02:00
curl_download url, to: path
rescue ErrorDuringExecution
2017-06-28 09:25:14 +02:00
raise CaskUnavailableError.new(token, "Failed to download #{Formatter.url(url)}.")
end
super
end
2016-10-08 13:25:38 +02:00
end
2020-08-24 22:50:29 +02:00
# Loads a cask from a tap path.
2017-07-29 16:27:54 +02:00
class FromTapPathLoader < FromPathLoader
def self.can_load?(ref)
super && !Tap.from_path(ref).nil?
end
2017-06-28 09:25:14 +02:00
attr_reader :tap
def initialize(path)
@tap = Tap.from_path(path)
super(path)
2017-07-29 16:27:54 +02:00
end
private
def cask(*args, &block)
super(*args, tap: tap, &block)
end
end
2020-08-24 22:50:29 +02:00
# Loads a cask from a specific tap.
2017-07-29 16:27:54 +02:00
class FromTapLoader < FromTapPathLoader
def self.can_load?(ref)
ref.to_s.match?(HOMEBREW_TAP_CASK_REGEX)
end
def initialize(tapped_name)
2017-06-28 09:25:14 +02:00
user, repo, token = tapped_name.split("/", 3)
2017-07-29 16:27:54 +02:00
super Tap.fetch(user, repo).cask_dir/"#{token}.rb"
end
2016-10-08 13:25:38 +02:00
2020-09-29 23:46:30 +02:00
def load(config:)
raise TapCaskUnavailableError.new(tap, token) unless tap.installed?
super
end
end
2020-08-24 22:50:29 +02:00
# Loads a cask from an existing {Cask} instance.
class FromInstanceLoader
def self.can_load?(ref)
ref.is_a?(Cask)
end
def initialize(cask)
@cask = cask
end
2020-09-29 23:46:30 +02:00
def load(config:)
@cask
end
end
# Loads a cask from the JSON API.
class FromAPILoader
attr_reader :token, :path
FLIGHT_STANZAS = [:preflight, :postflight, :uninstall_preflight, :uninstall_postflight].freeze
def self.can_load?(ref)
return false unless Homebrew::EnvConfig.install_from_api?
return false unless ref.is_a?(String)
return false unless ref.match?(HOMEBREW_MAIN_TAP_CASK_REGEX)
token = ref.delete_prefix("homebrew/cask/")
Homebrew::API::Cask.all_casks.key?(token)
end
def initialize(token)
@token = token.delete_prefix("homebrew/cask/")
@path = CaskLoader.default_path(token)
end
def load(config:)
json_cask = Homebrew::API::Cask.all_casks[token]
if (bottle_tag = ::Utils::Bottles.tag.to_s.presence) &&
(variations = json_cask["variations"].presence) &&
(variation = variations[bottle_tag].presence)
json_cask = json_cask.merge(variation)
end
json_cask.deep_symbolize_keys!
# Use the cask-source API if there are any `*flight` blocks or the cask has multiple languages
if json_cask[:artifacts].any? { |artifact| FLIGHT_STANZAS.include?(artifact.keys.first) } ||
json_cask[:languages].any?
2023-01-28 02:15:00 -06:00
cask_source = Homebrew::API::Cask.fetch_source(token, git_head: json_cask[:tap_git_head])
2023-01-06 02:41:35 -05:00
return FromContentLoader.new(cask_source).load(config: config)
end
# convert generic string replacements into actual ones
json_cask[:artifacts] = json_cask[:artifacts].map(&method(:from_h_hash_gsubs))
json_cask[:caveats] = from_h_string_gsubs(json_cask[:caveats])
tap = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/")
Cask.new(token, tap: tap, source: cask_source, config: config) do
version json_cask[:version]
if json_cask[:sha256] == "no_check"
sha256 :no_check
else
sha256 json_cask[:sha256]
end
url json_cask[:url]
appcast json_cask[:appcast] if json_cask[:appcast].present?
json_cask[:name].each do |cask_name|
name cask_name
end
desc json_cask[:desc]
homepage json_cask[:homepage]
auto_updates json_cask[:auto_updates] if json_cask[:auto_updates].present?
conflicts_with(**json_cask[:conflicts_with]) if json_cask[:conflicts_with].present?
if json_cask[:depends_on].present?
dep_hash = json_cask[:depends_on].to_h do |dep_key, dep_value|
2023-01-11 13:16:34 -05:00
# Arch dependencies are encoded like `{ type: :intel, bits: 64 }`
# but `depends_on arch:` only accepts `:intel` or `:arm64`
if dep_key == :arch
next [:arch, :intel] if dep_value.first[:type] == "intel"
next [:arch, :arm64]
end
next [dep_key, dep_value] unless dep_key == :macos
dep_type = dep_value.keys.first
if dep_type == :==
version_symbols = dep_value[dep_type].map do |version|
MacOSVersions::SYMBOLS.key(version) || version
end
next [dep_key, version_symbols]
end
version_symbol = dep_value[dep_type].first
version_symbol = MacOSVersions::SYMBOLS.key(version_symbol) || version_symbol
[dep_key, "#{dep_type} :#{version_symbol}"]
end.compact
depends_on(**dep_hash)
end
if json_cask[:container].present?
container_hash = json_cask[:container].to_h do |container_key, container_value|
next [container_key, container_value] unless container_key == :type
[container_key, container_value.to_sym]
end
container(**container_hash)
end
json_cask[:artifacts].each do |artifact|
key = artifact.keys.first
2023-01-04 02:30:24 -05:00
if FLIGHT_STANZAS.include?(key)
instance_eval(artifact[key])
else
send(key, *artifact[key])
end
end
caveats json_cask[:caveats] if json_cask[:caveats].present?
end
end
private
def from_h_string_gsubs(string)
string.to_s
.gsub("$HOME", Dir.home)
.gsub("$(brew --prefix)", HOMEBREW_PREFIX)
end
def from_h_array_gsubs(array)
array.to_a.map do |value|
from_h_gsubs(value)
end
end
def from_h_hash_gsubs(hash)
hash.to_h.transform_values do |value|
from_h_gsubs(value)
end
rescue TypeError
from_h_array_gsubs(hash)
end
def from_h_gsubs(value)
if value.respond_to? :to_h
from_h_hash_gsubs(value)
elsif value.respond_to? :to_a
from_h_array_gsubs(value)
else
from_h_string_gsubs(value)
end
end
end
2020-08-24 22:50:29 +02:00
# Pseudo-loader which raises an error when trying to load the corresponding cask.
class NullLoader < FromPathLoader
2020-10-20 12:03:48 +02:00
extend T::Sig
def self.can_load?(*)
true
end
2020-10-20 12:03:48 +02:00
sig { params(ref: T.any(String, Pathname)).void }
def initialize(ref)
2017-06-28 09:25:14 +02:00
token = File.basename(ref, ".rb")
super CaskLoader.default_path(token)
end
2020-09-29 23:46:30 +02:00
def load(config:)
2017-06-28 09:25:14 +02:00
raise CaskUnavailableError.new(token, "No Cask with this name exists.")
end
end
def self.path(ref)
self.for(ref, need_path: true).path
end
2020-09-29 23:46:30 +02:00
def self.load(ref, config: nil)
self.for(ref).load(config: config)
end
def self.for(ref, need_path: false)
[
FromInstanceLoader,
2017-10-08 15:20:58 +02:00
FromContentLoader,
FromURILoader,
FromAPILoader,
FromTapLoader,
2017-07-29 16:27:54 +02:00
FromTapPathLoader,
FromPathLoader,
].each do |loader_class|
return loader_class.new(ref) if loader_class.can_load?(ref)
end
return FromTapPathLoader.new(default_path(ref)) if FromTapPathLoader.can_load?(default_path(ref))
2016-10-08 13:25:38 +02:00
2017-07-29 16:27:54 +02:00
case (possible_tap_casks = tap_paths(ref)).count
when 1
return FromTapPathLoader.new(possible_tap_casks.first)
when 2..Float::INFINITY
loaders = possible_tap_casks.map(&FromTapPathLoader.method(:new))
2022-06-17 15:22:41 -04:00
raise TapCaskAmbiguityError.new(ref, loaders)
2016-10-08 13:25:38 +02:00
end
possible_installed_cask = Cask.new(ref)
return FromPathLoader.new(possible_installed_cask.installed_caskfile) if possible_installed_cask.installed?
NullLoader.new(ref)
end
def self.default_path(token)
2018-06-09 10:13:28 +02:00
Tap.default_cask_tap.cask_dir/"#{token.to_s.downcase}.rb"
end
def self.tap_paths(token)
Tap.map { |t| t.cask_dir/"#{token.to_s.downcase}.rb" }
.select(&:exist?)
2016-10-08 13:25:38 +02:00
end
end
end