install glibc/gcc automatically if too old.
Right now this is done through the gcc@5 formula.
See 9692318ca6/Formula/gcc%405.rb (L33)
This is fragile because when we will migrate to gcc@11
we have to think about migrating the installation from one gcc formula to another..
Also, not having the right glibc version results in a non-functional brew
installation on an older Linux: the glibc installation needs
to be done by brew, and not by a workaround in a specific formula
Co-Authored-By: Mike McQuaid <mike@mikemcquaid.com>
Co-Authored-By: Bo Anderson <mail@boanderson.me>
Co-Authored-By: Shaun Jackman <sjackman@gmail.com>
This commit is contained in:
parent
2415339124
commit
d271614872
@ -28,6 +28,8 @@ class DependencyCollector
|
||||
def initialize
|
||||
@deps = Dependencies.new
|
||||
@requirements = Requirements.new
|
||||
|
||||
init_global_dep_tree_if_needed!
|
||||
end
|
||||
|
||||
def initialize_copy(other)
|
||||
@ -68,6 +70,12 @@ class DependencyCollector
|
||||
parse_spec(spec, Array(tags))
|
||||
end
|
||||
|
||||
sig { params(related_formula_names: T::Array[String]).returns(T.nilable(Dependency)) }
|
||||
def gcc_dep_if_needed(related_formula_names); end
|
||||
|
||||
sig { params(related_formula_names: T::Array[String]).returns(T.nilable(Dependency)) }
|
||||
def glibc_dep_if_needed(related_formula_names); end
|
||||
|
||||
def git_dep_if_needed(tags)
|
||||
return if Utils::Git.available?
|
||||
|
||||
@ -110,6 +118,9 @@ class DependencyCollector
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def init_global_dep_tree_if_needed!; end
|
||||
|
||||
def parse_spec(spec, tags)
|
||||
case spec
|
||||
when String
|
||||
|
@ -98,6 +98,16 @@ class DevelopmentTools
|
||||
@gcc_version = {}
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def build_system_too_old?
|
||||
false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def system_gcc_too_old?
|
||||
false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def ca_file_handles_most_https_certificates?
|
||||
# The system CA file is too old for some modern HTTPS certificates on
|
||||
|
@ -1,4 +1,8 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "extend/os/mac/dependency_collector" if OS.mac?
|
||||
if OS.mac?
|
||||
require "extend/os/mac/dependency_collector"
|
||||
elsif OS.linux?
|
||||
require "extend/os/linux/dependency_collector"
|
||||
end
|
||||
|
76
Library/Homebrew/extend/os/linux/dependency_collector.rb
Normal file
76
Library/Homebrew/extend/os/linux/dependency_collector.rb
Normal file
@ -0,0 +1,76 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "os/linux/glibc"
|
||||
|
||||
class DependencyCollector
|
||||
extend T::Sig
|
||||
|
||||
undef gcc_dep_if_needed
|
||||
undef glibc_dep_if_needed
|
||||
undef init_global_dep_tree_if_needed!
|
||||
|
||||
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
|
||||
def gcc_dep_if_needed(related_formula_names)
|
||||
return unless DevelopmentTools.system_gcc_too_old?
|
||||
return if related_formula_names.include?(GCC)
|
||||
return if global_dep_tree[GCC]&.intersect?(related_formula_names)
|
||||
return if global_dep_tree[GLIBC]&.intersect?(related_formula_names) # gcc depends on glibc
|
||||
|
||||
Dependency.new(GCC)
|
||||
end
|
||||
|
||||
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
|
||||
def glibc_dep_if_needed(related_formula_names)
|
||||
return unless OS::Linux::Glibc.below_ci_version?
|
||||
return if global_dep_tree[GLIBC]&.intersect?(related_formula_names)
|
||||
|
||||
Dependency.new(GLIBC)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
GLIBC = "glibc"
|
||||
GCC = CompilerSelector.preferred_gcc.freeze
|
||||
|
||||
# Use class variables to avoid this expensive logic needing to be done more
|
||||
# than once.
|
||||
# rubocop:disable Style/ClassVars
|
||||
@@global_dep_tree = {}
|
||||
|
||||
sig { void }
|
||||
def init_global_dep_tree_if_needed!
|
||||
return unless DevelopmentTools.build_system_too_old?
|
||||
return if @@global_dep_tree.present?
|
||||
|
||||
# Defined in precedence order (gcc depends on glibc).
|
||||
global_deps = [GLIBC, GCC].freeze
|
||||
|
||||
@@global_dep_tree = global_deps.to_h { |name| [name, Set.new([name])] }
|
||||
|
||||
global_deps.each do |global_dep_name|
|
||||
# This is an arbitrary number picked based on testing the current tree
|
||||
# depth and just to ensure that this doesn't loop indefinitely if we
|
||||
# introduce a circular dependency by mistake.
|
||||
maximum_tree_depth = 10
|
||||
current_tree_depth = 0
|
||||
|
||||
deps = Formula[global_dep_name].deps
|
||||
while deps.present?
|
||||
current_tree_depth += 1
|
||||
if current_tree_depth > maximum_tree_depth
|
||||
raise "maximum tree depth (#{maximum_tree_depth}) exceeded calculating #{global_dep_name} dependency tree!"
|
||||
end
|
||||
|
||||
@@global_dep_tree[global_dep_name].merge(deps.map(&:name))
|
||||
deps = deps.flat_map { |dep| dep.to_formula.deps }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(T::Hash[String, T::Set[String]]) }
|
||||
def global_dep_tree
|
||||
@@global_dep_tree
|
||||
end
|
||||
# rubocop:enable Style/ClassVars
|
||||
end
|
@ -21,6 +21,18 @@ class DevelopmentTools
|
||||
:gcc
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def build_system_too_old?
|
||||
return @build_system_too_old if defined? @build_system_too_old
|
||||
|
||||
@build_system_too_old = (system_gcc_too_old? || OS::Linux::Glibc.below_ci_version?)
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def system_gcc_too_old?
|
||||
gcc_version("gcc") < OS::LINUX_GCC_CI_VERSION
|
||||
end
|
||||
|
||||
sig { returns(T::Hash[String, T.nilable(String)]) }
|
||||
def build_system_info
|
||||
generic_build_system_info.merge({
|
||||
|
@ -5,6 +5,7 @@ class Formula
|
||||
undef shared_library
|
||||
undef loader_path
|
||||
undef deuniversalize_machos
|
||||
undef add_global_deps_to_spec
|
||||
|
||||
sig { params(name: String, version: T.nilable(T.any(String, Integer))).returns(String) }
|
||||
def shared_library(name, version = nil)
|
||||
@ -23,4 +24,20 @@ class Formula
|
||||
|
||||
sig { params(targets: T.nilable(T.any(Pathname, String))).void }
|
||||
def deuniversalize_machos(*targets); end
|
||||
|
||||
sig { params(spec: SoftwareSpec).void }
|
||||
def add_global_deps_to_spec(spec)
|
||||
@global_deps ||= begin
|
||||
dependency_collector = spec.dependency_collector
|
||||
related_formula_names = Set.new([
|
||||
name,
|
||||
*versioned_formulae_names,
|
||||
])
|
||||
[
|
||||
dependency_collector.gcc_dep_if_needed(related_formula_names),
|
||||
dependency_collector.glibc_dep_if_needed(related_formula_names),
|
||||
].compact.freeze
|
||||
end
|
||||
@global_deps.each { |dep| spec.dependency_collector.add(dep) }
|
||||
end
|
||||
end
|
||||
|
@ -80,9 +80,5 @@ class LinkageChecker
|
||||
@unwanted_system_dylibs = @system_dylibs.reject do |s|
|
||||
SYSTEM_LIBRARY_ALLOWLIST.include? File.basename(s)
|
||||
end
|
||||
# FIXME: Remove this when these dependencies are injected correctly (e.g. through `DependencyCollector`)
|
||||
# See discussion at
|
||||
# https://github.com/Homebrew/brew/pull/13577
|
||||
@undeclared_deps -= [CompilerSelector.preferred_gcc, "glibc", "gcc"]
|
||||
end
|
||||
end
|
||||
|
@ -262,9 +262,13 @@ class Formula
|
||||
return unless spec.url
|
||||
|
||||
spec.owner = self
|
||||
add_global_deps_to_spec(spec)
|
||||
instance_variable_set("@#{name}", spec)
|
||||
end
|
||||
|
||||
sig { params(spec: SoftwareSpec).void }
|
||||
def add_global_deps_to_spec(spec); end
|
||||
|
||||
def determine_active_spec(requested)
|
||||
spec = send(requested) || stable || head
|
||||
spec || raise(FormulaSpecificationError, "formulae require at least a URL")
|
||||
@ -443,7 +447,7 @@ class Formula
|
||||
# Returns any `@`-versioned formulae names for any formula (including versioned formulae).
|
||||
sig { returns(T::Array[String]) }
|
||||
def versioned_formulae_names
|
||||
@versioned_formulae_names ||= Pathname.glob(path.to_s.gsub(/(@[\d.]+)?\.rb$/, "@*.rb")).map do |versioned_path|
|
||||
Pathname.glob(path.to_s.gsub(/(@[\d.]+)?\.rb$/, "@*.rb")).map do |versioned_path|
|
||||
next if versioned_path == path
|
||||
|
||||
versioned_path.basename(".rb").to_s
|
||||
@ -453,7 +457,7 @@ class Formula
|
||||
# Returns any `@`-versioned Formula objects for any Formula (including versioned formulae).
|
||||
sig { returns(T::Array[Formula]) }
|
||||
def versioned_formulae
|
||||
@versioned_formulae ||= versioned_formulae_names.map do |name|
|
||||
versioned_formulae_names.map do |name|
|
||||
Formula[name]
|
||||
rescue FormulaUnavailableError
|
||||
nil
|
||||
|
@ -581,11 +581,9 @@ class FormulaInstaller
|
||||
end
|
||||
|
||||
def expand_dependencies_for_formula(formula, inherited_options)
|
||||
any_bottle_used = false
|
||||
|
||||
# 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}"
|
||||
expanded_deps = Dependency.expand(formula, cache_key: cache_key) do |dependent, dep|
|
||||
Dependency.expand(formula, cache_key: cache_key) do |dependent, dep|
|
||||
inherited_options[dep.name] |= inherited_options_for(dep)
|
||||
build = effective_build_options_for(
|
||||
dependent,
|
||||
@ -601,36 +599,14 @@ class FormulaInstaller
|
||||
Dependency.prune
|
||||
elsif dep.satisfied?(inherited_options[dep.name])
|
||||
Dependency.skip
|
||||
else
|
||||
any_bottle_used ||= install_bottle_for?(dep.to_formula, build)
|
||||
end
|
||||
end
|
||||
|
||||
[expanded_deps, any_bottle_used]
|
||||
end
|
||||
|
||||
def expand_dependencies
|
||||
inherited_options = Hash.new { |hash, key| hash[key] = Options.new }
|
||||
any_bottle_used = pour_bottle?
|
||||
|
||||
expanded_deps, any_dep_bottle_used = expand_dependencies_for_formula(formula, inherited_options)
|
||||
any_bottle_used ||= any_dep_bottle_used
|
||||
|
||||
# We require some dependencies (glibc, GCC 5, etc.) if binaries were built.
|
||||
# Native binaries shouldn't exist in cross-platform `all` bottles.
|
||||
if any_bottle_used && !formula.bottled?(:all) && !Keg.bottle_dependencies.empty?
|
||||
all_bottle_deps = Keg.bottle_dependencies.flat_map do |bottle_dep|
|
||||
bottle_dep.recursive_dependencies.map(&:name) + [bottle_dep.name]
|
||||
end
|
||||
|
||||
if all_bottle_deps.exclude?(formula.name)
|
||||
bottle_deps = Keg.bottle_dependencies.flat_map do |bottle_dep|
|
||||
expanded_bottle_deps, = expand_dependencies_for_formula(bottle_dep, inherited_options)
|
||||
expanded_bottle_deps
|
||||
end
|
||||
expanded_deps = Dependency.merge_repeats(bottle_deps + expanded_deps)
|
||||
end
|
||||
end
|
||||
expanded_deps = expand_dependencies_for_formula(formula, inherited_options)
|
||||
|
||||
expanded_deps.map { |dep| [dep, inherited_options[dep.name]] }
|
||||
end
|
||||
|
@ -366,17 +366,6 @@ class Keg
|
||||
def self.file_linked_libraries(_file, _string)
|
||||
[]
|
||||
end
|
||||
|
||||
def self.bottle_dependencies
|
||||
return [] unless Homebrew::SimulateSystem.simulating_or_running_on_linux?
|
||||
|
||||
@bottle_dependencies ||= begin
|
||||
formulae = []
|
||||
gcc = Formulary.factory(CompilerSelector.preferred_gcc)
|
||||
formulae << gcc if DevelopmentTools.gcc_version("gcc") < gcc.version.to_i
|
||||
formulae
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require "extend/os/keg_relocate"
|
||||
|
@ -44,6 +44,11 @@ module OS
|
||||
def below_minimum_version?
|
||||
system_version < minimum_version
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def below_ci_version?
|
||||
system_version < LINUX_GLIBC_CI_VERSION
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -50,6 +50,11 @@ class SoftwareSpec
|
||||
@uses_from_macos_elements = []
|
||||
end
|
||||
|
||||
def initialize_copy(other)
|
||||
super
|
||||
@dependency_collector = @dependency_collector.dup
|
||||
end
|
||||
|
||||
def owner=(owner)
|
||||
@name = owner.name
|
||||
@full_name = owner.full_name
|
||||
|
@ -6,7 +6,20 @@ require "installed_dependents"
|
||||
describe InstalledDependents do
|
||||
include FileUtils
|
||||
|
||||
def setup_test_keg(name, version)
|
||||
def stub_formula(name, version = "1.0", &block)
|
||||
f = formula(name) do
|
||||
url "#{name}-#{version}"
|
||||
|
||||
instance_eval(&block) if block
|
||||
end
|
||||
stub_formula_loader f
|
||||
stub_formula_loader f, "homebrew/core/#{f}"
|
||||
f
|
||||
end
|
||||
|
||||
def setup_test_keg(name, version, &block)
|
||||
stub_formula(name, version, &block)
|
||||
|
||||
path = HOMEBREW_CELLAR/name/version
|
||||
(path/"bin").mkpath
|
||||
|
||||
@ -18,27 +31,25 @@ describe InstalledDependents do
|
||||
end
|
||||
|
||||
let!(:keg) { setup_test_keg("foo", "1.0") }
|
||||
let!(:keg_only_keg) do
|
||||
setup_test_keg("foo-keg-only", "1.0") do
|
||||
keg_only "a good reason"
|
||||
end
|
||||
end
|
||||
|
||||
describe "::find_some_installed_dependents" do
|
||||
def stub_formula_name(name)
|
||||
f = formula(name) { url "foo-1.0" }
|
||||
stub_formula_loader f
|
||||
stub_formula_loader f, "homebrew/core/#{f}"
|
||||
f
|
||||
end
|
||||
|
||||
def setup_test_keg(name, version)
|
||||
f = stub_formula_name(name)
|
||||
def setup_test_keg(name, version, &block)
|
||||
keg = super
|
||||
Tab.create(f, DevelopmentTools.default_compiler, :libcxx).write
|
||||
Tab.create(keg.to_formula, DevelopmentTools.default_compiler, :libcxx).write
|
||||
keg
|
||||
end
|
||||
|
||||
before do
|
||||
keg.link
|
||||
keg_only_keg.optlink
|
||||
end
|
||||
|
||||
def alter_tab(keg = dependent)
|
||||
def alter_tab(keg)
|
||||
tab = Tab.for_keg(keg)
|
||||
yield tab
|
||||
tab.write
|
||||
@ -46,24 +57,26 @@ describe InstalledDependents do
|
||||
|
||||
# 1.1.6 is the earliest version of Homebrew that generates correct runtime
|
||||
# dependency lists in {Tab}s.
|
||||
def dependencies(deps, homebrew_version: "1.1.6")
|
||||
alter_tab do |tab|
|
||||
def tab_dependencies(keg, deps, homebrew_version: "1.1.6")
|
||||
alter_tab(keg) do |tab|
|
||||
tab.homebrew_version = homebrew_version
|
||||
tab.tabfile = dependent/Tab::FILENAME
|
||||
tab.tabfile = keg/Tab::FILENAME
|
||||
tab.runtime_dependencies = deps
|
||||
end
|
||||
end
|
||||
|
||||
def unreliable_dependencies(deps)
|
||||
def unreliable_tab_dependencies(keg, deps)
|
||||
# 1.1.5 is (hopefully!) the last version of Homebrew that generates
|
||||
# incorrect runtime dependency lists in {Tab}s.
|
||||
dependencies(deps, homebrew_version: "1.1.5")
|
||||
tab_dependencies(keg, deps, homebrew_version: "1.1.5")
|
||||
end
|
||||
|
||||
let(:dependent) { setup_test_keg("bar", "1.0") }
|
||||
|
||||
specify "a dependency with no Tap in Tab" do
|
||||
tap_dep = setup_test_keg("baz", "1.0")
|
||||
dependent = setup_test_keg("bar", "1.0") do
|
||||
depends_on "foo"
|
||||
depends_on "baz"
|
||||
end
|
||||
|
||||
# allow tap_dep to be linked too
|
||||
FileUtils.rm_r tap_dep/"bin"
|
||||
@ -71,83 +84,93 @@ describe InstalledDependents do
|
||||
|
||||
alter_tab(keg) { |t| t.source["tap"] = nil }
|
||||
|
||||
dependencies nil
|
||||
Formula["bar"].class.depends_on "foo"
|
||||
Formula["bar"].class.depends_on "baz"
|
||||
tab_dependencies dependent, nil
|
||||
|
||||
result = described_class.find_some_installed_dependents([keg, tap_dep])
|
||||
expect(result).to eq([[keg, tap_dep], ["bar"]])
|
||||
end
|
||||
|
||||
specify "no dependencies anywhere" do
|
||||
dependencies nil
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, nil
|
||||
expect(described_class.find_some_installed_dependents([keg])).to be_nil
|
||||
end
|
||||
|
||||
specify "missing Formula dependency" do
|
||||
dependencies nil
|
||||
Formula["bar"].class.depends_on "foo"
|
||||
dependent = setup_test_keg("bar", "1.0") do
|
||||
depends_on "foo"
|
||||
end
|
||||
tab_dependencies dependent, nil
|
||||
expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]])
|
||||
end
|
||||
|
||||
specify "uninstalling dependent and dependency" do
|
||||
dependencies nil
|
||||
Formula["bar"].class.depends_on "foo"
|
||||
dependent = setup_test_keg("bar", "1.0") do
|
||||
depends_on "foo"
|
||||
end
|
||||
tab_dependencies dependent, nil
|
||||
expect(described_class.find_some_installed_dependents([keg, dependent])).to be_nil
|
||||
end
|
||||
|
||||
specify "renamed dependency" do
|
||||
dependencies nil
|
||||
dependent = setup_test_keg("bar", "1.0") do
|
||||
depends_on "foo"
|
||||
end
|
||||
tab_dependencies dependent, nil
|
||||
|
||||
stub_formula_loader Formula["foo"], "homebrew/core/foo-old"
|
||||
renamed_path = HOMEBREW_CELLAR/"foo-old"
|
||||
(HOMEBREW_CELLAR/"foo").rename(renamed_path)
|
||||
renamed_keg = Keg.new(renamed_path/"1.0")
|
||||
|
||||
Formula["bar"].class.depends_on "foo"
|
||||
renamed_keg = Keg.new(renamed_path/keg.version.to_s)
|
||||
|
||||
result = described_class.find_some_installed_dependents([renamed_keg])
|
||||
expect(result).to eq([[renamed_keg], ["bar"]])
|
||||
end
|
||||
|
||||
specify "empty dependencies in Tab" do
|
||||
dependencies []
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, []
|
||||
expect(described_class.find_some_installed_dependents([keg])).to be_nil
|
||||
end
|
||||
|
||||
specify "same name but different version in Tab" do
|
||||
dependencies [{ "full_name" => "foo", "version" => "1.1" }]
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, [{ "full_name" => keg.name, "version" => "1.1" }]
|
||||
expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]])
|
||||
end
|
||||
|
||||
specify "different name and same version in Tab" do
|
||||
stub_formula_name("baz")
|
||||
dependencies [{ "full_name" => "baz", "version" => keg.version.to_s }]
|
||||
stub_formula("baz")
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, [{ "full_name" => "baz", "version" => keg.version.to_s }]
|
||||
expect(described_class.find_some_installed_dependents([keg])).to be_nil
|
||||
end
|
||||
|
||||
specify "same name and version in Tab" do
|
||||
dependencies [{ "full_name" => "foo", "version" => "1.0" }]
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, [{ "full_name" => keg.name, "version" => keg.version.to_s }]
|
||||
expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]])
|
||||
end
|
||||
|
||||
specify "fallback for old versions" do
|
||||
unreliable_dependencies [{ "full_name" => "baz", "version" => "1.0" }]
|
||||
Formula["bar"].class.depends_on "foo"
|
||||
dependent = setup_test_keg("bar", "1.0") do
|
||||
depends_on "foo"
|
||||
end
|
||||
unreliable_tab_dependencies dependent, [{ "full_name" => "baz", "version" => "1.0" }]
|
||||
expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]])
|
||||
end
|
||||
|
||||
specify "non-opt-linked" do
|
||||
keg.remove_opt_record
|
||||
dependencies [{ "full_name" => "foo", "version" => "1.0" }]
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, [{ "full_name" => keg.name, "version" => keg.version.to_s }]
|
||||
expect(described_class.find_some_installed_dependents([keg])).to be_nil
|
||||
end
|
||||
|
||||
specify "keg-only" do
|
||||
keg.unlink
|
||||
Formula["foo"].class.keg_only "a good reason"
|
||||
dependencies [{ "full_name" => "foo", "version" => "1.1" }] # different version
|
||||
expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]])
|
||||
dependent = setup_test_keg("bar", "1.0")
|
||||
tab_dependencies dependent, [{ "full_name" => keg_only_keg.name, "version" => "1.1" }] # different version
|
||||
expect(described_class.find_some_installed_dependents([keg_only_keg])).to eq([[keg_only_keg], ["bar"]])
|
||||
end
|
||||
|
||||
def stub_cask_name(name, version, dependency)
|
||||
|
Loading…
x
Reference in New Issue
Block a user