diff --git a/Library/Homebrew/ast_constants.rb b/Library/Homebrew/ast_constants.rb index fa31aef6ed..c6ec8eb61c 100644 --- a/Library/Homebrew/ast_constants.rb +++ b/Library/Homebrew/ast_constants.rb @@ -52,4 +52,4 @@ FORMULA_COMPONENT_PRECEDENCE_LIST = T.let([ [{ name: :caveats, type: :method_definition }], [{ name: :plist_options, type: :method_call }, { name: :plist, type: :method_definition }], [{ name: :test, type: :block_call }], -].freeze, T::Array[[{ name: Symbol, type: Symbol }]]) +].freeze, T::Array[T::Array[{ name: Symbol, type: Symbol }]]) diff --git a/Library/Homebrew/cask/dsl.rb b/Library/Homebrew/cask/dsl.rb index 7b2d7f1bb3..22a55c4f1f 100644 --- a/Library/Homebrew/cask/dsl.rb +++ b/Library/Homebrew/cask/dsl.rb @@ -486,7 +486,7 @@ module Cask def add_implicit_macos_dependency return if (cask_depends_on = @depends_on).present? && cask_depends_on.macos.present? - depends_on macos: ">= :#{MacOSVersion::SYMBOLS.key MacOSVersion::SYMBOLS.values.min}" + depends_on macos: ">= #{MacOSVersion.new(HOMEBREW_MACOS_OLDEST_ALLOWED).to_sym.inspect}" end # Declare conflicts that keep a cask from installing or working correctly. diff --git a/Library/Homebrew/cask/dsl/depends_on.rb b/Library/Homebrew/cask/dsl/depends_on.rb index 7a5756f9c0..ddaf48698f 100644 --- a/Library/Homebrew/cask/dsl/depends_on.rb +++ b/Library/Homebrew/cask/dsl/depends_on.rb @@ -52,16 +52,17 @@ module Cask raise "Only a single 'depends_on macos' is allowed." if defined?(@macos) # workaround for https://github.com/sorbet/sorbet/issues/6860 - first_arg = args.first&.to_s + first_arg = args.first + first_arg_s = first_arg&.to_s begin @macos = if args.count > 1 MacOSRequirement.new([args], comparator: "==") - elsif MacOSVersion::SYMBOLS.key?(args.first) + elsif first_arg.is_a?(Symbol) && MacOSVersion::SYMBOLS.key?(first_arg) MacOSRequirement.new([args.first], comparator: "==") - elsif (md = /^\s*(?<|>|[=<>]=)\s*:(?\S+)\s*$/.match(first_arg)) + elsif (md = /^\s*(?<|>|[=<>]=)\s*:(?\S+)\s*$/.match(first_arg_s)) MacOSRequirement.new([T.must(md[:version]).to_sym], comparator: md[:comparator]) - elsif (md = /^\s*(?<|>|[=<>]=)\s*(?\S+)\s*$/.match(first_arg)) + elsif (md = /^\s*(?<|>|[=<>]=)\s*(?\S+)\s*$/.match(first_arg_s)) MacOSRequirement.new([md[:version]], comparator: md[:comparator]) # This is not duplicate of the first case: see `args.first` and a different comparator. else # rubocop:disable Lint/DuplicateBranch diff --git a/Library/Homebrew/macos_version.rb b/Library/Homebrew/macos_version.rb index f71a2e6e73..6d1b8239ed 100644 --- a/Library/Homebrew/macos_version.rb +++ b/Library/Homebrew/macos_version.rb @@ -1,4 +1,4 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strong # frozen_string_literal: true require "version" @@ -10,6 +10,7 @@ class MacOSVersion < Version sig { returns(T.nilable(T.any(String, Symbol))) } attr_reader :version + sig { params(version: T.nilable(T.any(String, Symbol))).void } def initialize(version) @version = version super "unknown or unsupported macOS version: #{version.inspect}" @@ -18,7 +19,7 @@ class MacOSVersion < Version # NOTE: When removing symbols here, ensure that they are added # to `DEPRECATED_MACOS_VERSIONS` in `MacOSRequirement`. - SYMBOLS = { + SYMBOLS = T.let({ tahoe: "26", sequoia: "15", sonoma: "14", @@ -30,7 +31,7 @@ class MacOSVersion < Version high_sierra: "10.13", sierra: "10.12", el_capitan: "10.11", - }.freeze + }.freeze, T::Hash[Symbol, String]) sig { params(macos_version: MacOSVersion).returns(Version) } def self.kernel_major_version(macos_version) @@ -57,7 +58,9 @@ class MacOSVersion < Version super(T.must(version)) - @comparison_cache = {} + @comparison_cache = T.let({}, T::Hash[T.untyped, T.nilable(Integer)]) + @pretty_name = T.let(nil, T.nilable(String)) + @sym = T.let(nil, T.nilable(Symbol)) end sig { override.params(other: T.untyped).returns(T.nilable(Integer)) } @@ -95,7 +98,7 @@ class MacOSVersion < Version sig { returns(Symbol) } def to_sym - return @sym if defined?(@sym) + return @sym if @sym sym = SYMBOLS.invert.fetch(strip_patch.to_s, :dunno) @@ -106,7 +109,7 @@ class MacOSVersion < Version sig { returns(String) } def pretty_name - return @pretty_name if defined?(@pretty_name) + return @pretty_name if @pretty_name pretty_name = to_sym.to_s.split("_").map(&:capitalize).join(" ").freeze @@ -154,5 +157,7 @@ class MacOSVersion < Version # Represents the absence of a version. # # NOTE: Constructor needs to called with an arbitrary macOS-like version which is then set to `nil`. - NULL = MacOSVersion.new("10.0").tap { |v| v.instance_variable_set(:@version, nil) }.freeze + NULL = T.let(MacOSVersion.new("10.0").tap do |v| + T.let(v, MacOSVersion).instance_variable_set(:@version, nil) + end.freeze, MacOSVersion) end diff --git a/Library/Homebrew/test/macos_version_spec.rb b/Library/Homebrew/test/macos_version_spec.rb index b6917f7797..706bcfc9a8 100644 --- a/Library/Homebrew/test/macos_version_spec.rb +++ b/Library/Homebrew/test/macos_version_spec.rb @@ -4,12 +4,15 @@ require "macos_version" RSpec.describe MacOSVersion do let(:version) { described_class.new("10.14") } + let(:tahoe_major) { described_class.new("26.0") } let(:big_sur_major) { described_class.new("11.0") } let(:big_sur_update) { described_class.new("11.1") } + let(:frozen_version) { described_class.new("10.14").freeze } - describe ".kernel_major_version" do + describe "::kernel_major_version" do it "returns the kernel major version" do expect(described_class.kernel_major_version(version)).to eq "18" + expect(described_class.kernel_major_version(tahoe_major)).to eq "25" expect(described_class.kernel_major_version(big_sur_major)).to eq "20" expect(described_class.kernel_major_version(big_sur_update)).to eq "20" end @@ -19,12 +22,43 @@ RSpec.describe MacOSVersion do end end + describe "::from_symbol" do + it "raises an error if the symbol is not a valid macOS version" do + expect do + described_class.from_symbol(:foo) + end.to raise_error(MacOSVersion::Error, "unknown or unsupported macOS version: :foo") + end + + it "creates a new version from a valid macOS version" do + symbol_version = described_class.from_symbol(:mojave) + expect(symbol_version).to eq(version) + end + end + + describe "#new" do + it "raises an error if the version is not a valid macOS version" do + expect do + described_class.new("1.2") + end.to raise_error(MacOSVersion::Error, 'unknown or unsupported macOS version: "1.2"') + end + + it "creates a new version from a valid macOS version" do + string_version = described_class.new("11") + expect(string_version).to eq(:big_sur) + end + end + specify "comparison with Symbol" do expect(version).to be > :high_sierra expect(version).to eq :mojave # We're explicitly testing the `===` operator results here. expect(version).to be === :mojave # rubocop:disable Style/CaseEquality expect(version).to be < :catalina + + # This should work like a normal comparison but the result won't be added + # to the `@comparison_cache` hash because the object is frozen. + expect(frozen_version).to eq :mojave + expect(frozen_version.instance_variable_get(:@comparison_cache)).to eq({}) end specify "comparison with Integer" do @@ -64,44 +98,90 @@ RSpec.describe MacOSVersion do end end - describe "#new" do - it "raises an error if the version is not a valid macOS version" do - expect do - described_class.new("1.2") - end.to raise_error(MacOSVersion::Error, 'unknown or unsupported macOS version: "1.2"') + describe "#strip_patch" do + let(:catalina_update) { described_class.new("10.15.1") } + + it "returns the version without the patch" do + expect(big_sur_update.strip_patch).to eq(described_class.new("11")) + expect(catalina_update.strip_patch).to eq(described_class.new("10.15")) end - it "creates a new version from a valid macOS version" do - string_version = described_class.new("11") - expect(string_version).to eq(:big_sur) + it "returns self if version is null" do + expect(described_class::NULL.strip_patch).to be described_class::NULL end end - describe "#from_symbol" do - it "raises an error if the symbol is not a valid macOS version" do - expect do - described_class.from_symbol(:foo) - end.to raise_error(MacOSVersion::Error, "unknown or unsupported macOS version: :foo") - end + specify "#to_sym" do + version_symbol = :mojave - it "creates a new version from a valid macOS version" do - symbol_version = described_class.from_symbol(:mojave) - expect(symbol_version).to eq(version) - end + # We call this more than once to exercise the caching logic + expect(version.to_sym).to eq(version_symbol) + expect(version.to_sym).to eq(version_symbol) + + # This should work like a normal but the symbol won't be stored as the + # `@sym` instance variable because the object is frozen. + expect(frozen_version.to_sym).to eq(version_symbol) + expect(frozen_version.instance_variable_get(:@sym)).to be_nil + + expect(described_class::NULL.to_sym).to eq(:dunno) end specify "#pretty_name" do + version_pretty_name = "Mojave" + expect(described_class.new("10.11").pretty_name).to eq("El Capitan") - expect(described_class.new("10.14").pretty_name).to eq("Mojave") + + # We call this more than once to exercise the caching logic + expect(version.pretty_name).to eq(version_pretty_name) + expect(version.pretty_name).to eq(version_pretty_name) + + # This should work like a normal but the computed name won't be stored as + # the `@pretty_name` instance variable because the object is frozen. + expect(frozen_version.pretty_name).to eq(version_pretty_name) + expect(frozen_version.instance_variable_get(:@pretty_name)).to be_nil end specify "#inspect" do expect(described_class.new("11").inspect).to eq("#") end - specify "#requires_nehalem_cpu?", :needs_macos do - expect(Hardware::CPU).to receive(:type).at_least(:twice).and_return(:intel) - expect(described_class.new("10.14").requires_nehalem_cpu?).to be true - expect(described_class.new("10.12").requires_nehalem_cpu?).to be false + specify "#outdated_release?" do + expect(described_class.new(described_class::SYMBOLS.values.first).outdated_release?).to be false + expect(described_class.new("10.0").outdated_release?).to be true + end + + specify "#prerelease?" do + expect(described_class.new("1000").prerelease?).to be true + end + + specify "#unsupported_release?" do + expect(described_class.new("10.0").unsupported_release?).to be true + expect(described_class.new("1000").prerelease?).to be true + end + + describe "#requires_nehalem_cpu?", :needs_macos do + context "when CPU is Intel" do + it "returns true if version requires a Nehalem CPU" do + allow(Hardware::CPU).to receive(:type).and_return(:intel) + expect(described_class.new("10.14").requires_nehalem_cpu?).to be true + end + + it "returns false if version does not require a Nehalem CPU" do + allow(Hardware::CPU).to receive(:type).and_return(:intel) + expect(described_class.new("10.12").requires_nehalem_cpu?).to be false + end + end + + context "when CPU is not Intel" do + it "raises an error" do + allow(Hardware::CPU).to receive(:type).and_return(:arm) + expect { described_class.new("10.14").requires_nehalem_cpu? } + .to raise_error(ArgumentError) + end + end + + it "returns false when version is null" do + expect(described_class::NULL.requires_nehalem_cpu?).to be false + end end end