Compare commits

...

6 Commits

Author SHA1 Message Date
Mike McQuaid
47b7bf378e
Merge pull request #20682 from Homebrew/libstdcxx
Check host libstdc++ for brew `gcc` dependency
2025-09-12 19:14:54 +00:00
Mike McQuaid
d7c0052e78
Merge pull request #20680 from Homebrew/optlink-when-cask-installed
formula_installer: optlink even with cask installed
2025-09-12 19:14:22 +00:00
Michael Cho
f5c11fa342
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++.
2025-09-12 14:15:56 -04:00
Patrick Linnane
d3e7e6bde5
Merge pull request #20679 from Homebrew/docs-archive-formats
docs: clarify listed archive formats aren't exclusive
2025-09-12 16:45:19 +00:00
Michael Cho
c35fe04c91
formula_installer: optlink even with cask installed 2025-09-12 12:24:14 -04:00
Patrick Linnane
8567e8bcd3
docs: clarify listed archive formats aren't exclusive
Signed-off-by: Patrick Linnane <patrick@linnane.io>
2025-09-12 09:15:47 -07:00
10 changed files with 140 additions and 28 deletions

View File

@ -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)]) }

View File

@ -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)

View File

@ -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

View File

@ -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|

View File

@ -1156,17 +1156,6 @@ on_request: installed_on_request?, options:)
def link(keg)
Formula.clear_cache
unless link_keg
begin
keg.optlink(verbose: verbose?, overwrite: overwrite?)
rescue Keg::LinkError => e
ofail "Failed to create #{formula.opt_prefix}"
puts "Things that depend on #{formula.full_name} will probably not build."
puts e
end
return
end
cask_installed_with_formula_name = begin
Cask::CaskLoader.load(formula.name, warn: false).installed?
rescue Cask::CaskUnavailableError, Cask::CaskInvalidError
@ -1175,6 +1164,17 @@ on_request: installed_on_request?, options:)
if cask_installed_with_formula_name
ohai "#{formula.name} cask is installed, skipping link."
@link_keg = false
end
unless link_keg
begin
keg.optlink(verbose: verbose?, overwrite: overwrite?)
rescue Keg::LinkError => e
ofail "Failed to create #{formula.opt_prefix}"
puts "Things that depend on #{formula.full_name} will probably not build."
puts e
end
return
end

View File

@ -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"

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

View File

@ -185,7 +185,7 @@ Fill in the following stanzas for your cask:
| ------------------ | ----------- |
| `version` | application version |
| `sha256` | SHA-256 checksum of the file downloaded from `url`, calculated by the command `shasum -a 256 <file>`. Can be suppressed by using the special value `:no_check`. (see [`sha256` Stanza Details](Cask-Cookbook.md#stanza-sha256)) |
| `url` | URL to the `.dmg`/`.zip`/`.tgz`/`.tbz2` file that contains the application.<br />A [`verified` parameter](Cask-Cookbook.md#when-url-and-homepage-domains-differ-add-verified) must be added if the hostnames in the `url` and `homepage` stanzas differ. |
| `url` | URL to the `.dmg`/`.zip`/`.tgz` file (or other common archive formats) that contains the application.<br />A [`verified` parameter](Cask-Cookbook.md#when-url-and-homepage-domains-differ-add-verified) must be added if the hostnames in the `url` and `homepage` stanzas differ. |
| `name` | the full and proper name defined by the vendor, and any useful alternate names (see [`name` Stanza Details](Cask-Cookbook.md#stanza-name)) |
| `desc` | one-line description of the software (see [`desc` Stanza Details](Cask-Cookbook.md#stanza-desc)) |
| `homepage` | application homepage; used for the `brew home` command |

View File

@ -120,7 +120,7 @@ Each of the following stanzas is required for every cask.
| ---------------------------------- | :---------------------------: | ----- |
| [`version`](#stanza-version) | no | Application version, or the special value `:latest`. |
| [`sha256`](#stanza-sha256) | no | SHA-256 checksum of the file downloaded from `url` as calculated by the command `shasum -a 256 <file>`, or the special value `:no_check`. |
| [`url`](#stanza-url) | no | URL to the `.dmg`/`.zip`/`.tgz`/`.tbz2` file that contains the application. A [comment](#when-url-and-homepage-domains-differ-add-verified) should be added if the domains in the `url` and `homepage` stanzas differ. |
| [`url`](#stanza-url) | no | URL to the `.dmg`/`.zip`/`.tgz` file (or other common archive formats) that contains the application. A [comment](#when-url-and-homepage-domains-differ-add-verified) should be added if the domains in the `url` and `homepage` stanzas differ. |
| [`name`](#stanza-name) | yes | String providing the full and proper name defined by the vendor. |
| [`desc`](#stanza-desc) | no | One-line description of the cask. Shown when running `brew info`. |
| `homepage` | no | Application homepage; used for the `brew home` command. |