brew/Library/Homebrew/extend/os/mac/keg_relocate.rb
Carlo Cabrera c7de544fe8
extend/os/mac/keg_relocate: fix post-bottling dylib ID relocation
Running `brew bottle` changes dylib IDs, install names, and rpaths into
placeholders for the bottle, creates a bottle tarball, and then changes
the placeholders back to their correct values.

With my refactoring in #11358, the behaviour of this relocation changed:
dylib IDs would no longer be changed back from placeholders into their
correct values after the creation of the bottle tarball.
2021-05-12 21:37:33 +01:00

201 lines
6.4 KiB
Ruby

# typed: true
# frozen_string_literal: true
class Keg
class << self
undef file_linked_libraries
def file_linked_libraries(file, string)
# Check dynamic library linkage. Importantly, do not perform for static
# libraries, which will falsely report "linkage" to themselves.
if file.mach_o_executable? || file.dylib? || file.mach_o_bundle?
file.dynamically_linked_libraries.select { |lib| lib.include? string }
else
[]
end
end
end
undef relocate_dynamic_linkage
def relocate_dynamic_linkage(relocation)
mach_o_files.each do |file|
file.ensure_writable do
if file.dylib?
id = relocated_name_for(file.dylib_id, relocation)
change_dylib_id(id, file)
end
each_install_name_for(file) do |old_name|
new_name = relocated_name_for(old_name, relocation)
change_install_name(old_name, new_name, file) if new_name
end
if ENV["HOMEBREW_RELOCATE_RPATHS"]
each_rpath_for(file) do |old_name|
new_name = relocated_name_for(old_name, relocation)
change_rpath(old_name, new_name, file) if new_name
end
end
end
end
end
def fix_dynamic_linkage
mach_o_files.each do |file|
file.ensure_writable do
change_dylib_id(dylib_id_for(file), file) if file.dylib?
each_install_name_for(file) do |bad_name|
# Don't fix absolute paths unless they are rooted in the build directory
next if bad_name.start_with?("/") &&
!bad_name.start_with?(HOMEBREW_TEMP.to_s) &&
!bad_name.start_with?(HOMEBREW_TEMP.realpath.to_s)
new_name = fixed_name(file, bad_name)
change_install_name(bad_name, new_name, file) unless new_name == bad_name
end
# If none of the install names reference RPATH(s), then we can safely
# remove all RPATHs from the file.
if ENV["HOMEBREW_RELOCATE_METAVARS"] &&
file.dynamically_linked_libraries.none? { |lib| lib.start_with?("@rpath") }
# NOTE: This could probably be made more efficient by reverse-sorting
# the RPATHs by offset and calling MachOFile#delete_command
# with repopulate: false.
file.rpaths.each { |r| file.delete_rpath(r) }
end
end
end
generic_fix_dynamic_linkage
end
def expand_rpath(file, bad_name)
suffix = bad_name.sub(/^@rpath/, "")
file.rpaths.each do |rpath|
return rpath/suffix if (rpath/suffix).exist?
end
opoo "Could not find library #{bad_name} for #{file}"
bad_name
end
# If file is a dylib or bundle itself, look for the dylib named by
# bad_name relative to the lib directory, so that we can skip the more
# expensive recursive search if possible.
def fixed_name(file, bad_name)
if bad_name.start_with? PREFIX_PLACEHOLDER
bad_name.sub(PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
elsif bad_name.start_with? CELLAR_PLACEHOLDER
bad_name.sub(CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
elsif (file.dylib? || file.mach_o_bundle?) && (file.dirname/bad_name).exist?
"@loader_path/#{bad_name}"
elsif file.mach_o_executable? && (lib/bad_name).exist?
"#{lib}/#{bad_name}"
elsif file.mach_o_executable? && (libexec/"lib"/bad_name).exist?
"#{libexec}/lib/#{bad_name}"
elsif bad_name.start_with?("@rpath") && ENV["HOMEBREW_RELOCATE_METAVARS"]
expand_rpath file, bad_name
elsif (abs_name = find_dylib(bad_name)) && abs_name.exist?
abs_name.to_s
else
opoo "Could not fix #{bad_name} in #{file}"
bad_name
end
end
def each_install_name_for(file, &block)
dylibs = file.dynamically_linked_libraries
dylibs.reject! { |fn| fn =~ /^@(loader|executable)_path/ }
dylibs.reject! { |fn| fn =~ /^@rpath/ } unless ENV["HOMEBREW_RELOCATE_METAVARS"]
dylibs.each(&block)
end
def each_rpath_for(file, &block)
rpaths = file.rpaths
.reject { |fn| fn =~ /^@(loader|executable)_path/ }
rpaths.each(&block)
end
def dylib_id_for(file)
# The new dylib ID should have the same basename as the old dylib ID, not
# the basename of the file itself.
basename = File.basename(file.dylib_id)
relative_dirname = file.dirname.relative_path_from(path)
(opt_record/relative_dirname/basename).to_s
end
def relocated_name_for(old_name, relocation)
old_prefix, new_prefix = relocation.replacement_pair_for(:prefix)
old_cellar, new_cellar = relocation.replacement_pair_for(:cellar)
if old_name.start_with? old_cellar
old_name.sub(old_cellar, new_cellar)
elsif old_name.start_with? old_prefix
old_name.sub(old_prefix, new_prefix)
end
end
# Matches framework references like `XXX.framework/Versions/YYY/XXX` and
# `XXX.framework/XXX`, both with or without a slash-delimited prefix.
FRAMEWORK_RX = %r{(?:^|/)(([^/]+)\.framework/(?:Versions/[^/]+/)?\2)$}.freeze
def find_dylib_suffix_from(bad_name)
if (framework = bad_name.match(FRAMEWORK_RX))
framework[1]
else
File.basename(bad_name)
end
end
def find_dylib(bad_name)
return unless lib.directory?
suffix = "/#{find_dylib_suffix_from(bad_name)}"
lib.find { |pn| break pn if pn.to_s.end_with?(suffix) }
end
def mach_o_files
hardlinks = Set.new
mach_o_files = []
path.find do |pn|
next if pn.symlink? || pn.directory?
next if !pn.dylib? && !pn.mach_o_bundle? && !pn.mach_o_executable?
# if we've already processed a file, ignore its hardlinks (which have the same dev ID and inode)
# this prevents relocations from being performed on a binary more than once
next unless hardlinks.add? [pn.stat.dev, pn.stat.ino]
mach_o_files << pn
end
mach_o_files
end
def prepare_relocation_to_locations
relocation = generic_prepare_relocation_to_locations
brewed_perl = runtime_dependencies&.any? { |dep| dep["full_name"] == "perl" && dep["declared_directly"] }
perl_path = if brewed_perl
"#{HOMEBREW_PREFIX}/opt/perl/bin/perl"
else
perl_version = if tab["built_on"].present?
tab["built_on"]["preferred_perl"]
else
MacOS.preferred_perl_version
end
"/usr/bin/perl#{perl_version}"
end
relocation.add_replacement_pair(:perl, PERL_PLACEHOLDER, perl_path)
relocation
end
def recursive_fgrep_args
# Don't recurse into symlinks; the man page says this is the default, but
# it's wrong. -O is a BSD-grep-only option.
"-lrO"
end
end