Files
brew/Library/Homebrew/cask/dsl.rb
T
Mike McQuaid 233377e66a Treat cask OS support as explicit data
- 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.
2026-08-05 16:23:33 +01:00

932 lines
29 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "autobump_constants"
require "locale"
require "livecheck"
require "utils/output"
require "utils/path"
require "cask/artifact"
require "cask/artifact_set"
require "cask/caskroom"
require "cask/exceptions"
require "cask/dsl/base"
require "cask/dsl/caveats"
require "cask/dsl/conflicts_with"
require "cask/dsl/container"
require "cask/dsl/depends_on"
require "cask/dsl/postflight"
require "cask/dsl/preflight"
require "cask/dsl/rename"
require "cask/dsl/uninstall_postflight"
require "cask/dsl/uninstall_preflight"
require "cask/dsl/version"
require "cask/url"
require "cask/utils"
require "on_system"
module Cask
# Class representing the domain-specific language used for casks.
class DSL
include ::Utils::Output::Mixin
include ::Utils::Path
ORDINARY_ARTIFACT_CLASSES = [
Artifact::Installer,
Artifact::App,
Artifact::AppImage,
Artifact::Artifact,
Artifact::AudioUnitPlugin,
Artifact::Binary,
Artifact::CommandWrapper,
Artifact::Colorpicker,
Artifact::Dictionary,
Artifact::Font,
Artifact::GeneratedScript,
Artifact::InputMethod,
Artifact::InternetPlugin,
Artifact::KeyboardLayout,
Artifact::Manpage,
Artifact::Pkg,
Artifact::Prefpane,
Artifact::Qlplugin,
Artifact::Mdimporter,
Artifact::ScreenSaver,
Artifact::Service,
Artifact::StageOnly,
Artifact::Suite,
Artifact::VstPlugin,
Artifact::Vst3Plugin,
Artifact::ZshCompletion,
Artifact::FishCompletion,
Artifact::BashCompletion,
Artifact::GeneratedCompletion,
Artifact::Uninstall,
Artifact::Zap,
].freeze
ACTIVATABLE_ARTIFACT_CLASSES = T.let(
(ORDINARY_ARTIFACT_CLASSES - [Artifact::StageOnly]).freeze,
T::Array[T.class_of(Artifact::AbstractArtifact)],
)
ARTIFACT_BLOCK_CLASSES = [
Artifact::PreflightBlock,
Artifact::PostflightBlock,
].freeze
INSTALL_STEP_ARTIFACT_CLASSES = [
Artifact::PreflightSteps,
Artifact::PostflightSteps,
Artifact::UninstallPreflightSteps,
Artifact::UninstallPostflightSteps,
].freeze
DSL_METHODS = T.let(Set.new([
:arch,
:artifacts,
:auto_updates,
:caveats,
:conflicts_with,
:container,
:desc,
:depends_on,
:homepage,
:homepage_browsed,
:language,
:name,
:os,
:rename,
:sha256,
:staged_path,
:url,
:version,
:appdir,
:deprecate!,
:deprecated?,
:deprecation_date,
:deprecation_reason,
:deprecation_replacement_cask,
:deprecation_replacement_formula,
:deprecate_args,
:disable!,
:disabled?,
:disable_date,
:disable_reason,
:disable_replacement_cask,
:disable_replacement_formula,
:disable_args,
:livecheck,
:livecheck_defined?,
:no_autobump!,
:autobump?,
:no_autobump_message,
:on_system_blocks_exist?,
:on_os_blocks_exist?,
:on_system_block_min_os,
:depends_on_set_in_block?,
*ORDINARY_ARTIFACT_CLASSES.map(&:dsl_key),
*ACTIVATABLE_ARTIFACT_CLASSES.map(&:dsl_key),
*ARTIFACT_BLOCK_CLASSES.flat_map { |klass| [klass.dsl_key, klass.uninstall_dsl_key] },
*INSTALL_STEP_ARTIFACT_CLASSES.map(&:dsl_key),
]).freeze, T::Set[Symbol])
include OnSystem::MacOSAndLinux
sig { returns(Cask) }
attr_reader :cask
sig { returns(String) }
attr_reader :token
sig { returns(T.nilable(T.any(String, Symbol))) }
attr_reader :no_autobump_message
sig { returns(ArtifactSet) }
attr_reader :artifacts
sig { returns(T.nilable(Date)) }
attr_reader :deprecation_date
sig { returns(T.nilable(T.any(String, Symbol))) }
attr_reader :deprecation_reason
sig { returns(T.nilable(String)) }
attr_reader :deprecation_replacement_cask
sig { returns(T.nilable(String)) }
attr_reader :deprecation_replacement_formula
sig { returns(T.nilable(T::Hash[Symbol, T.nilable(T.any(String, Symbol))])) }
attr_reader :deprecate_args
sig { returns(T.nilable(Date)) }
attr_reader :disable_date
sig { returns(T.nilable(T.any(String, Symbol))) }
attr_reader :disable_reason
sig { returns(T.nilable(String)) }
attr_reader :disable_replacement_cask
sig { returns(T.nilable(String)) }
attr_reader :disable_replacement_formula
sig { returns(T.nilable(Date)) }
attr_reader :homepage_browsed
sig { returns(T.nilable(T::Hash[Symbol, T.nilable(T.any(String, Symbol))])) }
attr_reader :disable_args
sig { returns(T.nilable(MacOSVersion)) }
attr_reader :on_system_block_min_os
sig { params(cask: Cask).void }
def initialize(cask)
# NOTE: `:"@#{stanza}"` variables set by `set_unique_stanza` must be
# initialized to `nil`.
@arch = T.let(nil, T.nilable(String))
@arch_set_in_block = T.let(false, T::Boolean)
@artifacts = T.let(ArtifactSet.new, ArtifactSet)
@auto_updates = T.let(nil, T.nilable(T::Boolean))
@auto_updates_set_in_block = T.let(false, T::Boolean)
@autobump = T.let(true, T::Boolean)
@called_in_on_system_block = T.let(false, T::Boolean)
@called_in_on_os_block = T.let(false, T::Boolean)
@cask = cask
@caveats = T.let(DSL::Caveats.new(cask), DSL::Caveats)
@conflicts_with = T.let(nil, T.nilable(DSL::ConflictsWith))
@conflicts_with_set_in_block = T.let(false, T::Boolean)
@container = T.let(nil, T.nilable(DSL::Container))
@container_set_in_block = T.let(false, T::Boolean)
@depends_on = T.let(DSL::DependsOn.new, DSL::DependsOn)
@depends_on_set_in_block = T.let(false, T::Boolean)
@deprecated = T.let(false, T::Boolean)
@deprecation_date = T.let(nil, T.nilable(Date))
@deprecation_reason = T.let(nil, T.nilable(T.any(String, Symbol)))
@deprecation_replacement_cask = T.let(nil, T.nilable(String))
@deprecation_replacement_formula = T.let(nil, T.nilable(String))
@deprecate_args = T.let(nil, T.nilable(T::Hash[Symbol, T.nilable(T.any(String, Symbol))]))
@desc = T.let(nil, T.nilable(String))
@desc_set_in_block = T.let(false, T::Boolean)
@disable_date = T.let(nil, T.nilable(Date))
@disable_reason = T.let(nil, T.nilable(T.any(String, Symbol)))
@disable_replacement_cask = T.let(nil, T.nilable(String))
@disable_replacement_formula = T.let(nil, T.nilable(String))
@disable_args = T.let(nil, T.nilable(T::Hash[Symbol, T.nilable(T.any(String, Symbol))]))
@disabled = T.let(false, T::Boolean)
@homepage = T.let(nil, T.nilable(String))
@homepage_browsed = T.let(nil, T.nilable(Date))
@homepage_set_in_block = T.let(false, T::Boolean)
@language_blocks = T.let({}, T::Hash[T::Array[String], Proc])
@language_eval = T.let(nil, T.nilable(String))
@livecheck = T.let(Livecheck.new(cask), Livecheck)
@livecheck_defined = T.let(false, T::Boolean)
@name = T.let([], T::Array[String])
@no_autobump_defined = T.let(false, T::Boolean)
@no_autobump_message = T.let(nil, T.nilable(T.any(String, Symbol)))
@on_system_blocks_exist = T.let(false, T::Boolean)
@on_os_blocks_exist = T.let(false, T::Boolean)
@on_system_block_min_os = T.let(nil, T.nilable(MacOSVersion))
@os = T.let(nil, T.nilable(String))
@os_set_in_block = T.let(false, T::Boolean)
@rename = T.let([], T::Array[DSL::Rename])
@sha256 = T.let(nil, T.nilable(T.any(Checksum, Symbol)))
@sha256_set_in_block = T.let(false, T::Boolean)
@staged_path = T.let(nil, T.nilable(Pathname))
@token = T.let(cask.token, String)
@url = T.let(nil, T.nilable(URL))
@url_set_in_block = T.let(false, T::Boolean)
@version = T.let(nil, T.nilable(DSL::Version))
@version_set_in_block = T.let(false, T::Boolean)
end
sig { returns(T::Boolean) }
def depends_on_set_in_block? = @depends_on_set_in_block
sig { returns(T::Boolean) }
def deprecated? = @deprecated
sig { returns(T::Boolean) }
def disabled? = @disabled
sig { returns(T::Boolean) }
def livecheck_defined? = @livecheck_defined
sig { returns(T::Boolean) }
def on_system_blocks_exist? = @on_system_blocks_exist
sig { returns(T::Boolean) }
def on_os_blocks_exist? = @on_os_blocks_exist
# Specifies the cask's name.
#
# NOTE: Multiple names can be specified.
#
# ### Example
#
# ```ruby
# name "Visual Studio Code"
# ```
#
# @api public
sig { params(args: T.any(String, T::Array[String])).returns(T::Array[String]) }
def name(*args)
return @name if args.empty?
@name.concat(args.flatten)
end
# Describes the cask.
#
# ### Example
#
# ```ruby
# desc "Open-source code editor"
# ```
#
# @api public
sig { params(description: T.nilable(String)).returns(T.nilable(String)) }
def desc(description = nil)
set_unique_stanza(:desc, description.nil?) { description }
end
# NOTE: Using `WithoutRuntime` to avoid Sorbet wrapping this method,
# which would interfere with `caller_locations` in methods like `url`.
T::Sig::WithoutRuntime.sig {
type_parameters(:U).params(
stanza: Symbol,
should_return: T::Boolean,
_block: T.proc.returns(T.all(BasicObject, T.type_parameter(:U))),
).returns(T.type_parameter(:U))
}
def set_unique_stanza(stanza, should_return, &_block)
return instance_variable_get(:"@#{stanza}") if should_return
unless @cask.allow_reassignment
if !instance_variable_get(:"@#{stanza}").nil? && !@called_in_on_system_block
raise CaskInvalidError.new(cask, "'#{stanza}' stanza may only appear once.")
end
if instance_variable_get(:"@#{stanza}_set_in_block") && @called_in_on_system_block
raise CaskInvalidError.new(cask, "'#{stanza}' stanza may only be overridden once.")
end
end
instance_variable_set(:"@#{stanza}_set_in_block", true) if @called_in_on_system_block
instance_variable_set(:"@#{stanza}", yield)
rescue CaskInvalidError
raise
rescue => e
raise CaskInvalidError.new(cask, "'#{stanza}' stanza failed with: #{e}")
end
# Sets the cask's homepage.
#
# ### Example
#
# ```ruby
# homepage "https://code.visualstudio.com/", browsed: "2026-07-26"
# ```
#
# `browsed` is the date when a human last checked the homepage in a browser.
# Automated homepage availability audits are skipped for one year.
#
# @api public
sig { params(homepage: T.nilable(String), browsed: T.nilable(String)).returns(T.nilable(String)) }
def homepage(homepage = nil, browsed: nil)
raise CaskInvalidError.new(cask, "`browsed` requires a homepage URL") if homepage.nil? && browsed
set_unique_stanza(:homepage, homepage.nil?) do
@homepage_browsed = Date.parse(browsed) if browsed
homepage
end
end
# Specifies language-specific values for the cask.
#
# @api public
sig {
params(
args: String,
default: T::Boolean,
block: T.nilable(T.proc.returns(String)),
).returns(T.nilable(String))
}
def language(*args, default: false, &block)
if args.empty?
language_eval
elsif block
@language_blocks[args] = block
return unless default
if !@cask.allow_reassignment && @language_blocks.default.present?
raise CaskInvalidError.new(cask, "Only one default language may be defined.")
end
@language_blocks.default = block
nil
else
raise CaskInvalidError.new(cask, "No block given to language stanza.")
end
end
sig { returns(T.nilable(String)) }
def language_eval
return @language_eval unless @language_eval.nil?
return @language_eval = nil if @language_blocks.empty?
if (language_blocks_default = @language_blocks.default).nil?
raise CaskInvalidError.new(cask, "No default language specified.")
end
locales = cask.config.languages
.filter_map do |language|
Locale.parse(language)
rescue Locale::ParserError
nil
end
locales.each do |locale|
key = T.cast(locale.detect(@language_blocks.keys), T.nilable(T::Array[String]))
next if key.nil? || (language_block = @language_blocks[key]).nil?
return @language_eval = language_block.call
end
@language_eval = language_blocks_default.call
end
sig { returns(T::Array[String]) }
def languages
@language_blocks.keys.flatten
end
sig { returns(T::Array[T::Array[String]]) }
def language_groups
@language_blocks.keys
end
sig { returns(T.nilable(T::Array[String])) }
def default_language_group
default_language_block = @language_blocks.default
return if default_language_block.nil?
@language_blocks.key(default_language_block)
end
# Sets the cask's download URL.
#
# ### Example
#
# ```ruby
# url "https://update.code.visualstudio.com/#{version}/#{arch}/stable"
# ```
#
# @api public
T::Sig::WithoutRuntime.sig { params(uri: T.nilable(T.any(URI::Generic, String)), options: T.untyped).returns(T.nilable(URL)) }
def url(uri = nil, **options)
caller_location = caller_locations.fetch(0)
return @url unless uri
# Keep accepting `verified` as a no-op for compatibility with existing casks.
# odeprecated "the `verified` parameter in the `url` stanza" if options[:verified]
set_unique_stanza(:url, false) do
URL.new(uri, **options, caller_location:)
end
end
# Sets the cask's container type or nested container path.
#
# ### Examples
#
# The container is a nested disk image:
#
# ```ruby
# container nested: "orca-#{version}.dmg"
# ```
#
# The container should not be unarchived:
#
# ```ruby
# container type: :naked
# ```
#
# @api public
sig { params(nested: T.nilable(String), type: T.nilable(Symbol)).returns(T.nilable(DSL::Container)) }
def container(nested: nil, type: nil)
set_unique_stanza(:container, nested.nil? && type.nil?) do
DSL::Container.new(nested:, type:)
end
end
# Renames files after extraction.
#
# This is useful when the downloaded file has unpredictable names
# that need to be normalized for proper artifact installation.
#
# ### Example
#
# ```ruby
# rename "RØDECaster App*.pkg", "RØDECaster App.pkg"
# ```
#
# @api public
sig {
params(from: String,
to: String).returns(T::Array[DSL::Rename])
}
def rename(from = T.unsafe(nil), to = T.unsafe(nil))
return @rename if from.nil?
@rename << DSL::Rename.new(from, to)
end
# Sets the cask's version.
#
# ### Example
#
# ```ruby
# version "1.88.1"
# ```
#
# @see DSL::Version
# @api public
sig { params(arg: T.nilable(T.any(String, Symbol))).returns(T.nilable(DSL::Version)) }
def version(arg = nil)
set_unique_stanza(:version, arg.nil?) do
if !arg.is_a?(String) && arg != :latest
raise CaskInvalidError.new(cask, "invalid 'version' value: #{arg.inspect}")
end
set_no_autobump(because: :latest_version) if arg == :latest && !no_autobump_defined?
DSL::Version.new(arg)
end
end
# Sets the cask's download checksum.
#
# ### Example
#
# For universal or single-architecture downloads:
#
# ```ruby
# sha256 "7bdb497080ffafdfd8cc94d8c62b004af1be9599e865e5555e456e2681e150ca"
# ```
#
# For architecture-dependent downloads:
#
# ```ruby
# sha256 arm: "7bdb497080ffafdfd8cc94d8c62b004af1be9599e865e5555e456e2681e150ca",
# x86_64: "b3c1c2442480a0219b9e05cf91d03385858c20f04b764ec08a3fa83d1b27e7b2"
# x86_64_linux: "1a2aee7f1ddc999993d4d7d42a150c5e602bc17281678050b8ed79a0500cc90f"
# arm64_linux: "bd766af7e692afceb727a6f88e24e6e68d9882aeb3e8348412f6c03d96537c75"
# ```
#
# @api public
sig {
params(
arg: T.nilable(T.any(String, Symbol)),
arm: T.nilable(String),
intel: T.nilable(String),
x86_64: T.nilable(String),
x86_64_linux: T.nilable(String),
arm64_linux: T.nilable(String),
).returns(T.nilable(T.any(Symbol, Checksum)))
}
def sha256(arg = nil, arm: nil, intel: nil, x86_64: nil, x86_64_linux: nil, arm64_linux: nil)
should_return = arg.nil? && arm.nil? && (intel.nil? || x86_64.nil?) && x86_64_linux.nil? && arm64_linux.nil?
x86_64 ||= intel if intel.present? && x86_64.nil?
set_unique_stanza(:sha256, should_return) do
if arm.present? || x86_64.present? || x86_64_linux.present? || arm64_linux.present?
@on_system_blocks_exist = true
end
val = arg || on_system_conditional(
macos: on_arch_conditional(arm:, intel: x86_64),
linux: on_arch_conditional(arm: arm64_linux, intel: x86_64_linux),
)
case val
when :no_check
:no_check
when String
Checksum.new(val)
when nil
# Checksums declared for only the other OS mean no checksum for the
# running OS, matching `sha256` inside an `on_macos`/`on_linux` block;
# `depends_on` governs whether the cask is usable there. A checksum
# declared for the running OS but missing the running architecture
# still raises on the real system but is nil under simulation so
# API variations can be generated for the missing architecture.
running_os_checksums = if OnSystem.os_condition_met?(:linux)
[x86_64_linux, arm64_linux]
else
[arm, x86_64]
end
if running_os_checksums.any?(&:present?) && !Homebrew::SimulateSystem.simulating?
raise CaskInvalidError.new(cask, "invalid 'sha256' value: nil")
end
nil
else
raise CaskInvalidError.new(cask, "invalid 'sha256' value: #{val.inspect}")
end
end
end
# Sets the cask's architecture strings.
#
# ### Example
#
# ```ruby
# arch arm: "darwin-arm64", intel: "darwin"
# ```
#
# @api public
sig { params(arm: T.nilable(String), intel: T.nilable(String)).returns(T.nilable(String)) }
def arch(arm: nil, intel: nil)
should_return = arm.nil? && intel.nil?
set_unique_stanza(:arch, should_return) do
@on_system_blocks_exist = true
on_arch_conditional(arm:, intel:)
end
end
# Sets the cask's os strings.
#
# ### Example
#
# ```ruby
# os macos: "darwin", linux: "tux"
# ```
#
# @api public
sig {
params(
macos: T.nilable(String),
linux: T.nilable(String),
).returns(T.nilable(String))
}
def os(macos: nil, linux: nil)
should_return = macos.nil? && linux.nil?
set_unique_stanza(:os, should_return) do
@on_system_blocks_exist = true
@on_os_blocks_exist = true
on_system_conditional(macos:, linux:)
end
end
# Declare dependencies and requirements for a cask.
#
# NOTE: Multiple dependencies can be specified.
#
# @api public
sig { params(arg: T.nilable(Symbol), kwargs: T.untyped).returns(DSL::DependsOn) }
def depends_on(arg = nil, **kwargs)
@depends_on_set_in_block = true if @called_in_on_system_block
if arg == :macos
if kwargs.key?(:macos) || kwargs.key?(:maximum_macos)
raise CaskInvalidError.new(cask, "`depends_on :macos` cannot be combined with another macOS `depends_on`")
end
kwargs[:macos] = :any
elsif arg == :linux
kwargs[:linux] = :any
elsif arg
raise CaskInvalidError.new(cask, "invalid 'depends_on' value: #{arg.inspect}")
end
return @depends_on if kwargs.empty?
begin
# Only OS blocks scope a dependency to one OS: `on_arm`/`on_intel`
# blocks are evaluated on every OS, so a macOS dependency inside one
# applies everywhere and marks the cask macOS-only.
@depends_on.load(kwargs, set_in_block: @called_in_on_system_block, os_scoped: @called_in_on_os_block)
rescue RuntimeError => e
raise CaskInvalidError.new(cask, e)
end
@depends_on
end
# Declare conflicts that keep a cask from installing or working correctly.
#
# @api public
sig { params(kwargs: T.anything).returns(T.nilable(DSL::ConflictsWith)) }
def conflicts_with(**kwargs)
# TODO: Remove this constraint and instead merge multiple `conflicts_with` stanzas
set_unique_stanza(:conflicts_with, kwargs.empty?) { DSL::ConflictsWith.new(**kwargs) }
end
sig { returns(Pathname) }
def caskroom_path
cask.caskroom_path
end
# The staged location for this cask, including version number.
#
# @api public
sig { returns(Pathname) }
def staged_path
return @staged_path if @staged_path
cask_version = version || :unknown
@staged_path = caskroom_path.join(cask_version.to_s)
end
# Provide the user with cask-specific information at install time.
#
# @api public
sig {
params(
strings: String,
block: T.nilable(T.proc.returns(T.nilable(T.any(Symbol, String)))),
).returns(T.any(String, DSL::Caveats))
}
def caveats(*strings, &block)
if block
@caveats.eval_caveats(&block)
elsif strings.any?
strings.each do |string|
@caveats.eval_caveats { string }
end
else
return @caveats.to_s
end
@caveats
end
sig { returns(DSL::Caveats) }
def caveats_object = @caveats
# Asserts that the cask artifacts auto-update.
#
# @api public
sig { params(auto_updates: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
def auto_updates(auto_updates = nil)
set_unique_stanza(:auto_updates, auto_updates.nil?) { auto_updates }
end
# Automatically fetch the latest version of a cask from changelogs.
#
# @api public
sig { params(block: T.nilable(T.proc.bind(Livecheck).void)).returns(Livecheck) }
def livecheck(&block)
return @livecheck unless block
if !@cask.allow_reassignment && @livecheck_defined
raise CaskInvalidError.new(cask, "'livecheck' stanza may only appear once.")
end
@livecheck_defined = true
@livecheck.instance_eval(&block)
set_no_autobump(because: :extract_plist) if @livecheck.strategy == :extract_plist && !no_autobump_defined?
@livecheck
end
# Excludes the cask from autobump list.
#
# @api public
sig { params(because: T.any(String, Symbol)).void }
def no_autobump!(because:)
tap = @cask.tap
if tap && !tap.official?
raise CaskInvalidError.new(cask, "'no_autobump!' can only be used in official Homebrew taps.")
end
set_no_autobump(because:)
end
# Is the cask in autobump list?
sig { returns(T::Boolean) }
def autobump?
@autobump == true
end
# Declare that a cask is no longer functional or supported.
#
# NOTE: A warning will be shown when trying to install this cask.
#
# @api public
sig {
params(
date: String,
because: T.any(String, Symbol),
replacement: T.nilable(String),
replacement_formula: T.nilable(String),
replacement_cask: T.nilable(String),
).void
}
def deprecate!(date:, because:, replacement: nil, replacement_formula: nil, replacement_cask: nil)
if [replacement, replacement_formula, replacement_cask].filter_map(&:presence).length > 1
raise ArgumentError, "more than one of replacement, replacement_formula and/or replacement_cask specified!"
end
if replacement
odeprecated(
"deprecate!(:replacement)",
"deprecate!(:replacement_formula) or deprecate!(:replacement_cask)",
)
end
@deprecate_args = { date:, because:, replacement_formula:, replacement_cask: }
@deprecation_date = Date.parse(date)
return if @deprecation_date > Date.today
@deprecation_reason = because
@deprecation_replacement_formula = replacement_formula.presence || replacement
@deprecation_replacement_cask = replacement_cask.presence || replacement
@deprecated = true
end
# Declare that a cask is no longer functional or supported.
#
# NOTE: An error will be thrown when trying to install this cask.
#
# @api public
sig {
params(
date: String,
because: T.any(String, Symbol),
replacement: T.nilable(String),
replacement_formula: T.nilable(String),
replacement_cask: T.nilable(String),
).void
}
def disable!(date:, because:, replacement: nil, replacement_formula: nil, replacement_cask: nil)
if [replacement, replacement_formula, replacement_cask].filter_map(&:presence).length > 1
raise ArgumentError, "more than one of replacement, replacement_formula and/or replacement_cask specified!"
end
# odeprecate: remove this remapping when the :unsigned reason is removed
because = :fails_gatekeeper_check if because == :unsigned
if replacement
odeprecated(
"disable!(:replacement)",
"disable!(:replacement_formula) or disable!(:replacement_cask)",
)
end
@disable_args = { date:, because:, replacement_formula:, replacement_cask: }
@disable_date = Date.parse(date)
if @disable_date > Date.today
@deprecation_reason = because
@deprecation_replacement_formula = replacement_formula.presence || replacement
@deprecation_replacement_cask = replacement_cask.presence || replacement
@deprecated = true
return
end
@disable_reason = because
@disable_replacement_formula = replacement_formula.presence || replacement
@disable_replacement_cask = replacement_cask.presence || replacement
@disabled = true
end
ORDINARY_ARTIFACT_CLASSES.each do |klass|
define_method(klass.dsl_key) do |*args, **kwargs|
T.bind(self, DSL)
if [*artifacts.map(&:class), klass].include?(Artifact::StageOnly) &&
artifacts.map(&:class).intersect?(ACTIVATABLE_ARTIFACT_CLASSES)
raise CaskInvalidError.new(cask, "'stage_only' must be the only activatable artifact.")
end
artifacts.add(klass.from_args(cask, *args, **kwargs))
rescue CaskInvalidError
raise
rescue => e
raise CaskInvalidError.new(cask, "invalid '#{klass.dsl_key}' stanza: #{e}")
end
end
ARTIFACT_BLOCK_CLASSES.each do |klass|
[klass.dsl_key, klass.uninstall_dsl_key].each do |dsl_key|
define_method(dsl_key) do |&block|
T.bind(self, DSL)
# odeprecated "`#{dsl_key}`", "`#{dsl_key}_steps`"
artifacts.add(klass.new(cask, dsl_key => block))
end
end
end
INSTALL_STEP_ARTIFACT_CLASSES.each do |klass|
define_method(klass.dsl_key) do |steps = nil, **kwargs, &block|
T.bind(self, DSL)
steps = if block
Homebrew::InstallSteps::DSL.build(default_base: :staged_path, default_source_base: :staged_path,
default_target_base: :staged_path, &block)
else
Homebrew::InstallSteps::DSL.normalise_steps([kwargs[:steps] || steps].flatten.compact)
end
artifacts.add(klass.new(cask, steps))
end
end
sig { override.params(method: Symbol, _args: T.anything).returns(T.noreturn) }
def method_missing(method, *_args)
raise NoMethodError, "undefined method '#{method}' for Cask '#{token}'"
end
sig { override.params(_method_name: T.any(String, Symbol), _include_private: T::Boolean).returns(T::Boolean) }
def respond_to_missing?(_method_name, _include_private = false)
false
end
sig { returns(T.nilable(MacOSVersion)) }
def os_version
nil
end
# The directory `app`s are installed into.
#
# @api public
sig { returns(T.any(Pathname, String)) }
def appdir
return HOMEBREW_CASK_APPDIR_PLACEHOLDER if Cask.generating_hash?
cask.config.appdir
end
private
sig { returns(T::Boolean) }
def no_autobump_defined?
@no_autobump_defined
end
sig { params(because: T.any(String, Symbol)).void }
def set_no_autobump(because:)
if because.is_a?(Symbol) && !NO_AUTOBUMP_REASONS_LIST.key?(because)
raise ArgumentError, "'because' argument should use valid symbol or a string!"
end
if !@cask.allow_reassignment && no_autobump_defined?
raise CaskInvalidError.new(cask, "'no_autobump!' stanza may only appear once.")
end
odisabled "no_autobump! because: :requires_manual_review" if because == :requires_manual_review
@no_autobump_defined = true
@no_autobump_message = because
@autobump = false
end
end
end