pantry/scripts/brewkit/fix-machos.rb

198 lines
5 KiB
Ruby
Raw Normal View History

#!/usr/bin/env ruby
# tea brewed ruby works with a tea shebang
# but normal ruby does not, macOS comes with ruby so we just use it
# ---
# dependencies:
# ruby-lang.org: '>=2'
# args: [ruby]
# ---
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'ruby-macho', '~> 3'
end
2022-09-16 21:39:00 +03:00
require 'fileutils'
require 'pathname'
require 'macho'
require 'find'
#TODO lazy & memoized
$tea_prefix = ENV['TEA_PREFIX'] || `tea --prefix`.chomp
abort "set TEA_PREFIX" if $tea_prefix.empty?
$pkg_prefix = ARGV.shift
abort "arg1 should be pkg-prefix" if $pkg_prefix.empty?
$pkg_prefix = Pathname.new($pkg_prefix).realpath.to_s
2022-09-16 21:39:00 +03:00
$inodes = Hash.new
2022-09-16 21:39:00 +03:00
def arm?
def type
case RUBY_PLATFORM
when /arm/, /aarch64/ then true
else false
end
end
end
2022-09-16 21:39:00 +03:00
class Fixer
def initialize(file)
@file = MachO::MachOFile.new(file)
@changed = false
end
2022-09-16 21:39:00 +03:00
def fix
case @file.filetype
when :dylib
fix_id
fix_rpaths
fix_install_names
when :execute
fix_rpaths
fix_install_names
when :bundle
fix_rpaths
fix_install_names
when :object
# noop
else
2022-09-16 21:39:00 +03:00
throw Error("unknown filetype: #{file.filetype}: #{file.filename}")
end
2022-09-16 21:39:00 +03:00
# M1 binaries must be signed
# changing the macho stuff invalidates the signature
# this resigns with the default adhoc signing profile
MachO.codesign!(@file.filename) if @changed and arm?
end
def fix_id
rel_path = Pathname.new(@file.filename).relative_path_from(Pathname.new($tea_prefix))
id = "@rpath/#{rel_path}"
if @file.dylib_id != id
2022-09-16 21:39:00 +03:00
# only do work if we must
@file.change_dylib_id id
2022-09-16 21:39:00 +03:00
write
end
2022-09-16 21:39:00 +03:00
end
2022-09-16 21:39:00 +03:00
def write
@file.write!
@changed = true
end
2022-09-16 21:39:00 +03:00
def links_to_other_tea_libs?
@file.linked_dylibs.each do |lib|
# starts_with? @rpath is not enough lol
# this because we are setting `id` to @rpath now so it's a reasonable indication
# that we link to tea libs, but the build system for the pkg may well do this for its
# own libs
return true if lib.start_with? $tea_prefix or lib.start_with? '@rpath'
2022-09-16 21:39:00 +03:00
end
return false
end
def fix_rpaths
#TODO remove spurious rpaths
dirty = false
2022-09-16 21:39:00 +03:00
rel_path = Pathname.new($tea_prefix).relative_path_from(Pathname.new(@file.filename).parent)
rpath = "@loader_path/#{rel_path}"
if not @file.rpaths.include? rpath and links_to_other_tea_libs?
@file.add_rpath rpath
dirty = true
end
while @file.rpaths.include? $tea_prefix
@file.delete_rpath $tea_prefix
dirty = true
end
2022-09-16 21:39:00 +03:00
write if dirty
2022-09-16 21:39:00 +03:00
end
def bad_install_names
@file.linked_dylibs.map do |lib|
if lib.start_with? '/'
if Pathname.new(lib).cleanpath.to_s.start_with? $tea_prefix
lib
end
elsif lib.start_with? '@rpath'
path = Pathname.new(lib.sub(%r{^@rpath}, $tea_prefix))
if path.exist?
lib
else
puts "warn:#{@file.filename}:#{lib}"
end
2022-09-16 21:39:00 +03:00
elsif lib.start_with? '@'
puts "warn:#{@file.filename}:#{lib}"
# noop
else
lib
end
end.compact
end
2022-09-16 21:39:00 +03:00
def fix_install_names
bad_names = bad_install_names
return if bad_names.empty?
def fix_tea_prefix s
s = Pathname.new(s).relative_path_from(Pathname.new($tea_prefix))
s = s.sub(%r{/v(\d+)\.\d+\.\d+/}, '/v\1/')
s = s.sub(%r{/(.+)\.(\d+)\.\d+\.\d+\.dylib$}, '/\1.dylib')
s = "@rpath/#{s}"
return s
end
2022-09-16 21:39:00 +03:00
bad_names.each do |old_name|
if old_name.start_with? $pkg_prefix
new_name = Pathname.new(old_name).relative_path_from(Pathname.new(@file.filename).parent)
new_name = "@loader_path/#{new_name}"
elsif old_name.start_with? '/'
new_name = fix_tea_prefix old_name
elsif old_name.start_with? '@rpath'
# so far we only feed bad @rpaths that are relative to the tea-prefix
new_name = fix_tea_prefix old_name.sub(%r{^@rpath}, $tea_prefix)
2022-09-16 21:39:00 +03:00
else
# assume they are meant to be relative to lib dir
new_name = Pathname.new($pkg_prefix).join("lib").relative_path_from(Pathname.new(@file.filename).parent)
new_name = "@loader_path/#{new_name}/#{old_name}"
end
@file.change_install_name old_name, new_name
end
write
end
end
ARGV.each do |arg|
Find.find(arg) do |file|
next unless File.file? file and !File.symlink? file
abs = Pathname.getwd.join(file).to_s
2022-09-16 21:39:00 +03:00
inode = File.stat(abs).ino
if $inodes[inode]
if arm?
# we have to code-sign on arm AND codesigning breaks the hard link
# so now we have to re-hardlink
puts "re-hardlinking #{abs} to #{$inodes[inode]}"
FileUtils.ln($inodes[inode], abs, :force => true)
end
# stuff like git has hardlinks to the same files
# avoid the work if we already did this inode
next
end
Fixer.new(abs).fix
$inodes[inode] = abs
rescue MachO::MagicError
#noop: not a Mach-O file
rescue MachO::TruncatedFileError
#noop: file cant be a Mach-O file
end
end