Merge pull request #23503 from Homebrew/cask-api-platform-support

Add supported_platforms to cask API data
This commit is contained in:
Mike McQuaid
2026-08-12 07:19:56 +00:00
committed by GitHub
7 changed files with 311 additions and 29 deletions
+13 -4
View File
@@ -344,10 +344,19 @@ module Cask
add_error "a #{sym} stanza is required" unless cask.public_send(sym)
end
add_error "at least one name stanza is required" if cask.name.empty?
# TODO: specific DSL knowledge should not be spread around in various files like this
rejected_artifacts = [:uninstall, :zap]
installable_artifacts = cask.artifacts.reject { |k| rejected_artifacts.include?(k) }
add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty?
installable_artifact = if cask.on_system_blocks_exist?
begin
OnSystem::VALID_OS_ARCH_TAGS.any? do |tag|
cask.refresh_for_tag(tag) { cask.installable_artifact? }
end
ensure
cask.refresh
end
else
cask.installable_artifact?
end
add_error "at least one installable artifact stanza is required" unless installable_artifact
end
sig { void }
+61 -2
View File
@@ -256,6 +256,31 @@ module Cask
artifacts.all?(Artifact::Font)
end
sig { returns(T::Boolean) }
def installable_artifact?
artifacts.any? do |artifact|
artifact.respond_to?(:install_phase) || artifact.is_a?(Artifact::StageOnly)
end
end
sig { params(os: Symbol).returns(T::Boolean) }
def artifacts_supported_on_os?(os)
case os
when :linux
artifacts.all? do |artifact|
if artifact.is_a?(Artifact::Installer)
!artifact.manual_install
else
Artifact::MACOS_ONLY_ARTIFACTS.exclude?(artifact.class)
end
end
when :macos
artifacts.none? { |artifact| Artifact::LINUX_ONLY_ARTIFACTS.include?(artifact.class) }
else
raise ArgumentError, "Unsupported operating system: #{os.inspect}"
end
end
sig { returns(T::Boolean) }
def supports_linux?
return true if depends_on.requires_linux?
@@ -612,17 +637,24 @@ module Cask
hash = to_h_with_language_variations
variations = {}
supported_platforms = []
on_system_blocks_exist = dsl!.on_system_blocks_exist?
if dsl!.on_system_blocks_exist?
if on_system_blocks_exist
begin
OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag|
macos_requirements = [depends_on.macos, depends_on.maximum_macos].compact
next if bottle_tag.macos? &&
macos_requirements.present? &&
!dsl!.depends_on_set_in_block? &&
macos_requirements.any? { |requirement| !requirement.allows?(bottle_tag.to_macos_version) }
macos_requirements.any? do |requirement|
# Avoid recursive equality between cached version-comparison keys across casks.
!requirement.allows?(MacOSVersion.from_symbol(bottle_tag.system))
end
refresh_for_tag(bottle_tag) do
supported_platforms << bottle_tag.to_sym if platform_supported?(bottle_tag)
to_h_with_language_variations.each do |key, value|
next if HASH_KEYS_TO_SKIP.include? key
next if value.to_s == hash[key].to_s
@@ -635,9 +667,14 @@ module Cask
ensure
refresh
end
else
supported_platforms = OnSystem::VALID_OS_ARCH_TAGS.filter_map do |bottle_tag|
bottle_tag.to_sym if platform_supported?(bottle_tag)
end
end
hash["variations"] = variations
hash["supported_platforms"] = supported_platforms
hash
end
@@ -720,6 +757,28 @@ module Cask
private
sig { params(bottle_tag: ::Utils::Bottles::Tag).returns(T::Boolean) }
def platform_supported?(bottle_tag)
return false if bottle_tag.linux? && !supports_linux?
return false if bottle_tag.macos? && !supports_macos?
return false if version.blank? || sha256.blank? || url.blank?
return false unless installable_artifact?
return false if bottle_tag.linux? && !artifacts_supported_on_os?(:linux)
return false if bottle_tag.macos? && !artifacts_supported_on_os?(:macos)
arch_supported = depends_on.arch&.any? do |arch|
required_arch = ::Utils::Bottles::Tag.new(system: bottle_tag.system, arch: arch[:type]).standardized_arch
required_arch == bottle_tag.standardized_arch
end
return false if arch_supported == false
return true unless bottle_tag.macos?
[depends_on.macos, depends_on.maximum_macos].compact.all? do |requirement|
requirement.allows?(bottle_tag.to_macos_version)
end
end
# Returns caveats text for API serialization, excluding conditional
# built-in caveats that depend on the current machine's state.
# These are stored as separate boolean fields (e.g. caveats_rosetta)
+2 -5
View File
@@ -372,13 +372,10 @@ on_request: true)
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.
# API data without an installable artifact 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
return if @cask.installable_artifact?
os_name = Homebrew::SimulateSystem.simulating_or_running_on_macos? ? "macOS" : "Linux"
raise CaskError, "#{@cask}: This cask is not available on #{os_name}."
@@ -11,19 +11,10 @@ module OS
sig { void }
def check_stanza_os_requirements
return if !cask.depends_on.requires_macos? && artifacts.all? { |artifact| supported_artifact?(artifact) }
return if !cask.depends_on.requires_macos? && cask.artifacts_supported_on_os?(:linux)
raise ::Cask::CaskError, "#{cask}: This cask requires macOS."
end
private
sig { params(artifact: ::Cask::Artifact::AbstractArtifact).returns(T::Boolean) }
def supported_artifact?(artifact)
return !artifact.manual_install if artifact.is_a?(::Cask::Artifact::Installer)
::Cask::Artifact::MACOS_ONLY_ARTIFACTS.exclude?(artifact.class)
end
end
end
end
@@ -11,17 +11,10 @@ module OS
sig { void }
def check_stanza_os_requirements
return if !cask.depends_on.requires_linux? && artifacts.all? { |artifact| supported_artifact?(artifact) }
return if !cask.depends_on.requires_linux? && cask.artifacts_supported_on_os?(:macos)
raise ::Cask::CaskError, "#{cask}: This cask requires Linux."
end
private
sig { params(artifact: ::Cask::Artifact::AbstractArtifact).returns(T::Boolean) }
def supported_artifact?(artifact)
::Cask::Artifact::LINUX_ONLY_ARTIFACTS.exclude?(artifact.class)
end
end
end
end
+36
View File
@@ -170,6 +170,42 @@ RSpec.describe Cask::Audit, :cask do
it { is_expected.to error_with(/#{stanza} stanza is required/) }
end
end
context "when a cask has only uninstall artifacts" do
let(:cask) do
Cask::Cask.new("uninstall-only") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.pkg"
name "Uninstall Only"
homepage "https://brew.sh/"
uninstall pkgutil: "org.example.foo"
zap trash: "~/Library/Caches/org.example.foo"
end
end
it { is_expected.to error_with("at least one installable artifact stanza is required") }
end
context "when a cask has an installable artifact only on Linux" do
let(:cask) do
Homebrew::SimulateSystem.with(os: :sequoia, arch: :arm) do
Cask::Cask.new("linux-only-artifact") do
version :latest
sha256 :no_check
url "https://brew.sh/foo"
name "Linux Only Artifact"
homepage "https://brew.sh/"
on_linux do
binary "foo"
end
end
end
end
it { is_expected.not_to error_with("at least one installable artifact stanza is required") }
end
end
describe "checking homepage availability" do
+197
View File
@@ -646,6 +646,13 @@ RSpec.describe Cask::Cask, :cask do
end
end
describe "#artifacts_supported_on_os?" do
it "rejects unknown operating systems" do
expect { cask.artifacts_supported_on_os?(:windows) }
.to raise_error(ArgumentError, "Unsupported operating system: :windows")
end
end
describe "#outdated_info" do
it "includes pinned cask details" do
cask = Cask::CaskLoader.load("local-caffeine")
@@ -979,6 +986,196 @@ RSpec.describe Cask::Cask, :cask do
)
end
context "when recording supported platforms" do
let(:platform_tags) do
[
Utils::Bottles::Tag.new(system: :sonoma, arch: :intel),
Utils::Bottles::Tag.new(system: :sonoma, arch: :arm),
Utils::Bottles::Tag.new(system: :monterey, arch: :intel),
Utils::Bottles::Tag.new(system: :monterey, arch: :arm),
Utils::Bottles::Tag.new(system: :catalina, arch: :intel),
Utils::Bottles::Tag.new(system: :linux, arch: :intel),
Utils::Bottles::Tag.new(system: :linux, arch: :arm),
]
end
let(:macos_platforms) { [:sonoma, :arm64_sonoma, :monterey, :arm64_monterey, :catalina] }
before do
stub_const("OnSystem::VALID_OS_ARCH_TAGS", platform_tags)
end
it "records platforms allowed by scoped macOS requirements" do
c = Cask::CaskLoader.load("with-depends-on-macos-in-on-macos")
expect(c.to_hash_with_variations["supported_platforms"]).to eq(
[:sonoma, :arm64_sonoma, :monterey, :arm64_monterey, :x86_64_linux, :arm64_linux],
)
end
it "excludes platforms without complete download data" do
c = Cask::CaskLoader.load("multiple-versions")
expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
end
it "excludes platforms rejected by architecture requirements" do
c = described_class.new("architecture-restricted") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
on_linux do
depends_on arch: :x86_64
end
binary "foo"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(
[*macos_platforms, :x86_64_linux],
)
end
it "excludes Linux for macOS-only artifacts" do
c = described_class.new("macos-artifact") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
app "Foo.app"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
end
it "excludes Linux for manual installers" do
c = described_class.new("manual-installer") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
installer manual: "Foo.app"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
end
it "excludes macOS for Linux-only artifacts" do
c = described_class.new("linux-artifact") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
app_image "Foo.AppImage"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq([:x86_64_linux, :arm64_linux])
end
it "includes stage-only casks" do
c = described_class.new("stage-only") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
stage_only true
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(platform_tags.map(&:to_sym))
end
it "records no supported platforms for a cask without an installable artifact" do
c = described_class.new("zap-only") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
zap trash: "~/Library/Caches/brew-test"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq([])
end
it "records every platform when a cask has no platform variations" do
c = described_class.new("no-platform-variations") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
binary "foo"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(platform_tags.map(&:to_sym))
end
it "records top-level platform requirements without variations" do
c = described_class.new("top-level-platform-requirements") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
depends_on macos: :monterey
depends_on arch: :x86_64
binary "foo"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq([:sonoma, :monterey])
end
it "serializes architecture-varying and universal casks with the same macOS requirement" do
tags = OnSystem::ALL_OS_ARCH_COMBINATIONS.filter_map do |os, arch|
tag = Utils::Bottles::Tag.new(system: os, arch:)
tag if tag.valid_combination?
end
stub_const("OnSystem::VALID_OS_ARCH_TAGS", tags)
architecture_varying = described_class.new("architecture-varying") do
arch arm: "ARM64", intel: "64"
version "1.2.3"
sha256 arm: "a" * 64,
intel: "b" * 64
url "https://brew.sh/#{arch}.zip"
depends_on macos: :big_sur
app "Foo.app"
end
universal = described_class.new("universal") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
depends_on macos: :big_sur
app "Foo.app"
end
architecture_varying.to_hash_with_variations
supported_platforms = Timeout.timeout(5) do
universal.to_hash_with_variations["supported_platforms"]
end
expected_platforms = tags.filter_map do |tag|
tag.to_sym if tag.macos? && tag.system != :catalina
end
expect(supported_platforms).to eq(expected_platforms)
end
it "isolates macOS requirement comparisons between casks" do
supported_platforms = [:monterey, :sonoma].map do |minimum_macos|
c = described_class.new("requires-#{minimum_macos}") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
depends_on macos: minimum_macos
binary "foo"
end
c.to_hash_with_variations["supported_platforms"]
end
expect(supported_platforms).to eq(
[
[:sonoma, :arm64_sonoma, :monterey, :arm64_monterey],
[:sonoma, :arm64_sonoma],
],
)
end
end
# NOTE: The calls to `Cask.generating_hash!` and `Cask.generated_hash!`
# are not idempotent so they can only be used in one test.
it "returns the correct hash placeholders" do