Skip to content

Commit 443e722

Browse files
CopilotMikeMcQuaid
andcommitted
Implement bottle OS-aware dependency resolution for uses_from_macos
Co-authored-by: MikeMcQuaid <[email protected]>
1 parent 3269773 commit 443e722

File tree

3 files changed

+71
-21
lines changed

3 files changed

+71
-21
lines changed

Library/Homebrew/dependency.rb

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ def to_formula(prefer_stub: false)
4242
formula
4343
end
4444

45-
sig { params(minimum_version: T.nilable(Version), minimum_revision: T.nilable(Integer)).returns(T::Boolean) }
46-
def installed?(minimum_version: nil, minimum_revision: nil)
45+
sig {
46+
params(
47+
minimum_version: T.nilable(Version),
48+
minimum_revision: T.nilable(Integer),
49+
bottle_os_version: T.nilable(String),
50+
).returns(T::Boolean)
51+
}
52+
def installed?(minimum_version: nil, minimum_revision: nil, bottle_os_version: nil)
4753
formula = begin
4854
to_formula(prefer_stub: true)
4955
rescue FormulaUnavailableError
@@ -80,8 +86,8 @@ def installed?(minimum_version: nil, minimum_revision: nil)
8086
end
8187
end
8288

83-
def satisfied?(inherited_options = [], minimum_version: nil, minimum_revision: nil)
84-
installed?(minimum_version:, minimum_revision:) &&
89+
def satisfied?(inherited_options = [], minimum_version: nil, minimum_revision: nil, bottle_os_version: nil)
90+
installed?(minimum_version:, minimum_revision:, bottle_os_version:) &&
8591
missing_options(inherited_options).empty?
8692
end
8793

@@ -261,22 +267,37 @@ def hash
261267
[name, tags, bounds].hash
262268
end
263269

264-
sig { params(minimum_version: T.nilable(Version), minimum_revision: T.nilable(Integer)).returns(T::Boolean) }
265-
def installed?(minimum_version: nil, minimum_revision: nil)
266-
use_macos_install? || super
270+
sig {
271+
params(
272+
minimum_version: T.nilable(Version),
273+
minimum_revision: T.nilable(Integer),
274+
bottle_os_version: T.nilable(String),
275+
).returns(T::Boolean)
276+
}
277+
def installed?(minimum_version: nil, minimum_revision: nil, bottle_os_version: nil)
278+
use_macos_install?(bottle_os_version:) || super
267279
end
268280

269-
sig { returns(T::Boolean) }
270-
def use_macos_install?
281+
sig { params(bottle_os_version: T.nilable(String)).returns(T::Boolean) }
282+
def use_macos_install?(bottle_os_version: nil)
271283
# Check whether macOS is new enough for dependency to not be required.
272284
if Homebrew::SimulateSystem.simulating_or_running_on_macos?
273-
# Assume the oldest macOS version when simulating a generic macOS version
274-
return true if Homebrew::SimulateSystem.current_os == :macos && !bounds.key?(:since)
285+
# If there's no since bound, the dependency is always available from macOS
286+
return true if !bounds.key?(:since) && Homebrew::SimulateSystem.current_os == :macos
287+
288+
# When installing a bottle built on an older macOS version, use that version
289+
# to determine if the dependency should come from macOS or Homebrew
290+
effective_os = if bottle_os_version.present? && Homebrew::SimulateSystem.current_os != :macos
291+
# bottle_os_version is a string like "14" for Sonoma, "15" for Sequoia
292+
# Convert it to a MacOS version symbol for comparison
293+
MacOSVersion.new(bottle_os_version)
294+
else
295+
MacOSVersion.from_symbol(Homebrew::SimulateSystem.current_os)
296+
end
275297

276-
if Homebrew::SimulateSystem.current_os != :macos
277-
current_os = MacOSVersion.from_symbol(Homebrew::SimulateSystem.current_os)
278-
since_os = MacOSVersion.from_symbol(bounds[:since]) if bounds.key?(:since)
279-
return true if current_os >= since_os
298+
if bounds.key?(:since)
299+
since_os = MacOSVersion.from_symbol(bounds[:since])
300+
return true if effective_os >= since_os
280301
end
281302
end
282303

