how to make Xcode project with homebrew libraries portable?
Asked Answered
P

3

6

I have installed FreeType on my mac using Brew. The code on my mac works fine, but when I try to run the project on other mac I get the below mentioned linking error.

dyld: Library not loaded: /usr/local/opt/freetype/lib/libfreetype.6.dylib
Referenced from: /Users/ashutosh/Library/Developer/Xcode/DerivedData/InstrumentChamp- 
etytleaabhxmokadgrjzbgtmwxfl/Build/Products/Debug/instrumentchamp.app/Contents/MacOS/instrumentchamp
Reason: image not found (lldb) 

All the library directories and include directories of Freetype are included in project's '$SRCROOT/' directory when I try to run the code on other mac.

The path you see in the linking error for library is where brew had installed freetype in the mac where I created this project.

/usr/local/opt/freetype/lib/libfreetype.6.dylib

I have copied all the lib/ include/ directories that were needed to my project's home folder.
And I have set the library and include paths in Xcode.

What is it that I am missing here? What else do I have to do to make my code portable on any other Mac. I got the project to run on other mac by installing Brew, but I want to do it without the need to install brew.

PS: I had to install freetype using brew, as I couldn't compile the .dylib for freetype for 32bit processor, a 64bit copy of .dylib was giving me error such as 'wrong architecture!'

Peak answered 6/10, 2013 at 13:42 Comment(3)
Are you using different versions of OS X by chance? The Freetype2 framework ships with newer versions of the OS X/Xcode, and this dylib will be hit-up before any .dylib you bundle in your .app if you do not configure your bundle properly. From the command-line toolchain, you would need to use something like install_name_tool -change to correct this issue.Belk
@AndonM.Coleman, both the OS are same. But somehow the .dylib for freetype is looking in /usr/local/opt/..... i do not know why this is happening, because I have included /lib and /include for freetype in project folder... and also set the paths in Xcode build settingPeak
I should mention that is not a flag, it is only the start of a command. When you use install_name_tool -change on a binary in OS X, it is used to change how .dylib files are located when run from a .app bundle. Usually it involves something like @executable_path/../Frameworks/MyLib.dylib. But if you are using the Xcode GUI, it should be doing this for you when you bundle your .app. Have a look here. This has been discussed on SO as well, but I cannot find an actual question at the moment.Belk
B
4

The basic idea I was getting at in my comments is that OS X is pretty stupid about where it searches for libraries, it will use the same absolute path used during compilation to resolve them at run-time.

Usually when you want to deploy/distribute your application to a different machine from the one that built it, you will include your libraries with the install package/bundle. But you probably want them to use a path relative to your application at run-time, thus install_name_tool -change allows you to replace the nasty absolute path with something relative.

Hope this makes sense, Apple makes it really easy to use system-wide frameworks on OS X, custom libraries not so much. If you compile using a system-wide framework, /System/Library/Frameworks/... is universally available on all OS X installs (given the same target release version).


To solve your problem, I would do the following:

install_name_tool -change /usr/local/opt/freetype/lib/libfreetype.6.dylib @executable_path/lib/libfreetype.6.dylib <executable_name_here>

Then it will stop looking for libfreetype.6.dylib in the location it was when you compiled the software, and instead search for it relative to your executable's location at run-time (in this case, in the sub-directory lib/).

Belk answered 7/10, 2013 at 17:8 Comment(6)
That worked.. :) One thing though, the new path which I have given, doesnt contain @executable_path I didn't include that because it wasn't finding the dylib at... @executable_path/Contents/Frameworks/libfreetype.6.dylib What is the syntax for that?Peak
never mind, i was using @executable_path/.../Contents instead of @executable_path/../ContentsPeak
Do you have any idea how we can do this using Xcode? Because I got the .app executable to work, but whenever I build my project, and try to run it, dylib not found error is still there. I have to archive the project everytime, and get the .app, and then use install_name_tool everytime before using it on another mac machine. Why can't Xcode build the .app in such a way that it doesn't look for dylib in /usr/local/opt/ but in /@executable_path/Peak
Take a look here, developer.apple.com/library/mac/documentation/MacOSX/Conceptual/… and scroll down to the section labeled Embedding a Private Framework in Your Application Bundle. Admittedly, I do not use the Xcode IDE very often (I work mostly with the command line and Makefiles) so I could not tell you the exact menus and so on to use. If you make your library a build-dependency and set the install location accordingly, I believe the Xcode IDE will do what you want.Belk
Exactly command line is so much faster than IDE, but for the sake of uploading our app to the App Store I've to use IDE.. I was able to run the .app without IDE.. Stupid IDE is taking time. :) Anyhow, you have been a great help, Thank you.!Peak
have a look at this EXC_BAD_INSTRUCTION on MacOS I had this crash when I executed the application on another machine, all the library dependancies were fixed using the solution which you suggested above...Peak
T
2

