Skip to content

Commit f5c11fa

Browse files
committed
Check host libstdc++ for brew gcc dependency
For most formulae, the bottles need a minimum libstdc++ rather than a minimum GCC version. This is particularly important when building on Ubuntu where the default compiler version is older than libstdc++. So, checking the host libstdc++ version is a more accurate way to determine whether brew GCC is needed at runtime. This can be improved in the future to check symbol versions (e.g. GLIBCXX, CXXABI, GLIBC) which can allow some bottles to be installed even with older glibc/libstdc++.
1 parent eda9e78 commit f5c11fa

File tree

7 files changed

+127
-15
lines changed

7 files changed

+127
-15
lines changed

Library/Homebrew/extend/os/linux/development_tools.rb

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ def needs_libc_formula?
4242
@needs_libc_formula ||= OS::Linux::Glibc.below_ci_version?
4343
end
4444

45-
# Keep this method around for now to make it easier to add this functionality later.
46-
# rubocop:disable Lint/UselessMethodDefinition
4745
sig { returns(Pathname) }
4846
def host_gcc_path
49-
# TODO: override this if/when we to pick the GCC based on e.g. the Ubuntu version.
47+
# Prioritise versioned path if installed
48+
path = Pathname.new("/usr/bin/#{OS::LINUX_PREFERRED_GCC_COMPILER_FORMULA.tr("@", "-")}")
49+
return path if path.exist?
50+
5051
super
5152
end
52-
# rubocop:enable Lint/UselessMethodDefinition
5353

5454
sig { returns(T::Boolean) }
5555
def needs_compiler_formula?
@@ -60,12 +60,7 @@ def needs_compiler_formula?
6060
# Undocumented environment variable to make it easier to test compiler
6161
# formula automatic installation.
6262
@needs_compiler_formula = true if ENV["HOMEBREW_FORCE_COMPILER_FORMULA"]
63-
64-
@needs_compiler_formula ||= if host_gcc_path.exist?
65-
::DevelopmentTools.gcc_version(host_gcc_path.to_s) < OS::LINUX_GCC_CI_VERSION
66-
else
67-
true
68-
end
63+
@needs_compiler_formula ||= OS::Linux::Libstdcxx.below_ci_version?
6964
end
7065

7166
sig { returns(T::Hash[String, T.nilable(String)]) }

Library/Homebrew/extend/os/linux/install.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# frozen_string_literal: true
33

44
require "os/linux/ld"
5+
require "os/linux/libstdcxx"
56
require "utils/output"
67

78
module OS
@@ -12,12 +13,12 @@ module ClassMethods
1213
# which are linked by the GCC formula. We only use the versioned shared libraries
1314
# as the other shared and static libraries are only used at build time where
1415
# GCC can find its own libraries.
15-
GCC_RUNTIME_LIBS = %w[
16+
GCC_RUNTIME_LIBS = T.let(%W[
1617
libatomic.so.1
1718
libgcc_s.so.1
1819
libgomp.so.1
19-
libstdc++.so.6
20-
].freeze
20+
#{OS::Linux::Libstdcxx::SONAME}
21+
].freeze, T::Array[String])
2122

2223
sig { params(all_fatal: T::Boolean).void }
2324
def perform_preinstall_checks(all_fatal: false)

Library/Homebrew/extend/os/linux/linkage_checker.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
# frozen_string_literal: true
33

44
require "compilers"
5+
require "os/linux/libstdcxx"
56

