Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Library/Homebrew/api/formula_struct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def self.from_hash(formula_hash)
const :bottle_checksums, T::Array[T::Hash[Symbol, T.any(String, Symbol)]], default: []
const :bottle_rebuild, Integer, default: 0
const :caveats, T.nilable(String)
const :compatibility_version, T.nilable(Integer)
const :conflicts, T::Array[[String, T::Hash[Symbol, String]]], default: []
const :deprecate_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
const :desc, String
Expand Down
8 changes: 5 additions & 3 deletions Library/Homebrew/dependency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,14 @@ def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibilit

return true if formula.latest_version_installed?

return false if minimum_version.blank?
return false if minimum_version.blank? && minimum_compatibility_version.blank?

installed_keg = formula.any_installed_keg
return false unless installed_keg

# If the keg name doesn't match, we may have moved from an alias to a full formula and need to upgrade.
return false unless formula.possible_names.include?(installed_keg.name)

installed_version = installed_keg.version

# If both the formula and minimum dependency have a compatibility_version set,
# and they match, the dependency is satisfied regardless of version/revision.
if minimum_compatibility_version.present? && formula.compatibility_version.present?
Expand All @@ -102,6 +100,10 @@ def installed?(minimum_version: nil, minimum_revision: nil, minimum_compatibilit
formula.compatibility_version == minimum_compatibility_version
end

return false if minimum_version.blank?

installed_version = installed_keg.version

# Tabs prior to 4.1.18 did not have revision or pkg_version fields.
# As a result, we have to be more conversative when we do not have
# a minimum revision from the tab and assume that if the formula has a
Expand Down
44 changes: 42 additions & 2 deletions Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -783,8 +783,14 @@ def expand_requirements
unsatisfied_reqs
end

sig { params(formula: Formula).returns(T::Array[Dependency]) }
def expand_dependencies_for_formula(formula)
sig {
params(
formula: Formula,
keepable_build_dependencies: T.nilable(T::Array[String]),
kept_build_dependencies: T::Hash[Formula, T::Array[String]],
).returns(T::Array[Dependency])
}
def expand_dependencies_for_formula_once(formula, keepable_build_dependencies, kept_build_dependencies)
# Cache for this expansion only. FormulaInstaller has a lot of inputs which can alter expansion.
cache_key = "FormulaInstaller-#{formula.full_name}-#{Time.now.to_f}"
formula_cache = T.let({}, T::Hash[Dependency, Formula])
Expand Down Expand Up @@ -819,7 +825,41 @@ def expand_dependencies_for_formula(formula)
end

next Dependable::SKIP if satisfied
next if !dep.build? || build_bottle?

dep_formula = dep.to_formula
next if keepable_build_dependencies&.exclude?(dep_formula.full_name)
next unless dep.satisfied?(minimum_compatibility_version: dep_formula.compatibility_version)

runtime_dependencies = dep_formula.any_installed_keg&.runtime_dependencies
next if runtime_dependencies.nil?

# Keep the installed build dependency as-is, including the dependencies it was built against.
kept_build_dependencies[dep_formula] = runtime_dependencies.map { |runtime_dep| runtime_dep["full_name"] }
Dependable::PRUNE
end
end

sig { params(formula: Formula).returns(T::Array[Dependency]) }
def expand_dependencies_for_formula(formula)
kept_build_dependencies = T.let({}, T::Hash[Formula, T::Array[String]])
deps = expand_dependencies_for_formula_once(formula, nil, kept_build_dependencies)

# Each pass keeps fewer build dependencies than the last, so at worst this ends with none kept.
kept_build_dependencies.size.times do
incompatible_upgrades = deps.filter_map do |dep|
dep_formula = dep.to_formula
dep_formula.full_name unless dep.installed?(minimum_compatibility_version: dep_formula.compatibility_version)
end << formula.full_name
keepable_build_dependencies = kept_build_dependencies.filter_map do |kept, runtime_dependency_names|
kept.full_name unless runtime_dependency_names.intersect?(incompatible_upgrades)
end
return deps if keepable_build_dependencies.size == kept_build_dependencies.size

kept_build_dependencies = {}
deps = expand_dependencies_for_formula_once(formula, keepable_build_dependencies, kept_build_dependencies)
end
deps
end

sig { returns(T::Array[Dependency]) }
Expand Down
1 change: 1 addition & 0 deletions Library/Homebrew/formulary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def self.load_formula_from_struct!(name, formula_struct, api_source:, tap_git_he
license formula_struct.license
revision formula_struct.revision
version_scheme formula_struct.version_scheme
compatibility_version formula_struct.compatibility_version

if formula_struct.stable?
stable do
Expand Down
25 changes: 25 additions & 0 deletions Library/Homebrew/test/dependency_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,31 @@
end
end

describe "Dependency#installed? with minimum_compatibility_version" do
subject(:dependency) { described_class.new("foo") }

let(:keg) { instance_double(Keg, name: "foo") }

before do
allow(dependency).to receive(:to_installed_formula).and_return(
instance_double(Formula, opt_prefix: instance_double(Pathname, exist?: true),
latest_version_installed?: false, any_installed_keg: keg,
possible_names: ["foo"], compatibility_version: 1),
)
allow(Tab).to receive(:for_keg).with(keg).and_return(
instance_double(Tab, source: { "versions" => { "compatibility_version" => 1 } }),
)
end

it "is true for an outdated keg with a matching compatibility_version" do
expect(dependency.installed?(minimum_compatibility_version: 1)).to be(true)
end

it "is false for an outdated keg with a different compatibility_version" do
expect(dependency.installed?(minimum_compatibility_version: 2)).to be(false)
end
end

describe "Dependency#satisfied? with bottle_os_version" do
subject(:dependency) { described_class.new("foo") }

Expand Down
77 changes: 77 additions & 0 deletions Library/Homebrew/test/formula_installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,83 @@ class Requested < Formula

expect(deps.map(&:name)).to eq(%w[shared first-parent second-parent])
end

context "with an outdated build dependency with a matching compatibility_version" do
let(:build_dep) { Dependency.new("homebrew-compatible-build-dependency", [:build]) }
let(:runtime_dep_formula) do
formula "homebrew-build-dependency-runtime-dependency" do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
end
end
let(:f) do
formula "homebrew-keep-compatible-build-dependency" do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
end
end
let(:build_bottle) { false }
let(:installer) { described_class.new(f, build_bottle:) }

before do
build_dep_formula = formula build_dep.name do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
end
keg = instance_double(Keg, runtime_dependencies: [{ "full_name" => runtime_dep_formula.full_name }])
allow(build_dep_formula).to receive_messages(
compatibility_version: 1,
deps: [Dependency.new(runtime_dep_formula.name)],
any_installed_keg: keg,
)
stub_formula_loader(build_dep_formula)
stub_formula_loader(runtime_dep_formula)
allow(build_dep).to receive(:satisfied?).and_return(false)
allow(build_dep).to receive(:satisfied?).with(minimum_compatibility_version: 1).and_return(true)
allow(installer).to receive_messages(effective_build_options_for: BuildOptions.new(Options.new, Options.new),
install_bottle_for?: false)
end

it "keeps it and the dependencies it was built against" do
allow(f).to receive(:deps).and_return([build_dep])

expect(installer.expand_dependencies_for_formula(f)).to be_empty
end

it "upgrades it if a dependency it was built against is upgraded" do
allow(f).to receive(:deps).and_return([build_dep, Dependency.new(runtime_dep_formula.name)])

expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(build_dep.name)
end

it "keeps other compatible build dependencies if one is upgraded" do
other_build_dep = Dependency.new("homebrew-other-compatible-build-dependency", [:build])
other_build_dep_formula = formula other_build_dep.name do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
end
allow(other_build_dep_formula).to receive_messages(
compatibility_version: 1,
any_installed_keg: instance_double(Keg, runtime_dependencies: []),
)
stub_formula_loader(other_build_dep_formula)
allow(other_build_dep).to receive(:satisfied?).and_return(false)
allow(other_build_dep).to receive(:satisfied?).with(minimum_compatibility_version: 1).and_return(true)
allow(f).to receive(:deps).and_return([build_dep, other_build_dep, Dependency.new(runtime_dep_formula.name)])

expect(installer.expand_dependencies_for_formula(f).map(&:name)).not_to include(other_build_dep.name)
end

context "when building a bottle" do
let(:build_bottle) { true }

it "upgrades it" do
allow(f).to receive(:deps).and_return([build_dep])

expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(build_dep.name)
end
end
end
end

describe "versioned keg-only linking defaults" do
Expand Down
7 changes: 7 additions & 0 deletions Library/Homebrew/test/formulary_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,13 @@ def formula_json_contents(extra_items = {})
expect(formula.loaded_from_internal_api?).to be true
end

it "returns a Formula with a compatibility_version" do
allow(Homebrew::API::Formula).to receive(:all_formulae)
.and_return formula_json_contents("compatibility_version" => 2)

expect(described_class.factory(formula_name).compatibility_version).to eq 2
end

it "returns a Formula with no_linkage dependencies" do
allow(Homebrew::API::Formula).to receive(:all_formulae)
.and_return formula_json_contents("no_linkage_dependencies" => ["dep"])
Expand Down
Loading