In the end, this did not end up as easy as I originally thought. It works but there are caveats.

Building on the answers from Andon M. Coleman & Alexander Klimetschek, here is a script function which takes dylib names from Homebrew, copies them into the app bundle's Contents/Frameworks directory, and sets the executable's corresponding search path using install_name_tool. I noticed that sometimes the actual .dylib location is different from what the Xcode linker finds when putting together the executable and this handles that as well by querying the exe with otool -L.

The script below copies the .dylib for the liblo library into the app bundle. Adding new libs should be as simple as adding a new copy lib ###.dlyib line to the bottom of the script.

# exit on error
set -e

# paths
LOCALLIBS_PATH="$SRCROOT/libs"
HOMEBREW_PATH="/usr/local"
FRAMEWORKS_PATH="$BUILT_PRODUCTS_DIR/$FRAMEWORKS_FOLDER_PATH"
EXE_PATH="$BUILT_PRODUCTS_DIR/$EXECUTABLE_FOLDER_PATH/$EXECUTABLE_NAME"

# code signing identity
IDENTITY="$EXPANDED_CODE_SIGN_IDENTITY_NAME"
if [ "$IDENTITY" == "" ] ; then
    IDENTITY="$CODE_SIGN_IDENTITY"
fi

# copy lib into Contents/Frameworks and set lib search path
# $1: library .dylib
# $2: optional path to libs folder, otherwise use Homebrew
copylib() {
    LIB=$1
    if [ "$2" == "" ] ; then
        # use homebrew
        LIB_PATH=$(find "$HOMEBREW_PATH" -type f -name $LIB)
    else
        # use given path
        LIB_PATH="$2/$LIB"
    fi
    echo "$LIB:"
    echo "  $LIB_PATH -> $FRAMEWORKS_FOLDER_PATH/$LIB"

    # copy lib, set permissions, and sign
    mkdir -p "$FRAMEWORKS_PATH"
    cp "$LIB_PATH" "$FRAMEWORKS_PATH/"
    chmod 755 "$FRAMEWORKS_PATH/$LIB"
    codesign --verify --force --sign "$IDENTITY" "$FRAMEWORKS_PATH/$LIB"

    # set new path within executable
    # note: grep --max 1 as multi-arch builds will print path once *per arch*
    OLD=$(otool -L "$EXE_PATH" | grep --max 1 $LIB | awk '{print $1}')
    NEW="@executable_path/../Frameworks/$LIB"
    install_name_tool -change "$OLD" "$NEW" "$EXE_PATH"

    # print to confirm new path
    otool -L "$EXE_PATH" | grep $LIB
}

##### GO

# use lib from homebrew
copy lib liblo.7.dylib

# or use local lib
#copylib liblo.7.dylib "$LOCALLIBS_PATH/liblo/lib"

Add this as a Run Script build phase in the Xcode project. You can also save it as an external file and call it using sh within the Run Script phase with:

sh $PROJECT_DIR/path-to/copy-libs.sh

Example build report output:

liblo.7.dylib:
    /usr/local/Cellar/liblo/0.29/lib/liblo.7.dylib ->> YOURAPP.app/Contents/Frameworks/liblo.7.dylib
    @executable_path/../Frameworks/liblo.7.dylib (compatibility version 11.0.0, current version 11.0.0)

The $(find $HOMEBREW_PATH -type f -name $LIB) could probably be improved by following the .dylib symlink in /usr/local/lib instead.