67
module OS
78
module Linux
89
module LinkageChecker
910
# Libraries provided by glibc and gcc.
10-
SYSTEM_LIBRARY_ALLOWLIST = %w[
11+
SYSTEM_LIBRARY_ALLOWLIST = %W[
1112
ld-linux-x86-64.so.2
1213
ld-linux-aarch64.so.1
1314
libanl.so.1
@@ -24,7 +25,7 @@ module LinkageChecker
2425
libutil.so.1
2526
libgcc_s.so.1
2627
libgomp.so.1
27-
libstdc++.so.6
28+
#{OS::Linux::Libstdcxx::SONAME}
2829
libquadmath.so.0
2930
].freeze
3031

Library/Homebrew/extend/os/linux/system_config.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
require "compilers"
55
require "os/linux/glibc"
6+
require "os/linux/libstdcxx"
67
require "system_command"
78

89
module OS
@@ -20,6 +21,13 @@ def host_glibc_version
2021
version
2122
end
2223

24+
def host_libstdcxx_version
25+
version = OS::Linux::Libstdcxx.system_version
26+
return "N/A" if version.null?
27+
28+
version
29+
end
30+
2331
def host_gcc_version
2432
gcc = ::DevelopmentTools.host_gcc_path
2533
return "N/A" unless gcc.executable?
@@ -49,6 +57,7 @@ def dump_verbose_config(out = $stdout)
4957
out.puts "OS: #{OS::Linux.os_version}"
5058
out.puts "WSL: #{OS::Linux.wsl_version}" if OS::Linux.wsl?
5159
out.puts "Host glibc: #{host_glibc_version}"
60+
out.puts "Host libstdc++: #{host_libstdcxx_version}"
5261
out.puts "#{::DevelopmentTools.host_gcc_path}: #{host_gcc_version}"
5362
out.puts "/usr/bin/ruby: #{host_ruby_version}" if RUBY_PATH != HOST_RUBY_PATH
5463
["glibc", ::CompilerSelector.preferred_gcc, OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA, "xorg"].each do |f|

Library/Homebrew/os.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def self.kernel_name
5050
LINUX_GLIBC_CI_VERSION = "2.35"
5151
LINUX_GLIBC_NEXT_CI_VERSION = "2.39"
5252
LINUX_GCC_CI_VERSION = "11.0"
53+
LINUX_LIBSTDCXX_CI_VERSION = "6.0.30" # https://packages.ubuntu.com/jammy/libstdc++6
5354
LINUX_PREFERRED_GCC_COMPILER_FORMULA = "gcc@11" # https://packages.ubuntu.com/jammy/gcc
5455
LINUX_PREFERRED_GCC_RUNTIME_FORMULA = "gcc"
5556

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "os/linux/ld"
5+
6+
module OS
7+
module Linux
8+
# Helper functions for querying `libstdc++` information.
9+
module Libstdcxx
10+
SOVERSION = 6
11+
SONAME = T.let("libstdc++.so.#{SOVERSION}".freeze, String)
12+
13+
sig { returns(T::Boolean) }
14+
def self.below_ci_version?
15+
system_version < LINUX_LIBSTDCXX_CI_VERSION
16+
end
17+
18+
sig { returns(Version) }
19+
def self.system_version
20+
@system_version ||= T.let(nil, T.nilable(Version))
21+
@system_version ||= if (path = system_path)
22+
Version.new("#{SOVERSION}#{path.realpath.basename.to_s.delete_prefix!(SONAME)}")
23+
else
24+
Version::NULL
25+
end
26+
end
27+
28+
sig { returns(T.nilable(Pathname)) }
29+
def self.system_path
30+
@system_path ||= T.let(nil, T.nilable(Pathname))
31+
@system_path ||= find_library(OS::Linux::Ld.library_paths(brewed: false))
32+
@system_path ||= find_library(OS::Linux::Ld.system_dirs(brewed: false))
33+
end
34+
35+
sig { params(paths: T::Array[String]).returns(T.nilable(Pathname)) }
36+
private_class_method def self.find_library(paths)
37+
paths.each do |path|
38+
next if path.start_with?(HOMEBREW_PREFIX)
39+
40+
candidate = Pathname(path)/SONAME
41+
return candidate if candidate.exist? && candidate.elf?
42+
end
43+
nil
44+
end
45+
end
46+
end
47+
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "os/linux/libstdcxx"
4+
5+
RSpec.describe OS::Linux::Libstdcxx do
6+
describe "::below_ci_version?" do
7+
it "returns false when system version matches CI version" do
8+
allow(described_class).to receive(:system_version).and_return(Version.new(OS::LINUX_LIBSTDCXX_CI_VERSION))
9+
expect(described_class.below_ci_version?).to be false
10+
end
11+
12+
it "returns true when system version cannot be detected" do
13+
allow(described_class).to receive(:system_version).and_return(Version::NULL)
14+
expect(described_class.below_ci_version?).to be true
15+
end
16+
end
17+
18+
describe "::system_version" do
19+
let(:tmpdir) { mktmpdir }
20+
let(:libstdcxx) { tmpdir/described_class::SONAME }
21+
let(:soversion) { Version.new(described_class::SOVERSION.to_s) }
22+
23+
before do
24+
tmpdir.mkpath
25+
described_class.instance_variable_set(:@system_version, nil)
26+
allow(described_class).to receive(:system_path).and_return(libstdcxx)
27+
end
28+
29+
after do
30+
FileUtils.rm_rf(tmpdir)
31+
end
32+
33+
it "returns NULL when unable to find system path" do
34+
allow(described_class).to receive(:system_path).and_return(nil)
35+
expect(described_class.system_version).to be Version::NULL
36+
end
37+
38+
it "returns full version from filename" do
39+
full_version = Version.new("#{soversion}.0.999")
40+
libstdcxx_real = libstdcxx.sub_ext(".#{full_version}")
41+
FileUtils.touch libstdcxx_real
42+
FileUtils.ln_s libstdcxx_real, libstdcxx
43+
expect(described_class.system_version).to eq full_version
44+
end
45+
46+
it "returns major version when non-standard libstdc++ filename without full version" do
47+
FileUtils.touch libstdcxx
48+
expect(described_class.system_version).to eq soversion
49+
end
50+
51+
it "returns major version when non-standard libstdc++ filename with unexpected realpath" do
52+
libstdcxx_real = tmpdir/"libstdc++.so.real"
53+
FileUtils.touch libstdcxx_real
54+
FileUtils.ln_s libstdcxx_real, libstdcxx
55+
expect(described_class.system_version).to eq soversion
56+
end
57+
end
58+
end

0 commit comments

Comments
 (0)