Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using otool (recursively) to find shared libraries needed by an app

I have a Cocoa app that uses otool to find required shared libraries that an app needs to function properly. For example, say I run otool -L on an app that uses QTKit.framework. I get a list of the shared libraries used by the program (including the basic frameworks like Cocoa.framework and 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

Which shows that the app uses QTKit.framework. However if I use "otool -L" again on the binary for QTKit.framework (/System/Library/Frameworks/QTKit.framework/Versions/A/QTKit) I get this:

/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)

That shows a load more frameworks that the original otool output on the app binary showed. Is there a way to have otool run recursively, meaning it grabs the frameworks that the app needs, then goes in and searches each of those frameworks for dependencies?

like image 590
indragie Avatar asked Oct 04 '09 22:10

indragie


1 Answers

Here's my take on the topic. My script is intended to start with the app main executable and traverse recursively all the frameworks. My use is around verifying if the app referenced frameworks match the embedded ones by Xcode. The key assumptions I made to focus on non-system frameworks were:

  • import path must begin with @rpath
  • must be a framework of X.framework/X format
  • weak frameworks are ignored

If any of these is not needed the awk regex /weak\)$/ { next }; match($1, /^@rpath.*(.framework)/) { ... } may be modified.
First I wrote a shell script:

#/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

It will fail on first referenced framework not found on the filesystem. That's all great, but the drawback is there is no track of visited frameworks, and there might be a lot of duplicate checks. Shell isn't particularly suited for global dictionary like structure to keep track of that.
That's why I rewrote this script using a python wrapper:

#!/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")

Conceptually the only difference is keeping track of visited frameworks which leads to dramatic elapsed time reduction (in my case from 7-8s to less than a second).

Finally this can be made an Xcode shell script in the Target build process (shell interpreter likewise set to /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)
like image 100
Kamil.S Avatar answered Sep 17 '22 07:09

Kamil.S