melonDS/tools/mac-libs.rb
Nadia Holmquist Pedersen c6cab9ed41 mac-libs.rb: Make bundling a bit smarter
* Resolve symlinks to avoid including the same thing twice (like
  version-numered dylib symlinks)
* Look in all Qt prefix paths for plugins - the package may not
  necessarily have the same path
* reduce install_name_tool invocations to make it a bit faster
* change dylib IDs to remove original source path
2024-08-13 07:53:09 +02:00

298 lines
7.5 KiB
Ruby
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env ruby
require "open3"
require "fileutils"
$app_name = "melonDS"
$build_dmg = false
$build_dir = ""
$bundle = ""
$fallback_rpaths = []
def frameworks_dir
File.join($bundle, "Contents", "Frameworks")
end
def executable
File.join($bundle, "Contents", "MacOS", $app_name)
end
def get_rpaths(lib)
out, _ = Open3.capture2("otool", "-l", lib)
out = out.split("\n")
rpaths = []
out.each_with_index do |line, i|
if line.match(/^ *cmd LC_RPATH$/)
rpaths << out[i + 2].strip.split(" ")[1]
end
end
return rpaths
end
def get_load_libs(lib)
out, _ = Open3.capture2("otool", "-L", lib)
out.split("\n")
.drop(1)
.map { |it| it.strip.gsub(/ \(.*/, "") }
end
def expand_load_path(lib, path)
if path.match(/@(rpath|loader_path|executable_path)/)
path_type = $1
file_name = path.gsub(/^@#{path_type}\//, "")
case path_type
when "rpath"
get_rpaths(lib).each do |rpath|
file = File.join(rpath, file_name)
return file, :rpath if File.exist? file
if rpath.match(/^@executable_path(.*)/) != nil
relative = rpath.sub(/^@executable_path/, "")
return "#{$bundle}/Contents/MacOS#{relative}/#{file_name}", :executable_path
end
end
file = $fallback_rpaths
.map { |it| File.join(it, file_name) }
.find { |it| File.exist? it }
if file == nil
path = File.join(File.dirname(lib), file_name)
file = path if File.exist? path
end
return file, :rpath if file
when "executable_path"
file = File.join(File.dirname(executable), file_name)
return file, :executable_path if File.exist? file
when "loader_path"
file = File.join(File.dirname(lib), file_name)
return file, :loader_path if File.exist? file
else
throw "Unknown @path type"
end
else
return File.absolute_path(path), :absolute
end
return nil
end
def detect_framework(lib)
framework = lib.match(/(.*).framework/)
framework = framework.to_s if framework
if framework
fwname = File.basename(framework)
fwlib = lib.sub(framework + "/", "")
return true, framework, fwname, fwlib
else
return false
end
end
def system_path?(path)
path.match(/^\/usr\/lib|^\/System/) != nil
end
def system_lib?(lib)
system_path? File.dirname(lib)
end
def install_name_tool(exec, *options)
args = options.map do |it|
if it.is_a? Symbol then "-#{it.to_s}" else it end
end
Open3.popen3("install_name_tool", *args, exec) do |stdin, stdout, stderr, thread|
print stdout.read
err = stderr.read
unless err.match? "code signature"
print err
end
end
end
def strip(lib)
out, _ = Open3.capture2("xcrun", "strip", "-no_code_signature_warning", "-Sx", lib)
print out
end
def fixup_libs(prog, orig_path)
throw "fixup_libs: #{prog} doesn't exist" unless File.exist? prog
libs = get_load_libs(prog)
.map { |it| expand_load_path(orig_path, it) }
.select { |it| not system_lib? it[0] }
FileUtils.chmod("u+w", prog)
strip prog
changes = []
isfw, _, fwname, fwlib = detect_framework(prog)
if isfw then
changes += [:id, File.join("@rpath", fwname, fwlib)]
else
changes += [:id, File.join("@rpath", File.basename(prog))]
end
libs.each do |lib|
libpath, libtype = lib
if File.basename(libpath) == File.basename(prog)
if libtype == :absolute
changes += [:change, libpath, File.join("@rpath", File.basename(libpath))]
end
next
end
is_framework, fwpath, fwname, fwlib = detect_framework(libpath)
if is_framework
unless libtype == :rpath
changes += [:change, libpath, File.join("@rpath", fwname, fwlib)]
end
next if File.exist? File.join(frameworks_dir, fwname)
expath, _ = expand_load_path(orig_path, fwpath)
FileUtils.cp_r(expath, frameworks_dir, preserve: true)
FileUtils.chmod_R("u+w", File.join(frameworks_dir, fwname))
fixup_libs File.join(frameworks_dir, fwname, fwlib), libpath
else
reallibpath = File.realpath(libpath)
libname = File.basename(reallibpath)
dest = File.join(frameworks_dir, libname)
if libtype == :absolute
changes += [:change, libpath, File.join("@rpath", libname)]
end
next if File.exist? dest
expath, _ = expand_load_path(orig_path, reallibpath)
FileUtils.copy expath, frameworks_dir
FileUtils.chmod("u+w", dest)
fixup_libs dest, reallibpath
end
end
install_name_tool(prog, *changes)
end
if ARGV[0] == "--dmg"
$build_dmg = true
ARGV.shift
end
if ARGV.length != 1
puts "Usage: #{Process.argv0} [--dmg] <build-dir>"
return
end
$build_dir = ARGV[0]
unless File.exist? $build_dir
puts "#{$build_dir} doesn't exist"
end
$bundle = File.join($build_dir, "#{$app_name}.app")
unless File.exist? $bundle and File.exist? File.join($build_dir, "CMakeCache.txt")
puts "#{$build_dir} doesn't look like a valid build directory"
exit 1
end
for lib in get_load_libs(executable) do
next if system_lib? lib
path = File.dirname(lib)
if path.match? ".framework"
path = path.sub(/\/[^\/]+\.framework.*/, "")
end
$fallback_rpaths << path unless $fallback_rpaths.include? path
end
$qt_major = nil
qt_dirs = File.read(File.join($build_dir, "CMakeCache.txt"))
.split("\n")
.select { |it| it.match /^Qt([\w]+)_DIR:PATH=.*/ }
.map { |dir|
dir.match /^Qt(5|6).*\=(.*)/
throw "Inconsistent Qt versions found." if $qt_major != nil && $qt_major != $1
$qt_major = $1
File.absolute_path("#{$2}/../../..")
}.uniq
def locate_plugin(dirs, plugin)
plugin_paths = [
File.join("plugins", plugin),
File.join("lib", "qt-#{$qt_major}", "plugins", plugin),
File.join("libexec", "qt-#{$qt_major}", "plugins", plugin),
File.join("share", "qt", "plugins", plugin)
]
dirs.each do |dir|
plugin_paths.each do |plug|
path = File.join(dir, plug)
return path if File.exists? path
end
end
puts "Couldn't find the required Qt plugin: #{plugin}"
puts "Tried the following prefixes: "
puts dirs.map { |dir| "- #{dir}"}.join("\n")
puts "With the following plugin paths:"
puts plugin_paths.map { |path| "- #{path}"}.join("\n")
exit 1
end
FileUtils.mkdir_p(frameworks_dir)
fixup_libs(executable, executable)
bundle_plugins = File.join($bundle, "Contents", "PlugIns")
want_plugins = [
"styles/libqmacstyle.dylib",
"platforms/libqcocoa.dylib",
"imageformats/libqsvg.dylib"
]
want_plugins.each do |plug|
pluginpath = locate_plugin(qt_dirs, plug)
destdir = File.join(bundle_plugins, File.dirname(plug))
FileUtils.mkdir_p(destdir)
FileUtils.copy(pluginpath, destdir)
fixup_libs File.join(bundle_plugins, plug), pluginpath
end
want_rpath = "@executable_path/../Frameworks"
exec_rpaths = get_rpaths(executable)
exec_rpaths.select { |path| path != want_rpath }.each do |path|
install_name_tool executable, :delete_rpath, path
end
unless exec_rpaths.include? want_rpath
install_name_tool executable, :add_rpath, want_rpath
end
exec_rpaths = get_rpaths(executable)
Dir.glob("#{frameworks_dir}/**/Headers").each do |dir|
FileUtils.rm_rf dir
end
out, _ = Open3.capture2("codesign", "-s", "-", "-f", "--deep", $bundle)
print out
if $build_dmg
dmg_dir = File.join($build_dir, "dmg")
FileUtils.mkdir_p(dmg_dir)
FileUtils.cp_r($bundle, dmg_dir, preserve: true)
FileUtils.ln_s("/Applications", File.join(dmg_dir, "Applications"))
`hdiutil create -fs HFS+ -volname melonDS -srcfolder "#{dmg_dir}" -ov -format UDBZ "#{$build_dir}/melonDS.dmg"`
FileUtils.rm_rf(dmg_dir)
end