OSX App Bundle erstellen

89

Angenommen, ich habe eine osX-App ohne Verwendung von Xcode erstellt. Nach dem Kompilieren mit GCC erhalte ich eine ausführbare Datei, die mit mehreren anderen Bibliotheken verknüpft ist. Einige dieser Bibliotheken sind möglicherweise wieder dynamisch mit anderen nicht standardmäßigen Systembibliotheken verknüpft

Gibt es ein Tool, das ein OSX-App-Bundle erstellt, indem zuerst die erforderlichen Verzeichnisstrukturen erstellt und dann rekursiv Links kopiert / überprüft / repariert werden, um sicherzustellen, dass alle dynamischen Abhängigkeiten auch im App-Bundle enthalten sind?

Ich denke, ich kann versuchen, so etwas zu schreiben, aber ich habe mich gefragt, ob so etwas schon existiert.

Yogi
quelle
4
Ich kann Xcode aus mehreren Gründen nicht verwenden. Eines davon ist, weil ich benutzerdefinierte gcc verwende. Mit Xcode kann ich kein anderes gcc angeben. Ich benutze cmake, um meine Makefiles zu erstellen.
Yogi
>> Einige dieser Bibliotheken sind möglicherweise wieder dynamisch mit anderen nicht standardmäßigen Systembibliotheken verknüpft. << Die ausgewählte Antwort hilft in diesem Fall nicht weiter. Wie haben Sie es gelöst?
FlowUI. SimpleUITesting.com

Antworten:

142

Es gibt zwei Möglichkeiten, ein App-Bundle unter MacOSX zu erstellen: Easy und Ugly.

Der einfache Weg ist, einfach XCode zu verwenden. Getan.

Das Problem ist manchmal, dass Sie nicht können.

In meinem Fall erstelle ich eine App, die andere Apps erstellt. Ich kann nicht davon ausgehen, dass der Benutzer XCode installiert hat. Ich verwende auch MacPorts , um die Bibliotheken zu erstellen, von denen meine App abhängt. Ich muss sicherstellen, dass diese Dylibs mit der App gebündelt werden, bevor ich sie verteile.

Haftungsausschluss: Ich bin völlig unqualifiziert, diesen Beitrag zu schreiben. Alles, was darin enthalten ist, wurde aus Apple-Dokumenten hervorgegangen, wobei vorhandene Apps und Versuche und Irrtümer auseinander genommen wurden. Es funktioniert bei mir, ist aber höchstwahrscheinlich falsch. Bitte mailen Sie mir, wenn Sie Korrekturen haben.

Als erstes sollten Sie wissen, dass ein App-Bundle nur ein Verzeichnis ist.
Lassen Sie uns die Struktur einer hypothetischen foo.app untersuchen.

foo.app/
    Inhalt/
        Info.plist
        Mac OS/
            foo
        Ressourcen/
            foo.icns

Info.plist ist eine einfache XML-Datei. Sie können es mit einem Texteditor oder der im Lieferumfang von XCode enthaltenen App App Property List Editor bearbeiten. (Es befindet sich im Verzeichnis / Developer / Applications / Utilities /).

Die wichtigsten Dinge, die Sie einbeziehen müssen, sind:

CFBundleName - Der Name der App.

CFBundleIcon - Eine Symboldatei , die sich im Verzeichnis Inhalt / Ressourcen befindet. Verwenden Sie die Icon Composer-App, um das Symbol zu erstellen. (Es befindet sich auch im Verzeichnis / Developer / Applications / Utilities /.) Sie können einfach ein PNG per Drag & Drop in das Fenster ziehen und sollten automatisch die Mip-Levels für Sie generieren.

CFBundleExecutable - Name der ausführbaren Datei, die sich vermutlich im Inhalts- / MacOS- / Unterordner befindet.

Es gibt viel mehr Optionen, die oben aufgeführten sind nur das absolute Minimum. Hier finden Sie einige Apple-Dokumentationen zur Info.plist- Datei und zur App-Bundle-Struktur .

Hier ist auch ein Beispiel für eine Info.plist.

