From 233377e66a01124915b916567a8b2ce2ce16f0f5 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Wed, 5 Aug 2026 16:23:33 +0100 Subject: [PATCH] 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. --- Library/Homebrew/cask/cask.rb | 2 - Library/Homebrew/cask/dsl.rb | 10 ----- Library/Homebrew/cask/installer.rb | 15 +++++++ Library/Homebrew/on_system.rb | 2 - Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi | 3 -- Library/Homebrew/test/cask/cask_spec.rb | 17 ++++++++ Library/Homebrew/test/cask/installer_spec.rb | 43 ++++++++++++++++++- 7 files changed, 73 insertions(+), 19 deletions(-) diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index 0df140a958..6e79bf1c84 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -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? && diff --git a/Library/Homebrew/cask/dsl.rb b/Library/Homebrew/cask/dsl.rb index 2f2a65ace9..c686821c01 100644 --- a/Library/Homebrew/cask/dsl.rb +++ b/Library/Homebrew/cask/dsl.rb @@ -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), diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index a0e2561798..b08089aa49 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -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? } diff --git a/Library/Homebrew/on_system.rb b/Library/Homebrew/on_system.rb index daccf5af48..316c0872de 100644 --- a/Library/Homebrew/on_system.rb +++ b/Library/Homebrew/on_system.rb @@ -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 diff --git a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi index 05043d698e..38343e962d 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi @@ -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 diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index 8b27af3d4f..a446dadb3a 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -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 diff --git a/Library/Homebrew/test/cask/installer_spec.rb b/Library/Homebrew/test/cask/installer_spec.rb index e373dabe75..e319e6af2e 100644 --- a/Library/Homebrew/test/cask/installer_spec.rb +++ b/Library/Homebrew/test/cask/installer_spec.rb @@ -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)