Merge pull request #20633 from Homebrew/ld-system

os/linux/ld: add support for using system ld.so
This commit is contained in:
Michael Cho 2025-09-12 13:19:40 +00:00 committed by GitHub
commit eda9e78529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 35 deletions

View File

@ -1,26 +1,13 @@
# typed: strict # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "os/linux/ld"
require "utils/output" require "utils/output"
module OS module OS
module Linux module Linux
module Install module Install
module ClassMethods module ClassMethods
# This is a list of known paths to the host dynamic linker on Linux if
# the host glibc is new enough. The symlink_ld_so method will fail if
# the host linker cannot be found in this list.
DYNAMIC_LINKERS = %w[
/lib64/ld-linux-x86-64.so.2
/lib64/ld64.so.2
/lib/ld-linux.so.3
/lib/ld-linux.so.2
/lib/ld-linux-aarch64.so.1
/lib/ld-linux-armhf.so.3
/system/bin/linker64
/system/bin/linker
].freeze
# We link GCC runtime libraries that are not specifically used for Fortran, # We link GCC runtime libraries that are not specifically used for Fortran,
# 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
@ -67,7 +54,7 @@ module OS
ld_so = HOMEBREW_PREFIX/"opt/glibc/bin/ld.so" ld_so = HOMEBREW_PREFIX/"opt/glibc/bin/ld.so"
unless ld_so.readable? unless ld_so.readable?
ld_so = DYNAMIC_LINKERS.find { |s| File.executable? s } ld_so = OS::Linux::Ld.system_ld_so
if ld_so.blank? if ld_so.blank?
::Kernel.raise "Unable to locate the system's dynamic linker" unless brew_ld_so.readable? ::Kernel.raise "Unable to locate the system's dynamic linker" unless brew_ld_so.readable?

View File

