Treat cask OS support as explicit data

- Align casks with formulae: platform support comes from
  `depends_on :macos`/`:linux`/`macos:` data rather than generation
  heuristics guessing intent from `os` stanzas, Linux checksums or
  `on_linux` blocks.
- `Cask#to_hash_with_variations` now emits variations for every
  valid OS/arch tag whenever `on_system` blocks exist, matching
  `Formula#to_hash_with_variations`; the Linux-specific gate and its
  `sha256_set_for_linux?` and `on_linux_blocks_exist?` tracking are
  removed. macOS-only casks publish truthful Linux variations, e.g.
  a `null` `sha256`, instead of omitting them.
- `Cask::Installer` gains a first-class unsupported-system error:
  API-loaded casks with no activatable artifact for the running
  system fail with "This cask is not available on macOS/Linux."
  instead of installing nothing. Audited casks always declare an
  activatable artifact for the systems they support, so missing
  artifacts in API data mean the system is unsupported. Source
  loads keep working for unaudited casks, e.g. naked containers.
- A sweep of the full homebrew/cask generation pipeline (all casks,
  both Linux tags, including internal per-tag payloads) confirmed
  no cask needs new `depends_on` annotations and nothing regresses.
This commit is contained in:
Mike McQuaid
2026-08-05 16:23:33 +01:00
parent 67658c8cf6
commit 233377e66a
7 changed files with 73 additions and 19 deletions
-2
View File
@@ -616,8 +616,6 @@ module Cask
if dsl!.on_system_blocks_exist?
begin
OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag|
next if bottle_tag.linux? && dsl!.os.nil? && !dsl!.sha256_set_for_linux? && !dsl!.on_linux_blocks_exist?
macos_requirements = [depends_on.macos, depends_on.maximum_macos].compact
next if bottle_tag.macos? &&
macos_requirements.present? &&
-10
View File
@@ -128,7 +128,6 @@ module Cask
:no_autobump_message,
:on_system_blocks_exist?,
:on_os_blocks_exist?,
:on_linux_blocks_exist?,
:on_system_block_min_os,
:depends_on_set_in_block?,
*ORDINARY_ARTIFACT_CLASSES.map(&:dsl_key),
@@ -233,13 +232,11 @@ module Cask
@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_linux_blocks_exist = T.let(false, T::Boolean)
@on_system_block_min_os = T.let(nil, T.nilable(MacOSVersion))
@os = T.let(nil, T.nilable(String))
@os_set_in_block = T.let(false, T::Boolean)
@rename = T.let([], T::Array[DSL::Rename])
@sha256 = T.let(nil, T.nilable(T.any(Checksum, Symbol)))
@sha256_set_for_linux = T.let(false, T::Boolean)
@sha256_set_in_block = T.let(false, T::Boolean)
@staged_path = T.let(nil, T.nilable(Pathname))
@token = T.let(cask.token, String)
@@ -267,12 +264,6 @@ module Cask
sig { returns(T::Boolean) }
def on_os_blocks_exist? = @on_os_blocks_exist
sig { returns(T::Boolean) }
def on_linux_blocks_exist? = @on_linux_blocks_exist
sig { returns(T::Boolean) }
def sha256_set_for_linux? = @sha256_set_for_linux
# Specifies the cask's name.
#
# NOTE: Multiple names can be specified.
@@ -560,7 +551,6 @@ module Cask
if arm.present? || x86_64.present? || x86_64_linux.present? || arm64_linux.present?
@on_system_blocks_exist = true
end
@sha256_set_for_linux = true if x86_64_linux.present? || arm64_linux.present?
val = arg || on_system_conditional(
macos: on_arch_conditional(arm:, intel: x86_64),
+15
View File
@@ -358,6 +358,7 @@ on_request: true)
sig { void }
def check_requirements
check_stanza_os_requirements
check_supported_system
check_macos_requirements
check_arch_requirements
end
@@ -369,6 +370,20 @@ on_request: true)
raise CaskError, "#{@cask}: This cask requires Linux."
end
sig { void }
def check_supported_system
# Audited casks always have an activatable artifact for the systems they
# support, so API data without one means this system is unsupported.
# Source loads keep working for unaudited casks, e.g. naked containers.
return unless @cask.loaded_from_api?
return if @cask.artifacts.any? do |artifact|
artifact.respond_to?(:install_phase) || artifact.is_a?(Artifact::StageOnly)
end
os_name = Homebrew::SimulateSystem.simulating_or_running_on_macos? ? "macOS" : "Linux"
raise CaskError, "#{@cask}: This cask is not available on #{os_name}."
end
sig { void }
def check_macos_requirements
macos_requirement = [@cask.depends_on.macos, @cask.depends_on.maximum_macos].compact.find { !it.satisfied? }
-2
View File
@@ -88,7 +88,6 @@ module OnSystem
base.define_method(:"on_#{base_os}") do |&block|
@on_system_blocks_exist = T.let(true, T.nilable(TrueClass))
@on_os_blocks_exist = T.let(true, T.nilable(TrueClass))
@on_linux_blocks_exist = T.let(true, T.nilable(TrueClass)) if base_os == :linux
return unless OnSystem.os_condition_met? OnSystem.condition_from_method_name(T.must(__method__))
@@ -105,7 +104,6 @@ module OnSystem
base.define_method(:on_system) do |linux, macos:, &block|
@on_system_blocks_exist = T.let(true, T.nilable(TrueClass))
@on_os_blocks_exist = T.let(true, T.nilable(TrueClass))
@on_linux_blocks_exist = T.let(true, T.nilable(TrueClass))
raise ArgumentError, "The first argument to `on_system` must be `:linux`" if linux != :linux
-3
View File
@@ -159,9 +159,6 @@ class Cask::Cask
sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) }
def no_autobump_message(*args, &block); end
sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) }
def on_linux_blocks_exist?(*args, &block); end
sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) }
def on_os_blocks_exist?(*args, &block); end
+17
View File
@@ -757,6 +757,16 @@ RSpec.describe Cask::Cask, :cask do
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin/1.0.0/intel.zip",
"version": "1.0.0",
"sha256": "1866dfa833b123bb8fe7fa7185ebf24d28d300d0643d75798bc23730af734216"
},
"x86_64_linux": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin//intel.zip",
"version": null,
"sha256": null
},
"arm64_linux": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine/darwin-arm64//arm.zip",
"version": null,
"sha256": null
}
}
JSON
@@ -795,6 +805,13 @@ RSpec.describe Cask::Cask, :cask do
"catalina": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
"sha256": "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
},
"x86_64_linux": {
"url": "file://#{TEST_FIXTURE_DIR}/cask/caffeine-intel.zip",
"sha256": null
},
"arm64_linux": {
"sha256": null
}
}
JSON
+41 -2
View File
@@ -231,6 +231,41 @@ RSpec.describe Cask::Installer, :cask do
end.to raise_error(Cask::CaskError, /\Awith-depends-on-arch: This cask depends on hardware architecture/)
end
it "names the cask when it has nothing to install on this system" do
no_artifacts_cask = Cask::Cask.new("with-no-artifacts", loaded_from_api: true) do
version "1.0"
sha256 :no_check
url "https://brew.sh/x.zip"
end
expect do
described_class.new(no_artifacts_cask).check_supported_system
end.to raise_error(Cask::CaskError, "with-no-artifacts: This cask is not available on macOS.")
end
it "treats uninstall-only artifacts as nothing to install" do
zap_only_cask = Cask::Cask.new("with-zap-only", loaded_from_api: true) do
version "1.0"
sha256 :no_check
url "https://brew.sh/x.zip"
zap trash: "~/Library/Caches/brew-test"
end
expect do
described_class.new(zap_only_cask).check_supported_system
end.to raise_error(Cask::CaskError, "with-zap-only: This cask is not available on macOS.")
end
it "does not treat stage_only casks as having nothing to install" do
stage_only_cask = Cask::Cask.new("with-stage-only", loaded_from_api: true) do
version "1.0"
sha256 :no_check
url "https://brew.sh/x.zip"
stage_only true
end
expect do
described_class.new(stage_only_cask).check_supported_system
end.not_to raise_error
end
it "installs fine if sha256 :no_check is used with --require-sha and --force" do
no_checksum = Cask::CaskLoader.load(cask_path("no-checksum"))
@@ -385,7 +420,7 @@ RSpec.describe Cask::Installer, :cask do
expect(Homebrew::API::Cask).to receive(:source_download_cask).once.and_return(source_caffeine)
caffeine = Cask::CaskLoader.load(path)
expect(caffeine).to receive(:loaded_from_api?).once.and_return(true)
allow(caffeine).to receive(:loaded_from_api?).and_return(true)
expect(caffeine).to receive(:caskfile_only?).once.and_return(true)
described_class.new(caffeine).install
@@ -511,7 +546,7 @@ RSpec.describe Cask::Installer, :cask do
expect(Homebrew::API::Cask).to receive(:source_download_cask).twice.and_return(source_caffeine)
caffeine = Cask::CaskLoader.load(path)
expect(caffeine).to receive(:loaded_from_api?).twice.and_return(true)
allow(caffeine).to receive(:loaded_from_api?).and_return(true)
expect(caffeine).to receive(:caskfile_only?).twice.and_return(true)
expect(caffeine).to receive(:installed_caskfile).once.and_return(invalid_path)
@@ -693,6 +728,7 @@ RSpec.describe Cask::Installer, :cask do
ENV["HOMEBREW_FORBIDDEN_CASKS"] = cask_name = "homebrew-forbidden-cask"
cask = Cask::Cask.new(cask_name) do
url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz"
app "Fake.app"
end
allow(cask).to receive_messages(loaded_from_api?: true, caskfile_only?: true)
installer = described_class.new(cask)
@@ -710,6 +746,7 @@ RSpec.describe Cask::Installer, :cask do
url "https://example.com/source-cask.zip"
version "0.9"
sha256 "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
app "Fake.app"
end
cask_struct = Homebrew::API::CaskStruct.new(
sha256: "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97",
@@ -755,6 +792,7 @@ RSpec.describe Cask::Installer, :cask do
it "enqueues source API caskfiles before the main cask download" do
cask = Cask::Cask.new("source-api-cask") do
url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz"
app "Fake.app"
end
allow(cask).to receive_messages(loaded_from_api?: true, caskfile_only?: true, languages: ["en"])
download_queue = instance_double(Homebrew::DownloadQueue)
@@ -772,6 +810,7 @@ RSpec.describe Cask::Installer, :cask do
it "leaves source API caskfiles in the main queue when their URL is known" do
cask = Cask::Cask.new("source-api-cask") do
url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz"
app "Fake.app"
end
allow(cask).to receive_messages(loaded_from_api?: true, caskfile_only?: true, languages: [])
download_queue = instance_double(Homebrew::DownloadQueue)