UPDATE: You will probably also need to make sure the new dylibs are signed otherwise the code signing step will fail when building, at least it eventually did for me. Following this SO post, adding --deep to the Other Code Signing Flags option in the project Build Settings works.

There might be a better way to sign only the files that the script adds, but I didn't feel like digging into running codesign manually. From what I've read, however, --deep is not suggested by Apple for anything but temporary fixes, so it's probably not a best practice.

UPDATE 2: Running --deep didn't actually solve the problem and the app still would not run on other machines. In the end, I needed to code sign the copied .dylib manually which turned out to be relatively easy.

One note: I ran into an ambiguous "Mac Developer" identities error when running codesign which seemed to be confused by two certificates with the same name in the Keychain: ie. the current developer certificate and the previous expired developer certificate. Opening Keychain Access and deleting the expired certificate solved the problem.

UPDATE 3: Quoted path variable usage to fix paths with spaces.

UPDATE 4: This solution works fine for building apps for recent systems, but I ran into a code signing problem when running the app on macOS 10.10. Judging from this SO post, older macOS versions use a different codesign hash algorithm and the app would run fine on a 10.12 system but fail with a code signing error on 10.10. On both systems, codesign verified that everything was signed correctly.

You basically need to build the libraries with the -mmacosx-version-min set so codesign will be able to tell which algorithms to use to support the min and the recent macOS versions. I tried to pass custom CFLAGS/LDFLAGS to Homebrew while building liblo from source, but it's not possible without editing the brew formula. In the end, I decided to write a build script to build liblo from source myself and I modified the copy script so it can also take a local location. Now everything is finally working.

TLDR: Homebrew libs work great for initial development & testing, but building the libs manually works better for deployment.

Timothytimour answered 10/4, 2018 at 12:17 Comment(0)
P
1

Here is an automated script based on Andon's answer that will replace all @executable_path/../Frameworks/ with @executable_path/, so that all frameworks can be in the same folder as the executable. This also adapts the frameworks themselves, if they have dependencies on each other.

Beware, this only makes sense for command-line applications. If it's a regular OSX bundles app, one probably shouldn't change the "../Frameworks" convention.

Instructions

  • Add this as "Run Script" using Shell = /bin/sh at the end of your Xcode project Build Phases.

  • Also make sure to have the "Copy Files" step with a "Destination = Products Directory" for all the frameworks.

Script

# change the hardcoded ../Frameworks relative path that Xcode does by rewriting the binaries after the build
# check with:
#   otool -L <binary>

# this is the new location
# make sure this is the same Destination the Frameworks are copied to in the "Copy Files" step
# the default "@executable_path/" would be Destination = Products Directory
NEW_FRAMEWORKS_PATH="@executable_path/"

# the one we want to replace
DEFAULT_FRAMEWORKS_PATH="@executable_path/../Frameworks/"

function change_binary {
    local libpaths=`otool -L "$1" | grep "$DEFAULT_FRAMEWORKS_PATH" | tr '\t' ' ' | cut -d " " -f2`
    local lib
    for lib in $libpaths; do
        if [ "$2" == "recursive" ]; then
            local libbinary=`echo $lib | sed "s,$DEFAULT_FRAMEWORKS_PATH,,"`
            change_binary "$libbinary"
        fi
        local newlib=`echo $lib | sed "s,$DEFAULT_FRAMEWORKS_PATH,$NEW_FRAMEWORKS_PATH,"`;
        echo "changing library path in '$1': '$lib' => '$newlib'"
        install_name_tool -change "$lib" "$newlib" "$1"
    done
}

cd $BUILT_PRODUCTS_DIR
change_binary "$EXECUTABLE_NAME" recursive

Note that changing NEW_FRAMEWORKS_PATH to e.g. a relative subpath does not work, the script would need to become a bit more complex to handle this.

Result Structure

$BUILT_PRODUCTS_DIR/
     executable
     Some.framework/
     Another.framework/
     ...

And otool -L executable would look like this:

executable:
    @executable_path/Some.framework/Versions/A/Some (...)
    @executable_path/Another.framework/Versions/A/Another (...)
    ...
Pelfrey answered 20/12, 2017 at 1:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.