<? xml version = "1.0" encoding = "UTF-8"?>
<! DOCTYPE plist PUBLIC "- // Apple Computer // DTD PLIST 1.0 // DE" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version = "1.0">
<Diktat>
  <key> CFBundleGetInfoString </ key>
  <string> Foo </ string>
  <key> CFBundleExecutable </ key>
  <string> foo </ string>
  <key> CFBundleIdentifier </ key>
  <string> com.Ihr-Firmenname.www </ string>
  <key> CFBundleName </ key>
  <string> foo </ string>
  <key> CFBundleIconFile </ key>
  <string> foo.icns </ string>
  <key> CFBundleShortVersionString </ key>
  <string> 0,01 </ string>
  <key> CFBundleInfoDictionaryVersion </ key>
  <string> 6.0 </ string>
  <key> CFBundlePackageType </ key>
  <string> APPL </ string>
  <key> IFMajorVersion </ key>
  <Ganzzahl> 0 </ Ganzzahl>
  <key> IFMinorVersion </ key>
  <Ganzzahl> 1 </ Ganzzahl>
</ dict>
</ plist>

In einer perfekten Welt können Sie Ihre ausführbare Datei einfach in Contents / MacOS / dir ablegen und fertig sein. Wenn Ihre App jedoch nicht standardmäßige Dylib-Abhängigkeiten aufweist, funktioniert sie nicht. Wie Windows verfügt auch MacOS über eine spezielle DLL-Hölle .

Wenn Sie MacPorts verwenden , um Bibliotheken zu erstellen, mit denen Sie verknüpfen, werden die Speicherorte der Dylibs in Ihrer ausführbaren Datei fest codiert. Wenn Sie die App auf einem Computer ausführen, auf dem sich die Dylibs genau am selben Ort befinden, läuft sie einwandfrei. Die meisten Benutzer haben sie jedoch nicht installiert. Wenn sie auf Ihre App doppelklicken, stürzt sie einfach ab.

Bevor Sie Ihre ausführbare Datei verteilen, müssen Sie alle geladenen Dylibs sammeln und in das App-Bundle kopieren. Sie müssen auch die ausführbare Datei bearbeiten, damit sie an der richtigen Stelle nach den Dylibs sucht. dh wohin Sie sie kopiert haben.

Handbearbeitung einer ausführbaren Datei klingt gefährlich, oder? Zum Glück gibt es Kommandozeilen-Tools, die helfen.

otool -L ausführbarer_name

Dieser Befehl listet alle Dylibs auf, von denen Ihre App abhängt. Wenn Sie solche sehen, die sich NICHT im Ordner System / Library oder usr / lib befinden, müssen Sie diese in das App-Bundle kopieren. Kopieren Sie sie in den Ordner / Contents / MacOS /. Als nächstes müssen Sie die ausführbare Datei bearbeiten, um die neuen Dylibs zu verwenden.

Zunächst müssen Sie sicherstellen, dass Sie mit dem Flag -headerpad_max_install_names verknüpfen. Dies stellt nur sicher, dass, wenn der neue Dylib-Pfad länger als der vorherige ist, Platz dafür vorhanden ist.

Verwenden Sie zweitens das Installationsname-Tool, um jeden Dylib-Pfad zu ändern.

Installationsname_Tool -Ändern Sie den vorhandenen_Pfad_zu_Dylib @ ausführbaren_Pfad / blah.dylib ausführbaren_Namen

Nehmen wir als praktisches Beispiel an, Ihre App verwendet libSDL und otool listet den Speicherort als "/opt/local/lib/libSDL-1.2.0.dylib" auf.

Kopieren Sie es zuerst in das App-Bundle.

cp /opt/local/lib/libSDL-1.2.0.dylib foo.app/Contents/MacOS/

Bearbeiten Sie dann die ausführbare Datei, um den neuen Speicherort zu verwenden (HINWEIS: Stellen Sie sicher, dass Sie sie mit dem Flag -headerpad_max_install_names erstellt haben).

install_name_tool -change /opt/local/lib/libSDL-1.2.0.dylib @ executeable_path / libSDL-1.2.0.dylib foo.app/Contents/MacOS/foo

Puh, wir sind fast fertig. Jetzt gibt es ein kleines Problem mit dem aktuellen Arbeitsverzeichnis.

