Création d'un bundle d'applications OSX

90

Supposons que j'ai créé une application osX sans utiliser Xcode. Après avoir compilé avec GCC, j'obtiens un exécutable qui est lié à plusieurs autres bibliothèques. Certaines de ces bibliothèques peuvent à nouveau être liées dynamiquement à d'autres bibliothèques système non standard

Existe-t-il un outil qui crée un bundle d'applications OSX en créant d'abord les structures de répertoire requises, puis en copiant / vérifiant / corrigeant les liens de manière récursive pour s'assurer que toutes les dépendances dynamiques sont également dans le bundle d'applications?

Je suppose que je peux essayer d'écrire quelque chose comme ça, mais je me demandais si quelque chose comme ça existe déjà.

Yogi
la source
4
Je ne peux pas utiliser Xcode pour plusieurs raisons. dont l'un est parce que j'utilise gcc personnalisé. Xcode ne me permet pas de spécifier un gcc différent. J'utilise cmake pour construire mes makefiles.
Yogi
>> Certaines de ces bibliothèques peuvent à nouveau être liées dynamiquement à d'autres bibliothèques système non standard << La réponse sélectionnée n'aide pas dans ce cas. Comment as-tu résolu ça?
FlowUI. SimpleUITesting.com

Réponses:

143

Il existe deux façons de créer un bundle d'applications sur MacOSX, le Easy et le Ugly.

Le moyen le plus simple consiste simplement à utiliser XCode. Terminé.

Le problème est que parfois vous ne pouvez pas.

Dans mon cas, je construis une application qui crée d'autres applications. Je ne peux pas supposer que l'utilisateur a installé XCode. J'utilise également MacPorts pour créer les bibliothèques dont dépend mon application. Je dois m'assurer que ces dylibs sont fournis avec l'application avant de la distribuer.

Clause de non-responsabilité: je ne suis absolument pas qualifié pour écrire cet article, tout ce qu'il contient a été lu dans les documents Apple, en sélectionnant les applications existantes et les essais et erreurs. Cela fonctionne pour moi, mais c'est probablement faux. Veuillez m'envoyer un courriel si vous avez des corrections.

La première chose que vous devez savoir est qu'un bundle d'applications n'est qu'un répertoire.
Examinons la structure d'un hypothétique foo.app.

foo.app/
    Contenu/
        Info.plist
        MacOS /
            toto
        Ressources/
            foo.icns

Info.plist est un simple fichier XML. Vous pouvez le modifier avec un éditeur de texte ou l'application Property List Editor fournie avec XCode. (Il se trouve dans le répertoire / Developer / Applications / Utilities /).

Les éléments clés que vous devez inclure sont:

CFBundleName - Le nom de l'application.

CFBundleIcon - Un fichier Icon supposé être dans le répertoire Contents / Resources. Utilisez l'application Icon Composer pour créer l'icône. (Il se trouve également dans le répertoire / Developer / Applications / Utilities /) Vous pouvez simplement faire glisser et déposer un png sur sa fenêtre et devrait générer automatiquement les niveaux mip pour vous.

CFBundleExecutable - Nom du fichier exécutable supposé se trouver dans le sous-dossier Contents / MacOS /.

Il y a beaucoup plus d'options, celles énumérées ci-dessus ne sont que le strict minimum. Voici une documentation Apple sur le fichier Info.plist et la structure du bundle App .

En outre, voici un exemple d'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> CFBundleGetInfoString </key>
  <string> Foo </string>
  <key> CFBundleExecutable </key>
  <string> toto </string>
  <key> CFBundleIdentifier </key>
  <string> com.your-company-name.www </string>
  <key> CFBundleName </key>
  <string> toto </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>
  <integer> 0 </integer>
  <key> IFMinorVersion </key>
  <integer> 1 </integer>
</dict>
</plist>

Dans un monde parfait, vous pouvez simplement déposer votre exécutable dans le répertoire Contents / MacOS / et c'est terminé. Cependant, si votre application a des dépendances dylib non standard, cela ne fonctionnera pas. Comme Windows, MacOS est livré avec son propre type spécial de DLL Hell .

Si vous utilisez MacPorts pour créer des bibliothèques avec lesquelles vous liez, les emplacements des dylibs seront codés en dur dans votre exécutable. Si vous exécutez l'application sur une machine qui a les dylibs exactement au même endroit, elle fonctionnera correctement. Cependant, la plupart des utilisateurs ne les auront pas installés; quand ils double-cliquent sur votre application, cela plante.

Avant de distribuer votre exécutable, vous devrez collecter toutes les dylibs qu'il charge et les copier dans le bundle d'applications. Vous devrez également éditer l'exécutable pour qu'il recherche les dylibs au bon endroit. c'est à dire où vous les avez copiés.

L'édition manuelle d'un exécutable semble dangereuse, non? Heureusement, il existe des outils de ligne de commande pour vous aider.

otool -L nom_exécutable

Cette commande répertorie tous les dylibs dont dépend votre application. Si vous en voyez qui ne sont PAS dans le dossier System / Library ou usr / lib, ce sont ceux que vous devrez copier dans le bundle d'applications. Copiez-les dans le dossier / Contents / MacOS /. Ensuite, vous devrez éditer l'exécutable pour utiliser les nouveaux dylibs.

Tout d'abord, vous devez vous assurer que vous liez à l'aide de l'indicateur -headerpad_max_install_names. Cela garantit simplement que si le nouveau chemin dylib est plus long que le précédent, il y aura de la place pour cela.

