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
8 changes: 8 additions & 0 deletions Library/Homebrew/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4701,6 +4701,14 @@ def resource(name, klass = Resource, &block)
# depends_on "python@3.14" => :no_linkage
# ```
#
# On a `:build` dependency, such as a compiler, `:no_linkage` also lets an
# outdated installed version be used when building from source, whatever
# its `compatibility_version`.
#
# ```ruby
# depends_on "llvm" => [:build, :no_linkage]
# ```
#
# `:recommended` dependencies are built by default.
# But a `--without-...` option is generated to opt-out.
#
Expand Down
7 changes: 6 additions & 1 deletion Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,12 @@ def expand_dependencies_for_formula_once(formula, keepable_build_dependencies, k

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)

if dep.no_linkage?
next if !dep_formula.opt_prefix.exist? || dep.missing_options.present?
Comment thread
MikeMcQuaid marked this conversation as resolved.
else
next unless dep.satisfied?(minimum_compatibility_version: dep_formula.compatibility_version)
end

runtime_dependencies = dep_formula.any_installed_keg&.runtime_dependencies
next if runtime_dependencies.nil?
Expand Down
108 changes: 105 additions & 3 deletions Library/Homebrew/test/formula_installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,22 @@ class Requested < Formula
end

describe "#expand_dependencies_for_formula" do
shared_examples "the old behaviour when building a bottle" do
before { allow(f).to receive(:deps).and_return([build_dep]) }

it "upgrades it" do
expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(build_dep.name)
end

context "with --build-from-source" do
let(:build_from_source_formulae) { [f.full_name] }

it "upgrades it" do
expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(build_dep.name)
end
end
end

it "checks equal dependency satisfaction once per expansion" do
shared_formula = instance_double(Formula, deps: [], name: "shared", full_name: "shared")
shared_dep = Dependency.new("shared")
Expand Down Expand Up @@ -1190,7 +1206,8 @@ class Requested < Formula
end
end
let(:build_bottle) { false }
let(:installer) { described_class.new(f, build_bottle:) }
let(:build_from_source_formulae) { [] }
let(:installer) { described_class.new(f, build_bottle:, build_from_source_formulae:) }

before do
build_dep_formula = formula build_dep.name do
Expand Down Expand Up @@ -1241,12 +1258,97 @@ class Requested < Formula
expect(installer.expand_dependencies_for_formula(f).map(&:name)).not_to include(other_build_dep.name)
end

it "upgrades it with a different compatibility_version" do
allow(build_dep).to receive(:satisfied?).with(minimum_compatibility_version: 1).and_return(false)
allow(f).to receive(:deps).and_return([build_dep])

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

it "upgrades it if its install receipt has no runtime dependencies" do
keg = instance_double(Keg, runtime_dependencies: nil)
allow(build_dep.to_formula).to receive(:any_installed_keg).and_return(keg)
allow(f).to receive(:deps).and_return([build_dep])

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

it "upgrades an outdated runtime dependency with a matching compatibility_version" do
runtime_dep = Dependency.new(build_dep.name)
allow(runtime_dep).to receive(:satisfied?).and_return(false)
allow(runtime_dep).to receive(:satisfied?).with(minimum_compatibility_version: 1).and_return(true)
allow(f).to receive(:deps).and_return([runtime_dep])

expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(runtime_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])
include_examples "the old behaviour when building a bottle"

it "upgrades the dependencies it was built against" do
expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(runtime_dep_formula.name)
end
end
end

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

before do
build_dep_formula = formula build_dep.name do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
end
allow(build_dep_formula).to receive_messages(
compatibility_version: 2,
opt_prefix: instance_double(Pathname, exist?: true),
any_installed_keg: instance_double(Keg, runtime_dependencies: []),
)
stub_formula_loader(build_dep_formula)
allow(build_dep).to receive_messages(satisfied?: false, missing_options: Options.new)
allow(f).to receive(:deps).and_return([build_dep])
allow(installer).to receive_messages(effective_build_options_for: BuildOptions.new(Options.new, Options.new),
install_bottle_for?: false)
end

it "keeps it" do
expect(installer.expand_dependencies_for_formula(f)).to be_empty
end

it "upgrades it if it isn't opt-linked" do
allow(build_dep.to_formula).to receive(:opt_prefix).and_return(instance_double(Pathname, exist?: false))

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

it "upgrades it if it's missing required options" do
allow(build_dep).to receive(:missing_options).and_return(Options.create(["--with-foo"]))

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

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

include_examples "the old behaviour when building a bottle"
end

context "without :build" do
let(:tags) { [:no_linkage] }

it "upgrades it" do
expect(installer.expand_dependencies_for_formula(f).map(&:name)).to include(build_dep.name)
end
end
Expand Down
1 change: 1 addition & 0 deletions docs/Formula-Cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ A `Hash` (e.g. `=>`) adds information to a dependency. Given a string or symbol,
* `:test` means this is only required when running `brew test`.
* `:no_linkage` means the formula does not link against this dependency; it does not change when the dependency is required.
All explicit declarations of the same dependency within a spec and platform must agree on `:no_linkage`; stable, HEAD and different platforms may differ.
On a `:build` dependency, such as a compiler, it also lets an outdated installed version be used when building from source, whatever its `compatibility_version`.
* `:optional` (not allowed in `Homebrew/homebrew-core`) generates an implicit `with-foo` option for the formula. This means that, given `depends_on "foo" => :optional`, the user must pass `--with-foo` to use the dependency.
* `:recommended` (not allowed in `Homebrew/homebrew-core`) generates an implicit `without-foo` option, meaning that the dependency is enabled by default and the user must pass `--without-foo` to disable this dependency. The default description can be overridden using the [`option`](/rubydoc/Formula.html#option-class_method) syntax (in this case, the [`option` declaration](#adding-optional-steps) must precede the dependency):

Expand Down
Loading