Wenn Sie Ihre App starten, ist das aktuelle Verzeichnis das Verzeichnis über dem sich die Anwendung befindet. Beispiel: Wenn Sie die Datei foo.app im Ordner / Applcations ablegen, ist das aktuelle Verzeichnis beim Starten der App der Ordner / Applications. Nicht die /Applications/foo.app/Contents/MacOS/, wie Sie vielleicht erwarten.

Sie können Ihre App ändern, um dies zu berücksichtigen, oder Sie können dieses magische kleine Startskript verwenden, das das aktuelle Verzeichnis ändert und Ihre App startet.

#! / bin / bash
cd "$ {0% / *}"
./foo

Stellen Sie sicher, dass Sie die Datei Info.plist so anpassen, dass CFBundleExecutable auf das Startskript und nicht auf die vorherige ausführbare Datei verweist.

Ok, alles jetzt erledigt. Glücklicherweise begraben Sie, sobald Sie all dieses Zeug kennen, es in einem Build-Skript.

hyperlogisch
quelle
7
+1. Der Zombie-Hund macht mich jedoch verrückt. Könnten Sie ein weniger vernarbendes Bild verwenden, um die DLL-Hölle zu veranschaulichen? (Ganz zu schweigen davon, dass der Begriff "DLL-Hölle" nicht gilt. Die bevorzugte Methode zum Verteilen dynamischer Bibliotheken sind Frameworks, die die meisten Probleme vermeiden. Dennoch ist der .dylib-Ansatz für Bibliotheken im UNIX-Stil nützlich.)
Heinrich Apfelmus
1
Wie beheben wir doppelte Abhängigkeiten? Wie verweist eine Dylib auf eine andere Dylib?
FlowUI. SimpleUITesting.com
Gibt es keine Möglichkeit, die ausführbare Datei mit den richtigen Speicherorten zu kompilieren, damit Sie sie nicht bearbeiten müssen?
Synchronizer
Würde dies bei Catalina überhaupt noch funktionieren, wenn man bedenkt, wie streng die Sicherheit geworden ist? (Modifizieren von Dylibs, Binärdateien usw.)
Synchronizer
20

Ich habe tatsächlich ein sehr praktisches Tool gefunden, das etwas Anerkennung verdient ... NEIN - ich habe es nicht entwickelt;)

https://github.com/auriamg/macdylibbundler/

Es löst alle Abhängigkeiten auf und "repariert" Ihre ausführbare Datei sowie Ihre Dylib-Dateien, damit sie in Ihrem App-Bundle reibungslos funktionieren.

... prüft auch die Abhängigkeiten Ihrer abhängigen dynamischen Bibliotheken: D.

Chris
quelle
Dies ist ein ziemlich nützliches Werkzeug! Vielen Dank für das Teilen und Entwickeln.
xpnimi
Hier fehlen die Abhängigkeiten der Abhängigkeiten. Fix?
Peter
8

Die einfachste Lösung ist: Erstellen Sie einmal ein Xcode-Projekt, ohne etwas zu ändern (dh behalten Sie die einfache Ein-Fenster-App, die Xcode für Sie erstellt), erstellen Sie es und kopieren Sie das für Sie erstellte Bundle. Bearbeiten Sie dann die Dateien (insbesondere die Info.plist) entsprechend Ihrem Inhalt und legen Sie Ihre eigene Binärdatei im Verzeichnis Contents / MacOS / ab.

F'x
quelle
4
In der Frage wird explizit gefragt, wie es ohne xcode geht. Ich bin im selben Boot und möchte eine echte Antwort.
hyperlogic
5
Nun, damit müssen Sie XCode nur einmal in Ihrem Leben verwenden :) oder jemanden bitten, dies für Sie zu tun.
F'x
@ F'x Kannst du die generierte .app einfach irgendwo als Beispiel veröffentlichen?
Endolith
8

Ich benutze dies in meinem Makefile ... Es erstellt ein App-Bundle. Lesen Sie es und verstehen Sie es, denn Sie benötigen eine PNG-Symboldatei in einem Macosx / Ordner zusammen mit den hier enthaltenen Dateien PkgInfo und Info.plist ...

"es funktioniert auf meinem Computer" ... ich benutze dies für mehrere Apps auf Mavericks ...

