-
Notifications
You must be signed in to change notification settings - Fork 105
WIP: [ruby] Resolve @rpath/@loader_path dylib references when relinking #5160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f71a570
e090be5
7fb1c65
feb4db8
cde7b60
39e4de3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| require "fileutils" | ||
| require "shellwords" | ||
|
|
||
|
|
||
|
|
||
|
|
@@ -7,7 +8,54 @@ | |
|
|
||
| FileUtils.mkdir_p(@new_dylib_path) | ||
|
|
||
| def fix_dylib_for_file(file) | ||
| # Extracts the LC_RPATH entries from `otool -l` output. | ||
| def parse_rpaths(otool_l_output) | ||
| otool_l_output.scan(/cmd\s+LC_RPATH\s+cmdsize\s+\d+\s+path\s+(.+?)\s+\(offset\s+\d+\)/).flatten | ||
| end | ||
|
|
||
| # Returns the LC_RPATH entries of a Mach-O file. | ||
| def rpaths_for_file(file) | ||
| parse_rpaths(`otool -l #{Shellwords.escape(file)} 2> /dev/null`) | ||
| end | ||
|
Comment on lines
+17
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using string interpolation directly inside backticks ( def rpaths_for_file(file)
require "shellwords" unless defined?(Shellwords)
parse_rpaths(`otool -l #{Shellwords.escape(file)} 2> /dev/null`)
end |
||
|
|
||
| # Resolves a dependency reported by `otool -L` to a real file on disk. | ||
| # | ||
| # Absolute paths are returned unchanged. Homebrew bottles (e.g. brotli 1.2.0) | ||
| # now reference sibling libraries as `@rpath/libfoo.dylib` (or `@loader_path/`, | ||
| # `@executable_path/`), which is not a filesystem path and cannot be copied | ||
| # directly. For those, look for the library next to the referencing library's | ||
| # original location (source_dir), then in each of its LC_RPATH entries, then | ||
| # among the dylibs that were already copied. Returns nil if nothing matches. | ||
| # | ||
| # `@loader_path` is expanded relative to source_dir. `@executable_path` depends | ||
| # on whichever executable ends up loading the library, which is not known at | ||
| # packaging time, so those references are only matched by basename. | ||
| # | ||
| # See https://github.com/flutter/flutter/issues/164665 and | ||
| # https://ci.chromium.org/b/8669644619614462833. | ||
| def resolve_dylib_path(libfile, source_dir, rpaths) | ||
| return libfile unless libfile.start_with?("@") | ||
|
|
||
| basename = File.basename(libfile) | ||
| candidates = [] | ||
| if libfile.start_with?("@loader_path/") | ||
| candidates << File.expand_path(libfile.sub(/\A@loader_path/, source_dir)) | ||
| end | ||
| candidates << File.join(source_dir, basename) | ||
| rpaths.each do |rpath| | ||
| next if rpath.start_with?("@executable_path") | ||
| rpath = File.expand_path(rpath.sub(/\A@loader_path/, source_dir)) | ||
| candidates << File.join(rpath, basename) | ||
| end | ||
| candidates << File.join(@new_dylib_path, basename) | ||
|
|
||
| candidates.find { |candidate| File.file?(candidate) } | ||
| end | ||
|
Comment on lines
+36
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, We can resolve Additionally, we can simplify the substitution logic and use the more idiomatic def resolve_dylib_path(libfile, source_dir, rpaths)
return libfile unless libfile.start_with?("@")
basename = File.basename(libfile)
executable_dir = File.expand_path(File.join(@new_dylib_path, "..", "bin"))
candidates = []
if libfile.start_with?("@loader_path/", "@executable_path/")
resolved_libfile = libfile.sub(/\A@loader_path/, source_dir).sub(/\A@executable_path/, executable_dir)
candidates << File.expand_path(resolved_libfile)
end
candidates << File.join(source_dir, basename)
rpaths.each do |rpath|
resolved_rpath = rpath.sub(/\A@loader_path/, source_dir).sub(/\A@executable_path/, executable_dir)
candidates << File.join(File.expand_path(resolved_rpath), basename)
end
candidates << File.join(@new_dylib_path, basename)
candidates.find { |candidate| File.file?(candidate) }
end |
||
|
|
||
| # `source_dir` is the directory the file was originally copied from. It is used | ||
| # to resolve `@rpath`-style dependencies, since a copied dylib no longer sits | ||
| # next to its siblings. | ||
| def fix_dylib_for_file(file, source_dir = File.dirname(File.expand_path(file))) | ||
| results = `otool -L "#{file}"` #Will get information about which dylibs to link | ||
|
|
||
| if results.is_a?(String) && results != "" && !results.include?("is not an object file") && !results.include?("Assertion failed:") | ||
|
|
@@ -25,6 +73,7 @@ def fix_dylib_for_file(file) | |
| itterate = lines[1..-1] | ||
| itterate = [] unless itterate | ||
|
|
||
| rpaths = nil | ||
|
|
||
| itterate.each do |libfile_line| | ||
| libfile = libfile_line.split(" (compatibility version")[0].strip | ||
|
|
@@ -34,14 +83,22 @@ def fix_dylib_for_file(file) | |
| next if libfile.include?("/usr/lib/") # These are global and assumed to be present on all versions of osx | ||
| next if libfile.include?("/System/Library/") # Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation | ||
|
|
||
| new_libfile = File.join(@new_dylib_path, File.split(libfile)[-1]) | ||
| rpaths ||= rpaths_for_file(file) if libfile.start_with?("@") | ||
| source_libfile = resolve_dylib_path(libfile, source_dir, rpaths || []) | ||
| if source_libfile.nil? | ||
| puts "ERROR: could not resolve #{libfile} referenced by #{file} (source dir: #{source_dir}, rpaths: #{rpaths.inspect})" | ||
| exit 1 | ||
| end | ||
|
|
||
| new_libfile = File.join(@new_dylib_path, File.split(source_libfile)[-1]) | ||
|
|
||
| unless File.file?(new_libfile) | ||
| FileUtils.copy(libfile, new_libfile) | ||
| FileUtils.copy(source_libfile, new_libfile) | ||
| puts "--COPIED-- #{new_libfile}" | ||
| fix_dylib_for_file(new_libfile) | ||
| fix_dylib_for_file(new_libfile, File.dirname(source_libfile)) | ||
| end | ||
|
|
||
| # `libfile` must be the exact string from the load command (e.g. `@rpath/libfoo.dylib`). | ||
| relink_command_results = `install_name_tool -change #{libfile} #{new_libfile} #{file} 2> /dev/null` # Will relink external library | ||
| puts "Linked: #{new_libfile} to #{file}" | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current regular expression for parsing
otool -loutput assumes strict newline (\n) and spacing formatting. To make the parsing more robust against variations in whitespace, tabs, or line endings (e.g., carriage returns) across different macOS/Xcode versions, it is safer to use\s+instead of literal newlines and spaces.