macOS下,应用加载依赖的dylib或Framework需要根据rpath等信息去加载,而链接后默认依赖的路径是机器上的路径(可以通过otool命令查看),不方便打包出去使用。

下面的脚本信息能把应用依赖的dylib等拷贝到相同的输出目录,并且使用install_name_tool设置好相应的依赖,输出的目录下可以单独使用。

collect_bin.sh

#!/bin/bash

python $(dirname "$0")/collect_bin.py "$@"

collect_bin.py

# -*- coding: UTF-8 -*-

import sys
import os
import argparse

# not copied
blacklist = """/usr /System""".split()

# copied
whitelist = """/usr/local""".split()

from sys import argv
from glob import glob
from subprocess import check_output, call
from collections import namedtuple
from shutil import copy, copytree, rmtree
from os import makedirs, rename, walk, path as ospath
import plistlib
import subprocess

LibTarget = namedtuple("LibTarget", ("path", "external", "copy_as"))

inspect = list()

inspected = set()

build_path = ""

def cmd(cmd):
    import subprocess
    import shlex
    return subprocess.check_output(shlex.split(cmd)).rstrip('\r\n')

def add(name, external=False, copy_as=None):
  if external and copy_as is None:
    copy_as = name.split("/")[-1]

  t = LibTarget(name, external, copy_as)
  if t in inspected:
    return
  inspect.append(t)
  inspected.add(t)


def otoolAnalyse(bin_file, target_path):
  add(bin_file, True)

  rmtree(target_path)
  makedirs(target_path)

  # 基于otool分析循环依赖
  while inspect:
    target = inspect.pop()
    print("inspecting", repr(target))
    path = target.path
    if path[0] == "@":
      continue
    out = check_output("otool -L '{0}'".format(path), shell=True,
                       universal_newlines=True)

    for line in out.split("\n")[1:]:
      new = line.strip().split(" (")[0]

      if not new or new[0] == "@" or new.endswith(path.split("/")[-1]):
        continue

      whitelisted = False
      for i in whitelist:
        if new.startswith(i):
          whitelisted = True
          break

      if not whitelisted:
        blacklisted = False
        for i in blacklist:
          if new.startswith(i):
            blacklisted = True
            break
        if blacklisted:
          continue

      add(new, True)

  # 所有需要处理的依赖
  changes = list()
  for path, external, copy_as in inspected:
    if not external:
      continue  # built with install_rpath hopefully
    changes.append("-change '%s' '@rpath/%s'" % (path, copy_as))
  changes = " ".join(changes)

  # install_name_tool逐个处理 
  for path, external, copy_as in inspected:
    id_ = ""
    filename = path
    rpath = ""

    if external:
      id_ = "-id '@rpath/%s'" % copy_as
      filename = target_path + copy_as
      rpath = "-add_rpath @loader_path/ -add_rpath @executable_path/"
      if "/" in copy_as:
        try:
          dirs = copy_as.rsplit("/", 1)[0]
          makedirs( target_path + dirs)
        except:
          pass
      copy(path, filename)
      os.chmod(filename, 0755)

    tool_cmd = "install_name_tool {0} {1} {2} '{3}'".format(changes, id_, rpath, filename)
    call(tool_cmd, shell=True)


def collect_run():
  parser = argparse.ArgumentParser()  
  parser.add_argument("--bin", help="bin file")  
  parser.add_argument("--target", help="target path dir")   
  args = parser.parse_args()  
  
  bin_file = args.bin
  target_path = args.target

  if not target_path.endswith("/"):
    target_path = target_path + "/"

  print "Bin file: " + bin_file
  print "Target path: " + target_path
  otoolAnalyse(bin_file, target_path)

if __name__ == '__main__':
    collect_run()

使用

./collect_bin.sh --bin "/usr/local/bin/ffmpeg" --target "/Users/xxx/tmpbin/"