brew/Library/Homebrew/cask/cask_loader.rb

586 lines
18 KiB
Ruby
Raw Normal View History

2023-03-12 17:06:29 -07:00
# typed: true
# 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"
require "utils/curl"
2024-01-12 09:38:49 -08:00
require "extend/hash/keys"
2018-03-07 16:14:55 +00:00
2018-09-06 08:29:14 +02:00
module Cask
2020-08-24 22:50:29 +02:00
# Loads a cask from various sources.
module CaskLoader
extend Context
2023-03-12 17:06:29 -07:00
module ILoader
extend T::Helpers
interface!
2024-02-20 18:08:55 +01:00
sig { abstract.params(config: T.nilable(Config)).returns(Cask) }
2023-03-12 17:06:29 -07:00
def load(config:); end
end
2020-08-24 22:50:29 +02:00
# Loads a cask from a string.
class AbstractContentLoader
2023-03-12 17:06:29 -07:00
include ILoader
extend T::Helpers
abstract!
sig { returns(String) }
attr_reader :content
sig { returns(T.nilable(Tap)) }
attr_reader :tap
2017-06-28 09:25:14 +02:00
private
sig {
overridable.params(
header_token: String,
options: T.untyped,
block: T.nilable(T.proc.bind(DSL).void),
).returns(Cask)
}
def cask(header_token, **options, &block)
2024-03-07 16:20:20 +00:00
Cask.new(header_token, source: content, tap:, **options, config: @config, &block)
end
end
# Loads a cask from a string.
class FromContentLoader < AbstractContentLoader
2024-02-06 21:59:55 +01:00
def self.try_new(ref, warn: false)
2017-10-08 15:20:58 +02:00
return false unless ref.respond_to?(:to_str)
2018-09-17 02:45:00 +02:00
content = T.unsafe(ref).to_str
2017-10-08 15:20:58 +02:00
# 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
2024-02-06 21:59:55 +01:00
return unless content.match?(@regex)
new(content)
2017-10-08 15:20:58 +02:00
end
sig { params(content: String, tap: Tap).void }
def initialize(content, tap: T.unsafe(nil))
super()
2024-02-06 21:59:55 +01:00
@content = content.dup.force_encoding("UTF-8")
@tap = tap
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
end
2020-08-24 22:50:29 +02:00
# Loads a cask from a path.
class FromPathLoader < AbstractContentLoader
2024-02-06 21:59:55 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, warn: false)
path = case ref
when String
Pathname(ref)
when Pathname
ref
else
return
end
2024-02-06 21:59:55 +01:00
return if %w[.rb .json].exclude?(path.extname)
return unless path.expand_path.exist?
new(path)
end
attr_reader :token, :path
sig { params(path: T.any(Pathname, String), token: String).void }
def initialize(path, token: T.unsafe(nil))
super()
2017-06-28 09:25:14 +02:00
path = Pathname(path).expand_path
@token = path.basename(path.extname).to_s
@path = path
@tap = Tap.from_path(path) || Homebrew::API.tap_from_source_download(path)
end
2024-02-20 18:08:55 +01:00
sig { override.params(config: T.nilable(Config)).returns(Cask) }
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
if path.extname == ".json"
return FromAPILoader.new(token, from_json: JSON.parse(@content), path:).load(config:)
end
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
2024-02-06 21:59:55 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, warn: false)
# Cache compiled regex
@uri_regex ||= begin
uri_regex = ::URI::DEFAULT_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end
uri = ref.to_s
2024-02-06 21:59:55 +01:00
return unless uri.match?(@uri_regex)
uri = URI(uri)
2024-02-06 21:59:55 +01:00
return unless uri.path
2024-02-06 21:59:55 +01:00
new(uri)
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)
2023-03-12 17:06:29 -07:00
super Cache.path/File.basename(T.must(@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}"
::Utils::Curl.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 specific tap.
class FromTapLoader < FromPathLoader
sig { returns(Tap) }
attr_reader :tap
2024-02-06 21:59:55 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, warn: false)
ref = ref.to_s
2024-03-07 16:20:20 +00:00
return unless (token_tap_type = CaskLoader.tap_cask_token_type(ref, warn:))
2024-02-06 21:59:55 +01:00
token, tap, = token_tap_type
2024-02-06 21:59:55 +01:00
new("#{tap}/#{token}")
2017-07-29 16:27:54 +02:00
end
sig { params(tapped_token: String).void }
def initialize(tapped_token)
2024-02-23 15:02:10 +01:00
tap, token = Tap.with_cask_token(tapped_token)
cask = CaskLoader.find_cask_in_tap(token, tap)
super cask
end
2016-10-08 13:25:38 +02:00
2024-02-20 18:08:55 +01:00
sig { override.params(config: T.nilable(Config)).returns(Cask) }
2020-09-29 23:46:30 +02:00
def load(config:)
raise TapCaskUnavailableError.new(tap, token) unless T.must(tap).installed?
super
end
end
2020-08-24 22:50:29 +02:00
# Loads a cask from an existing {Cask} instance.
class FromInstanceLoader
2023-03-12 17:06:29 -07:00
include ILoader
2024-02-06 21:59:55 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, warn: false)
new(ref) if ref.is_a?(Cask)
end
sig { params(cask: Cask).void }
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
2023-03-12 17:06:29 -07:00
include ILoader
2024-02-08 16:18:05 +01:00
sig { returns(String) }
attr_reader :token
sig { returns(Pathname) }
attr_reader :path
sig { returns(T.nilable(Hash)) }
attr_reader :from_json
2024-02-06 21:59:55 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, warn: false)
return if Homebrew::EnvConfig.no_install_from_api?
return unless ref.is_a?(String)
2024-02-09 16:59:27 +01:00
return unless (token = ref[HOMEBREW_DEFAULT_TAP_CASK_REGEX, :token])
2024-02-09 18:27:58 +01:00
if !Homebrew::API::Cask.all_casks.key?(token) &&
!Homebrew::API::Cask.all_renames.key?(token)
return
end
2024-02-06 21:59:55 +01:00
ref = "#{CoreCaskTap.instance}/#{token}"
2024-03-07 16:20:20 +00:00
token, tap, = CaskLoader.tap_cask_token_type(ref, warn:)
2024-02-06 21:59:55 +01:00
new("#{tap}/#{token}")
end
sig { params(token: String, from_json: Hash, path: T.nilable(Pathname)).void }
def initialize(token, from_json: T.unsafe(nil), path: nil)
@token = token.sub(%r{^homebrew/(?:homebrew-)?cask/}i, "")
@sourcefile_path = path
@path = path || CaskLoader.default_path(@token)
@from_json = from_json
end
def load(config:)
2024-02-08 16:18:05 +01:00
json_cask = from_json || Homebrew::API::Cask.all_casks.fetch(token)
cask_options = {
loaded_from_api: true,
sourcefile_path: @sourcefile_path,
source: JSON.pretty_generate(json_cask),
2024-03-07 16:20:20 +00:00
config:,
loader: self,
}
json_cask = Homebrew::API.merge_variations(json_cask).deep_symbolize_keys.freeze
2023-01-06 02:41:35 -05:00
cask_options[:tap] = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/")
2023-02-24 13:56:46 +00:00
user_agent = json_cask.dig(:url_specs, :user_agent)
json_cask[:url_specs][:user_agent] = user_agent[1..].to_sym if user_agent && user_agent[0] == ":"
if (using = json_cask.dig(:url_specs, :using))
json_cask[:url_specs][:using] = using.to_sym
end
api_cask = Cask.new(token, **cask_options) do
version json_cask[:version]
if json_cask[:sha256] == "no_check"
sha256 :no_check
else
sha256 json_cask[:sha256]
end
url json_cask[:url], **json_cask.fetch(:url_specs, {}) if json_cask[:url].present?
json_cask[:name]&.each do |cask_name|
name cask_name
end
desc json_cask[:desc]
homepage json_cask[:homepage]
if (deprecation_date = json_cask[:deprecation_date].presence)
reason = DeprecateDisable.to_reason_string_or_symbol json_cask[:deprecation_reason], type: :cask
deprecate! date: deprecation_date, because: reason
end
if (disable_date = json_cask[:disable_date].presence)
reason = DeprecateDisable.to_reason_string_or_symbol json_cask[:disable_reason], type: :cask
disable! date: disable_date, because: reason
end
auto_updates json_cask[:auto_updates] unless json_cask[:auto_updates].nil?
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
2023-04-18 15:06:50 -07:00
next [dep_key, dep_value] if dep_key != :macos
dep_type = dep_value.keys.first
if dep_type == :==
version_symbols = dep_value[dep_type].map do |version|
MacOSVersion::SYMBOLS.key(version) || version
end
next [dep_key, version_symbols]
end
version_symbol = dep_value[dep_type].first
version_symbol = MacOSVersion::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|
2023-04-18 15:06:50 -07:00
next [container_key, container_value] if container_key != :type
[container_key, container_value.to_sym]
end
container(**container_hash)
end
json_cask[:artifacts].each do |artifact|
# convert generic string replacements into actual ones
artifact = cask.loader.from_h_gsubs(artifact, appdir)
key = artifact.keys.first
if artifact[key].nil?
# for artifacts with blocks that can't be loaded from the API
send(key) {} # empty on purpose
else
2023-09-29 04:47:44 +01:00
args = artifact[key]
kwargs = if args.last.is_a?(Hash)
args.pop
else
{}
end
send(key, *args, **kwargs)
end
end
if json_cask[:caveats].present?
# convert generic string replacements into actual ones
caveats cask.loader.from_h_string_gsubs(json_cask[:caveats], appdir)
end
end
api_cask.populate_from_api!(json_cask)
api_cask
end
def from_h_string_gsubs(string, appdir)
string.to_s
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(HOMEBREW_CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
.gsub(HOMEBREW_CASK_APPDIR_PLACEHOLDER, appdir)
end
def from_h_array_gsubs(array, appdir)
array.to_a.map do |value|
from_h_gsubs(value, appdir)
end
end
def from_h_hash_gsubs(hash, appdir)
hash.to_h.transform_values do |value|
from_h_gsubs(value, appdir)
end
end
def from_h_gsubs(value, appdir)
return value if value.blank?
case value
when Hash
from_h_hash_gsubs(value, appdir)
when Array
from_h_array_gsubs(value, appdir)
when String
from_h_string_gsubs(value, appdir)
else
2023-02-06 15:07:28 +01:00
value
end
end
end
2024-02-06 21:59:55 +01:00
# Loader which tries loading casks from tap paths, failing
# if the same token exists in multiple taps.
2024-02-12 07:43:29 +01:00
class FromNameLoader < FromTapLoader
2024-02-09 19:28:45 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
2024-02-12 07:43:29 +01:00
.returns(T.nilable(T.attached_class))
2024-02-09 19:28:45 +01:00
}
2024-02-06 21:59:55 +01:00
def self.try_new(ref, warn: false)
return unless ref.is_a?(String)
return unless ref.match?(/\A#{HOMEBREW_TAP_CASK_TOKEN_REGEX}\Z/o)
token = ref
# If it exists in the default tap, never treat it as ambiguous with another tap.
if (core_cask_tap = CoreCaskTap.instance).installed? &&
2024-03-07 16:20:20 +00:00
(loader= super("#{core_cask_tap}/#{token}", warn:))&.path&.exist?
return loader
end
loaders = Tap.select { |tap| tap.installed? && !tap.core_cask_tap? }
2024-03-07 16:20:20 +00:00
.filter_map { |tap| super("#{tap}/#{token}", warn:) }
.uniq(&:path)
.select { |tap| tap.path.exist? }
case loaders.count
2024-02-06 21:59:55 +01:00
when 1
loaders.first
2024-02-06 21:59:55 +01:00
when 2..Float::INFINITY
2024-02-20 18:08:55 +01:00
raise TapCaskAmbiguityError.new(token, loaders)
2024-02-06 21:59:55 +01:00
end
end
end
# Loader which loads a cask from the installed cask file.
class FromInstalledPathLoader < FromPathLoader
2024-02-09 19:28:45 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
2024-02-06 21:59:55 +01:00
def self.try_new(ref, warn: false)
2024-02-09 19:28:45 +01:00
return unless ref.is_a?(String)
2024-02-06 21:59:55 +01:00
possible_installed_cask = Cask.new(ref)
return unless (installed_caskfile = possible_installed_cask.installed_caskfile)
new(installed_caskfile)
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
2024-02-06 21:59:55 +01:00
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, warn: false)
return if ref.is_a?(Cask)
return if ref.is_a?(URI::Generic)
new(ref)
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
2023-04-08 14:10:58 +02:00
def self.load(ref, config: nil, warn: true)
2024-03-07 16:20:20 +00:00
self.for(ref, warn:).load(config:)
end
sig { params(tapped_token: String, warn: T::Boolean).returns(T.nilable([String, Tap, T.nilable(Symbol)])) }
def self.tap_cask_token_type(tapped_token, warn:)
return unless (tap_with_token = Tap.with_cask_token(tapped_token))
tap, token = tap_with_token
type = nil
if (new_token = tap.cask_renames[token].presence)
2024-02-13 04:09:49 +01:00
old_token = tap.core_cask_tap? ? token : tapped_token
token = new_token
new_token = tap.core_cask_tap? ? token : "#{tap}/#{token}"
type = :rename
elsif (new_tap_name = tap.tap_migrations[token].presence)
2024-02-23 15:02:10 +01:00
new_tap, new_token = Tap.with_cask_token(new_tap_name) || [Tap.fetch(new_tap_name), token]
new_tap.ensure_installed!
2024-02-13 04:09:49 +01:00
new_tapped_token = "#{new_tap}/#{new_token}"
if tapped_token == new_tapped_token
opoo "Tap migration for #{tapped_token} points to itself, stopping recursion."
else
2024-02-13 04:09:49 +01:00
old_token = tap.core_cask_tap? ? token : tapped_token
return unless (token_tap_type = tap_cask_token_type(new_tapped_token, warn: false))
token, tap, = token_tap_type
2024-02-13 04:09:49 +01:00
new_token = new_tap.core_cask_tap? ? token : "#{tap}/#{token}"
type = :migration
end
end
opoo "Cask #{old_token} was renamed to #{new_token}." if warn && old_token && new_token
[token, tap, type]
end
2023-04-08 14:10:58 +02:00
def self.for(ref, need_path: false, warn: true)
[
FromInstanceLoader,
2017-10-08 15:20:58 +02:00
FromContentLoader,
FromURILoader,
FromAPILoader,
FromTapLoader,
FromNameLoader,
FromPathLoader,
2024-02-06 21:59:55 +01:00
FromInstalledPathLoader,
NullLoader,
].each do |loader_class|
2024-03-07 16:20:20 +00:00
if (loader = loader_class.try_new(ref, warn:))
2024-02-07 11:29:39 +01:00
$stderr.puts "#{$PROGRAM_NAME} (#{loader.class}): loading #{ref}" if debug?
2024-02-06 21:59:55 +01:00
return loader
end
2016-10-08 13:25:38 +02:00
end
end
def self.default_path(token)
find_cask_in_tap(token.to_s.downcase, CoreCaskTap.instance)
end
def self.find_cask_in_tap(token, tap)
filename = "#{token}.rb"
2024-02-23 15:03:05 +01:00
tap.cask_files_by_name.fetch(token, tap.cask_dir/filename)
2016-10-08 13:25:38 +02:00
end
end
end