@ -5,37 +5,69 @@ module OS
module Linux module Linux
# Helper functions for querying `ld` information. # Helper functions for querying `ld` information.
module Ld module Ld
sig { returns(String) } # This is a list of known paths to the host dynamic linker on Linux if
def self.brewed_ld_so_diagnostics # the host glibc is new enough. Brew will fail to create a symlink for
@brewed_ld_so_diagnostics ||= T.let({}, T.nilable(T::Hash[Pathname, String])) # ld.so if the host linker cannot be found in this list.
DYNAMIC_LINKERS = %w[
/lib64/ld-linux-x86-64.so.2
/lib64/ld64.so.2
/lib/ld-linux.so.3
/lib/ld-linux.so.2
/lib/ld-linux-aarch64.so.1
/lib/ld-linux-armhf.so.3
/system/bin/linker64
/system/bin/linker
].freeze
brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so" # The path to the system's dynamic linker or `nil` if not found
return "" unless brewed_ld_so.exist? sig { returns(T.nilable(Pathname)) }
def self.system_ld_so
@system_ld_so ||= T.let(nil, T.nilable(Pathname))
@system_ld_so ||= begin
linker = DYNAMIC_LINKERS.find { |s| File.executable? s }
Pathname(linker) if linker
end
end
brewed_ld_so_target = brewed_ld_so.readlink sig { params(brewed: T::Boolean).returns(String) }
@brewed_ld_so_diagnostics[brewed_ld_so_target] ||= begin def self.ld_so_diagnostics(brewed: true)
ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics") @ld_so_diagnostics ||= T.let({}, T.nilable(T::Hash[Pathname, String]))
ld_so_target = if brewed
ld_so = HOMEBREW_PREFIX/"lib/ld.so"
return "" unless ld_so.exist?
ld_so.readlink
else
ld_so = system_ld_so
return "" unless ld_so&.exist?
ld_so
end
@ld_so_diagnostics[ld_so_target] ||= begin
ld_so_output = Utils.popen_read(ld_so, "--list-diagnostics")
ld_so_output if $CHILD_STATUS.success? ld_so_output if $CHILD_STATUS.success?
end end
@brewed_ld_so_diagnostics[brewed_ld_so_target].to_s @ld_so_diagnostics[ld_so_target].to_s
end end
sig { returns(String) } sig { params(brewed: T::Boolean).returns(String) }
def self.sysconfdir def self.sysconfdir(brewed: true)
fallback_sysconfdir = "/etc" fallback_sysconfdir = "/etc"
match = brewed_ld_so_diagnostics.match(/path.sysconfdir="(.+)"/) match = ld_so_diagnostics(brewed:).match(/path.sysconfdir="(.+)"/)
return fallback_sysconfdir unless match return fallback_sysconfdir unless match
match.captures.compact.first || fallback_sysconfdir match.captures.compact.first || fallback_sysconfdir
end end
sig { returns(T::Array[String]) } sig { params(brewed: T::Boolean).returns(T::Array[String]) }
def self.system_dirs def self.system_dirs(brewed: true)
dirs = [] dirs = []
brewed_ld_so_diagnostics.split("\n").each do |line| ld_so_diagnostics(brewed:).split("\n").each do |line|
match = line.match(/path.system_dirs\[0x.*\]="(.*)"/) match = line.match(/path.system_dirs\[0x.*\]="(.*)"/)
next unless match next unless match
@ -45,9 +77,9 @@ module OS
dirs dirs
end end
sig { params(conf_path: T.any(Pathname, String)).returns(T::Array[String]) } sig { params(conf_path: T.any(Pathname, String), brewed: T::Boolean).returns(T::Array[String]) }
def self.library_paths(conf_path = Pathname(sysconfdir)/"ld.so.conf") def self.library_paths(conf_path = "ld.so.conf", brewed: true)
conf_file = Pathname(conf_path) conf_file = Pathname(sysconfdir(brewed:))/conf_path
return [] unless conf_file.exist? return [] unless conf_file.exist?
return [] unless conf_file.file? return [] unless conf_file.file?
return [] unless conf_file.readable? return [] unless conf_file.readable?
@ -68,8 +100,7 @@ module OS
line.sub!(/\s*#.*$/, "") line.sub!(/\s*#.*$/, "")
if line.start_with?(/\s*include\s+/) if line.start_with?(/\s*include\s+/)
include_path = Pathname(line.sub(/^\s*include\s+/, "")).expand_path wildcard = Pathname(line.sub(/^\s*include\s+/, "")).expand_path(directory)
wildcard = include_path.absolute? ? include_path : directory/include_path
Dir.glob(wildcard.to_s).each do |include_file| Dir.glob(wildcard.to_s).each do |include_file|
paths += library_paths(include_file) paths += library_paths(include_file)

View File

@ -4,6 +4,61 @@ require "os/linux/ld"
require "tmpdir" require "tmpdir"
RSpec.describe OS::Linux::Ld do RSpec.describe OS::Linux::Ld do
let(:diagnostics) do
<<~EOS
path.prefix="/usr"
path.sysconfdir="/usr/local/etc"
path.system_dirs[0x0]="/lib64"
path.system_dirs[0x1]="/var/lib"
EOS
end
describe "::system_ld_so" do
let(:ld_so) { "/lib/ld-linux.so.3" }
before do
allow(File).to receive(:executable?).and_return(false)
described_class.instance_variable_set(:@system_ld_so, nil)
end
it "returns the path to a known dynamic linker" do
allow(File).to receive(:executable?).with(ld_so).and_return(true)
expect(described_class.system_ld_so).to eq(Pathname(ld_so))
end
it "returns nil when there is no known dynamic linker" do
expect(described_class.system_ld_so).to be_nil
end
end
describe "::sysconfdir" do
it "returns path.sysconfdir" do
allow(described_class).to receive(:ld_so_diagnostics).and_return(diagnostics)
expect(described_class.sysconfdir).to eq("/usr/local/etc")
expect(described_class.sysconfdir(brewed: false)).to eq("/usr/local/etc")
end
it "returns fallback on blank diagnostics" do
allow(described_class).to receive(:ld_so_diagnostics).and_return("")
expect(described_class.sysconfdir).to eq("/etc")
expect(described_class.sysconfdir(brewed: false)).to eq("/etc")
end
end
describe "::system_dirs" do
it "returns all path.system_dirs" do
allow(described_class).to receive(:ld_so_diagnostics).and_return(diagnostics)
expect(described_class.system_dirs).to eq(["/lib64", "/var/lib"])
expect(described_class.system_dirs(brewed: false)).to eq(["/lib64", "/var/lib"])
end
it "returns an empty array on blank diagnostics" do
allow(described_class).to receive(:ld_so_diagnostics).and_return("")
expect(described_class.system_dirs).to eq([])
expect(described_class.system_dirs(brewed: false)).to eq([])
end
end
describe "::library_paths" do describe "::library_paths" do
ld_etc = Pathname("") ld_etc = Pathname("")
before do before do