10

我有一个 Cocoa 应用程序,它使用 otool 来查找应用程序正常运行所需的共享库。例如,假设我在使用 QTKit.framework 的应用程序上运行 otool -L。我得到程序使用的共享库列表(包括基本框架,如 Cocoa.framework 和 AppKit.framework):

/System/Library/Frameworks/QTKit.framework/Versions/A/QTKit (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 476.0.0)
    /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 949.0.0)

..... and so on for a bunch of other frameworks

这表明该应用程序使用 QTKit.framework。但是,如果我在 QTKit.framework (/System/Library/Frameworks/QTKit.framework/Versions/A/QTKit) 的二进制文件上再次使用“otool -L”,我会得到:

/System/Library/Frameworks/QTKit.framework/Versions/A/QTKit (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox (compatibility version 1.0.0, current version 1.0.0)
/System/Library/PrivateFrameworks/CoreMedia.framework/Versions/A/CoreMedia (compatibility version 1.0.0, current version 1.0.0)
/System/Library/PrivateFrameworks/MediaToolbox.framework/Versions/A/MediaToolbox (compatibility version 1.0.0, current version 1.0.0)
/System/Library/PrivateFrameworks/VideoToolbox.framework/Versions/A/VideoToolbox (compatibility version 1.0.0, current version 1.0.0)
/System/Library/PrivateFrameworks/CoreMediaIOServices.framework/Versions/A/CoreMediaIOServices (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 751.0.0)
/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 1038.0.0)
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
/System/Library/Frameworks/QuickTime.framework/Versions/A/QuickTime (compatibility version 1.0.0, current version 1584.0.0)
/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore (compatibility version 1.2.0, current version 1.6.0)
/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox (compatibility version 1.0.0, current version 435.0.0)
/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 123.0.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 227.0.0)
/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 44.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 550.0.0)
/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices (compatibility version 1.0.0, current version 38.0.0)
/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo (compatibility version 1.2.0, current version 1.6.0)

这显示了应用程序二进制文件中原始 otool 输出显示的加载更多框架。有没有办法让 otool 递归运行,这意味着它会抓取应用程序需要的框架,然后进入并搜索每个框架的依赖关系?

4

4 回答 4

11

不,您必须重复运行 otool,或合并其解析代码(此处)。不要忘记处理@executable_path

这是在 Python 中(不@executable_path支持,规范化或带空格的文件名),因为这比尝试调试伪代码更容易:

import subprocess

def otool(s):
    o = subprocess.Popen(['/usr/bin/otool', '-L', s], stdout=subprocess.PIPE)
    for l in o.stdout:
        if l[0] == '\t':
            yield l.split(' ', 1)[0][1:]

need = set(['/Applications/iTunes.app/Contents/MacOS/iTunes'])
done = set()

while need:
    needed = set(need)
    need = set()
    for f in needed:
        need.update(otool(f))
    done.update(needed)
    need.difference_update(done)

for f in sorted(done):
    print f
于 2009-10-04T23:06:00.803 回答
3

这是我macdeployqt在使用 Homebrew 安装的库时用来修复输出的解决方案。我发现macdeployqt将 dylib 放入 Framework 文件夹中做得很好,但它无法修复路径。

https://github.com/jveitchmichaelis/deeplabel/blob/master/fix_paths_mac.py

我修改了 Nicholas 的脚本,使其更实用——它更正了@executable_path,@rpath@loader_path. 这不完全是生产代码,但它让我可以在其他 Mac 上运行应用程序,而无需安装任何依赖项。

运行:python fix_paths_mac.py ./path/to/your.app/Contents/MacOS/your_exe。即指向应用程序包内的二进制文件,它会找出其余的。

我假设大多数问题都来自与/usr/local. 因此,如果代码检测到存在指向 中的文件的依赖项/usr/local,它将适当地修复路径。pass如果文件不在文件夹中,您可以更改语句以复制到文件中Frameworks,但我没有遇到缺少 dylib 的情况,只是链接错误。

import subprocess
import os
import sys
from shutil import copyfile

executable = sys.argv[1]
app_folder = os.path.join(*executable.split('/')[:-3])
content_folder = os.path.join(app_folder, "Contents")
framework_path = os.path.join(content_folder, "Frameworks")

print(executable)
print("Working in {} ".format(app_folder))

def file_in_folder(file, folder):
    return os.path.exists(os.path.join(folder, file))

def otool(s):
    o = subprocess.Popen(['/usr/bin/otool', '-L', s], stdout=subprocess.PIPE)

    for l in o.stdout:
        l = l.decode()

        if l[0] == '\t':
            path = l.split(' ', 1)[0][1:]

            if "@executable_path" in path:
                path = path.replace("@executable_path", "")
                # fudge here to strip /../ from the start of the path.
                path = os.path.join(content_folder, path[4:])

            if "@loader_path" in path:
                path = path.replace("@loader_path", framework_path)

            if "@rpath" in path:
                path = path.replace("@rpath", framework_path)

            dependency_dylib_name = os.path.split(path)[-1]

            if "usr/local" in path:
                if app_folder in s:

                    print("Warning: {} depends on {}".format(s, path))

                    if file_in_folder(dependency_dylib_name, framework_path):
                        print("Dependent library {} is already in framework folder".format(dependency_dylib_name))

                        print("Running install name tool to fix {}.".format(s))

                        if dependency_dylib_name == os.path.split(s)[-1]:
                            _ = subprocess.Popen(['install_name_tool', '-id', os.path.join("@loader_path", dependency_dylib_name), s], stdout=subprocess.PIPE)

                        _ = subprocess.Popen(['install_name_tool', '-change', path, os.path.join("@loader_path", dependency_dylib_name), s], stdout=subprocess.PIPE)
                else:
                    # Potentially you could copy in the offending dylib here.
                    pass

            yield path

