diff --git a/tools/mac-libs.rb b/tools/mac-libs.rb new file mode 100755 index 00000000..1a379f96 --- /dev/null +++ b/tools/mac-libs.rb @@ -0,0 +1,227 @@ +#!/usr/bin/env ruby + +require "open3" +require "fileutils" + +$app_name = "melonDS" +$build_dmg = false +$build_dir = "" +$bundle = "" +$fallback_rpaths = ["/usr/local/lib", "/opt/local/lib"] + +def frameworks_dir + File.join($bundle, "Contents", "Frameworks") +end + +def executable + File.join($bundle, "Contents", "MacOS", $app_name) +end + +def get_rpaths(lib) + out = `otool -l #{lib}`.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) + `otool -L #{lib}` + .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(.*)/ + relative = rpath.sub(/^@executable_path/, "") + return "#{$bundle}/Contents/MacOS#{relative}", :executable_path + end + end + file = $fallback_rpaths + .map { |it| File.join(it, file_name) } + .find { |it| File.exist? it } + 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 + + puts lib + puts path + exit + return nil +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, action, path1, path2 = nil) + args = ["-#{action.to_s}", path1] + args << path2 if path2 != nil + + FileUtils.chmod("u+w", exec) + out, status = Open3.capture2e("install_name_tool", *args, exec) + if status != 0 + puts out + exit status + end +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] } + + libs.each do |lib| + libpath, libtype = lib + if File.basename(libpath) == File.basename(prog) + if libtype == :absolute + install_name_tool prog, :change, libpath, File.join("@rpath", File.basename(libpath)) + end + next + end + + framework = libpath.match(/(.*).framework/) + framework = framework.to_s if framework + + if framework + fwlib = libpath.sub(framework + "/", "") + fwname = File.basename(framework) + + unless libtype == :rpath + install_name_tool prog, :change, libpath, File.join("@rpath", fwname, fwlib) + end + + next if File.exist? File.join(frameworks_dir, fwname) + expath, _ = expand_load_path(orig_path, framework) + FileUtils.cp_r(expath, frameworks_dir, preserve: true) + fixup_libs File.join(frameworks_dir, fwname, fwlib), libpath + else + libname = File.basename(libpath) + dest = File.join(frameworks_dir, libname) + + if libtype == :absolute + install_name_tool prog, :change, libpath, File.join("@rpath", libname) + end + + next if File.exist? dest + expath, _ = expand_load_path(orig_path, libpath) + FileUtils.copy expath, frameworks_dir + fixup_libs dest, libpath + end + end +end + +if ARGV[0] == "--dmg" + $build_dmg = true + ARGV.shift +end + +if ARGV.length != 1 + puts "Usage: #{Process.argv0} [--dmg] " + 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 + +File.read(File.join($build_dir, "CMakeCache.txt")) + .split("\n") + .find { |it| it.match /Qt(.)_DIR:PATH=(.*)/ } + +qt_major = $1 +qt_dir = $2 +qt_dir = File.absolute_path("#{qt_dir}/../../..") + +$fallback_rpaths << File.join(qt_dir, "lib") + +plugin_paths = [ + File.join(qt_dir, "libexec", "qt#{qt_major}", "plugins"), + File.join(qt_dir, "plugins"), + File.join(qt_dir, "share", "qt", "plugins") +] + +qt_plugins = plugin_paths.find { |file| File.exist? file } + +if qt_plugins == nil + puts "Couldn't find Qt plugins, tried looking for:" + plugin_paths.each { |path| puts " - #{path}" } + 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"] +want_plugins.each do |plug| + destdir = File.join(bundle_plugins, File.dirname(plug)) + FileUtils.mkdir_p(destdir) + FileUtils.copy(File.join(qt_plugins, plug), destdir) + fixup_libs File.join(bundle_plugins, plug), File.join(qt_plugins, plug) +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 + +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