mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Use CaskStruct for loading casks from the API
This commit is contained in:
@@ -6,6 +6,7 @@ require "api/cask"
|
||||
require "api/formula"
|
||||
require "api/internal"
|
||||
require "api/formula_struct"
|
||||
require "api/cask_struct"
|
||||
require "base64"
|
||||
require "utils/output"
|
||||
|
||||
|
||||
@@ -145,6 +145,116 @@ module Homebrew
|
||||
|
||||
Homebrew::API.write_names_file!(all_casks.keys, "cask", regenerate:)
|
||||
end
|
||||
|
||||
sig { params(hash: T::Hash[String, T.untyped]).returns(CaskStruct) }
|
||||
def self.generate_cask_struct_hash(hash)
|
||||
hash = Homebrew::API.merge_variations(hash).dup.deep_symbolize_keys.transform_keys(&:to_s)
|
||||
|
||||
hash["conflicts_with_args"] = hash["conflicts_with"]
|
||||
|
||||
hash["container_args"] = hash["container"].to_h do |key, value|
|
||||
next [key, value.to_sym] if key == :type
|
||||
|
||||
[key, value]
|
||||
end
|
||||
|
||||
hash["depends_on_args"] = hash["depends_on"].to_h do |key, value|
|
||||
# Arch dependencies are encoded like `{ type: :intel, bits: 64 }`
|
||||
# but `depends_on arch:` only accepts `:intel` or `:arm64`
|
||||
if key == :arch
|
||||
next [:arch, :intel] if value.first[:type] == "intel"
|
||||
|
||||
next [:arch, :arm64]
|
||||
end
|
||||
|
||||
next [key, value] if key != :macos
|
||||
|
||||
dep_type = value.keys.first
|
||||
if dep_type == :==
|
||||
version_symbols = value[dep_type].filter_map do |version|
|
||||
MacOSVersion::SYMBOLS.key(version)
|
||||
end
|
||||
next [key, version_symbols.presence]
|
||||
end
|
||||
|
||||
version_symbol = value[dep_type].first
|
||||
version_symbol = MacOSVersion::SYMBOLS.key(version_symbol)
|
||||
version_dep = "#{dep_type} :#{version_symbol}" if version_symbol
|
||||
[key, version_dep]
|
||||
end.compact_blank
|
||||
|
||||
if (deprecate_args = hash["deprecate_args"])
|
||||
deprecate_args = deprecate_args.dup
|
||||
deprecate_args[:because] =
|
||||
DeprecateDisable.to_reason_string_or_symbol(deprecate_args[:because], type: :cask)
|
||||
hash["deprecate_args"] = deprecate_args
|
||||
end
|
||||
|
||||
if (disable_args = hash["disable_args"])
|
||||
disable_args = disable_args.dup
|
||||
disable_args[:because] = DeprecateDisable.to_reason_string_or_symbol(disable_args[:because], type: :cask)
|
||||
hash["disable_args"] = disable_args
|
||||
end
|
||||
|
||||
hash["names"] = hash["name"]
|
||||
|
||||
hash["raw_artifacts"] = Array(hash["artifacts"]).map do |artifact|
|
||||
key = artifact.keys.first
|
||||
|
||||
# Pass an empty block to artifacts like postflight that can't be loaded from the API,
|
||||
# but need to be set to something.
|
||||
next [key, [], {}, -> {}] if artifact[key].nil?
|
||||
|
||||
args = artifact[key]
|
||||
kwargs = if args.last.is_a?(Hash)
|
||||
args.pop
|
||||
else
|
||||
{}
|
||||
end
|
||||
[key, args, kwargs, nil]
|
||||
end
|
||||
|
||||
hash["raw_caveats"] = hash["caveats"]
|
||||
|
||||
hash["renames"] = hash["rename"].map do |operation|
|
||||
[operation[:from], operation[:to]]
|
||||
end
|
||||
|
||||
hash["ruby_source_checksum"] = {
|
||||
sha256: hash.dig("ruby_source_checksum", :sha256),
|
||||
}
|
||||
|
||||
hash["sha256"] = :no_check if hash["sha256"] == "no_check"
|
||||
|
||||
hash["tap_string"] = hash["tap"]
|
||||
|
||||
hash["url_args"] = [hash["url"]]
|
||||
|
||||
hash["url_kwargs"] = hash["url_specs"].to_h do |key, value|
|
||||
value = case key
|
||||
when :user_agent
|
||||
Utils.convert_to_string_or_symbol(value)
|
||||
when :using
|
||||
value.to_sym
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
[key, value]
|
||||
end.compact_blank
|
||||
|
||||
# Should match CaskStruct::PREDICATES
|
||||
hash["auto_updates_present"] = hash["auto_updates"].present?
|
||||
hash["caveats_present"] = hash["caveats"].present?
|
||||
hash["conflicts_present"] = hash["conflicts_with"].present?
|
||||
hash["container_present"] = hash["container"].present?
|
||||
hash["depends_on_present"] = !hash["depends_on_args"].empty?
|
||||
hash["deprecate_present"] = hash["deprecate_args"].present?
|
||||
hash["desc_present"] = hash["desc"].present?
|
||||
hash["disable_present"] = hash["disable_args"].present?
|
||||
|
||||
CaskStruct.from_hash(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Homebrew
|
||||
module API
|
||||
class CaskStruct < T::Struct
|
||||
PREDICATES = [
|
||||
:auto_updates,
|
||||
:caveats,
|
||||
:conflicts,
|
||||
:container,
|
||||
:depends_on,
|
||||
:deprecate,
|
||||
:desc,
|
||||
:disable,
|
||||
].freeze
|
||||
|
||||
ArtifactArgs = T.type_alias do
|
||||
[
|
||||
Symbol,
|
||||
T::Array[T.anything],
|
||||
T::Hash[Symbol, T.anything],
|
||||
T.nilable(T.proc.void),
|
||||
]
|
||||
end
|
||||
|
||||
PREDICATES.each do |predicate_name|
|
||||
present_method_name = :"#{predicate_name}_present"
|
||||
predicate_method_name = :"#{predicate_name}?"
|
||||
|
||||
const present_method_name, T::Boolean, default: false
|
||||
|
||||
define_method(predicate_method_name) do
|
||||
send(present_method_name)
|
||||
end
|
||||
end
|
||||
|
||||
# Changes to this struct must be mirrored in Homebrew::API::Cask.generate_cask_struct_hash
|
||||
const :auto_updates, T::Boolean, default: false
|
||||
const :conflicts_with_args, T::Hash[Symbol, T::Array[String]], default: {}
|
||||
const :container_args, T::Hash[Symbol, T.any(Symbol, T.anything)], default: {}
|
||||
const :depends_on_args, T::Hash[Symbol, T::Array[T.any(String, Symbol)]], default: {}
|
||||
const :deprecate_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
|
||||
const :desc, T.nilable(String)
|
||||
const :disable_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
|
||||
const :homepage, String
|
||||
const :languages, T::Array[String], default: []
|
||||
const :names, T::Array[String], default: []
|
||||
const :renames, T::Array[[String, String]], default: []
|
||||
const :ruby_source_checksum, T::Hash[Symbol, String]
|
||||
const :ruby_source_path, String
|
||||
const :sha256, T.any(String, Symbol)
|
||||
const :tap_git_head, String
|
||||
const :tap_string, String
|
||||
const :url_args, T::Array[String], default: []
|
||||
const :url_kwargs, T::Hash[Symbol, T.anything], default: {}
|
||||
const :version, T.any(String, Symbol)
|
||||
|
||||
sig { params(appdir: T.any(Pathname, String)).returns(T::Array[ArtifactArgs]) }
|
||||
def artifacts(appdir:)
|
||||
deep_remove_placeholders(raw_artifacts, appdir.to_s)
|
||||
end
|
||||
|
||||
sig { params(appdir: T.any(Pathname, String)).returns(T.nilable(String)) }
|
||||
def caveats(appdir:)
|
||||
deep_remove_placeholders(raw_caveats, appdir.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
const :raw_artifacts, T::Array[ArtifactArgs], default: []
|
||||
const :raw_caveats, T.nilable(String)
|
||||
|
||||
sig {
|
||||
type_parameters(:U)
|
||||
.params(
|
||||
value: T.type_parameter(:U),
|
||||
appdir: String,
|
||||
)
|
||||
.returns(T.type_parameter(:U))
|
||||
}
|
||||
def deep_remove_placeholders(value, appdir)
|
||||
value = case value
|
||||
when Hash
|
||||
value.transform_values do |v|
|
||||
deep_remove_placeholders(v, appdir)
|
||||
end
|
||||
when Array
|
||||
value.map do |v|
|
||||
deep_remove_placeholders(v, appdir)
|
||||
end
|
||||
when String
|
||||
value.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
|
||||
.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
|
||||
.gsub(HOMEBREW_CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
|
||||
.gsub(HOMEBREW_CASK_APPDIR_PLACEHOLDER, appdir)
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
T.cast(value, T.type_parameter(:U))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -351,18 +351,14 @@ module Cask
|
||||
nil
|
||||
end
|
||||
|
||||
def populate_from_api!(json_cask)
|
||||
sig { params(cask_struct: Homebrew::API::CaskStruct).void }
|
||||
def populate_from_api!(cask_struct)
|
||||
raise ArgumentError, "Expected cask to be loaded from the API" unless loaded_from_api?
|
||||
|
||||
@languages = json_cask.fetch(:languages, [])
|
||||
@tap_git_head = json_cask.fetch(:tap_git_head, "HEAD")
|
||||
|
||||
@ruby_source_path = json_cask[:ruby_source_path]
|
||||
|
||||
# TODO: Clean this up when we deprecate the current JSON API and move to the internal JSON v3.
|
||||
ruby_source_sha256 = json_cask.dig(:ruby_source_checksum, :sha256)
|
||||
ruby_source_sha256 ||= json_cask[:ruby_source_sha256]
|
||||
@ruby_source_checksum = { sha256: ruby_source_sha256 }
|
||||
@languages = cask_struct.languages
|
||||
@tap_git_head = cask_struct.tap_git_head
|
||||
@ruby_source_path = cask_struct.ruby_source_path
|
||||
@ruby_source_checksum = cask_struct.ruby_source_checksum
|
||||
end
|
||||
|
||||
# @api public
|
||||
|
||||
@@ -343,6 +343,8 @@ module Cask
|
||||
Homebrew::API::Cask.all_casks.fetch(token)
|
||||
end
|
||||
|
||||
cask_struct = Homebrew::API::Cask.generate_cask_struct_hash(json_cask)
|
||||
|
||||
cask_options = {
|
||||
loaded_from_api: true,
|
||||
api_source: json_cask,
|
||||
@@ -350,157 +352,51 @@ module Cask
|
||||
source: JSON.pretty_generate(json_cask),
|
||||
config:,
|
||||
loader: self,
|
||||
tap: Tap.fetch(cask_struct.tap_string),
|
||||
}
|
||||
|
||||
json_cask = Homebrew::API.merge_variations(json_cask).deep_symbolize_keys.freeze
|
||||
|
||||
cask_options[:tap] = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/")
|
||||
|
||||
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]
|
||||
version cask_struct.version
|
||||
sha256 cask_struct.sha256
|
||||
|
||||
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|
|
||||
url(*cask_struct.url_args, **cask_struct.url_kwargs)
|
||||
cask_struct.names.each do |cask_name|
|
||||
name cask_name
|
||||
end
|
||||
desc json_cask[:desc]
|
||||
homepage json_cask[:homepage]
|
||||
desc cask_struct.desc if cask_struct.desc?
|
||||
homepage cask_struct.homepage
|
||||
|
||||
if (date = json_cask[:deprecation_date].presence)
|
||||
because = DeprecateDisable.to_reason_string_or_symbol json_cask[:deprecation_reason], type: :cask
|
||||
deprecate! date:, because:
|
||||
deprecate!(**cask_struct.deprecate_args) if cask_struct.deprecate?
|
||||
disable!(**cask_struct.disable_args) if cask_struct.disable?
|
||||
|
||||
auto_updates cask_struct.auto_updates if cask_struct.auto_updates?
|
||||
conflicts_with(**cask_struct.conflicts_with_args) if cask_struct.conflicts?
|
||||
|
||||
cask_struct.renames.each do |from, to|
|
||||
rename from, to
|
||||
end
|
||||
|
||||
if (date = json_cask[:disable_date].presence)
|
||||
disable_reason = json_cask[:disable_reason].presence || json_cask[:deprecation_reason]
|
||||
because = DeprecateDisable.to_reason_string_or_symbol disable_reason, type: :cask
|
||||
disable! date:, because:
|
||||
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[:rename].present?
|
||||
json_cask[:rename].each do |rename_operation|
|
||||
rename rename_operation.fetch(:from), rename_operation.fetch(:to)
|
||||
end
|
||||
end
|
||||
|
||||
if json_cask[:depends_on].present?
|
||||
dep_hash = json_cask[:depends_on].to_h do |dep_key, dep_value|
|
||||
# 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] if dep_key != :macos
|
||||
|
||||
dep_type = dep_value.keys.first
|
||||
if dep_type == :==
|
||||
version_symbols = dep_value[dep_type].filter_map do |version|
|
||||
MacOSVersion::SYMBOLS.key(version)
|
||||
end
|
||||
next [dep_key, version_symbols.presence]
|
||||
end
|
||||
|
||||
version_symbol = dep_value[dep_type].first
|
||||
version_symbol = MacOSVersion::SYMBOLS.key(version_symbol)
|
||||
version_dep = "#{dep_type} :#{version_symbol}" if version_symbol
|
||||
[dep_key, version_dep]
|
||||
end.compact
|
||||
if cask_struct.depends_on?
|
||||
args = cask_struct.depends_on_args
|
||||
begin
|
||||
depends_on(**dep_hash)
|
||||
depends_on(**args)
|
||||
rescue MacOSVersion::Error => e
|
||||
odebug "Ignored invalid macOS version dependency in cask '#{token}': #{dep_hash.inspect} (#{e.message})"
|
||||
odebug "Ignored invalid macOS version dependency in cask '#{token}': #{args.inspect} (#{e.message})"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
if json_cask[:container].present?
|
||||
container_hash = json_cask[:container].to_h do |container_key, container_value|
|
||||
next [container_key, container_value] if container_key != :type
|
||||
container(**cask_struct.container_args) if cask_struct.container?
|
||||
|
||||
[container_key, container_value.to_sym]
|
||||
end
|
||||
container(**container_hash)
|
||||
cask_struct.artifacts(appdir:).each do |key, args, kwargs, block|
|
||||
send(key, *args, **kwargs, &block)
|
||||
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
|
||||
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
|
||||
caveats cask_struct.caveats(appdir:) if cask_struct.caveats?
|
||||
end
|
||||
api_cask.populate_from_api!(json_cask)
|
||||
api_cask.populate_from_api!(cask_struct)
|
||||
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
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Loader which tries loading casks from tap paths, failing
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for dynamic methods in `Homebrew::API::CaskStruct`.
|
||||
# Please instead update this file by running `bin/tapioca dsl Homebrew::API::CaskStruct`.
|
||||
|
||||
|
||||
class Homebrew::API::CaskStruct
|
||||
sig { returns(T::Boolean) }
|
||||
def auto_updates?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def caveats?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def conflicts?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def container?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def depends_on?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def deprecate?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def desc?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def disable?; end
|
||||
end
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
require_relative "../../../global"
|
||||
require "api/formula_struct"
|
||||
require "api/cask_struct"
|
||||
|
||||
module Tapioca
|
||||
module Compilers
|
||||
@@ -10,12 +11,12 @@ module Tapioca
|
||||
ConstantType = type_member { { fixed: T.class_of(T::Struct) } }
|
||||
|
||||
sig { override.returns(T::Enumerable[T::Module[T.anything]]) }
|
||||
def self.gather_constants = [::Homebrew::API::FormulaStruct]
|
||||
def self.gather_constants = [::Homebrew::API::FormulaStruct, ::Homebrew::API::CaskStruct]
|
||||
|
||||
sig { override.void }
|
||||
def decorate
|
||||
root.create_class(T.must(constant.name)) do |klass|
|
||||
::Homebrew::API::FormulaStruct::PREDICATES.each do |predicate_name|
|
||||
constant.const_get(:PREDICATES).each do |predicate_name|
|
||||
klass.create_method("#{predicate_name}?", return_type: "T::Boolean")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,8 @@ RSpec.describe Cask::CaskLoader::FromAPILoader, :cask do
|
||||
let(:cask_from_source) { Cask::CaskLoader.load(local_token) }
|
||||
let(:cask_json) do
|
||||
hash = cask_from_source.to_hash_with_variations
|
||||
# This value will always be present in the json API, but is skipped in tests
|
||||
hash["tap_git_head"] = "abcdef1234567890abcdef1234567890abcdef12"
|
||||
json = JSON.pretty_generate(hash)
|
||||
JSON.parse(json)
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"token": "caffeine",
|
||||
"full_token": "caffeine",
|
||||
"tap": null,
|
||||
"tap": "homebrew/cask",
|
||||
"name": [
|
||||
|
||||
],
|
||||
@@ -31,9 +31,14 @@
|
||||
},
|
||||
"conflicts_with": null,
|
||||
"container": null,
|
||||
"rename": [],
|
||||
"auto_updates": null,
|
||||
"tap_git_head": null,
|
||||
"tap_git_head": "abcdef1234567890abcdef1234567890abcdef12",
|
||||
"languages": [
|
||||
|
||||
]
|
||||
],
|
||||
"ruby_source_path": "Casks/caffeine.rb",
|
||||
"ruby_source_checksum": {
|
||||
"sha256": "d3c19b564ee5a17f22191599ad795a6cc9c4758d0e1269f2d13207155b378dea"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user