From f5c11fa34212719c48733da96e94f191f14eb0bf Mon Sep 17 00:00:00 2001 From: Michael Cho Date: Sat, 30 Aug 2025 12:03:37 -0400 Subject: [PATCH] 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++. --- .../extend/os/linux/development_tools.rb | 15 ++--- Library/Homebrew/extend/os/linux/install.rb | 7 ++- .../extend/os/linux/linkage_checker.rb | 5 +- .../Homebrew/extend/os/linux/system_config.rb | 9 +++ Library/Homebrew/os.rb | 1 + Library/Homebrew/os/linux/libstdcxx.rb | 47 +++++++++++++++ .../Homebrew/test/os/linux/libstdcxx_spec.rb | 58 +++++++++++++++++++ 7 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 Library/Homebrew/os/linux/libstdcxx.rb create mode 100644 Library/Homebrew/test/os/linux/libstdcxx_spec.rb diff --git a/Library/Homebrew/extend/os/linux/development_tools.rb b/Library/Homebrew/extend/os/linux/development_tools.rb index 04827adee3..2ab9898841 100644 --- a/Library/Homebrew/extend/os/linux/development_tools.rb +++ b/Library/Homebrew/extend/os/linux/development_tools.rb @@ -42,14 +42,14 @@ module OS @needs_libc_formula ||= OS::Linux::Glibc.below_ci_version? end - # Keep this method around for now to make it easier to add this functionality later. - # rubocop:disable Lint/UselessMethodDefinition sig { returns(Pathname) } 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 end - # rubocop:enable Lint/UselessMethodDefinition sig { returns(T::Boolean) } def needs_compiler_formula? @@ -60,12 +60,7 @@ module OS # Undocumented environment variable to make it easier to test compiler # formula automatic installation. @needs_compiler_formula = true if ENV["HOMEBREW_FORCE_COMPILER_FORMULA"] - - @needs_compiler_formula ||= if host_gcc_path.exist? - ::DevelopmentTools.gcc_version(host_gcc_path.to_s) < OS::LINUX_GCC_CI_VERSION - else - true - end + @needs_compiler_formula ||= OS::Linux::Libstdcxx.below_ci_version? end sig { returns(T::Hash[String, T.nilable(String)]) } diff --git a/Library/Homebrew/extend/os/linux/install.rb b/Library/Homebrew/extend/os/linux/install.rb index 9098dce7ce..6287faa84f 100644 --- a/Library/Homebrew/extend/os/linux/install.rb +++ b/Library/Homebrew/extend/os/linux/install.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "os/linux/ld" +require "os/linux/libstdcxx" require "utils/output" module OS @@ -12,12 +13,12 @@ module OS # 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 # GCC can find its own libraries. - GCC_RUNTIME_LIBS = %w[ + GCC_RUNTIME_LIBS = T.let(%W[ libatomic.so.1 libgcc_s.so.1 libgomp.so.1 - libstdc++.so.6 - ].freeze + #{OS::Linux::Libstdcxx::SONAME} + ].freeze, T::Array[String]) sig { params(all_fatal: T::Boolean).void } def perform_preinstall_checks(all_fatal: false) diff --git a/Library/Homebrew/extend/os/linux/linkage_checker.rb b/Library/Homebrew/extend/os/linux/linkage_checker.rb index 634bd5b71f..82a3d46d27 100644 --- a/Library/Homebrew/extend/os/linux/linkage_checker.rb +++ b/Library/Homebrew/extend/os/linux/linkage_checker.rb @@ -2,12 +2,13 @@ # frozen_string_literal: true require "compilers" +require "os/linux/libstdcxx" module OS module Linux module LinkageChecker # Libraries provided by glibc and gcc. - SYSTEM_LIBRARY_ALLOWLIST = %w[ + SYSTEM_LIBRARY_ALLOWLIST = %W[ ld-linux-x86-64.so.2 ld-linux-aarch64.so.1 libanl.so.1 @@ -24,7 +25,7 @@ module OS libutil.so.1 libgcc_s.so.1 libgomp.so.1 - libstdc++.so.6 + #{OS::Linux::Libstdcxx::SONAME} libquadmath.so.0 ].freeze diff --git a/Library/Homebrew/extend/os/linux/system_config.rb b/Library/Homebrew/extend/os/linux/system_config.rb index 6a967b3a2d..77343a36ff 100644 --- a/Library/Homebrew/extend/os/linux/system_config.rb +++ b/Library/Homebrew/extend/os/linux/system_config.rb @@ -3,6 +3,7 @@ require "compilers" require "os/linux/glibc" +require "os/linux/libstdcxx" require "system_command" module OS @@ -20,6 +21,13 @@ module OS version end + def host_libstdcxx_version + version = OS::Linux::Libstdcxx.system_version + return "N/A" if version.null? + + version + end + def host_gcc_version gcc = ::DevelopmentTools.host_gcc_path return "N/A" unless gcc.executable? @@ -49,6 +57,7 @@ module OS out.puts "OS: #{OS::Linux.os_version}" out.puts "WSL: #{OS::Linux.wsl_version}" if OS::Linux.wsl? 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 "/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| diff --git a/Library/Homebrew/os.rb b/Library/Homebrew/os.rb index 3cd2b7f6f1..ffa463e864 100644 --- a/Library/Homebrew/os.rb +++ b/Library/Homebrew/os.rb @@ -50,6 +50,7 @@ module OS LINUX_GLIBC_CI_VERSION = "2.35" LINUX_GLIBC_NEXT_CI_VERSION = "2.39" 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_RUNTIME_FORMULA = "gcc" diff --git a/Library/Homebrew/os/linux/libstdcxx.rb b/Library/Homebrew/os/linux/libstdcxx.rb new file mode 100644 index 0000000000..86efb98830 --- /dev/null +++ b/Library/Homebrew/os/linux/libstdcxx.rb @@ -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 diff --git a/Library/Homebrew/test/os/linux/libstdcxx_spec.rb b/Library/Homebrew/test/os/linux/libstdcxx_spec.rb new file mode 100644 index 0000000000..5f6200064e --- /dev/null +++ b/Library/Homebrew/test/os/linux/libstdcxx_spec.rb @@ -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