diff --git a/Library/Homebrew/dependency.rb b/Library/Homebrew/dependency.rb index 30189a48fef27..5d6c0173da9d0 100644 --- a/Library/Homebrew/dependency.rb +++ b/Library/Homebrew/dependency.rb @@ -43,10 +43,15 @@ def to_formula(prefer_stub: false) end sig { - params(minimum_version: T.nilable(Version), minimum_revision: T.nilable(Integer), - minimum_compatibility_version: T.nilable(Integer)).returns(T::Boolean) + params( + minimum_version: T.nilable(Version), + minimum_revision: T.nilable(Integer), + minimum_compatibility_version: T.nilable(Integer), + bottle_os_version: T.nilable(String), + ).returns(T::Boolean) } - def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibility_version: nil) + def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibility_version: nil, + bottle_os_version: nil) formula = begin to_formula(prefer_stub: true) rescue FormulaUnavailableError @@ -95,8 +100,8 @@ def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibilit end def satisfied?(inherited_options = [], minimum_version: nil, minimum_revision: nil, - minimum_compatibility_version: nil) - installed?(minimum_version:, minimum_revision:, minimum_compatibility_version:) && + minimum_compatibility_version: nil, bottle_os_version: nil) + installed?(minimum_version:, minimum_revision:, minimum_compatibility_version:, bottle_os_version:) && missing_options(inherited_options).empty? end @@ -277,25 +282,39 @@ def hash end sig { - params(minimum_version: T.nilable(Version), minimum_revision: T.nilable(Integer), - minimum_compatibility_version: T.nilable(Integer)).returns(T::Boolean) + params( + minimum_version: T.nilable(Version), + minimum_revision: T.nilable(Integer), + minimum_compatibility_version: T.nilable(Integer), + bottle_os_version: T.nilable(String), + ).returns(T::Boolean) } - def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibility_version: nil) - use_macos_install? || super + def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibility_version: nil, + bottle_os_version: nil) + use_macos_install?(bottle_os_version:) || super end - sig { returns(T::Boolean) } - def use_macos_install? + sig { params(bottle_os_version: T.nilable(String)).returns(T::Boolean) } + def use_macos_install?(bottle_os_version: nil) # Check whether macOS is new enough for dependency to not be required. if Homebrew::SimulateSystem.simulating_or_running_on_macos? - # Assume the oldest macOS version when simulating a generic macOS version - return true if Homebrew::SimulateSystem.current_os == :macos && !bounds.key?(:since) - - if Homebrew::SimulateSystem.current_os != :macos - current_os = MacOSVersion.from_symbol(Homebrew::SimulateSystem.current_os) - since_os = MacOSVersion.from_symbol(bounds[:since]) if bounds.key?(:since) - return true if current_os >= since_os + # If there's no since bound, the dependency is always available from macOS + since_os_bounds = bounds[:since] + return true if since_os_bounds.blank? + + # When installing a bottle built on an older macOS version, use that version + # to determine if the dependency should come from macOS or Homebrew + effective_os = if bottle_os_version.present? && + bottle_os_version.start_with?("macOS ") + # bottle_os_version is a string like "14" for Sonoma, "15" for Sequoia + # Convert it to a MacOS version symbol for comparison + MacOSVersion.new(bottle_os_version.delete_prefix("macOS ")) + else + MacOSVersion.from_symbol(Homebrew::SimulateSystem.current_os) end + + since_os = MacOSVersion.from_symbol(since_os_bounds) + return true if effective_os >= since_os end false diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 305f51c55f42f..3e99e5a21e02a 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -138,6 +138,7 @@ def initialize( @poured_bottle = T.let(false, T::Boolean) @start_time = T.let(nil, T.nilable(Time)) @bottle_tab_runtime_dependencies = T.let({}.freeze, T::Hash[String, T::Hash[String, String]]) + @bottle_built_os_version = T.let(nil, T.nilable(String)) @hold_locks = T.let(false, T::Boolean) @show_summary_heading = T.let(false, T::Boolean) @etc_var_preinstall = T.let([], T::Array[Pathname]) @@ -338,12 +339,22 @@ def prelude Tab.clear_cache - # Setup bottle_tab_runtime_dependencies for compute_dependencies + # Setup bottle_tab_runtime_dependencies for compute_dependencies and + # bottle_built_os_version for dependency resolution. begin - @bottle_tab_runtime_dependencies = formula.bottle_tab_attributes - .fetch("runtime_dependencies", []).then { |deps| deps || [] } - .each_with_object({}) { |dep, h| h[dep["full_name"]] = dep } - .freeze + bottle_tab_attributes = formula.bottle_tab_attributes + @bottle_tab_runtime_dependencies = bottle_tab_attributes + .fetch("runtime_dependencies", []).then { |deps| deps || [] } + .each_with_object({}) { |dep, h| h[dep["full_name"]] = dep } + .freeze + + if (bottle_tag = formula.bottle_for_tag(Utils::Bottles.tag)&.tag) && + bottle_tag.system != :all + # Extract the OS version the bottle was built on. + # This ensures that when installing older bottles (e.g. Sonoma bottle on Sequoia), + # we resolve dependencies according to the bottle's built OS, not the current OS. + @bottle_built_os_version = bottle_tab_attributes.dig("built_on", "os_version") + end rescue Resource::BottleManifest::Error # If we can't get the bottle manifest, assume a full dependencies install. end @@ -772,14 +783,14 @@ def expand_dependencies_for_formula(formula, inherited_options) keep_build_test ||= dep.build? && !install_bottle_for?(dependent, build) && (formula.head? || !dependent.latest_version_installed?) - bottle_runtime_version = @bottle_tab_runtime_dependencies.dig(dep.name, "version").presence - bottle_runtime_version = Version.new(bottle_runtime_version) if bottle_runtime_version - bottle_runtime_revision = @bottle_tab_runtime_dependencies.dig(dep.name, "revision") + minimum_version = @bottle_tab_runtime_dependencies.dig(dep.name, "version").presence + minimum_version = Version.new(minimum_version) if minimum_version + minimum_revision = @bottle_tab_runtime_dependencies.dig(dep.name, "revision") + bottle_os_version = @bottle_built_os_version if dep.prune_from_option?(build) || ((dep.build? || dep.test?) && !keep_build_test) Dependency.prune - elsif dep.satisfied?(inherited_options[dep.name], minimum_version: bottle_runtime_version, - minimum_revision: bottle_runtime_revision) + elsif dep.satisfied?(inherited_options[dep.name], minimum_version:, minimum_revision:, bottle_os_version:) Dependency.skip end end diff --git a/Library/Homebrew/test/dependency_spec.rb b/Library/Homebrew/test/dependency_spec.rb index 2fbb9cea55075..846ba1fd59ed5 100644 --- a/Library/Homebrew/test/dependency_spec.rb +++ b/Library/Homebrew/test/dependency_spec.rb @@ -122,4 +122,40 @@ expect(dep).not_to be_test end end + + describe "Dependency#installed? with bottle_os_version" do + subject(:dependency) { described_class.new("foo") } + + it "accepts macOS bottle_os_version parameter" do + expect { dependency.installed?(bottle_os_version: "macOS 14") }.not_to raise_error + end + + it "accepts Ubuntu bottle_os_version parameter" do + expect { dependency.installed?(bottle_os_version: "Ubuntu 22.04") }.not_to raise_error + end + end + + describe "Dependency#satisfied? with bottle_os_version" do + subject(:dependency) { described_class.new("foo") } + + it "accepts bottle_os_version parameter" do + expect { dependency.satisfied?([], bottle_os_version: "macOS 14") }.not_to raise_error + end + + it "accepts Ubuntu bottle_os_version parameter" do + expect { dependency.installed?(bottle_os_version: "Ubuntu 22.04") }.not_to raise_error + end + end + + describe "UsesFromMacOSDependency#installed? with bottle_os_version" do + subject(:uses_from_macos) { described_class.new("foo", bounds: { since: :sonoma }) } + + it "accepts macOS bottle_os_version parameter" do + expect { uses_from_macos.installed?(bottle_os_version: "macOS 14") }.not_to raise_error + end + + it "accepts Ubuntu bottle_os_version parameter" do + expect { uses_from_macos.installed?(bottle_os_version: "Ubuntu 22.04") }.not_to raise_error + end + end end