need = set([executable])
done = set()

while need:
    needed = set(need)
    need = set()
    for f in needed:
        need.update(otool(f))
    done.update(needed)
    need.difference_update(done)
于 2019-02-19T18:24:09.740 回答
1

这是我对这个话题的看法。我的脚本旨在从应用程序主可执行文件开始并递归遍历所有框架。我的用途是验证应用程序引用的框架是否与 Xcode 嵌入的框架相匹配。我对非系统框架所做的主要假设是:

  • 导入路径必须以@rpath
  • 必须是X.framework/X格式框架
  • weak框架被忽略

如果不需要其中任何一个,/weak\)$/ { next }; match($1, /^@rpath.*(.framework)/) { ... }可以修改 awk 正则表达式。
首先我写了一个shell脚本:

#/bin/sh
recursiveFrameworksParseStep() {
#fail on 1st otool error
set -e
set -o pipefail #not really POSIX compliant but good enough in MacOS where sh is emulated by bash
otool -L $1|awk -v pwd=${PWD} '/weak\)$/ { next }; match($1, /^@rpath.*(.framework)/) { gsub("@rpath",pwd"/MyApp.app/Frameworks",$1); print $1 }'| while read line; do
   if [ $1 != $line ]; then #safety check for otool -L output not to self reference resulting in infinite loop
      recursiveFrameworksParseStep $line
   fi
done
}
recursiveFrameworksParseStep MyApp.app/MyApp

它将在文件系统上找不到的第一个引用框架上失败。这一切都很好,但缺点是没有访问过的框架的跟踪,并且可能有很多重复检查。Shell 并不特别适合像全局字典这样的结构来跟踪它。
这就是为什么我使用 python 包装器重写了这个脚本:

#!/usr/bin/python
import subprocess
import os.path
from sys import exit

visitedFrameworks = set()

def fn(executableToProcess):
    try:
        otoolOut = subprocess.check_output(['otool','-L',executableToProcess])
    except subprocess.CalledProcessError: 
        exit(-1)

    pipeOutput = subprocess.Popen(['awk', '-v', os.path.expandvars('pwd=$PWD'),'/weak\)$/ { next };match($1, /@rpath.*(.framework)/) { gsub(\"@rpath\",pwd\"/MyApp.app/MyApp\",$1); print $1 }'], 
        stdin=subprocess.PIPE, stdout=subprocess.PIPE).communicate(otoolOut)

    lines = pipeOutput[0].split('\n')

    for outputLine in lines[1:-1]:
        if executableToProcess != outputLine:
            if outputLine not in visitedFrameworks:
                visitedFrameworks.add(outputLine)
                fn(outputLine)

fn("MyApp.app/MyApp")

从概念上讲,唯一的区别是跟踪访问过的框架,这会显着减少经过的时间(在我的例子中,从 7-8 秒到不到一秒)。

最后,这可以在 Target 构建过程中制作一个 Xcode shell 脚本(shell 解释器同样设置为/usr/bin/python)。

import subprocess
import os.path
from sys import exit

visitedFrameworks = set()
numberOfMissingFrameworks = 0

def fn(executableToProcess):
    global numberOfMissingFrameworks
    try:
        otoolOut = subprocess.check_output(['otool','-L',executableToProcess])
    except subprocess.CalledProcessError: 
        numberOfMissingFrameworks += 1
        return


    pipeOutput = subprocess.Popen(['awk', '-v', os.path.expandvars('frameworkPath=$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH'),'/weak\)$/ { next };match($1, /@rpath.*(.framework)/) { gsub(\"@rpath\",frameworkPath,$1); print $1 }'], 
        stdin=subprocess.PIPE, stdout=subprocess.PIPE).communicate(otoolOut)

    lines = pipeOutput[0].split('\n')

    for outputLine in lines[1:-1]:
        if executableToProcess != outputLine:
            if outputLine not in visitedFrameworks:
                visitedFrameworks.add(outputLine)
                fn(outputLine)

fn(os.path.expandvars('$TARGET_BUILD_DIR/$EXECUTABLE_PATH'))
exit(numberOfMissingFrameworks)
于 2021-10-13T18:00:04.920 回答
0

我有以下运行良好的虚拟脚本。没有过度设计,因为它只是一个很少用于调试目的的简单实用程序脚本。

#!/usr/bin/env python

import subprocess
import re
import sys

discovered = []

def library_finder(lib):
    lib = lib.split(':')[0]
    lib = lib.split(' ')[0]
    lib = re.sub(r"[\n\t\s]*", "", lib)

    if lib in discovered:
        return

    discovered.append(lib)

    print(lib)

    if lib.startswith("@rpath"):
      return

    process = subprocess.Popen(['otool', '-L', lib],
                               stdout=subprocess.PIPE,
                               universal_newlines=True)

    deps = process.stdout.readlines()

    for dep in deps:
        library_finder(dep)

if len(sys.argv) < 2:
    print("usage: {} <binary path>".format(sys.argv[0]))
    sys.exit(1)

library_finder(sys.argv[1])
于 2020-08-09T05:04:31.757 回答