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++.
This commit is contained in:
Michael Cho 2025-08-30 12:03:37 -04:00
parent eda9e78529
commit f5c11fa342
No known key found for this signature in database
GPG Key ID: 55E85E28A7CD1E85
7 changed files with 127 additions and 15 deletions

View File

@ -42,14 +42,14 @@ module OS
@needs_libc_formula ||= OS::Linux::Glibc.below_ci_version? @needs_libc_formula ||= OS::Linux::Glibc.below_ci_version?
end end
# Keep this method around for now to make it easier to add this functionality later.
# rubocop:disable Lint/UselessMethodDefinition
sig { returns(Pathname) } sig { returns(Pathname) }
def host_gcc_path def host_gcc_path
# TODO: override this if/when we to pick the GCC based on e.g. the Ubuntu version. # Prioritise versioned path if installed
path = Pathname.new("/usr/bin/#{OS::LINUX_PREFERRED_GCC_COMPILER_FORMULA.tr("@", "-")}")
return path if path.exist?
super super
end end
# rubocop:enable Lint/UselessMethodDefinition
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def needs_compiler_formula? def needs_compiler_formula?
@ -60,12 +60,7 @@ module OS
# Undocumented environment variable to make it easier to test compiler # Undocumented environment variable to make it easier to test compiler
# formula automatic installation. # formula automatic installation.
@needs_compiler_formula = true if ENV["HOMEBREW_FORCE_COMPILER_FORMULA"] @needs_compiler_formula = true if ENV["HOMEBREW_FORCE_COMPILER_FORMULA"]
@needs_compiler_formula ||= OS::Linux::Libstdcxx.below_ci_version?
@needs_compiler_formula ||= if host_gcc_path.exist?
::DevelopmentTools.gcc_version(host_gcc_path.to_s) < OS::LINUX_GCC_CI_VERSION
else
true
end
end end
sig { returns(T::Hash[String, T.nilable(String)]) } sig { returns(T::Hash[String, T.nilable(String)]) }

View File

@ -2,6 +2,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "os/linux/ld" require "os/linux/ld"
require "os/linux/libstdcxx"
require "utils/output" require "utils/output"
module OS module OS
@ -12,12 +13,12 @@ module OS
# which are linked by the GCC formula. We only use the versioned shared libraries # which are linked by the GCC formula. We only use the versioned shared libraries
# as the other shared and static libraries are only used at build time where # as the other shared and static libraries are only used at build time where
# GCC can find its own libraries. # GCC can find its own libraries.
GCC_RUNTIME_LIBS = %w[ GCC_RUNTIME_LIBS = T.let(%W[
libatomic.so.1 libatomic.so.1
libgcc_s.so.1 libgcc_s.so.1
libgomp.so.1 libgomp.so.1
libstdc++.so.6 #{OS::Linux::Libstdcxx::SONAME}
].freeze ].freeze, T::Array[String])
sig { params(all_fatal: T::Boolean).void } sig { params(all_fatal: T::Boolean).void }
def perform_preinstall_checks(all_fatal: false) def perform_preinstall_checks(all_fatal: false)

View File

@ -2,12 +2,13 @@
# frozen_string_literal: true # frozen_string_literal: true
require "compilers" require "compilers"
require "os/linux/libstdcxx"
module OS module OS
module Linux module Linux
module LinkageChecker module LinkageChecker
# Libraries provided by glibc and gcc. # Libraries provided by glibc and gcc.
SYSTEM_LIBRARY_ALLOWLIST = %w[ SYSTEM_LIBRARY_ALLOWLIST = %W[
ld-linux-x86-64.so.2 ld-linux-x86-64.so.2
ld-linux-aarch64.so.1 ld-linux-aarch64.so.1
libanl.so.1 libanl.so.1
@ -24,7 +25,7 @@ module OS
libutil.so.1 libutil.so.1
libgcc_s.so.1 libgcc_s.so.1
libgomp.so.1 libgomp.so.1
libstdc++.so.6 #{OS::Linux::Libstdcxx::SONAME}
libquadmath.so.0 libquadmath.so.0
].freeze ].freeze

View File

@ -3,6 +3,7 @@
require "compilers" require "compilers"
require "os/linux/glibc" require "os/linux/glibc"
require "os/linux/libstdcxx"
require "system_command" require "system_command"
module OS module OS
@ -20,6 +21,13 @@ module OS
version version
end end
def host_libstdcxx_version
version = OS::Linux::Libstdcxx.system_version
return "N/A" if version.null?
version
end
def host_gcc_version def host_gcc_version
gcc = ::DevelopmentTools.host_gcc_path gcc = ::DevelopmentTools.host_gcc_path
return "N/A" unless gcc.executable? return "N/A" unless gcc.executable?
@ -49,6 +57,7 @@ module OS
out.puts "OS: #{OS::Linux.os_version}" out.puts "OS: #{OS::Linux.os_version}"
out.puts "WSL: #{OS::Linux.wsl_version}" if OS::Linux.wsl? out.puts "WSL: #{OS::Linux.wsl_version}" if OS::Linux.wsl?
out.puts "Host glibc: #{host_glibc_version}" out.puts "Host glibc: #{host_glibc_version}"
out.puts "Host libstdc++: #{host_libstdcxx_version}"
out.puts "#{::DevelopmentTools.host_gcc_path}: #{host_gcc_version}" out.puts "#{::DevelopmentTools.host_gcc_path}: #{host_gcc_version}"
out.puts "/usr/bin/ruby: #{host_ruby_version}" if RUBY_PATH != HOST_RUBY_PATH out.puts "/usr/bin/ruby: #{host_ruby_version}" if RUBY_PATH != HOST_RUBY_PATH
["glibc", ::CompilerSelector.preferred_gcc, OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA, "xorg"].each do |f| ["glibc", ::CompilerSelector.preferred_gcc, OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA, "xorg"].each do |f|

