Skip to content

Instantly share code, notes, and snippets.

@niw
Created October 11, 2024 13:57
Show Gist options
  • Save niw/e9cfac7d77e8aea21a7fee6ce212b6eb to your computer and use it in GitHub Desktop.
Save niw/e9cfac7d77e8aea21a7fee6ce212b6eb to your computer and use it in GitHub Desktop.
Make frameworks portable.
#!/usr/bin/env ruby
require 'optparse'
require 'pathname'
def update_name(name, options)
pattern = /#{options[:rpath]}/
if name =~ pattern
suffix = $'
if options[:path].join(suffix).exist?
"@rpath/#{suffix}"
end
end
end
def update_install_names(file, type, options)
puts "Process #{file} (#{type})"
otool_output = `otool -L #{file}`
install_names = nil
otool_output.each_line do |line|
if !install_names and line =~ /^(.+?)(?: \(.+?\))?:$/
install_names = []
elsif line =~ /^\t(.+?)(?: \(.+?\))?$/
install_name = $1
install_names << install_name
end
end
if type == :dylib
install_id = install_names.shift
if updated_name = update_name(install_id, options)
puts "Update intall ID from #{install_id} to #{updated_name}"
unless options[:dry_run]
`install_name_tool -id "#{updated_name}" "#{file}"`
end
end
end
install_names.each do |install_name|
if updated_name = update_name(install_name, options)
puts "Update name from #{install_name} to #{updated_name}"
unless options[:dry_run]
`install_name_tool -change "#{install_name}" "#{updated_name}" "#{file}"`
end
end
end
if type == :executable
relative_path = options[:path].relative_path_from(Pathname(File.dirname(file)))
rpath = File.join("@executable_path", relative_path.to_s)
puts "Add rpath #{rpath}"
unless options[:dry_run]
`install_name_tool -add_rpath "#{rpath}" "#{file}"`
end
end
end
def update_all_install_names(options)
options[:path].glob("**/*").each do |file|
file_output = `file -h "#{file}"`.lines.first
type = case file_output
when /Mach-O .+ executable/
:executable
when /Mach-O .+ dynamically linked shared library/
:dylib
when /Mach-O .+ bundle/
:bundle
when /Mach-O/
STDERR.puts "Unknown Mach-O type: #{file_output}"
next
else
next
end
update_install_names(file, type, options)
end
end
options = {}
OptionParser.new do |opts|
opts.on("--dry-run", "Print what would be done without doing it.") do
options[:dry_run] = true
end
opts.on("--path PATH", "Path where is searched Mach-O files and repalced with @rpath.") do |value|
options[:path] = Pathname(value)
end
opts.on("--rpath PATH", "Path in Mach-O binaries where is replaced with @rpath, such as '/Library/Frameworks/'") do |value|
unless value =~ /\/$/
value += "/"
end
options[:rpath] = Pathname(value)
end
opts.parse!(ARGV)
end
update_all_install_names(options)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment