diff --git a/Library/Homebrew/os/linux/elf.rb b/Library/Homebrew/os/linux/elf.rb index efd855db952ca3..2244ffe503b61b 100644 --- a/Library/Homebrew/os/linux/elf.rb +++ b/Library/Homebrew/os/linux/elf.rb @@ -1,6 +1,8 @@ # typed: true # frozen_string_literal: true +require "os/linux/ld" + # {Pathname} extension for dealing with ELF files. # @see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header # @@ -130,19 +132,7 @@ def initialize(path) @dylib_id, needed = needed_libraries path return if needed.empty? - ldd = DevelopmentTools.locate "ldd" - ldd_output = Utils.popen_read(ldd, path.expand_path.to_s).split("\n") - return unless $CHILD_STATUS.success? - - ldd_paths = ldd_output.filter_map do |line| - match = line.match(/\t.+ => (.+) \(.+\)|\t(.+) => not found/) - next unless match - - match.captures.compact.first - end - @dylibs = ldd_paths.select do |ldd_path| - needed.include? File.basename(ldd_path) - end + @dylibs = needed.map { |lib| find_full_lib_path(lib).to_s } end private @@ -157,6 +147,63 @@ def needed_libraries_using_patchelf_rb(path) patcher = path.patchelf_patcher [patcher.soname, patcher.needed] end + + def dt_flags_1(path) + readelf = DevelopmentTools.locate "readelf" + readelf_output = Utils.popen_read(readelf, "--dynamic", path.expand_path.to_s) + return [] unless $CHILD_STATUS.success? + + match = readelf_output.match(/^\s*0x0*6ffffffb\s+\(FLAGS_1\)\s+Flags:\s*(.*)$/) + return [] unless match + + match.captures.compact.first.split(/\s+/) + end + + def find_full_lib_path(basename) + local_paths = (path.patchelf_patcher.runpath || path.patchelf_patcher.rpath)&.split(":") + + # Search for dependencies in the runpath/rpath first + local_paths&.each do |local_path| + candidate = Pathname(local_path)/basename + return candidate if candidate.exist? && candidate.elf? + end + + # Check if DF_1_NODEFLIB is set + nodeflib_flag = dt_flags_1(path).include?("NODEFLIB") + + linker_library_paths = OS::Linux::Ld::library_paths + linker_system_dirs = OS::Linux::Ld::system_dirs + + # If DF_1_NODEFLIB is set, exclude any library paths that are subdirectories + # of the system dirs + if nodeflib_flag + linker_library_paths = linker_library_paths.reject do |linker_library_path| + child = Pathname(linker_library_path).realdirpath + + linker_system_dirs.any? do |linker_system_dir| + parent = Pathname(linker_system_dir).realdirpath + child.ascend { |p| break true if p == parent } || false + end + end + end + + # If not found, search recursively in the paths listed in ld.so.conf (skipping + # paths that are subdirectories of the system dirs if DF_1_NODEFLIB is set) + linker_library_paths.each do |linker_library_path| + candidate = Pathname(linker_library_path)/basename + return candidate if candidate.exist? && candidate.elf? + end + + # If not found, search in the system dirs, unless DF_1_NODEFLIB is set + unless nodeflib_flag + linker_system_dirs.each do |linker_system_dir| + candidate = Pathname(linker_system_dir)/basename + return candidate if candidate.exist? && candidate.elf? + end + end + + basename + end end private_constant :Metadata diff --git a/Library/Homebrew/os/linux/ld.rb b/Library/Homebrew/os/linux/ld.rb new file mode 100644 index 00000000000000..532f7870b6acff --- /dev/null +++ b/Library/Homebrew/os/linux/ld.rb @@ -0,0 +1,75 @@ +# typed: true +# frozen_string_literal: true + +module OS + module Linux + # Helper functions for querying `ld` information. + # + # @api private + module Ld + module_function + + sig { returns(String) } + def sysconfdir + fallback_sysconfdir = "/etc" + + brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so" + return fallback_sysconfdir unless brewed_ld_so.exist? + + ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics") + return fallback_sysconfdir unless $CHILD_STATUS.success? + + match = ld_so_output.match(/path.sysconfdir="(.+)"/) + return fallback_sysconfdir unless match + + match.captures.compact.first + end + + sig { returns(T::Array[String]) } + def system_dirs + system_dirs = [] + + brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so" + return system_dirs unless brewed_ld_so.exist? + + ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics").split("\n") + return system_dirs unless $CHILD_STATUS.success? + + ld_so_output.each do |line| + match = line.match(/path.system_dirs\[0x.*\]="(.*)"/) + next unless match + system_dirs << match.captures.compact.first + end + + system_dirs + end + + def library_paths(conf_path = Pathname(sysconfdir)/"ld.so.conf") + conf_file = Pathname(conf_path) + paths = Set.new + directory = conf_file.realpath.dirname + + conf_file.readlines.each do |line| + # Remove comments and leading/trailing whitespace + line.strip! + 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 + + Dir.glob(wildcard.to_s).each do |include_file| + paths += library_paths(include_file) + end + elsif line.empty? + next + else + paths << line + end + end + + paths.to_a + end + end + end + end