APPNAME=MyApp
APPBUNDLE=$(APPNAME).app
APPBUNDLECONTENTS=$(APPBUNDLE)/Contents
APPBUNDLEEXE=$(APPBUNDLECONTENTS)/MacOS
APPBUNDLERESOURCES=$(APPBUNDLECONTENTS)/Resources
APPBUNDLEICON=$(APPBUNDLECONTENTS)/Resources
appbundle: macosx/$(APPNAME).icns
    rm -rf $(APPBUNDLE)
    mkdir $(APPBUNDLE)
    mkdir $(APPBUNDLE)/Contents
    mkdir $(APPBUNDLE)/Contents/MacOS
    mkdir $(APPBUNDLE)/Contents/Resources
    cp macosx/Info.plist $(APPBUNDLECONTENTS)/
    cp macosx/PkgInfo $(APPBUNDLECONTENTS)/
    cp macosx/$(APPNAME).icns $(APPBUNDLEICON)/
    cp $(OUTFILE) $(APPBUNDLEEXE)/$(APPNAME)

macosx/$(APPNAME).icns: macosx/$(APPNAME)Icon.png
    rm -rf macosx/$(APPNAME).iconset
    mkdir macosx/$(APPNAME).iconset
    sips -z 16 16     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_16x16.png
    sips -z 32 32     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_16x16@2x.png
    sips -z 32 32     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_32x32.png
    sips -z 64 64     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_32x32@2x.png
    sips -z 128 128   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_128x128.png
    sips -z 256 256   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_128x128@2x.png
    sips -z 256 256   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_256x256.png
    sips -z 512 512   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_256x256@2x.png
    sips -z 512 512   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_512x512.png
    cp macosx/$(APPNAME)Icon.png macosx/$(APPNAME).iconset/icon_512x512@2x.png
    iconutil -c icns -o macosx/$(APPNAME).icns macosx/$(APPNAME).iconset
    rm -r macosx/$(APPNAME).iconset

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleExecutable</key>
    <string>MyApp</string>
    <key>CFBundleGetInfoString</key>
    <string>0.48.2, Copyright 2013 my company</string>
    <key>CFBundleIconFile</key>
    <string>MyApp.icns</string>
    <key>CFBundleIdentifier</key>
    <string>com.mycompany.MyApp</string>
    <key>CFBundleDocumentTypes</key>
    <array>
    </array>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>0.48.2</string>
    <key>CFBundleSignature</key>
    <string>MyAp</string>
    <key>CFBundleVersion</key>
    <string>0.48.2</string>
    <key>NSHumanReadableCopyright</key>
    <string>Copyright 2013 my company.</string>
    <key>LSMinimumSystemVersion</key>
    <string>10.3</string>
</dict>
</plist>

PkgInfo

APPLMyAp
subatomar
quelle
Sie können die Info.plist mit Textplatzhaltern versehen und dann mit etwas wie sed oder awk die endgültige Info.plist mit den richtigen Feldern erstellen. Oder verwenden Sie sogar Plutil, um die Plist zu erstellen.
Mark Heath
3

Es gibt einige Open Source-Tools, mit denen Sie App-Bundles mit abhängigen Bibliotheken für bestimmte Umgebungen erstellen können, z. B. py2app für Python-basierte Anwendungen. Wenn Sie keine allgemeinere finden, können Sie sie möglicherweise an Ihre Bedürfnisse anpassen.

Ned Deily
quelle
2

Ich wünschte, ich hätte diesen Beitrag früher gefunden ...

Hier ist meine skizzenhafte Art, dieses Problem mithilfe einer Run scriptPhase zu lösen, die jedes Mal aufgerufen wird, wenn ich eine ReleaseVersion meiner App erstelle :

# this is an array of my dependencies' libraries paths 
# which will be iterated in order to find those dependencies using otool -L
libpaths=("$NDNRTC_LIB_PATH" "$BOOST_LIB_PATH" "$NDNCHAT_LIB_PATH" "$NDNCPP_LIB_PATH" "/opt/local/lib")
frameworksDir=$BUILT_PRODUCTS_DIR/$FRAMEWORKS_FOLDER_PATH
executable=$BUILT_PRODUCTS_DIR/$EXECUTABLE_PATH