Deuxièmement, utilisez l'outil nom_installation_tool pour modifier chaque chemin dylib.

outil_nom_installation -modifier le chemin_existant_à_dylib @ chemin_exécutable / blah.dylib nom_exécutable

À titre d'exemple pratique, disons que votre application utilise libSDL et que otool répertorie son emplacement sous la forme "/opt/local/lib/libSDL-1.2.0.dylib".

Copiez-le d'abord dans le bundle d'applications.

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

Modifiez ensuite l'exécutable pour utiliser le nouvel emplacement (REMARQUE: assurez-vous de l'avoir créé avec l'indicateur -headerpad_max_install_names)

outil_nom_installation -change /opt/local/lib/libSDL-1.2.0.dylib @ chemin_exécutable / libSDL-1.2.0.dylib foo.app/Contents/MacOS/foo

Ouf, nous avons presque fini. Il y a maintenant un petit problème avec le répertoire de travail actuel.

Lorsque vous démarrez votre application, le répertoire actuel sera le répertoire ci-dessus où se trouve l'application. Par exemple: si vous placez le fichier foo.app dans le dossier / Applcations, le répertoire actuel lorsque vous lancerez l'application sera le dossier / Applications. Pas le /Applications/foo.app/Contents/MacOS/ comme vous vous en doutez.

Vous pouvez modifier votre application pour en tenir compte, ou vous pouvez utiliser ce petit script de lancement magique qui changera le répertoire actuel et lancera votre application.

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

Assurez-vous d'ajuster le fichier Info.plist afin que CFBundleExecutable pointe vers le script de lancement et non vers l'exécutable précédent.

Ok, tout est fait maintenant. Heureusement, une fois que vous connaissez tout cela, vous les enterrez dans un script de construction.

hyperlogique
la source
7
+1. Cependant, le chien zombie me fait peur. Pourriez-vous utiliser une image moins cicatricielle pour illustrer l'enfer des DLL? (Sans oublier que le terme «enfer DLL» ne s'applique pas. La méthode préférée pour distribuer des bibliothèques dynamiques est via des frameworks qui évitent la plupart des problèmes. Néanmoins, l'approche .dylib est utile pour les bibliothèques de style UNIX.)
Heinrich Apfelmus
1
Comment corriger les doubles dépendances? Comme un dylib fait référence à un autre dylib?
FlowUI. SimpleUITesting.com
N'existe-t-il aucun moyen de compiler l'exécutable avec les emplacements corrects pour ne pas avoir à le modifier?
synchroniseur
Cela fonctionnerait-il même plus sur Catalina, compte tenu de la rigueur de la sécurité? (modification de dylibs, binaires, etc.)
synchroniseur
20

J'ai en fait trouvé un outil très pratique qui mérite un peu de crédit ... NON - je n'ai pas développé cela;)

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

Il résoudra toutes les dépendances et «corrigera» votre exécutable ainsi que vos fichiers dylib pour qu'ils fonctionnent correctement dans votre bundle d'applications.

... il vérifiera également les dépendances de vos bibliothèques dynamiques dépendantes: D

Chris
la source
c'est un outil très utile! Merci de l'avoir partagé et développé.
xpnimi
Il manque ici les dépendances des dépendances. Réparer?
Peter le
9

J'utilise ceci dans mon Makefile ... Cela crée un bundle d'applications. Lisez-le et comprenez-le, car vous aurez besoin d'un fichier d'icône png dans un dossier macosx / avec les fichiers PkgInfo et Info.plist que j'inclus ici ...

"cela fonctionne sur mon ordinateur" ... Je l'utilise pour plusieurs applications sur 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
subatomique
la source
Vous pouvez faire en sorte que Info.plist ait des espaces réservés de texte, puis utiliser quelque chose comme sed ou awk pour créer le Info.plist final avec les champs corrects. Ou peut-être même utiliser plutil pour créer le plist.
Mark Heath
8

La solution la plus simple est la suivante: créez une fois un projet Xcode sans rien changer (c'est-à-dire gardez l'application simple à une fenêtre que Xcode crée pour vous), construisez-le et copiez le bundle qu'il a créé pour vous. Ensuite, modifiez les fichiers (notamment Info.plist) en fonction de votre contenu, et placez votre propre binaire dans le répertoire Contents / MacOS /.

F'x
la source
4
La question demande explicitement comment le faire sans xcode. Je suis dans le même bateau et j'aimerais une vraie réponse.
hyperlogic
5
Eh bien, avec ça, vous n'avez qu'à utiliser XCode une fois dans votre vie :) ou à demander à quelqu'un de le faire pour vous.
F'x
@ F'x Pouvez-vous simplement publier cette .app générée comme exemple quelque part?
endolith
3

Il existe des outils open source pour aider à créer des bundles d'applications avec des bibliothèques dépendantes pour des environnements spécifiques, par exemple, py2app pour les applications basées sur Python. Si vous n'en trouvez pas un plus général, vous pouvez peut-être l'adapter à vos besoins.

Ned Deily
la source
2

J'aurais aimé trouver ce post plus tôt ....

Voici ma façon schématique de résoudre ce problème en utilisant une Run scriptphase qui est appelée à chaque fois que je crée une Releaseversion de mon application:

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

J'espère que cela pourrait être utile à quelqu'un.

peetonn
la source