Library/Homebrew/formula_installer.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class FormulaInstaller
4040
sig { returns(T::Hash[String, T::Hash[String, String]]) }
4141
attr_reader :bottle_tab_runtime_dependencies
4242

43+
sig { returns(T.nilable(String)) }
44+
attr_reader :bottle_built_os_version
45+
4346
sig { returns(Options) }
4447
attr_accessor :options
4548

@@ -138,6 +141,7 @@ def initialize(
138141
@poured_bottle = T.let(false, T::Boolean)
139142
@start_time = T.let(nil, T.nilable(Time))
140143
@bottle_tab_runtime_dependencies = T.let({}.freeze, T::Hash[String, T::Hash[String, String]])
144+
@bottle_built_os_version = T.let(nil, T.nilable(String))
141145
@hold_locks = T.let(false, T::Boolean)
142146
@show_summary_heading = T.let(false, T::Boolean)
143147
@etc_var_preinstall = T.let([], T::Array[Pathname])
@@ -340,10 +344,18 @@ def prelude
340344

341345
# Setup bottle_tab_runtime_dependencies for compute_dependencies
342346
begin
343-
@bottle_tab_runtime_dependencies = formula.bottle_tab_attributes
344-
.fetch("runtime_dependencies", []).then { |deps| deps || [] }
345-
.each_with_object({}) { |dep, h| h[dep["full_name"]] = dep }
346-
.freeze
347+
bottle_tab_attrs = formula.bottle_tab_attributes
348+
@bottle_tab_runtime_dependencies = bottle_tab_attrs
349+
.fetch("runtime_dependencies", []).then { |deps| deps || [] }
350+
.each_with_object({}) { |dep, h| h[dep["full_name"]] = dep }
351+
.freeze
352+
# Extract the OS version the bottle was built on.
353+
# Only use this for bottles built on a different OS version than the current OS.
354+
# This ensures that when installing older bottles (e.g., sonoma bottle on sequoia),
355+
# we resolve dependencies according to the bottle's built OS, not the current OS.
356+
bottle_os_version = bottle_tab_attrs.dig("built_on", "os_version")
357+
current_os_version = OS_VERSION
358+
@bottle_built_os_version = bottle_os_version if bottle_os_version != current_os_version
347359
rescue Resource::BottleManifest::Error
348360
# If we can't get the bottle manifest, assume a full dependencies install.
349361
end
@@ -778,8 +790,9 @@ def expand_dependencies_for_formula(formula, inherited_options)
778790

779791
if dep.prune_from_option?(build) || ((dep.build? || dep.test?) && !keep_build_test)
780792
Dependency.prune
781-
elsif dep.satisfied?(inherited_options[dep.name], minimum_version: bottle_runtime_version,
782-
minimum_revision: bottle_runtime_revision)
793+
elsif dep.satisfied?(inherited_options[dep.name], minimum_version: bottle_runtime_version,
794+
minimum_revision: bottle_runtime_revision,
795+
bottle_os_version:)
783796
Dependency.skip
784797
end
785798
end

Library/Homebrew/test/dependency_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,20 @@
122122
expect(dep).not_to be_test
123123
end
124124
end
125+
126+
describe "Dependency#installed? with bottle_os_version" do
127+
it "accepts bottle_os_version parameter" do
128+
dep = described_class.new("foo")
129+
# Should not raise an error with the new parameter
130+
expect { dep.installed?(bottle_os_version: "14") }.not_to raise_error
131+
end
132+
end
133+
134+
describe "Dependency#satisfied? with bottle_os_version" do
135+
it "accepts bottle_os_version parameter" do
136+
dep = described_class.new("foo")
137+
# Should not raise an error with the new parameter
138+
expect { dep.satisfied?([], bottle_os_version: "14") }.not_to raise_error
139+
end
140+
end
125141
end

0 commit comments

Comments
 (0)