From 896edb4451dae3bbfd678686b3f1f03c7d1bb1b3 Mon Sep 17 00:00:00 2001 From: Michael Cho Date: Thu, 4 Sep 2025 13:19:07 -0400 Subject: [PATCH] os/linux/ld: add support for using system ld.so --- Library/Homebrew/extend/os/linux/install.rb | 17 +---- Library/Homebrew/os/linux/ld.rb | 71 +++++++++++++++------ Library/Homebrew/test/os/linux/ld_spec.rb | 55 ++++++++++++++++ 3 files changed, 108 insertions(+), 35 deletions(-) diff --git a/Library/Homebrew/extend/os/linux/install.rb b/Library/Homebrew/extend/os/linux/install.rb index cbfa62ece1..9098dce7ce 100644 --- a/Library/Homebrew/extend/os/linux/install.rb +++ b/Library/Homebrew/extend/os/linux/install.rb @@ -1,26 +1,13 @@ # typed: strict # frozen_string_literal: true +require "os/linux/ld" require "utils/output" module OS module Linux module Install 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, # 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 @@ -67,7 +54,7 @@ module OS ld_so = HOMEBREW_PREFIX/"opt/glibc/bin/ld.so" 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? ::Kernel.raise "Unable to locate the system's dynamic linker" unless brew_ld_so.readable? diff --git a/Library/Homebrew/os/linux/ld.rb b/Library/Homebrew/os/linux/ld.rb index ec6295e1d6..a8d3f7b697 100644 --- a/Library/Homebrew/os/linux/ld.rb +++ b/Library/Homebrew/os/linux/ld.rb @@ -5,37 +5,69 @@ module OS module Linux # Helper functions for querying `ld` information. module Ld - sig { returns(String) } - def self.brewed_ld_so_diagnostics - @brewed_ld_so_diagnostics ||= T.let({}, T.nilable(T::Hash[Pathname, String])) + # This is a list of known paths to the host dynamic linker on Linux if + # the host glibc is new enough. Brew will fail to create a symlink for + # 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" - return "" unless brewed_ld_so.exist? + # The path to the system's dynamic linker or `nil` if not found + 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 - @brewed_ld_so_diagnostics[brewed_ld_so_target] ||= begin - ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics") + sig { params(brewed: T::Boolean).returns(String) } + def self.ld_so_diagnostics(brewed: true) + @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? end - @brewed_ld_so_diagnostics[brewed_ld_so_target].to_s + @ld_so_diagnostics[ld_so_target].to_s end - sig { returns(String) } - def self.sysconfdir + sig { params(brewed: T::Boolean).returns(String) } + def self.sysconfdir(brewed: true) fallback_sysconfdir = "/etc" - match = brewed_ld_so_diagnostics.match(/path.sysconfdir="(.+)"/) + match = ld_so_diagnostics(brewed:).match(/path.sysconfdir="(.+)"/) return fallback_sysconfdir unless match match.captures.compact.first || fallback_sysconfdir end - sig { returns(T::Array[String]) } - def self.system_dirs + sig { params(brewed: T::Boolean).returns(T::Array[String]) } + def self.system_dirs(brewed: true) 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.*\]="(.*)"/) next unless match @@ -45,9 +77,9 @@ module OS dirs end - sig { params(conf_path: T.any(Pathname, String)).returns(T::Array[String]) } - def self.library_paths(conf_path = Pathname(sysconfdir)/"ld.so.conf") - conf_file = Pathname(conf_path) + sig { params(conf_path: T.any(Pathname, String), brewed: T::Boolean).returns(T::Array[String]) } + def self.library_paths(conf_path = "ld.so.conf", brewed: true) + conf_file = Pathname(sysconfdir(brewed:))/conf_path return [] unless conf_file.exist? return [] unless conf_file.file? return [] unless conf_file.readable? @@ -68,8 +100,7 @@ module OS line.sub!(/\s*#.*$/, "") if line.start_with?(/\s*include\s+/) - include_path = Pathname(line.sub(/^\s*include\s+/, "")).expand_path - wildcard = include_path.absolute? ? include_path : directory/include_path + wildcard = Pathname(line.sub(/^\s*include\s+/, "")).expand_path(directory) Dir.glob(wildcard.to_s).each do |include_file| paths += library_paths(include_file) diff --git a/Library/Homebrew/test/os/linux/ld_spec.rb b/Library/Homebrew/test/os/linux/ld_spec.rb index b738bbe253..76361c63c5 100644 --- a/Library/Homebrew/test/os/linux/ld_spec.rb +++ b/Library/Homebrew/test/os/linux/ld_spec.rb @@ -4,6 +4,61 @@ require "os/linux/ld" require "tmpdir" 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 ld_etc = Pathname("") before do