sorbet: add strict type signatures to cask files

Add Sorbet `sig` annotations to the cask-related files that were
already marked `# typed: strict` but lacked complete signatures.

Key changes:
- Add `sig` and `T.let` declarations to cask/cask.rb, cask/dsl.rb,
  cask/cask_loader.rb, cask/installer.rb, cask/exceptions.rb,
  cask/quarantine.rb, and cask/dsl/caveats.rb
- Use `T::Sig::WithoutRuntime.sig` for `DSL#url`, `DSL#set_unique_stanza`,
  and `CaskLoader.{path,load,for}` to avoid Sorbet runtime wrappers
  interfering with `caller_locations` (used by `URL#unversioned?`) and
  RSpec mocking of class methods
- Fix nil guards using raise-unless patterns instead of `T.must`
- Avoid `T.unsafe`; use `T.untyped` only as hash value types
- Fix cascade type errors in callers (audit.rb, info.rb, upgrade.rb,
  livecheck, bump-cask-pr.rb, etc.) that now see explicit nilable types
  on `Cask#tap`, `CaskLoader.load`, and related methods
This commit is contained in:
Douglas Eichelberger
2026-04-10 13:08:44 -07:00
parent 23768ed5e0
commit 6c237beceb
28 changed files with 739 additions and 301 deletions
+2 -1
View File
@@ -70,7 +70,8 @@ module Homebrew
const :auto_updates, T::Boolean, default: false
const :caveats_rosetta, 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 :container_args, { nested: T.nilable(String), type: T.nilable(Symbol) },
default: { nested: nil, type: nil }
const :depends_on_args, DependsOnArgs, default: {}
const :deprecate_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
const :desc, T.nilable(String)
+1 -1
View File
@@ -17,7 +17,7 @@ class String
# @!visibility private
sig { params(config: T.nilable(T::Hash[Symbol, T.untyped])).returns(Cask::Cask) }
def c(config: nil)
Cask::CaskLoader.load(self, config:)
Cask::CaskLoader.load(self, config: Cask::Config.new(**config))
end
end
+9 -11
View File
@@ -501,7 +501,7 @@ module Cask
url = cask.url
return if url.nil?
return if !cask.tap.official? && !signing?
return if !cask.tap&.official? && !signing?
return if cask.deprecated? && cask.deprecation_reason != :fails_gatekeeper_check
unless Quarantine.available?
@@ -575,7 +575,7 @@ module Cask
#{result.merged_output}
EOS
if cask.tap.official?
if cask.tap&.official?
signing_failure_message += <<~EOS
The homebrew/cask tap requires all casks to be signed and notarized by Apple.
Please contact the upstream developer and ask them to sign and notarize their software.
@@ -1165,7 +1165,7 @@ module Cask
sig { void }
def audit_conflicts_with
return if !cask.tap.official? || cask.conflicts_with.nil?
return if !cask.tap&.official? || cask.conflicts_with.nil?
Homebrew.with_no_api_env do
nonexisting_conflicting_casks = cask.conflicts_with.fetch(:cask, Set.new) - core_cask_tokens
@@ -1177,8 +1177,7 @@ module Cask
sig { void }
def audit_denylist
return unless cask.tap
return unless cask.tap.official?
return unless cask.tap&.official?
return unless (reason = Denylist.reason(cask.token))
add_error "#{cask.token} is not allowed: #{reason}"
@@ -1187,9 +1186,8 @@ module Cask
sig { void }
def audit_reverse_migration
return unless new_cask?
return unless cask.tap
return unless cask.tap.official?
return unless cask.tap.tap_migrations.key?(cask.token)
return unless cask.tap&.official?
return unless cask.tap&.tap_migrations&.key?(cask.token)
add_error "#{cask.token} is listed in tap_migrations.json"
end
@@ -1254,11 +1252,11 @@ module Cask
sig { void }
def audit_cask_path
return unless cask.tap.core_cask_tap?
return unless (tap = cask.tap)&.core_cask_tap?
expected_path = cask.tap.new_cask_path(cask.token)
expected_path = tap.new_cask_path(cask.token)
return if cask.sourcefile_path.to_s.end_with?(expected_path)
return if cask.sourcefile_path.to_s.end_with?(expected_path.to_s)
add_error "Cask should be located in '#{expected_path}'"
end
+7 -5
View File
@@ -116,12 +116,14 @@ module Cask
sig { params(languages: T::Array[String]).returns(::Cask::Audit) }
def audit_languages(languages)
original_config = cask.config
localized_config = original_config.merge(Config.new(explicit: { languages: }))
cask.config = localized_config
begin
localized_config = original_config.merge(Config.new(explicit: { languages: }))
cask.config = localized_config
audit_cask_instance(cask)
ensure
cask.config = original_config
audit_cask_instance(cask)
ensure
cask.config = original_config
end
end
sig { params(cask: ::Cask::Cask).returns(::Cask::Audit) }
+137 -51
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "bundle_version"
@@ -21,16 +21,34 @@ module Cask
# The token of this {Cask}.
#
# @api internal
sig { returns(String) }
attr_reader :token
# The configuration of this {Cask}.
#
# @api internal
sig { returns(Config) }
attr_reader :config
attr_reader :sourcefile_path, :source, :default_config, :loader
attr_accessor :download, :allow_reassignment
sig { returns(T.nilable(Pathname)) }
attr_reader :sourcefile_path
sig { returns(T.nilable(String)) }
attr_reader :source
sig { returns(Config) }
attr_reader :default_config
sig { returns(T.nilable(CaskLoader::ILoader)) }
attr_reader :loader
sig { returns(T.nilable(Pathname)) }
attr_accessor :download
sig { returns(T::Boolean) }
attr_accessor :allow_reassignment
sig { params(eval_all: T::Boolean).returns(T::Array[Cask]) }
def self.all(eval_all: false)
if !eval_all && !Homebrew::EnvConfig.eval_all?
raise ArgumentError, "Cask::Cask#all cannot be used without `--eval-all` or `HOMEBREW_EVAL_ALL=1`"
@@ -48,8 +66,15 @@ module Cask
end
end
def tap
return super if block_given? # Object#tap
# This collides with Kernel#tap, complicating the type signature.
# Overload sigs are not supported by Sorbet, otherwise we would use:
# sig { params(blk: T.proc.params(arg0: Cask).void).returns(T.self_type) }
# sig { params(blk: NilClass).returns(T.nilable(Tap)) }
# Using a union type would require casts or type guards at call sites,
# so T.untyped is used as the return type instead.
sig { params(blk: T.nilable(T.proc.params(arg0: Cask).void)).returns(T.untyped) }
def tap(&blk)
return super if block_given? # Kernel#tap
@tap
end
@@ -83,15 +108,16 @@ module Cask
@loader = loader
# Sorbet has trouble with bound procs assigned to instance variables:
# https://github.com/sorbet/sorbet/issues/6843
instance_variable_set(:@block, block)
@block = T.let(block, T.untyped)
@default_config = config || Config.new
@default_config = T.let(config || Config.new, Config)
self.config = if config_path.exist?
Config.from_json(File.read(config_path), ignore_invalid_keys: true)
else
@default_config
end
@config = T.let(if config_path.exist?
Config.from_json(File.read(config_path), ignore_invalid_keys: true)
else
@default_config
end, Config)
refresh
end
sig { returns(T::Boolean) }
@@ -106,28 +132,33 @@ module Cask
# An old name for the cask.
sig { returns(T::Array[String]) }
def old_tokens
@old_tokens ||= if (tap = self.tap)
Tap.tap_migration_oldnames(tap, token) +
tap.cask_reverse_renames.fetch(token, [])
else
[]
end
@old_tokens ||= T.let(
if (t = tap)
Tap.tap_migration_oldnames(t, token) +
t.cask_reverse_renames.fetch(token, [])
else
[]
end,
T.nilable(T::Array[String]),
)
end
sig { params(config: Config).void }
def config=(config)
@config = config
refresh
end
sig { void }
def refresh
@dsl = DSL.new(self)
@dsl = T.let(DSL.new(self), T.nilable(DSL))
@contains_os_specific_artifacts = nil
return unless @block
@dsl.instance_eval(&@block)
@dsl.add_implicit_macos_dependency
@dsl.language_eval
dsl!.instance_eval(&@block)
dsl!.add_implicit_macos_dependency
dsl!.language_eval
rescue NoMethodError => e
raise CaskInvalidError.new(token, e.message), e.backtrace
end
@@ -135,7 +166,7 @@ module Cask
def_delegators :@dsl, *::Cask::DSL::DSL_METHODS
sig { returns(DSL::Caveats) }
def caveats_object = @dsl.caveats_object
def caveats_object = dsl!.caveats_object
sig { params(caskroom_path: Pathname).returns(T::Array[[String, String]]) }
def timestamped_versions(caskroom_path: self.caskroom_path)
@@ -150,16 +181,18 @@ module Cask
# The fully-qualified token of this {Cask}.
#
# @api internal
sig { returns(String) }
def full_token
return token if tap.nil?
return token if tap.core_cask_tap?
return token if (t = tap).nil?
return token if t.core_cask_tap?
"#{tap.name}/#{token}"
"#{t.name}/#{token}"
end
# Alias for {#full_token}.
#
# @api internal
sig { returns(String) }
def full_name = full_token
sig { returns(T::Boolean) }
@@ -178,12 +211,12 @@ module Cask
# Cache the os value before contains_os_specific_artifacts? refreshes the cask
# (the refresh clears @dsl.os in generic/non-OS-specific contexts)
os_value = @dsl.os
os_value = dsl!.os
return false if contains_os_specific_artifacts?
# Casks with OS-specific blocks rely on the os stanza for Linux support
return os_value.present? if @dsl.on_os_blocks_exist?
return os_value.present? if dsl!.on_os_blocks_exist?
# Platform-agnostic casks: reject macOS-only artifacts and manual installers
artifacts.none? do |a|
@@ -194,7 +227,7 @@ module Cask
sig { returns(T::Boolean) }
def contains_os_specific_artifacts?
return false unless @dsl.on_system_blocks_exist?
return false unless @dsl&.on_system_blocks_exist?
return @contains_os_specific_artifacts unless @contains_os_specific_artifacts.nil?
@@ -221,10 +254,12 @@ module Cask
# The caskfile is needed during installation when there are
# `*flight` blocks or the cask has multiple languages
sig { returns(T::Boolean) }
def caskfile_only?
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
end
sig { returns(T::Boolean) }
def uninstall_flight_blocks?
artifacts.any? do |artifact|
case artifact
@@ -287,33 +322,42 @@ module Cask
bundle_version&.version
end
sig { returns(Tab) }
def tab
Tab.for_cask(self)
end
sig { returns(Pathname) }
def config_path
metadata_main_container_path/"config.json"
end
sig { returns(T::Boolean) }
def checksumable?
return false if (url = self.url).nil?
DownloadStrategyDetector.detect(url.to_s, url.using) <= AbstractFileDownloadStrategy
DownloadStrategyDetector.detect(url.to_s, url.using) <= AbstractFileDownloadStrategy || false
end
sig { returns(Pathname) }
def download_sha_path
metadata_main_container_path/"LATEST_DOWNLOAD_SHA256"
end
sig { returns(String) }
def new_download_sha
require "cask/installer"
# Call checksumable? before hashing
@new_download_sha ||= Installer.new(self, verify_download_integrity: false)
.download(quiet: true)
.instance_eval { |x| Digest::SHA256.file(x).hexdigest }
@new_download_sha ||= T.let(
Installer.new(self, verify_download_integrity: false)
.download(quiet: true)
.instance_eval { |x| Digest::SHA256.file(x).hexdigest },
T.nilable(String),
)
end
sig { returns(T::Boolean) }
def outdated_download_sha?
return true unless checksumable?
@@ -323,17 +367,25 @@ module Cask
sig { returns(Pathname) }
def caskroom_path
@caskroom_path ||= Caskroom.path.join(token)
@caskroom_path ||= T.let(Caskroom.path.join(token), T.nilable(Pathname))
end
# Check if the installed cask is outdated.
#
# @api internal
sig {
params(greedy: T::Boolean, greedy_latest: T.nilable(T::Boolean), greedy_auto_updates: T.nilable(T::Boolean))
.returns(T::Boolean)
}
def outdated?(greedy: false, greedy_latest: false, greedy_auto_updates: false)
!outdated_version(greedy:, greedy_latest:,
greedy_auto_updates:).nil?
end
sig {
params(greedy: T::Boolean, greedy_latest: T.nilable(T::Boolean), greedy_auto_updates: T.nilable(T::Boolean))
.returns(T.nilable(String))
}
def outdated_version(greedy: false, greedy_latest: false, greedy_auto_updates: false)
# special case: tap version is not available
return if version.nil?
@@ -355,6 +407,15 @@ module Cask
installed_version
end
sig {
params(
greedy: T::Boolean,
verbose: T::Boolean,
json: T::Boolean,
greedy_latest: T::Boolean,
greedy_auto_updates: T::Boolean,
).returns(T.any(String, T::Hash[Symbol, T.untyped]))
}
def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates)
return token if !verbose && !json
@@ -372,28 +433,37 @@ module Cask
end
end
sig { returns(T.nilable(String)) }
def ruby_source_path
return @ruby_source_path if defined?(@ruby_source_path)
return unless sourcefile_path
return unless tap
return unless (sfp = sourcefile_path)
return unless (t = tap)
@ruby_source_path = sourcefile_path.relative_path_from(tap.path)
@ruby_source_path = T.let(sfp.relative_path_from(t.path).to_s, T.nilable(String))
end
sig { returns(T::Hash[Symbol, String]) }
sig { returns(T::Hash[Symbol, T.nilable(String)]) }
def ruby_source_checksum
@ruby_source_checksum ||= {
sha256: Digest::SHA256.file(sourcefile_path).hexdigest,
}.freeze
@ruby_source_checksum ||= T.let(
begin
sfp = sourcefile_path
{
sha256: sfp ? Digest::SHA256.file(sfp).hexdigest : nil,
}.freeze
end,
T.nilable(T::Hash[Symbol, T.nilable(String)]),
)
end
sig { returns(T::Array[String]) }
def languages
@languages ||= @dsl.languages
@languages ||= T.let(dsl!.languages, T.nilable(T::Array[String]))
end
sig { returns(T.nilable(String)) }
def tap_git_head
@tap_git_head ||= tap&.git_head
@tap_git_head ||= T.let(tap&.git_head, T.nilable(String))
rescue TapUnavailableError
nil
end
@@ -417,15 +487,18 @@ module Cask
"#<Cask #{token}#{sourcefile_path&.to_s&.prepend(" ")}>"
end
sig { returns(Integer) }
def hash
token.hash
end
sig { params(other: T.untyped).returns(T::Boolean) }
def eql?(other)
instance_of?(other.class) && token == other.token
end
alias == eql?
sig { returns(T::Hash[String, T.untyped]) }
def to_h
{
"token" => token,
@@ -474,9 +547,10 @@ module Cask
}
end
HASH_KEYS_TO_SKIP = %w[outdated installed versions].freeze
HASH_KEYS_TO_SKIP = T.let(%w[outdated installed versions].freeze, T::Array[String])
private_constant :HASH_KEYS_TO_SKIP
sig { returns(T::Hash[String, T.untyped]) }
def to_hash_with_variations
if loaded_from_internal_api?
raise UsageError, "Cannot call #to_hash_with_variations on casks loaded from the internal API"
@@ -489,13 +563,13 @@ module Cask
hash = to_h
variations = {}
if @dsl.on_system_blocks_exist?
if dsl!.on_system_blocks_exist?
begin
OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag|
next if bottle_tag.linux? && @dsl.os.nil?
next if bottle_tag.linux? && dsl!.os.nil?
next if bottle_tag.macos? &&
depends_on.macos &&
!@dsl.depends_on_set_in_block? &&
!dsl!.depends_on_set_in_block? &&
!depends_on.macos.allows?(bottle_tag.to_macos_version)
Homebrew::SimulateSystem.with_tag(bottle_tag) do
@@ -519,6 +593,7 @@ module Cask
hash
end
sig { params(uninstall_only: T::Boolean).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
def artifacts_list(uninstall_only: false)
artifacts.filter_map do |artifact|
case artifact
@@ -539,6 +614,7 @@ module Cask
end
end
sig { params(uninstall_only: T::Boolean).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
def rename_list(uninstall_only: false)
rename.filter_map do |rename|
{ from: rename.from, to: rename.to }
@@ -559,10 +635,18 @@ module Cask
sig { returns(T.nilable(Homebrew::BundleVersion)) }
def bundle_version
@bundle_version ||= if (bundle = artifacts.find { |a| a.is_a?(Artifact::App) }&.target) &&
(plist = Pathname("#{bundle}/Contents/Info.plist")) && plist.exist? && plist.readable?
Homebrew::BundleVersion.from_info_plist(plist)
end
@bundle_version ||= T.let(
if (bundle = artifacts.find { |a| a.is_a?(Artifact::App) }&.target) &&
(plist = Pathname("#{bundle}/Contents/Info.plist")) && plist.exist? && plist.readable?
Homebrew::BundleVersion.from_info_plist(plist)
end,
T.nilable(Homebrew::BundleVersion),
)
end
sig { returns(DSL) }
def dsl!
@dsl || raise("unexpected nil @dsl")
end
sig { returns(T.nilable(Artifact::App)) }
@@ -622,6 +706,7 @@ module Cask
build_comparisons.include?(-1)
end
sig { params(hash: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
def api_to_local_hash(hash)
hash["token"] = token
hash["installed"] = installed_version
@@ -629,6 +714,7 @@ module Cask
hash
end
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
def url_specs
url&.specs.dup.tap do |url_specs|
case url_specs&.dig(:user_agent)
+114 -43
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "cask/cache"
@@ -39,9 +39,16 @@ module Cask
sig { returns(String) }
attr_reader :content
sig { returns(T.nilable(Tap)) }
sig { overridable.returns(T.nilable(Tap)) }
attr_reader :tap
sig { void }
def initialize
@content = T.let("", String)
@tap = T.let(nil, T.nilable(Tap))
@config = T.let(nil, T.nilable(Config))
end
private
sig {
@@ -68,12 +75,15 @@ module Cask
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
@regex ||= T.let(
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,
T.nilable(Regexp),
)
return unless content.match?(@regex)
@@ -84,10 +94,11 @@ module Cask
def initialize(content, tap: T.unsafe(nil))
super()
@content = content.dup.force_encoding("UTF-8")
@tap = tap
@content = T.let(content.dup.force_encoding("UTF-8"), String)
@tap = T.let(tap, T.nilable(Tap))
end
sig { override.params(config: T.nilable(Config)).returns(Cask) }
def load(config:)
@config = config
@@ -122,11 +133,15 @@ module Cask
def self.invalid_path?(pathname, valid_extnames: %w[.rb .json])
return true if valid_extnames.exclude?(pathname.extname)
@invalid_basenames ||= %w[INSTALL_RECEIPT.json sbom.spdx.json].freeze
@invalid_basenames ||= T.let(%w[INSTALL_RECEIPT.json sbom.spdx.json].freeze, T.nilable(T::Array[String]))
@invalid_basenames.include?(pathname.basename.to_s)
end
attr_reader :token, :path
sig { returns(String) }
attr_reader :token
sig { returns(Pathname) }
attr_reader :path
sig { params(path: T.any(Pathname, String), token: String).void }
def initialize(path, token: T.unsafe(nil))
@@ -134,10 +149,10 @@ module Cask
path = Pathname(path).expand_path
@token = path.basename(path.extname).basename(".internal").to_s
@path = path
@tap = Tap.from_path(path) || Homebrew::API.tap_from_source_download(path)
@from_installed_caskfile = false
@token = T.let(path.basename(path.extname).basename(".internal").to_s, String)
@path = T.let(path, Pathname)
@tap = T.let(Tap.from_path(path) || Homebrew::API.tap_from_source_download(path), T.nilable(Tap))
@from_installed_caskfile = T.let(false, T::Boolean)
end
sig { override.params(config: T.nilable(Config)).returns(Cask) }
@@ -172,7 +187,7 @@ module Cask
end
begin
instance_eval(content, path).tap do |cask|
instance_eval(content, path.to_s).tap do |cask|
raise CaskUnreadableError.new(token, "'#{path}' does not contain a cask.") unless cask.is_a?(Cask)
end
rescue NameError, ArgumentError, ScriptError => e
@@ -192,6 +207,13 @@ module Cask
private
sig {
override.params(
header_token: String,
options: T.untyped,
block: T.nilable(T.proc.bind(DSL).void),
).returns(Cask)
}
def cask(header_token, **options, &block)
raise CaskTokenMismatchError.new(token, header_token) if token != header_token
@@ -209,10 +231,13 @@ module Cask
return if Homebrew::EnvConfig.forbid_packages_from_paths?
# Cache compiled regex
@uri_regex ||= begin
uri_regex = ::URI::RFC2396_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end
@uri_regex ||= T.let(
begin
uri_regex = ::URI::RFC2396_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end,
T.nilable(Regexp),
)
uri = ref.to_s
return unless uri.match?(@uri_regex)
@@ -223,15 +248,23 @@ module Cask
new(uri)
end
attr_reader :url, :name
sig { returns(URI::Generic) }
attr_reader :url
sig { returns(String) }
attr_reader :name
sig { params(url: T.any(URI::Generic, String)).void }
def initialize(url)
@url = URI(url)
@name = File.basename(T.must(@url.path))
@url = T.let(URI(url), URI::Generic)
url_path = @url.path
raise "unexpected nil url.path" unless url_path
@name = T.let(File.basename(url_path), String)
super Cache.path/name
end
sig { override.params(config: T.nilable(Config)).returns(Cask) }
def load(config:)
path.dirname.mkpath
@@ -255,7 +288,7 @@ module Cask
# Loads a cask from a specific tap.
class FromTapLoader < FromPathLoader
sig { returns(Tap) }
sig { override.returns(Tap) }
attr_reader :tap
sig {
@@ -279,14 +312,18 @@ module Cask
sig { params(tapped_token: String).void }
def initialize(tapped_token)
tap, token = Tap.with_cask_token(tapped_token)
tap_with_token = Tap.with_cask_token(tapped_token)
raise "unexpected nil Tap.with_cask_token" unless tap_with_token
tap, token = tap_with_token
cask = CaskLoader.find_cask_in_tap(token, tap)
super cask
@tap = T.let(tap, Tap)
end
sig { override.params(config: T.nilable(Config)).returns(Cask) }
def load(config:)
raise TapCaskUnavailableError.new(tap, token) unless T.must(tap).installed?
raise TapCaskUnavailableError.new(tap, token) unless tap.installed?
super
end
@@ -298,7 +335,7 @@ module Cask
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
.returns(T.nilable(FromInstanceLoader))
}
def self.try_new(ref, warn: false)
new(ref) if ref.is_a?(Cask)
@@ -309,6 +346,8 @@ module Cask
@cask = cask
end
# This is a false positive incompatibililty warning, due to Kernel#load being overridden.
sig { override(allow_incompatible: true).params(config: T.nilable(Config)).returns(Cask) } # rubocop:disable Sorbet/AllowIncompatibleOverride
def load(config:)
@cask
end
@@ -329,7 +368,7 @@ module Cask
sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
.returns(T.nilable(FromAPILoader))
}
def self.try_new(ref, warn: false)
return if Homebrew::EnvConfig.no_install_from_api?
@@ -357,20 +396,25 @@ module Cask
}
def initialize(token, from_json: T.unsafe(nil), path: nil, from_installed_caskfile: false,
from_internal_json: false)
@token = token.sub(%r{^homebrew/(?:homebrew-)?cask/}i, "")
@sourcefile_path = if path
path
elsif from_json
from_internal_json ? Homebrew::API::Internal.cached_cask_json_file_path : Homebrew::API::Cask.cached_json_file_path
else
Homebrew::API.cached_cask_json_file_path
end
@path = path || CaskLoader.default_path(@token)
@token = T.let(token.sub(%r{^homebrew/(?:homebrew-)?cask/}i, ""), String)
@sourcefile_path = T.let(
if path
path
elsif from_json
from_internal_json ? Homebrew::API::Internal.cached_cask_json_file_path : Homebrew::API::Cask.cached_json_file_path
else
Homebrew::API.cached_cask_json_file_path
end,
Pathname,
)
@path = T.let(path || CaskLoader.default_path(@token), Pathname)
@from_json = from_json
@from_installed_caskfile = from_installed_caskfile
@from_internal_json = from_internal_json
end
# This is a false positive incompatibililty warning, due to Kernel#load being overridden.
sig { override(allow_incompatible: true).params(config: T.nilable(Config)).returns(Cask) } # rubocop:disable Sorbet/AllowIncompatibleOverride
def load(config:)
if (api_source = from_json)
if @from_internal_json
@@ -387,6 +431,7 @@ module Cask
private
sig { params(config: T.nilable(Config)).returns(Cask) }
def load_from_api(config:)
api_source = Homebrew::API::Cask.all_casks.fetch(token)
tap_git_head = api_source["tap_git_head"]
@@ -397,6 +442,7 @@ module Cask
load_from_struct(config:, cask_struct:, api_source:, tap_git_head:)
end
sig { params(config: T.nilable(Config)).returns(Cask) }
def load_from_internal_api(config:)
cask_struct = Homebrew::API::Internal.cask_struct(token)
api_source = Homebrew::API::Internal.cask_hashes.fetch(token)
@@ -405,6 +451,7 @@ module Cask
load_from_struct(config:, cask_struct:, api_source:, tap_git_head:, internal_api: true)
end
sig { params(config: T.nilable(Config), api_source: T::Hash[String, T.untyped]).returns(Cask) }
def load_from_json(config:, api_source:)
tap_git_head = api_source["tap_git_head"]
cask_struct = Homebrew::API::Cask::CaskStructGenerator.generate_cask_struct_hash(
@@ -414,6 +461,7 @@ module Cask
load_from_struct(config:, cask_struct:, api_source:, tap_git_head:)
end
sig { params(config: T.nilable(Config), api_source: T::Hash[String, T.untyped]).returns(Cask) }
def load_from_internal_json(config:, api_source:)
api_source = api_source.dup
tap_git_head = api_source.delete("tap_git_head")
@@ -477,13 +525,15 @@ module Cask
end
end
container(**cask_struct.container_args) if cask_struct.container?
if cask_struct.container?
container(nested: cask_struct.container_args[:nested], type: cask_struct.container_args[:type])
end
cask_struct.artifacts(appdir:).each do |key, args, kwargs, block|
send(key, *args, **kwargs, &block)
end
caveats cask_struct.caveats(appdir:) if cask_struct.caveats?
caveats T.must(cask_struct.caveats(appdir:)) if cask_struct.caveats?
if cask_struct.caveats_rosetta
caveats do
@@ -556,7 +606,7 @@ module Cask
installed_tap = Cask.new(@token).tab.tap
@tap = installed_tap if installed_tap
@from_installed_caskfile = true
@from_installed_caskfile = T.let(true, T::Boolean)
end
end
@@ -579,17 +629,28 @@ module Cask
super CaskLoader.default_path(token)
end
sig { override.params(config: T.nilable(Config)).returns(Cask) }
def load(config:)
raise CaskUnavailableError.new(token, "No Cask with this name exists.")
end
end
# NOTE: Using `WithoutRuntime` to avoid Sorbet wrapping this method,
# which would interfere with RSpec mocking of this class method.
T::Sig::WithoutRuntime.sig { params(ref: T.any(String, Pathname, Cask, URI::Generic)).returns(Pathname) }
def self.path(ref)
self.for(ref, need_path: true).path
T.cast(self.for(ref, need_path: true), T.any(FromAPILoader, FromPathLoader)).path
end
# NOTE: Using `WithoutRuntime` to avoid Sorbet wrapping this method,
# which would interfere with RSpec mocking of this class method.
T::Sig::WithoutRuntime.sig {
params(ref: T.any(String, Symbol, Pathname, Cask, URI::Generic), config: T.nilable(Config),
warn: T::Boolean).returns(Cask)
}
def self.load(ref, config: nil, warn: true)
self.for(ref, warn:).load(config:)
normalized_ref = ref.is_a?(Symbol) ? ref.to_s : ref
self.for(normalized_ref, warn:).load(config:)
end
sig { params(tapped_token: String, warn: T::Boolean).returns(T.nilable([String, Tap, T.nilable(Symbol)])) }
@@ -633,6 +694,12 @@ module Cask
[token, tap, type]
end
# NOTE: Using `WithoutRuntime` to avoid Sorbet wrapping this method,
# which would interfere with RSpec mocking of this class method.
T::Sig::WithoutRuntime.sig {
params(ref: T.any(String, Pathname, Cask, URI::Generic), need_path: T::Boolean, warn: T::Boolean)
.returns(ILoader)
}
def self.for(ref, need_path: false, warn: true)
[
FromInstanceLoader,
@@ -650,6 +717,8 @@ module Cask
return loader
end
end
raise CaskError, "No cask loader found for #{ref.inspect}"
end
sig { params(ref: String, config: T.nilable(Config), warn: T::Boolean).returns(Cask) }
@@ -678,10 +747,12 @@ module Cask
loader.load(config:)
end
sig { params(token: T.any(String, Symbol)).returns(Pathname) }
def self.default_path(token)
find_cask_in_tap(token.to_s.downcase, CoreCaskTap.instance)
end
sig { params(token: String, tap: Tap).returns(Pathname) }
def self.find_cask_in_tap(token, tap)
filename = "#{token}.rb"
+123 -21
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "autobump_constants"
@@ -65,14 +65,17 @@ module Cask
Artifact::Zap,
].freeze
ACTIVATABLE_ARTIFACT_CLASSES = (ORDINARY_ARTIFACT_CLASSES - [Artifact::StageOnly]).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
DSL_METHODS = Set.new([
DSL_METHODS = T.let(Set.new([
:arch,
:artifacts,
:auto_updates,
@@ -117,14 +120,54 @@ module Cask
*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
]).freeze, T::Set[Symbol])
include OnSystem::MacOSAndLinux
attr_reader :cask, :token, :no_autobump_message, :artifacts, :deprecation_date, :deprecation_reason,
:deprecation_replacement_cask, :deprecation_replacement_formula, :deprecate_args,
:disable_date, :disable_reason, :disable_replacement_cask,
:disable_replacement_formula, :disable_args, :on_system_block_min_os
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(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)
@@ -167,6 +210,7 @@ module Cask
@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))
@@ -212,6 +256,7 @@ module Cask
# ```
#
# @api public
sig { params(args: T.any(String, T::Array[String])).returns(T::Array[String]) }
def name(*args)
return @name if args.empty?
@@ -227,11 +272,21 @@ module Cask
# ```
#
# @api public
sig { params(description: T.nilable(String)).returns(T.nilable(String)) }
def desc(description = nil)
set_unique_stanza(:desc, description.nil?) { description }
end
def set_unique_stanza(stanza, should_return)
# 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
@@ -261,10 +316,18 @@ module Cask
# ```
#
# @api public
sig { params(homepage: T.nilable(String)).returns(T.nilable(String)) }
def homepage(homepage = nil)
set_unique_stanza(:homepage, homepage.nil?) { homepage }
end
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
@@ -278,11 +341,13 @@ module Cask
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?
@@ -300,7 +365,7 @@ module Cask
end
locales.each do |locale|
key = locale.detect(@language_blocks.keys)
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
@@ -309,6 +374,7 @@ module Cask
@language_eval = language_blocks_default.call
end
sig { returns(T::Array[String]) }
def languages
@language_blocks.keys.flatten
end
@@ -322,11 +388,13 @@ module Cask
# ```
#
# @api public
def url(*args, **options)
caller_location = T.must(caller_locations).fetch(0)
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
set_unique_stanza(:url, args.empty? && options.empty?) do
URL.new(*args, **options, caller_location:)
set_unique_stanza(:url, false) do
URL.new(uri, **options, caller_location:)
end
end
@@ -347,9 +415,10 @@ module Cask
# ```
#
# @api public
def container(**kwargs)
set_unique_stanza(:container, kwargs.empty?) do
DSL::Container.new(**kwargs)
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
@@ -372,7 +441,7 @@ module Cask
def rename(from = T.unsafe(nil), to = T.unsafe(nil))
return @rename if from.nil?
@rename << DSL::Rename.new(T.must(from), T.must(to))
@rename << DSL::Rename.new(from, to)
end
# Sets the cask's version.
@@ -443,7 +512,7 @@ module Cask
)
case val
when :no_check
val
:no_check
when String
Checksum.new(val)
else
@@ -461,6 +530,7 @@ module Cask
# ```
#
# @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?
@@ -502,6 +572,7 @@ module Cask
# NOTE: Multiple dependencies can be specified.
#
# @api public
sig { params(kwargs: T.untyped).returns(DSL::DependsOn) }
def depends_on(**kwargs)
@depends_on_set_in_block = true if @called_in_on_system_block
return @depends_on if kwargs.empty?
@@ -515,6 +586,7 @@ module Cask
end
# @api private
sig { void }
def add_implicit_macos_dependency
return if (cask_depends_on = @depends_on).present? && cask_depends_on.macos.present?
@@ -524,6 +596,7 @@ module Cask
# 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) }
@@ -548,6 +621,12 @@ module Cask
# 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)
@@ -567,6 +646,7 @@ module Cask
# 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
@@ -574,6 +654,7 @@ module Cask
# Automatically fetch the latest version of a cask from changelogs.
#
# @api public
sig { params(block: T.nilable(T.proc.void)).returns(Livecheck) }
def livecheck(&block)
return @livecheck unless block
@@ -611,6 +692,7 @@ module Cask
end
# Is the cask in autobump list?
sig { returns(T::Boolean) }
def autobump?
@autobump == true
end
@@ -620,6 +702,15 @@ module Cask
# 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!"
@@ -648,6 +739,15 @@ module Cask
# 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!"
@@ -706,11 +806,13 @@ module Cask
end
end
def method_missing(method, *)
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
def respond_to_missing?(*)
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
+32 -18
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Cask
@@ -26,12 +26,16 @@ module Cask
@invoked_caveats = T.let(Set.new, T::Set[Symbol])
end
sig {
params(name: Symbol, block: T.proc.bind(Caveats).void).void
}
def self.caveat(name, &block)
define_method(name) do |*args|
T.bind(self, Caveats)
key = [name, *args]
@invoked_caveats.add(name)
invoked_caveats.add(name)
text = instance_exec(*args, &block)
@built_in_caveats[key] = text if text
built_in_caveats[key] = text if text
:built_in_caveat
end
end
@@ -89,7 +93,7 @@ module Cask
end
<<~EOS
#{@cask} requires a kernel extension to work.
#{cask} requires a kernel extension to work.
If the installation fails, retry after you enable it in:
#{navigation_path}
@@ -108,18 +112,18 @@ module Cask
end
<<~EOS
#{@cask} is not signed and requires Accessibility access,
#{cask} is not signed and requires Accessibility access,
so you will need to re-grant Accessibility access every time the app is updated.
Enable or re-enable it in:
#{navigation_path} → #{access}
To re-enable, untick and retick #{@cask}.app.
To re-enable, untick and retick #{cask}.app.
EOS
end
caveat :path_environment_variable do |path|
<<~EOS
To use #{@cask}, you may need to add the #{path} directory
To use #{cask}, you may need to add the #{path} directory
to your PATH environment variable, e.g. (for Bash shell):
export PATH=#{path}:"$PATH"
EOS
@@ -127,7 +131,7 @@ module Cask
caveat :zsh_path_helper do |path|
<<~EOS
To use #{@cask}, zsh users may need to add the following line to their
To use #{cask}, zsh users may need to add the following line to their
~/.zprofile. (Among other effects, #{path} will be added to the
PATH environment variable):
eval `/usr/libexec/path_helper -s`
@@ -138,7 +142,7 @@ module Cask
next unless HOMEBREW_PREFIX.to_s.downcase.start_with?("/usr/local")
<<~EOS
Cask #{@cask} installs files under /usr/local. The presence of such
Cask #{cask} installs files under /usr/local. The presence of such
files can cause warnings when running `brew doctor`, which is considered
to be a bug in Homebrew Cask.
EOS
@@ -147,17 +151,17 @@ module Cask
caveat :depends_on_java do |java_version = :any|
if java_version == :any
<<~EOS
#{@cask} requires Java. You can install the latest version with:
#{cask} requires Java. You can install the latest version with:
brew install --cask temurin
EOS
elsif java_version.include?("+")
elsif java_version.to_s.include?("+")
<<~EOS
#{@cask} requires Java #{java_version}. You can install the latest version with:
#{cask} requires Java #{java_version}. You can install the latest version with:
brew install --cask temurin
EOS
else
<<~EOS
#{@cask} requires Java #{java_version}. You can install it with:
#{cask} requires Java #{java_version}. You can install it with:
brew install --cask temurin@#{java_version}
EOS
end
@@ -167,7 +171,7 @@ module Cask
next if Homebrew::SimulateSystem.current_arch != :arm
<<~EOS
#{@cask} is built for Intel macOS and so requires Rosetta 2 to be installed.
#{cask} is built for Intel macOS and so requires Rosetta 2 to be installed.
You can install Rosetta 2 with:
softwareupdate --install-rosetta --agree-to-license
Note that it is very difficult to remove Rosetta 2 once it is installed.
@@ -176,29 +180,39 @@ module Cask
caveat :logout do
<<~EOS
You must log out and log back in for the installation of #{@cask} to take effect.
You must log out and log back in for the installation of #{cask} to take effect.
EOS
end
caveat :reboot do
<<~EOS
You must reboot for the installation of #{@cask} to take effect.
You must reboot for the installation of #{cask} to take effect.
EOS
end
caveat :license do |web_page|
<<~EOS
Installing #{@cask} means you have AGREED to the license at:
Installing #{cask} means you have AGREED to the license at:
#{Formatter.url(web_page.to_s)}
EOS
end
caveat :free_license do |web_page|
<<~EOS
The vendor offers a free license for #{@cask} at:
The vendor offers a free license for #{cask} at:
#{Formatter.url(web_page.to_s)}
EOS
end
private
# These attrs are required as a workaround for https://github.com/sorbet/sorbet/issues/8106
sig { returns(T::Set[Symbol]) }
attr_reader :invoked_caveats
sig { returns(T::Hash[T::Array[Symbol], String]) }
attr_reader :built_in_caveats
end
end
end
+20 -5
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Cask
@@ -7,10 +7,11 @@ module Cask
# Cask error containing multiple other errors.
class MultipleCaskErrors < CaskError
sig { params(errors: T::Array[StandardError]).void }
def initialize(errors)
super()
@errors = errors
@errors = T.let(errors, T::Array[StandardError])
end
sig { returns(String) }
@@ -30,11 +31,12 @@ module Cask
sig { returns(String) }
attr_reader :reason
sig { params(token: T.any(String, Symbol, Cask), reason: T.nilable(Object)).void }
def initialize(token, reason = nil)
super()
@token = token.to_s
@reason = reason.to_s
@token = T.let(token.to_s, String)
@reason = T.let(reason.to_s, String)
end
end
@@ -48,8 +50,10 @@ module Cask
# Error when a cask cannot be installed.
class CaskCannotBeInstalledError < AbstractCaskErrorWithToken
sig { returns(String) }
attr_reader :message
sig { params(token: T.any(String, Symbol, Cask), message: String).void }
def initialize(token, message)
super(token)
@message = message
@@ -63,8 +67,10 @@ module Cask
# Error when a cask conflicts with another cask.
class CaskConflictError < AbstractCaskErrorWithToken
sig { returns(Cask) }
attr_reader :conflicting_cask
sig { params(token: T.any(String, Symbol, Cask), conflicting_cask: Cask).void }
def initialize(token, conflicting_cask)
super(token)
@conflicting_cask = conflicting_cask
@@ -94,8 +100,10 @@ module Cask
# Error when a cask in a specific tap is not available.
class TapCaskUnavailableError < CaskUnavailableError
sig { returns(Tap) }
attr_reader :tap
sig { params(tap: Tap, token: String).void }
def initialize(tap, token)
super("#{tap}/#{token}")
@tap = tap
@@ -122,6 +130,7 @@ module Cask
sig { params(token: String, loaders: T::Array[CaskLoader::FromNameLoader]).void }
def initialize(token, loaders)
@token = token
@loaders = loaders
taps = loaders.map(&:tap)
@@ -178,6 +187,7 @@ module Cask
# Error when a cask token does not match the file name.
class CaskTokenMismatchError < CaskInvalidError
sig { params(token: T.any(String, Symbol, Cask), header_token: String).void }
def initialize(token, header_token)
super(token, "Token '#{header_token}' in header line does not match the file name.")
end
@@ -185,8 +195,13 @@ module Cask
# Error during quarantining of a file.
class CaskQuarantineError < CaskError
attr_reader :path, :reason
sig { returns(T.any(String, Pathname)) }
attr_reader :path
sig { returns(String) }
attr_reader :reason
sig { params(path: T.any(String, Pathname), reason: String).void }
def initialize(path, reason)
super()
+5 -5
View File
@@ -41,7 +41,7 @@ module Cask
def self.info(cask, args:)
puts get_info(cask)
return unless cask.tap.core_cask_tap?
return unless cask.tap&.core_cask_tap?
require "utils/analytics"
::Utils::Analytics.cask_output(cask, args:)
@@ -177,12 +177,12 @@ module Cask
sig { params(cask: Cask).returns(T.nilable(String)) }
def self.repo_info(cask)
return if cask.tap.nil?
return unless (tap = cask.tap)
url = if cask.tap.custom_remote? && !cask.tap.remote.nil?
cask.tap.remote
url = if tap.custom_remote? && !tap.remote.nil?
tap.remote
else
"#{cask.tap.default_remote}/blob/HEAD/#{cask.tap.relative_cask_path(cask.token)}"
"#{tap.default_remote}/blob/HEAD/#{tap.relative_cask_path(cask.token)}"
end
"From: #{Formatter.url(url)}"
+80 -23
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "formula_installer"
@@ -53,6 +53,7 @@ module Cask
@download_queue = download_queue
@defer_fetch = T.let(defer_fetch, T::Boolean)
@ran_prelude = T.let(false, T::Boolean)
@cask_and_formula_dependencies = T.let(nil, T.nilable(T::Array[T.any(Formula, ::Cask::Cask)]))
end
sig { returns(T::Boolean) }
@@ -94,6 +95,7 @@ module Cask
sig { returns(T::Boolean) }
def zap? = @zap
sig { params(cask: ::Cask::Cask).returns(T.nilable(String)) }
def self.caveats(cask)
odebug "Printing caveats"
@@ -242,7 +244,7 @@ on_request: true)
sig { returns(Download) }
def downloader
@downloader ||= Download.new(@cask, quarantine: quarantine?)
@downloader ||= T.let(Download.new(@cask, quarantine: quarantine?), T.nilable(Download))
end
sig { params(quiet: T.nilable(T::Boolean), timeout: T.nilable(T.any(Integer, Float))).returns(Pathname) }
@@ -263,11 +265,15 @@ on_request: true)
EOS
end
sig { returns(UnpackStrategy) }
def primary_container
@primary_container ||= begin
downloaded_path = download(quiet: true)
UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
end
@primary_container ||= T.let(
begin
downloaded_path = download(quiet: true)
UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
end,
T.nilable(UnpackStrategy),
)
end
sig { returns(ArtifactSet) }
@@ -279,14 +285,17 @@ on_request: true)
def extract_primary_container(to: @cask.staged_path)
odebug "Extracting primary container"
odebug "Using container class #{primary_container.class} for #{primary_container.path}"
container = primary_container
raise "unexpected nil primary_container" unless container
odebug "Using container class #{container.class} for #{container.path}"
basename = downloader.basename
if (nested_container = @cask.container&.nested)
Dir.mktmpdir("cask-installer", HOMEBREW_TEMP) do |tmpdir|
tmpdir = Pathname(tmpdir)
primary_container.extract(to: tmpdir, basename:, verbose: verbose?)
container.extract(to: tmpdir, basename:, verbose: verbose?)
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose?
@@ -294,13 +303,13 @@ on_request: true)
.extract_nestedly(to:, verbose: verbose?)
end
else
primary_container.extract_nestedly(to:, basename:, verbose: verbose?)
container.extract_nestedly(to:, basename:, verbose: verbose?)
end
return unless quarantine?
return unless Quarantine.available?
Quarantine.propagate(from: primary_container.path, to:)
Quarantine.propagate(from: container.path, to:)
end
sig { params(target_dir: T.nilable(Pathname)).void }
@@ -384,6 +393,7 @@ on_request: true)
nil
end
sig { void }
def check_macos_requirements
return unless @cask.depends_on.macos
return if @cask.depends_on.macos.satisfied?
@@ -395,6 +405,7 @@ on_request: true)
def check_arch_requirements
return if @cask.depends_on.arch.nil?
@current_arch = T.let(@current_arch, T.nilable(T::Hash[Symbol, T.untyped]))
@current_arch ||= { type: Hardware::CPU.type, bits: Hardware::CPU.bits }
return if @cask.depends_on.arch.any? do |arch|
arch[:type] == @current_arch[:type] &&
@@ -407,7 +418,7 @@ on_request: true)
"but you are running #{@current_arch}."
end
sig { returns(T::Array[T.untyped]) }
sig { returns(T::Array[T.any(Formula, ::Cask::Cask)]) }
def cask_and_formula_dependencies
return @cask_and_formula_dependencies if @cask_and_formula_dependencies
@@ -415,7 +426,10 @@ on_request: true)
raise CaskSelfReferencingDependencyError, @cask.token if graph.fetch(@cask).include?(@cask)
::Utils::TopologicalHash.graph_package_dependencies(primary_container.dependencies, graph)
pc = primary_container
raise "unexpected nil primary_container" unless pc
::Utils::TopologicalHash.graph_package_dependencies(pc.dependencies, graph)
begin
@cask_and_formula_dependencies = graph.tsort - [@cask]
@@ -426,6 +440,7 @@ on_request: true)
end
end
sig { returns(T::Array[T.any(Formula, ::Cask::Cask)]) }
def missing_cask_and_formula_dependencies
cask_and_formula_dependencies.reject do |cask_or_formula|
case cask_or_formula
@@ -437,6 +452,7 @@ on_request: true)
end
end
sig { void }
def satisfy_cask_and_formula_dependencies
return if installed_as_dependency?
@@ -498,14 +514,25 @@ on_request: true)
end
end
sig { returns(T.nilable(String)) }
def caveats
self.class.caveats(@cask)
end
sig { returns(Pathname) }
def metadata_subdir
@metadata_subdir ||= @cask.metadata_subdir("Casks", timestamp: :now, create: true)
@metadata_subdir ||= T.let(
begin
msd = @cask.metadata_subdir("Casks", timestamp: :now, create: true)
raise "unexpected nil metadata_subdir" unless msd
msd
end,
T.nilable(Pathname),
)
end
sig { void }
def save_caskfile
old_savedir = @cask.metadata_timestamped_path
@@ -530,12 +557,16 @@ on_request: true)
FileUtils.rm_r(old_savedir) if old_savedir
end
sig { void }
def save_config_file
@cask.config_path.atomic_write(@cask.config.to_json)
end
sig { void }
def save_download_sha
@cask.download_sha_path.atomic_write(@cask.new_download_sha) if @cask.checksumable?
return unless @cask.checksumable?
@cask.download_sha_path.atomic_write(@cask.new_download_sha)
end
sig { params(successor: T.nilable(Cask)).void }
@@ -552,17 +583,20 @@ on_request: true)
purge_caskroom_path if force?
end
sig { void }
def remove_tabfile
tabfile = @cask.tab.tabfile
FileUtils.rm_f tabfile if tabfile
@cask.config_path.parent.rmdir_if_possible
end
sig { void }
def remove_config_file
FileUtils.rm_f @cask.config_path
@cask.config_path.parent.rmdir_if_possible
end
sig { void }
def remove_download_sha
FileUtils.rm_f @cask.download_sha_path
@cask.download_sha_path.parent.rmdir_if_possible
@@ -574,19 +608,33 @@ on_request: true)
backup
end
sig { void }
def backup
@cask.staged_path.rename backup_path
@cask.metadata_versioned_path.rename backup_metadata_path
bp = backup_path
raise "unexpected nil backup_path" unless bp
bmp = backup_metadata_path
raise "unexpected nil backup_metadata_path" unless bmp
@cask.staged_path.rename bp.to_s
@cask.metadata_versioned_path.rename bmp.to_s
end
sig { void }
def restore_backup
return if !backup_path.directory? || !backup_metadata_path.directory?
bp = backup_path
return unless bp
bmp = backup_metadata_path
return unless bmp
return if !bp.directory? || !bmp.directory?
FileUtils.rm_r(@cask.staged_path) if @cask.staged_path.exist?
FileUtils.rm_r(@cask.metadata_versioned_path) if @cask.metadata_versioned_path.exist?
backup_path.rename @cask.staged_path
backup_metadata_path.rename @cask.metadata_versioned_path
bp.rename @cask.staged_path.to_s
bmp.rename @cask.metadata_versioned_path.to_s
end
sig { params(predecessor: Cask).void }
@@ -596,6 +644,7 @@ on_request: true)
install_artifacts(predecessor:)
end
sig { void }
def finalize_upgrade
ohai "Purging files for version #{@cask.version} of Cask #{@cask}"
@@ -651,6 +700,7 @@ on_request: true)
end
end
sig { void }
def zap
load_installed_caskfile!
uninstall_artifacts
@@ -666,35 +716,41 @@ on_request: true)
purge_caskroom_path
end
sig { returns(T.nilable(Pathname)) }
def backup_path
return if @cask.staged_path.nil?
Pathname("#{@cask.staged_path}.upgrading")
end
sig { returns(T.nilable(Pathname)) }
def backup_metadata_path
return if @cask.metadata_versioned_path.nil?
Pathname("#{@cask.metadata_versioned_path}.upgrading")
end
sig { params(path: Pathname).void }
def gain_permissions_remove(path)
Utils.gain_permissions_remove(path, command: @command)
end
sig { void }
def purge_backed_up_versioned_files
# versioned staged distribution
gain_permissions_remove(backup_path) if backup_path&.exist?
gain_permissions_remove(T.must(backup_path)) if backup_path&.exist?
# Homebrew Cask metadata
return unless backup_metadata_path.directory?
bmp = backup_metadata_path
return unless bmp&.directory?
backup_metadata_path.children.each do |subdir|
bmp.children.each do |subdir|
gain_permissions_remove(subdir)
end
backup_metadata_path.rmdir_if_possible
bmp.rmdir_if_possible
end
sig { void }
def purge_versioned_files
ohai "Purging files for version #{@cask.version} of Cask #{@cask}"
@@ -721,6 +777,7 @@ on_request: true)
end
end
sig { void }
def purge_caskroom_path
odebug "Purging all staged versions of Cask #{@cask}"
gain_permissions_remove(@cask.caskroom_path)
+50 -25
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "development_tools"
@@ -14,29 +14,35 @@ module Cask
QUARANTINE_ATTRIBUTE = "com.apple.quarantine"
QUARANTINE_SCRIPT = (HOMEBREW_LIBRARY_PATH/"cask/utils/quarantine.swift").freeze
COPY_XATTRS_SCRIPT = (HOMEBREW_LIBRARY_PATH/"cask/utils/copy-xattrs.swift").freeze
QUARANTINE_SCRIPT = T.let((HOMEBREW_LIBRARY_PATH/"cask/utils/quarantine.swift").freeze, Pathname)
COPY_XATTRS_SCRIPT = T.let((HOMEBREW_LIBRARY_PATH/"cask/utils/copy-xattrs.swift").freeze, Pathname)
sig { returns(T.nilable(T.any(String, Pathname))) }
def self.swift
@swift ||= begin
# /usr/bin/swift (which runs via xcrun) adds `/usr/local/include` to the top of the include path,
# which allows really broken local setups to break our Swift usage here. Using the underlying
# Swift executable directly however (returned by `xcrun -find`) avoids this CPATH mess.
xcrun_swift = ::Utils.popen_read("/usr/bin/xcrun", "-find", "swift", err: :close).chomp
if $CHILD_STATUS.success? && File.executable?(xcrun_swift)
xcrun_swift
else
DevelopmentTools.locate("swift")
end
end
@swift ||= T.let(
begin
# /usr/bin/swift (which runs via xcrun) adds `/usr/local/include` to the top of the include path,
# which allows really broken local setups to break our Swift usage here. Using the underlying
# Swift executable directly however (returned by `xcrun -find`) avoids this CPATH mess.
xcrun_swift = ::Utils.popen_read("/usr/bin/xcrun", "-find", "swift", err: :close).chomp
if $CHILD_STATUS.success? && File.executable?(xcrun_swift)
xcrun_swift
else
DevelopmentTools.locate("swift")
end
end,
T.nilable(T.any(String, Pathname)),
)
end
private_class_method :swift
sig { returns(T.nilable(Pathname)) }
def self.xattr
@xattr ||= DevelopmentTools.locate("xattr")
@xattr ||= T.let(DevelopmentTools.locate("xattr"), T.nilable(Pathname))
end
private_class_method :xattr
sig { returns(T::Array[String]) }
def self.swift_target_args
["-target", "#{Hardware::CPU.arch}-apple-macosx#{MacOS.version}"]
end
@@ -47,14 +53,17 @@ module Cask
odebug "Checking quarantine support"
check_output = nil
status = if xattr.nil? || !system_command(xattr, args: ["-h"], print_stderr: false).success?
status = if xattr.nil? || !system_command(T.must(xattr), args: ["-h"], print_stderr: false).success?
odebug "There's no working version of `xattr` on this system."
:xattr_broken
elsif swift.nil?
odebug "Swift is not available on this system."
:no_swift
else
api_check = system_command(swift,
s = swift
raise "unexpected nil swift" unless s
api_check = system_command(s,
args: [*swift_target_args, QUARANTINE_SCRIPT],
print_stderr: false)
@@ -90,11 +99,12 @@ module Cask
sig { returns(T::Boolean) }
def self.available?
@quarantine_support ||= check_quarantine_support
@quarantine_support ||= T.let(check_quarantine_support, T.nilable([Symbol, T.nilable(String)]))
@quarantine_support[0] == :quarantine_available
end
sig { params(file: T.nilable(T.any(String, Pathname))).returns(T.nilable(T::Boolean)) }
def self.detect(file)
return if file.nil?
@@ -107,12 +117,16 @@ module Cask
quarantine_status
end
sig { params(file: T.any(String, Pathname)).returns(String) }
def self.status(file)
system_command(xattr,
raise "unexpected nil xattr" unless xattr
system_command(T.must(xattr),
args: ["-p", QUARANTINE_ATTRIBUTE, file],
print_stderr: false).stdout.rstrip
end
sig { params(attribute: String).returns(String) }
def self.toggle_no_translocation_bit(attribute)
fields = attribute.split(";")
@@ -120,17 +134,20 @@ module Cask
# Let's toggle the app translocation bit, bit 8
# http://www.openradar.me/radar?id=5022734169931776
fields[0] = (fields[0].to_i(16) | 0x0100).to_s(16).rjust(4, "0")
fields[0] = (fields.fetch(0).to_i(16) | 0x0100).to_s(16).rjust(4, "0")
fields.join(";")
end
sig { params(download_path: T.nilable(Pathname)).void }
def self.release!(download_path: nil)
return unless detect(download_path)
return if !download_path || !detect(download_path)
odebug "Releasing #{download_path} from quarantine"
quarantiner = system_command(xattr,
raise "unexpected nil xattr" unless xattr
quarantiner = system_command(T.must(xattr),
args: [
"-d",
QUARANTINE_ATTRIBUTE,
@@ -143,6 +160,7 @@ module Cask
raise CaskQuarantineReleaseError.new(download_path, quarantiner.stderr)
end
sig { params(cask: T.nilable(Cask), download_path: T.nilable(Pathname), action: T::Boolean).void }
def self.cask!(cask: nil, download_path: nil, action: true)
return if cask.nil? || download_path.nil?
@@ -150,7 +168,9 @@ module Cask
odebug "Quarantining #{download_path}"
quarantiner = system_command(swift,
raise "unexpected nil swift" unless swift
quarantiner = system_command(T.must(swift),
args: [
*swift_target_args,
QUARANTINE_SCRIPT,
@@ -170,6 +190,7 @@ module Cask
end
end
sig { params(from: T.nilable(Pathname), to: T.nilable(Pathname)).void }
def self.propagate(from: nil, to: nil)
return if from.nil? || to.nil?
@@ -191,11 +212,13 @@ module Cask
],
input: resolved_paths.join("\0"))
raise "unexpected nil xattr" unless xattr
quarantiner = system_command("/usr/bin/xargs",
args: [
"-0",
"--",
xattr,
T.must(xattr),
"-w",
QUARANTINE_ATTRIBUTE,
quarantine_status,
@@ -212,8 +235,10 @@ module Cask
def self.copy_xattrs(from, to, command:)
odebug "Copying xattrs from #{from} to #{to}"
raise "unexpected nil swift" unless swift
command.run!(
swift,
T.must(swift),
args: [
*swift_target_args,
COPY_XATTRS_SCRIPT,
+1 -1
View File
@@ -161,7 +161,7 @@ module Cask
invalid_cask = !c.installed?
invalid_cask ||= begin
loaded_cask = CaskLoader.load(c.installed_caskfile)
loaded_cask = CaskLoader.load(T.must(c.installed_caskfile))
false
rescue CaskInvalidError, CaskUnavailableError
true
+4 -1
View File
@@ -8,7 +8,10 @@ class CaskDependent
# Defines a dependency on another cask
class Requirement < ::Requirement
satisfy(build_env: false) do
Cask::CaskLoader.load(cask).installed?
cask_token = cask
raise "unexpected nil cask" unless cask_token
Cask::CaskLoader.load(cask_token).installed?
end
end
+6 -1
View File
@@ -70,7 +70,12 @@ module Homebrew
end
when Cask::Cask
cask = formula_or_cask
ref = cask.loaded_from_api? ? cask.full_name : cask.sourcefile_path
if cask.loaded_from_api?
ref = cask.full_name
else
ref = cask.sourcefile_path
raise "unexpected nil cask sourcefile_path" unless ref
end
os_arch_combinations.each do |os, arch|
next if os == :linux
+9 -4
View File
@@ -331,7 +331,7 @@ module Homebrew
when Formula
Utils::Analytics.formula_output(obj, args:) if obj.core_formula?
when Cask::Cask
Utils::Analytics.cask_output(obj, args:) if obj.tap.core_cask_tap?
Utils::Analytics.cask_output(obj, args:) if obj.tap&.core_cask_tap?
when FormulaOrCaskUnavailableError
Utils::Analytics.output(filter: obj.name, args:)
else
@@ -431,6 +431,7 @@ module Homebrew
sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(String) }
def github_info(formula_or_cask)
tap = T.let(nil, T.nilable(Tap))
path = case formula_or_cask
when Formula
formula = formula_or_cask
@@ -443,14 +444,18 @@ module Homebrew
tap = cask.tap
return cask.sourcefile_path.to_s if tap.blank? || tap.remote.blank?
if cask.sourcefile_path.blank? || cask.sourcefile_path.extname != ".rb"
sourcefile_path = cask.sourcefile_path
if sourcefile_path.blank? || sourcefile_path.extname != ".rb"
return "#{tap.default_remote}/blob/HEAD/#{tap.relative_cask_path(cask.token)}"
end
cask.sourcefile_path.relative_path_from(tap.path)
sourcefile_path.relative_path_from(tap.path)
end
github_remote_path(tap.remote, path.to_s)
remote = tap.remote
raise "unexpected nil tap.remote" unless remote
github_remote_path(remote, path.to_s)
end
sig { params(formula: Formula).void }
+5 -2
View File
@@ -155,8 +155,11 @@ module Homebrew
else
c = formula_or_cask
c.outdated_info(upgrade_greedy_cask?(args.greedy?, formula_or_cask),
verbose?, true, args.greedy_latest?, args.greedy_auto_updates?)
T.cast(
c.outdated_info(upgrade_greedy_cask?(args.greedy?, formula_or_cask),
verbose?, true, args.greedy_latest?, args.greedy_auto_updates?),
T::Hash[Symbol, T.untyped],
)
end
end
end
+34 -14
View File
@@ -83,10 +83,12 @@ module Homebrew
retry
end
odie "This cask is not in a tap!" if cask.tap.blank?
odie "This cask's tap is not a Git repository!" unless cask.tap.git?
tap = cask.tap
odie "This cask is not in a tap!" if tap.nil?
odie <<~EOS unless cask.tap.allow_bump?(cask.token)
odie "This cask's tap is not a Git repository!" unless tap.git?
odie <<~EOS unless tap.allow_bump?(cask.token)
Whoops, the #{cask.token} cask has its version update
pull requests automatically opened by BrewTestBot every ~3 hours!
We'd still love your contributions, though, so try another one
@@ -131,7 +133,10 @@ module Homebrew
branch_name = "bump-#{cask.token}"
commit_message = nil
old_contents = File.read(cask.sourcefile_path)
sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless sourcefile_path
old_contents = File.read(sourcefile_path)
if new_base_url
commit_message ||= "#{cask.token}: update URL"
@@ -174,7 +179,7 @@ module Homebrew
# Remove nested arrays where elements are identical
replacement_pairs = replacement_pairs.reject { |pair| pair[0] == pair[1] }.uniq.compact
Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
Utils::Inreplace.inreplace_pairs(sourcefile_path,
replacement_pairs,
read_only_run: args.dry_run?,
silent: args.quiet?)
@@ -275,11 +280,14 @@ module Homebrew
).returns(T::Array[[T.any(Regexp, String), T.any(Pathname, String)]])
}
def replace_version_and_checksum(cask, new_hash, new_version, replacement_pairs)
cask_sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless cask_sourcefile_path
generate_system_options(cask, new_version).each do |os, arch|
SimulateSystem.with(os:, arch:) do
# Handle the cask being invalid for specific os/arch combinations
old_cask = begin
Cask::CaskLoader.load(cask.sourcefile_path)
Cask::CaskLoader.load(cask_sourcefile_path)
rescue Cask::CaskInvalidError, Cask::CaskUnreadableError
raise unless cask.on_system_blocks_exist?
end
@@ -296,7 +304,7 @@ module Homebrew
"version #{bump_version.latest? ? ":latest" : %Q("#{bump_version}")}"]
# We are replacing our version here so we can get the new hash
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
tmp_contents = Utils::Inreplace.inreplace_pairs(cask_sourcefile_path,
replacement_pairs.uniq.compact,
read_only_run: true,
silent: true)
@@ -359,11 +367,17 @@ module Homebrew
sig { params(cask: Cask::Cask, new_version: BumpVersionParser).void }
def check_pull_requests(cask, new_version:)
tap_remote_repo = cask.tap.full_name || cask.tap.remote_repository
tap = cask.tap
raise "unexpected nil cask.tap" unless tap
file = cask.sourcefile_path.relative_path_from(cask.tap.path).to_s
tap_remote_repo = tap.full_name || tap.remote_repository
sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless sourcefile_path
file = sourcefile_path.relative_path_from(tap.path).to_s
quiet = args.quiet?
official_tap = cask.tap.official?
official_tap = tap.official?
GitHub.check_for_duplicate_pull_requests(cask.token, tap_remote_repo,
state: "open", file:, quiet:, official_tap:)
@@ -398,17 +412,23 @@ module Homebrew
end
return unless failed_audit
cask.sourcefile_path.atomic_write(old_contents)
sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless sourcefile_path
sourcefile_path.atomic_write(old_contents)
odie "`brew audit` failed!"
end
sig { params(cask: Cask::Cask, old_contents: String).void }
def run_cask_style(cask, old_contents)
sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless sourcefile_path
if args.dry_run?
if args.no_style?
ohai "Skipping `brew style --fix`"
else
ohai "brew style --fix #{cask.sourcefile_path.basename}"
ohai "brew style --fix #{sourcefile_path.basename}"
end
return
end
@@ -416,12 +436,12 @@ module Homebrew
if args.no_style?
ohai "Skipping `brew style --fix`"
else
system HOMEBREW_BREW_FILE, "style", "--fix", cask.sourcefile_path
system HOMEBREW_BREW_FILE, "style", "--fix", sourcefile_path.to_s
failed_style = !$CHILD_STATUS.success?
end
return unless failed_style
cask.sourcefile_path.atomic_write(old_contents)
sourcefile_path.atomic_write(old_contents)
odie "`brew style --fix` failed!"
end
end
@@ -144,12 +144,15 @@ module Homebrew
if cask.version == version
oh1 "Cask #{cask} is up-to-date at #{version}"
else
sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless sourcefile_path
bump_cask_pr_args = [
"bump-cask-pr",
"--version", version.to_s,
"--sha256", ":no_check",
"--message", "Automatic update via `brew bump-unversioned-casks`.",
cask.sourcefile_path
sourcefile_path
]
if args.dry_run?
+15 -7
View File
@@ -147,7 +147,7 @@ module Homebrew
casks = args.formula? ? [] : Cask::Caskroom.casks
formulae + casks
elsif args.named.present?
args.named.to_formulae_and_casks_with_taps
T.cast(args.named.to_formulae_and_casks_with_taps, T::Array[T.any(Formula, Cask::Cask)])
elsif eval_all
formulae = args.cask? ? [] : Formula.all(eval_all:)
casks = args.formula? ? [] : Cask::Cask.all(eval_all:)
@@ -158,15 +158,15 @@ module Homebrew
"`HOMEBREW_EVAL_ALL=1` set!"
end
if args.start_with
if (start_with = args.start_with)
formulae_and_casks.select! do |formula_or_cask|
name = formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
name.start_with?(args.start_with)
name = formula_or_cask.is_a?(Cask::Cask) ? formula_or_cask.token : formula_or_cask.name
name.start_with?(start_with)
end
end
formulae_and_casks = formulae_and_casks.sort_by do |formula_or_cask|
formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
formula_or_cask.is_a?(Cask::Cask) ? formula_or_cask.token : formula_or_cask.name
end
formulae_and_casks -= excluded_autobump
@@ -334,6 +334,8 @@ module Homebrew
}
def retrieve_pull_requests(formula_or_cask, name, version: nil)
tap_remote_repo = formula_or_cask.tap&.remote_repository || formula_or_cask.tap&.full_name
odie "unexpected nil tap remote repository" if tap_remote_repo.nil?
pull_requests = begin
GitHub.fetch_pull_requests(name, tap_remote_repo, version:)
rescue GitHub::API::ValidationFailedError => e
@@ -379,9 +381,15 @@ module Homebrew
# correct version for the current arch
if formula_or_cask.is_a?(Formula)
loaded_formula_or_cask = formula_or_cask
current_version_value = T.must(loaded_formula_or_cask.stable).version
stable = loaded_formula_or_cask.stable
raise "unexpected nil stable" unless stable
current_version_value = stable.version
else
loaded_formula_or_cask = Cask::CaskLoader.load(formula_or_cask.sourcefile_path)
sourcefile_path = formula_or_cask.sourcefile_path
raise "unexpected nil sourcefile_path" unless sourcefile_path
loaded_formula_or_cask = Cask::CaskLoader.load(sourcefile_path)
current_version_value = Version.new(loaded_formula_or_cask.version)
end
@@ -130,7 +130,7 @@ module Homebrew
filtered_macos_runners = RUNNERS.select do |runner, _|
runner[:symbol] != :linux &&
cask.depends_on.macos.present? &&
cask.depends_on.macos.allows?(MacOSVersion.from_symbol(T.must(runner[:symbol]).to_sym))
cask.depends_on.macos.allows?(MacOSVersion.from_symbol(runner.fetch(:symbol).to_sym))
end
filtered_runners = if filtered_macos_runners.any?
@@ -208,8 +208,10 @@ module Homebrew
Float]).returns(T::Hash[Symbol, T.any(Symbol, String)])
}
def random_runner(available_runners = ARM_MACOS_RUNNERS)
T.must(available_runners.max_by { |(_, weight)| rand ** (1.0 / weight) })
.first
max_runner = available_runners.max_by { |(_, weight)| rand ** (1.0 / weight) }
raise "unexpected nil max_runner" unless max_runner
max_runner.first
end
sig { params(cask: Cask::Cask).returns([T::Array[T::Hash[Symbol, T.any(Symbol, String)]], T::Boolean]) }
@@ -217,7 +219,7 @@ module Homebrew
filtered_runners = filter_runners(cask)
filtered_macos_found = filtered_runners.keys.any? do |runner|
cask.to_hash_with_variations["variations"].key?(T.must(runner[:symbol]).to_sym)
cask.to_hash_with_variations["variations"].key?(runner.fetch(:symbol).to_sym)
end
if filtered_macos_found
@@ -244,10 +246,10 @@ module Homebrew
changed_files = find_changed_files(tap)
ruby_files_in_wrong_directory =
T.must(changed_files[:modified_ruby_files]) - (
T.must(changed_files[:modified_cask_files]) +
T.must(changed_files[:modified_command_files]) +
T.must(changed_files[:modified_github_actions_files])
changed_files[:modified_ruby_files] - (
changed_files[:modified_cask_files] +
changed_files[:modified_command_files] +
changed_files[:modified_github_actions_files]
)
if ruby_files_in_wrong_directory.any?
@@ -263,7 +265,7 @@ module Homebrew
Cask::CaskLoader.find_cask_in_tap(cask_name, tap).relative_path_from(tap.path)
end
else
T.must(changed_files[:modified_cask_files])
changed_files[:modified_cask_files]
end
jobs = cask_files_to_check.count
@@ -273,7 +275,7 @@ module Homebrew
cask_token = path.basename(".rb")
audit_args = ["--online", "--signing"]
audit_args << "--new" if T.must(changed_files[:added_files]).include?(path) || new_cask
audit_args << "--new" if changed_files.fetch(:added_files).include?(path) || new_cask
audit_exceptions = []
@@ -326,7 +328,16 @@ module Homebrew
end
end
sig { params(tap: Tap).returns(T::Hash[Symbol, T::Array[String]]) }
sig {
params(tap: Tap).returns({
modified_files: T::Array[Pathname],
added_files: T::Array[Pathname],
modified_ruby_files: T::Array[Pathname],
modified_command_files: T::Array[Pathname],
modified_github_actions_files: T::Array[Pathname],
modified_cask_files: T::Array[Pathname],
})
}
def find_changed_files(tap)
commit_range_start = Utils.safe_popen_read("git", "rev-parse", "origin").chomp
commit_range_end = Utils.safe_popen_read("git", "rev-parse", "HEAD").chomp
+40 -37
View File
@@ -60,44 +60,47 @@ module Homebrew
puts Homebrew::EnvConfig.livecheck_watchlist if Homebrew::EnvConfig.livecheck_watchlist.present?
end
formulae_and_casks_to_check = Homebrew.with_no_api_env do
if args.tap
tap = Tap.fetch(args.tap)
formulae = args.cask? ? [] : tap.formula_files.map { |path| Formulary.factory(path) }
casks = args.formula? ? [] : tap.cask_files.map { |path| Cask::CaskLoader.load(path) }
formulae + casks
elsif args.installed?
formulae = args.cask? ? [] : Formula.installed
casks = args.formula? ? [] : Cask::Caskroom.casks
formulae + casks
elsif args.named.present?
args.named.to_formulae_and_casks_with_taps
elsif eval_all
formulae = args.cask? ? [] : Formula.all(eval_all:)
casks = args.formula? ? [] : Cask::Cask.all(eval_all:)
formulae + casks
elsif File.exist?(watchlist_path)
begin
# This removes blank lines, comment lines, and trailing comments
names = Pathname.new(watchlist_path).read.lines
.filter_map do |line|
comment_index = line.index("#")
next if comment_index&.zero?
formulae_and_casks_to_check = T.let(
Homebrew.with_no_api_env do
if args.tap
tap = Tap.fetch(args.tap)
formulae = args.cask? ? [] : tap.formula_files.map { |path| Formulary.factory(path) }
casks = args.formula? ? [] : tap.cask_files.map { |path| Cask::CaskLoader.load(path) }
formulae + casks
elsif args.installed?
formulae = args.cask? ? [] : Formula.installed
casks = args.formula? ? [] : Cask::Caskroom.casks
formulae + casks
elsif args.named.present?
args.named.to_formulae_and_casks_with_taps
elsif eval_all
formulae = args.cask? ? [] : Formula.all(eval_all:)
casks = args.formula? ? [] : Cask::Cask.all(eval_all:)
formulae + casks
elsif File.exist?(watchlist_path)
begin
# This removes blank lines, comment lines, and trailing comments
names = Pathname.new(watchlist_path).read.lines
.filter_map do |line|
comment_index = line.index("#")
next if comment_index&.zero?
line = line[0...comment_index] if comment_index
line&.strip.presence
end
line = line[0...comment_index] if comment_index
line&.strip.presence
end
named_args = CLI::NamedArgs.new(*names, parent: args)
named_args.to_formulae_and_casks(ignore_unavailable: true)
rescue Errno::ENOENT => e
onoe e
named_args = CLI::NamedArgs.new(*names, parent: args)
named_args.to_formulae_and_casks(ignore_unavailable: true)
rescue Errno::ENOENT => e
onoe e
end
else
raise UsageError,
"`brew livecheck` with no arguments needs a watchlist file to be present or `--eval-all` passed!"
end
else
raise UsageError,
"`brew livecheck` with no arguments needs a watchlist file to be present or `--eval-all` passed!"
end
end
end,
T::Array[T.any(Formula, Cask::Cask)],
)
skipped_autobump = T.let(false, T::Boolean)
if skip_autobump?
@@ -109,7 +112,7 @@ module Homebrew
autobump_lists[tap] ||= tap.autobump
name = formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
name = formula_or_cask.is_a?(Cask::Cask) ? formula_or_cask.token : formula_or_cask.name
next unless autobump_lists[tap].include?(name)
odebug "Skipping #{name} as it is autobumped in #{tap}."
@@ -119,7 +122,7 @@ module Homebrew
end
formulae_and_casks_to_check = formulae_and_casks_to_check.sort_by do |formula_or_cask|
formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
formula_or_cask.is_a?(Cask::Cask) ? formula_or_cask.token : formula_or_cask.name
end
raise UsageError, "No formulae or casks to check." if formulae_and_casks_to_check.blank? && !skipped_autobump
+1 -1
View File
@@ -469,7 +469,7 @@ module Homebrew
end
end
sig { params(tap: Tap, original_commit: String).returns(T::Array[String]) }
sig { params(tap: Tap, original_commit: String).returns(T::Array[T.any(Formula, Cask::Cask)]) }
def changed_packages(tap, original_commit)
formulae = Utils.popen_read("git", "-C", tap.path, "diff-tree",
"-r", "--name-only", "--diff-filter=AM",
+8 -5
View File
@@ -52,12 +52,13 @@ module Homebrew
def self.load_other_tap_strategies(formulae_and_casks_to_check)
other_taps = {}
formulae_and_casks_to_check.each do |formula_or_cask|
next if formula_or_cask.tap.blank?
next if formula_or_cask.tap.core_tap?
next if formula_or_cask.tap.core_cask_tap?
next if other_taps[formula_or_cask.tap.name]
tap = formula_or_cask.tap
next unless tap
next if tap.core_tap?
next if tap.core_cask_tap?
next if other_taps[tap.name]
other_taps[formula_or_cask.tap.name] = formula_or_cask.tap
other_taps[tap.name] = tap
end
other_taps = other_taps.sort.to_h
@@ -97,6 +98,8 @@ module Homebrew
Formulary.factory(livecheck_formula)
elsif livecheck_cask
Cask::CaskLoader.load(livecheck_cask)
else
raise "livecheck formula or cask not found"
end
end
@@ -283,7 +283,7 @@ module Homebrew
verbose:,
extract_plist:,
)
return if skip_info.blank?
return if skip_info.empty?
referenced_name = Livecheck.package_or_resource_name(livecheck_package_or_resource, full_name:)
referenced_type = case livecheck_package_or_resource
@@ -128,7 +128,10 @@ module Homebrew
# Create a copy of the cask that overrides the artifact URL with the
# provided URL and supported `livecheck` block URL options
cask_copy = Cask::CaskLoader.load(cask.sourcefile_path)
sourcefile_path = cask.sourcefile_path
raise "unexpected nil cask.sourcefile_path" unless sourcefile_path
cask_copy = Cask::CaskLoader.load(sourcefile_path)
cask_copy.allow_reassignment = true
cask_copy.url(url, **url_kwargs)
cask_copy
+4 -4
View File
@@ -7,10 +7,10 @@ end
module OnSystem::MacOSAndLinux
sig {
params(
macos: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)),
linux: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)),
).returns(T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)))
type_parameters(:U).params(
macos: T.all(T.type_parameter(:U), T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname))),
linux: T.all(T.type_parameter(:U), T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname))),
).returns(T.type_parameter(:U))
}
def on_system_conditional(macos: nil, linux: nil); end
+1 -1
View File
@@ -185,7 +185,7 @@ module Homebrew
end
results.sort.filter_map do |name|
cask = Cask::CaskLoader.load(name)
cask = Cask::CaskLoader.load(name.to_s)
next if ignore_cask?(cask)
display_name = if cask.installed?