650 lines
18 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "locale"
2018-07-30 21:07:36 +02:00
require "lazy_object"
2020-09-02 12:24:21 -07:00
require "livecheck"
2016-08-18 22:11:42 +03:00
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/uninstall_postflight"
require "cask/dsl/uninstall_preflight"
require "cask/dsl/version"
2016-08-18 22:11:42 +03:00
require "cask/url"
2020-08-01 02:30:46 +02:00
require "cask/utils"
require "extend/on_system"
2018-09-06 08:29:14 +02:00
module Cask
2020-08-24 23:09:43 +02:00
# Class representing the domain-specific language used for casks.
2016-09-24 13:52:43 +02:00
class DSL
ORDINARY_ARTIFACT_CLASSES = [
Artifact::Installer,
Artifact::App,
Artifact::Artifact,
Artifact::AudioUnitPlugin,
Artifact::Binary,
Artifact::Colorpicker,
Artifact::Dictionary,
Artifact::Font,
Artifact::InputMethod,
Artifact::InternetPlugin,
2023-03-26 08:10:40 +02:00
Artifact::KeyboardLayout,
2019-10-23 16:28:00 +03:00
Artifact::Manpage,
Artifact::Pkg,
Artifact::Prefpane,
Artifact::Qlplugin,
2020-04-05 15:30:37 +02:00
Artifact::Mdimporter,
Artifact::ScreenSaver,
Artifact::Service,
Artifact::StageOnly,
Artifact::Suite,
Artifact::VstPlugin,
Artifact::Vst3Plugin,
2025-03-03 17:54:54 +01:00
Artifact::ZshCompletion,
Artifact::FishCompletion,
Artifact::BashCompletion,
Artifact::Uninstall,
Artifact::Zap,
2016-10-14 20:33:16 +02:00
].freeze
2016-09-24 13:52:43 +02:00
2019-04-19 21:46:20 +09:00
ACTIVATABLE_ARTIFACT_CLASSES = (ORDINARY_ARTIFACT_CLASSES - [Artifact::StageOnly]).freeze
2016-09-24 13:52:43 +02:00
ARTIFACT_BLOCK_CLASSES = [
Artifact::PreflightBlock,
Artifact::PostflightBlock,
2016-10-14 20:33:16 +02:00
].freeze
2016-08-18 22:11:42 +03:00
2019-04-19 21:46:20 +09:00
DSL_METHODS = Set.new([
:appcast,
2023-03-27 11:28:59 +11:00
:arch,
:artifacts,
:auto_updates,
:caveats,
:conflicts_with,
:container,
:desc,
:depends_on,
:homepage,
:language,
:name,
:os,
:sha256,
:staged_path,
:url,
:version,
:appdir,
:deprecate!,
:deprecated?,
:deprecation_date,
:deprecation_reason,
:deprecation_replacement,
:deprecation_replacement_formula,
:deprecation_replacement_cask,
:disable!,
:disabled?,
:disable_date,
:disable_reason,
:disable_replacement,
:disable_replacement_formula,
:disable_replacement_cask,
:discontinued?, # TODO: remove once discontinued? is removed (4.5.0)
:livecheck,
:livecheck_defined?,
:livecheckable?, # TODO: remove once `#livecheckable?` is removed
:on_system_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] },
]).freeze
2016-09-24 13:52:43 +02:00
2025-01-19 16:36:37 +01:00
include OnSystem::MacOSAndLinux
attr_reader :cask, :token, :deprecation_date, :deprecation_reason, :deprecation_replacement,
:deprecation_replacement_formula, :deprecation_replacement_caks, :disable_date,
:disable_reason, :disable_replacement, :disable_replacement_formula,
:disable_replacement_cask, :on_system_block_min_os
2017-10-04 15:47:53 +02:00
def initialize(cask)
@cask = cask
2025-02-24 14:49:33 -08:00
@depends_on_set_in_block = T.let(false, T::Boolean)
@deprecated = T.let(false, T::Boolean)
@disabled = T.let(false, T::Boolean)
@livecheck_defined = T.let(false, T::Boolean)
@on_system_blocks_exist = T.let(false, T::Boolean)
@token = cask.token
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2025-02-24 14:49:33 -08:00
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
# Specifies the cask's name.
#
# NOTE: Multiple names can be specified.
#
# ### Example
#
# ```ruby
# name "Visual Studio Code"
# ```
#
2021-03-31 06:14:41 +02:00
# @api public
2016-09-24 13:52:43 +02:00
def name(*args)
@name ||= []
return @name if args.empty?
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
@name.concat(args.flatten)
end
2016-08-18 22:11:42 +03:00
# Describes the cask.
#
# ### Example
#
# ```ruby
# desc "Open-source code editor"
# ```
#
2021-03-31 06:14:41 +02:00
# @api public
2020-07-29 15:40:31 +01:00
def desc(description = nil)
set_unique_stanza(:desc, description.nil?) { description }
end
def set_unique_stanza(stanza, should_return)
2023-12-18 09:34:01 -08:00
return instance_variable_get(:"@#{stanza}") if should_return
unless @cask.allow_reassignment
2023-12-18 09:34:01 -08:00
if instance_variable_defined?(:"@#{stanza}") && !@called_in_on_system_block
raise CaskInvalidError.new(cask, "'#{stanza}' stanza may only appear once.")
end
2023-12-18 09:34:01 -08:00
if instance_variable_defined?(:"@#{stanza}_set_in_block") && @called_in_on_system_block
raise CaskInvalidError.new(cask, "'#{stanza}' stanza may only be overridden once.")
end
end
2023-12-18 09:34:01 -08:00
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}")
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
# Sets the cask's homepage.
#
# ### Example
#
# ```ruby
# homepage "https://code.visualstudio.com/"
# ```
#
2021-03-31 06:14:41 +02:00
# @api public
2016-09-24 13:52:43 +02:00
def homepage(homepage = nil)
set_unique_stanza(:homepage, homepage.nil?) { homepage }
2016-08-18 22:11:42 +03:00
end
def language(*args, default: false, &block)
if args.empty?
language_eval
2020-11-16 22:18:56 +01:00
elsif block
@language_blocks ||= {}
@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
2016-09-14 23:11:21 +02:00
else
raise CaskInvalidError.new(cask, "No block given to language stanza.")
2016-09-14 23:11:21 +02:00
end
end
2016-09-18 04:15:28 +02:00
def language_eval
return @language_eval if defined?(@language_eval)
2020-12-01 17:04:59 +00:00
return @language_eval = nil if @language_blocks.blank?
raise CaskInvalidError.new(cask, "No default language specified.") if @language_blocks.default.nil?
locales = cask.config.languages
.filter_map do |language|
2020-07-22 00:50:27 +02:00
Locale.parse(language)
rescue Locale::ParserError
nil
end
2018-08-19 22:10:20 +02:00
locales.each do |locale|
key = locale.detect(@language_blocks.keys)
2016-10-03 02:34:32 +02:00
next if key.nil?
return @language_eval = @language_blocks[key].call
2016-10-03 02:34:32 +02:00
end
@language_eval = @language_blocks.default.call
2016-09-18 04:15:28 +02:00
end
def languages
return [] if @language_blocks.nil?
@language_blocks.keys.flatten
end
# Sets the cask's download URL.
#
# ### Example
#
# ```ruby
# url "https://update.code.visualstudio.com/#{version}/#{arch}/stable"
# ```
#
2021-03-31 06:14:41 +02:00
# @api public
def url(*args, **options, &block)
2023-03-12 17:06:29 -07:00
caller_location = T.must(caller_locations).fetch(0)
2021-03-31 06:14:41 +02:00
set_unique_stanza(:url, args.empty? && options.empty? && !block) do
if block
2024-03-07 16:20:20 +00:00
URL.new(*args, **options, caller_location:, dsl: self, &block)
2018-07-30 21:07:36 +02:00
else
2024-03-07 16:20:20 +00:00
URL.new(*args, **options, caller_location:)
end
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
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
# ```
#
2021-03-31 06:14:41 +02:00
# @api public
def container(**kwargs)
set_unique_stanza(:container, kwargs.empty?) do
DSL::Container.new(**kwargs)
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
# Sets the cask's version.
#
# ### Example
#
# ```ruby
# version "1.88.1"
# ```
#
# @see DSL::Version
2021-03-31 06:14:41 +02:00
# @api public
sig { params(arg: T.nilable(T.any(String, Symbol))).returns(T.nilable(DSL::Version)) }
2016-09-24 13:52:43 +02:00
def version(arg = nil)
set_unique_stanza(:version, arg.nil?) do
if !arg.is_a?(String) && arg != :latest
2021-01-26 15:21:24 -05:00
raise CaskInvalidError.new(cask, "invalid 'version' value: #{arg.inspect}")
end
2018-09-17 02:45:00 +02:00
DSL::Version.new(arg)
end
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
# 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"
# ```
#
2021-03-31 06:14:41 +02:00
# @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),
)
2023-03-12 17:06:29 -07:00
case val
2020-11-19 18:12:16 +01:00
when :no_check
2023-03-12 17:06:29 -07:00
val
2020-11-19 18:12:16 +01:00
when String
2023-03-12 17:06:29 -07:00
Checksum.new(val)
2020-11-19 18:12:16 +01:00
else
2023-03-12 17:06:29 -07:00
raise CaskInvalidError.new(cask, "invalid 'sha256' value: #{val.inspect}")
end
end
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
# Sets the cask's architecture strings.
#
# ### Example
#
# ```ruby
# arch arm: "darwin-arm64", intel: "darwin"
# ```
#
2022-08-05 15:51:02 -04:00
# @api public
def arch(arm: nil, intel: nil)
should_return = arm.nil? && intel.nil?
2022-08-05 15:51:02 -04:00
set_unique_stanza(:arch, should_return) do
@on_system_blocks_exist = true
2024-03-07 16:20:20 +00:00
on_arch_conditional(arm:, intel:)
2022-08-05 15:51:02 -04:00
end
end
2025-01-19 16:15:19 +00:00
# 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))
}
2025-01-19 16:15:19 +00:00
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_system_conditional(macos:, linux:)
end
end
# Declare dependencies and requirements for a cask.
#
# NOTE: Multiple dependencies can be specified.
#
2021-03-31 06:14:41 +02:00
# @api public
def depends_on(**kwargs)
2016-09-24 13:52:43 +02:00
@depends_on ||= DSL::DependsOn.new
@depends_on_set_in_block = true if @called_in_on_system_block
return @depends_on if kwargs.empty?
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
begin
@depends_on.load(**kwargs)
2016-09-24 13:52:43 +02:00
rescue RuntimeError => e
raise CaskInvalidError.new(cask, e)
2016-09-24 13:52:43 +02:00
end
@depends_on
2016-08-18 22:11:42 +03:00
end
# @api private
def add_implicit_macos_dependency
return if @depends_on.present? && @depends_on.macos.present?
depends_on macos: ">= :#{MacOSVersion::SYMBOLS.key MacOSVersion::SYMBOLS.values.min}"
end
# Declare conflicts that keep a cask from installing or working correctly.
#
2021-03-31 06:14:41 +02:00
# @api public
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) }
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def artifacts
@artifacts ||= ArtifactSet.new
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2025-03-03 17:55:10 +01:00
sig { returns(Pathname) }
2016-09-24 13:52:43 +02:00
def caskroom_path
cask.caskroom_path
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
# The staged location for this cask, including version number.
#
2021-03-31 06:14:41 +02:00
# @api public
2025-03-03 17:55:10 +01:00
sig { returns(Pathname) }
2016-09-24 13:52:43 +02:00
def staged_path
return @staged_path if @staged_path
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
cask_version = version || :unknown
@staged_path = caskroom_path.join(cask_version.to_s)
end
2016-08-18 22:11:42 +03:00
# Provide the user with cask-specific information at install time.
#
2021-03-31 06:14:41 +02:00
# @api public
def caveats(*strings, &block)
@caveats ||= DSL::Caveats.new(cask)
2020-11-16 22:18:56 +01:00
if block
@caveats.eval_caveats(&block)
elsif strings.any?
strings.each do |string|
@caveats.eval_caveats { string }
end
else
return @caveats.to_s
2016-09-24 13:52:43 +02:00
end
@caveats
2016-08-18 22:11:42 +03:00
end
def discontinued?
odisabled "`discontinued?`", "`deprecated?` or `disabled?`"
@caveats&.discontinued? == true
end
# Asserts that the cask artifacts auto-update.
#
2021-03-31 06:14:41 +02:00
# @api public
2016-09-24 13:52:43 +02:00
def auto_updates(auto_updates = nil)
set_unique_stanza(:auto_updates, auto_updates.nil?) { auto_updates }
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
# Automatically fetch the latest version of a cask from changelogs.
#
2021-03-31 06:14:41 +02:00
# @api public
2020-09-02 12:24:21 -07:00
def livecheck(&block)
2023-04-21 01:21:38 +02:00
@livecheck ||= Livecheck.new(cask)
2020-12-11 16:24:49 +01:00
return @livecheck unless block
2020-09-02 12:24:21 -07:00
if !@cask.allow_reassignment && @livecheck_defined
raise CaskInvalidError.new(cask, "'livecheck' stanza may only appear once.")
end
2020-09-02 12:24:21 -07:00
@livecheck_defined = true
2020-09-02 12:24:21 -07:00
@livecheck.instance_eval(&block)
end
# Whether the cask contains a `livecheck` block. This is a legacy alias
# for `#livecheck_defined?`.
sig { returns(T::Boolean) }
def livecheckable?
# odeprecated "`livecheckable?`", "`livecheck_defined?`"
@livecheck_defined == 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
def deprecate!(date:, because:, replacement: nil, replacement_formula: nil, replacement_cask: nil)
if replacement_formula && replacement_cask
raise ArgumentError, "replacement_formula and replacement_cask specified!"
end
# TODO: deprecate in >= 4.5.0
# if replacement
# odeprecated(
# "deprecate!(:replacement)",
# "deprecate!(:replacement_formula) or deprecate!(:replacement_cask)",
# disable_on: Time.new(2025, 10, 15),
# )
# end
@deprecation_date = Date.parse(date)
return if @deprecation_date > Date.today
@deprecation_reason = because
@deprecation_replacement = replacement
@deprecation_replacement_formula = replacement_formula
@deprecation_replacement_cask = replacement_cask
@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
def disable!(date:, because:, replacement: nil, replacement_formula: nil, replacement_cask: nil)
if replacement_formula && replacement_cask
raise ArgumentError, "replacement_formula and replacement_cask specified!"
end
# TODO: deprecate in >= 4.5.0
# if replacement
# odeprecated(
# "disable!(:replacement)",
# "disable!(:replacement_formula) or disable!(:replacement_cask)",
# disable_on: Time.new(2025, 10, 15),
# )
# end
@disable_date = Date.parse(date)
if @disable_date > Date.today
@deprecation_reason = because
@deprecation_replacement = replacement
@deprecation_replacement_formula = replacement_formula
@deprecation_replacement_cask = replacement_cask
@deprecated = true
return
end
@disable_reason = because
@disable_replacement = replacement
@disable_replacement_formula = replacement_formula
@disable_replacement_cask = replacement_cask
@disabled = true
2020-09-02 12:24:21 -07:00
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.")
2016-09-24 13:52:43 +02:00
end
artifacts.add(klass.from_args(cask, *args, **kwargs))
rescue CaskInvalidError
raise
rescue => e
raise CaskInvalidError.new(cask, "invalid '#{klass.dsl_key}' stanza: #{e}")
2016-08-18 22:11:42 +03:00
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)
2017-10-04 17:08:35 +02:00
artifacts.add(klass.new(cask, dsl_key => block))
end
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def method_missing(method, *)
if method
Utils.method_missing_message(method, token)
nil
else
super
end
2016-09-20 15:11:33 +02:00
end
2021-03-31 06:14:41 +02:00
def respond_to_missing?(*)
true
end
2025-03-03 17:55:10 +01:00
sig { returns(T.nilable(MacOSVersion)) }
def os_version
nil
end
# The directory `app`s are installed into.
#
2021-03-31 06:14:41 +02:00
# @api public
2025-03-03 17:55:10 +01:00
sig { returns(T.any(Pathname, String)) }
2016-09-24 13:52:43 +02:00
def appdir
return HOMEBREW_CASK_APPDIR_PLACEHOLDER if Cask.generating_hash?
cask.config.appdir
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
end