View File

@ -50,6 +50,7 @@ module OS
LINUX_GLIBC_CI_VERSION = "2.35" LINUX_GLIBC_CI_VERSION = "2.35"
LINUX_GLIBC_NEXT_CI_VERSION = "2.39" LINUX_GLIBC_NEXT_CI_VERSION = "2.39"
LINUX_GCC_CI_VERSION = "11.0" LINUX_GCC_CI_VERSION = "11.0"
LINUX_LIBSTDCXX_CI_VERSION = "6.0.30" # https://packages.ubuntu.com/jammy/libstdc++6
LINUX_PREFERRED_GCC_COMPILER_FORMULA = "gcc@11" # https://packages.ubuntu.com/jammy/gcc LINUX_PREFERRED_GCC_COMPILER_FORMULA = "gcc@11" # https://packages.ubuntu.com/jammy/gcc
LINUX_PREFERRED_GCC_RUNTIME_FORMULA = "gcc" LINUX_PREFERRED_GCC_RUNTIME_FORMULA = "gcc"

View File

@ -0,0 +1,47 @@
# typed: strict
# frozen_string_literal: true
require "os/linux/ld"
module OS
module Linux
# Helper functions for querying `libstdc++` information.
module Libstdcxx
SOVERSION = 6
SONAME = T.let("libstdc++.so.#{SOVERSION}".freeze, String)
sig { returns(T::Boolean) }
def self.below_ci_version?
system_version < LINUX_LIBSTDCXX_CI_VERSION
end
sig { returns(Version) }
def self.system_version
@system_version ||= T.let(nil, T.nilable(Version))
@system_version ||= if (path = system_path)
Version.new("#{SOVERSION}#{path.realpath.basename.to_s.delete_prefix!(SONAME)}")
else
Version::NULL
end
end
sig { returns(T.nilable(Pathname)) }
def self.system_path
@system_path ||= T.let(nil, T.nilable(Pathname))
@system_path ||= find_library(OS::Linux::Ld.library_paths(brewed: false))
@system_path ||= find_library(OS::Linux::Ld.system_dirs(brewed: false))
end
sig { params(paths: T::Array[String]).returns(T.nilable(Pathname)) }
private_class_method def self.find_library(paths)
paths.each do |path|
next if path.start_with?(HOMEBREW_PREFIX)
candidate = Pathname(path)/SONAME
return candidate if candidate.exist? && candidate.elf?
end
nil
end
end
end
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
require "os/linux/libstdcxx"
RSpec.describe OS::Linux::Libstdcxx do
describe "::below_ci_version?" do
it "returns false when system version matches CI version" do
allow(described_class).to receive(:system_version).and_return(Version.new(OS::LINUX_LIBSTDCXX_CI_VERSION))
expect(described_class.below_ci_version?).to be false
end
it "returns true when system version cannot be detected" do
allow(described_class).to receive(:system_version).and_return(Version::NULL)
expect(described_class.below_ci_version?).to be true
end
end
describe "::system_version" do
let(:tmpdir) { mktmpdir }
let(:libstdcxx) { tmpdir/described_class::SONAME }
let(:soversion) { Version.new(described_class::SOVERSION.to_s) }
before do
tmpdir.mkpath
described_class.instance_variable_set(:@system_version, nil)
allow(described_class).to receive(:system_path).and_return(libstdcxx)
end
after do
FileUtils.rm_rf(tmpdir)
end
it "returns NULL when unable to find system path" do
allow(described_class).to receive(:system_path).and_return(nil)
expect(described_class.system_version).to be Version::NULL
end
it "returns full version from filename" do
full_version = Version.new("#{soversion}.0.999")
libstdcxx_real = libstdcxx.sub_ext(".#{full_version}")
FileUtils.touch libstdcxx_real
FileUtils.ln_s libstdcxx_real, libstdcxx
expect(described_class.system_version).to eq full_version
end
it "returns major version when non-standard libstdc++ filename without full version" do
FileUtils.touch libstdcxx
expect(described_class.system_version).to eq soversion
end
it "returns major version when non-standard libstdc++ filename with unexpected realpath" do
libstdcxx_real = tmpdir/"libstdc++.so.real"
FileUtils.touch libstdcxx_real
FileUtils.ln_s libstdcxx_real, libstdcxx
expect(described_class.system_version).to eq soversion
end
end
end