#echo "libpaths $libpaths"
bRecursion=0
lRecursion=0

# this function iterates through libpaths array
# and checks binary with "otool -L" command for containment
# of dependency which has "libpath" path
# if such dependency has been found, it will be copied to Frameworks 
# folder and binary will be fixed with "install_name_tool -change" command
# to point to Frameworks/<dependency> library
# then, dependency is checked recursively with resolveDependencies function
function resolveDependencies()
{
    local binfile=$1
    local prefix=$2
    local binname=$(basename $binfile)
    local offset=$((lRecursion*20))
    printf "%s :\t%s\n" $prefix "resolving $binname..."

    for path in ${libpaths[@]}; do
        local temp=$path
        #echo "check lib path $path"
        local pattern="$path/([A-z0-9.-]+\.dylib)"
        while [[ "$(otool -L ${binfile})" =~ $pattern ]]; do
            local libname=${BASH_REMATCH[1]}
            otool -L ${binfile}
            #echo "found match $libname"
            printf "%s :\t%s\n" $prefix "fixing $libname..."
            local libpath="${path}/$libname"
            #echo "cp $libpath $frameworksDir"
            ${SRCROOT}/sudocp.sh $libpath $frameworksDir/$libname $(whoami)
            local installLibPath="@rpath/$libname"
            #echo "install_name_tool -change $libpath $installLibPath $binfile"
            if [ "$libname" == "$binname" ]; then
                install_name_tool -id "@rpath/$libname" $binfile
                printf "%s :\t%s\n" $prefix "fixed id for $libname."
            else
                install_name_tool -change $libpath $installLibPath $binfile
                printf "%s :\t%s\n" $prefix "$libname dependency resolved."
                let lRecursion++
                resolveDependencies "$frameworksDir/$libname" "$prefix>$libname"
                resolveBoostDependencies "$frameworksDir/$libname" "$prefix>$libname"
                let lRecursion--
            fi
            path=$temp
        done # while
    done # for

    printf "%s :\t%s\n" $prefix "$(basename $binfile) resolved."
} # resolveDependencies

# for some reason, unlike other dependencies which maintain full path
# in "otool -L" output, boost libraries do not - they just appear 
# as "libboost_xxxx.dylib" entries, without fully qualified path
# thus, resolveDependencies can't be used and a designated function is needed
# this function works pretty much in a similar way to resolveDependencies
# but targets only dependencies starting with "libboost_", copies them
# to the Frameworks folder and resolves them recursively
function resolveBoostDependencies()
{
    local binfile=$1
    local prefix=$2
    local binname=$(basename $binfile)
    local offset=$(((bRecursion+lRecursion)*20))
    printf "%s :\t%s\n" $prefix "resolving Boost for $(basename $binfile)..."

    local pattern="[[:space:]]libboost_([A-z0-9.-]+\.dylib)"
    while [[ "$(otool -L ${binfile})" =~ $pattern ]]; do
        local libname="libboost_${BASH_REMATCH[1]}"
        #echo "found match $libname"
        local libpath="${BOOST_LIB_PATH}/$libname"
        #echo "cp $libpath $frameworksDir"
        ${SRCROOT}/sudocp.sh $libpath $frameworksDir/$libname $(whoami)
        installLibPath="@rpath/$libname"
        #echo "install_name_tool -change $libname $installLibPath $binfile"
        if [ "$libname" == "$binname" ]; then
            install_name_tool -id "@rpath/$libname" $binfile
            printf "%s :\t%s\n" $prefix "fixed id for $libname."
        else
            install_name_tool -change $libname $installLibPath $binfile
            printf "%s :\t%s\n" $prefix "$libname Boost dependency resolved."
            let bRecursion++
            resolveBoostDependencies "$frameworksDir/$libname" "$prefix>$libname"
            let bRecursion--
        fi
    done # while

    printf "%s :\t%s\n" $prefix "$(basename $binfile) resolved."
}

resolveDependencies $executable $(basename $executable)
resolveBoostDependencies $executable $(basename $executable)

Hoffe, dass dies für jemanden nützlich sein könnte.

Peetonn
quelle