Compilación del paquete de aplicaciones OSX

90

Supongamos que he creado una aplicación osX sin usar Xcode. Después de compilar con GCC, obtengo un ejecutable que está vinculado a varias otras bibliotecas. Algunas de esas bibliotecas podrían volver a estar vinculadas dinámicamente a otras bibliotecas del sistema no estándar.

¿Existe alguna herramienta que cree un paquete de aplicaciones OSX creando primero las estructuras de directorio requeridas y luego copiando / comprobando / arreglando enlaces de forma recursiva para asegurarse de que todas las dependencias dinámicas también estén en el paquete de aplicaciones?

Supongo que puedo intentar escribir algo como esto, pero me preguntaba si ya existe algo como esto.

Yogui
fuente
4
No puedo usar Xcode por varias razones. uno de los cuales es porque uso gcc personalizado. Xcode no me permite especificar un gcc diferente. Estoy usando cmake para construir mis archivos MAKE.
Yogi
>> Algunas de esas bibliotecas pueden volver a estar vinculadas dinámicamente a otras bibliotecas del sistema no estándar << La respuesta seleccionada no ayuda con este caso. ¿Cómo lo resolviste?
FlowUI. SimpleUITesting.com

Respuestas:

143

Hay dos formas de crear un paquete de aplicaciones en MacOSX, Easy y Ugly.

La forma más sencilla es utilizar XCode. Hecho.

El problema es que a veces no puedes.

En mi caso, estoy creando una aplicación que crea otras aplicaciones. No puedo asumir que el usuario tiene XCode instalado. También estoy usando MacPorts para crear las bibliotecas de las que depende mi aplicación. Necesito asegurarme de que estos dylibs se incluyan con la aplicación antes de distribuirla.

Descargo de responsabilidad: no estoy calificado para escribir esta publicación, todo lo que contiene ha sido extraído de los documentos de Apple, seleccionando aplicaciones existentes y prueba y error. Funciona para mí, pero probablemente sea incorrecto. Por favor envíeme un correo electrónico si tiene alguna corrección.

Lo primero que debe saber es que un paquete de aplicaciones es solo un directorio.
Examinemos la estructura de un foo.app hipotético.

foo.app/
    Contenido/
        Info.plist
        Mac OS/
            foo
        Recursos /
            foo.icns

Info.plist es un archivo XML simple. Puede editarlo con un editor de texto o la aplicación Property List Editor que viene con XCode. (Está en el directorio / Developer / Applications / Utilities /).

Las cosas clave que debe incluir son:

CFBundleName : el nombre de la aplicación.

CFBundleIcon : un archivo de icono que se supone que está en el directorio de contenidos / recursos. Utilice la aplicación Icon Composer para crear el icono. (También está en el directorio / Developer / Applications / Utilities /). Puede simplemente arrastrar y soltar un png en su ventana y debería generar automáticamente los niveles de mip para usted.

CFBundleExecutable : nombre del archivo ejecutable que se supone que está en la subcarpeta Contenidos / MacOS /.

Hay muchas más opciones, las enumeradas anteriormente son solo las mínimas. Aquí hay algo de documentación de Apple sobre el archivo Info.plist y la estructura del paquete de aplicaciones .

Además, aquí hay una muestra de 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">
<dicto>
  <key> CFBundleGetInfoString </key>
  <string> Foo </string>
  <key> CFBundleExecutable </key>
  <string> foo </string>
  <key> CFBundleIdentifier </key>
  <string> com.your-company-name.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>
  <integer> 0 </integer>
  <key> IFMinorVersion </key>
  <integer> 1 </integer>
</dict>
</plist>

En un mundo perfecto, simplemente podría colocar su ejecutable en el directorio Contenidos / MacOS / y listo. Sin embargo, si su aplicación tiene dependencias dylib no estándar, no funcionará. Al igual que Windows, MacOS viene con su propio tipo especial de DLL Hell .

Si está utilizando MacPorts para crear bibliotecas con las que se vincula, las ubicaciones de los dylibs estarán codificadas en su ejecutable. Si ejecuta la aplicación en una máquina que tiene los dylibs en la misma ubicación exacta, funcionará bien. Sin embargo, la mayoría de los usuarios no los instalarán; cuando hagan doble clic en su aplicación, simplemente se bloqueará.

Antes de distribuir su ejecutable, deberá recopilar todos los archivos dylib que carga y copiarlos en el paquete de aplicaciones. También necesitará editar el ejecutable para que busque los dylibs en el lugar correcto. es decir, dónde los copió.

La edición manual de un ejecutable suena peligroso, ¿verdad? Afortunadamente, existen herramientas de línea de comandos para ayudar.

otool -L nombre_ejecutable

Este comando enumerará todos los dylibs de los que depende su aplicación. Si ve alguno que NO esté en la carpeta System / Library o usr / lib, esos son los que deberá copiar en el paquete de la aplicación. Cópielos en la carpeta / Contenido / MacOS /. A continuación, deberá editar el ejecutable para usar los nuevos dylibs.

Primero, debes asegurarte de vincular usando la marca -headerpad_max_install_names. Esto solo asegura que si la nueva ruta dylib es más larga que la anterior, habrá espacio para ella.

En segundo lugar, use install_name_tool para cambiar cada ruta de dylib.

install_name_tool -cambiar ruta_existente_a_dylib @ ruta_ejecutable / blah.dylib nombre_ejecutable

Como ejemplo práctico, digamos que su aplicación usa libSDL y otool enumera su ubicación como "/opt/local/lib/libSDL-1.2.0.dylib".

Primero cópielo en el paquete de la aplicación.

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

Luego edite el ejecutable para usar la nueva ubicación (NOTA: asegúrese de haberlo construido con el indicador -headerpad_max_install_names)

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

Vaya, casi hemos terminado. Ahora hay un pequeño problema con el directorio de trabajo actual.

Cuando inicie su aplicación, el directorio actual será el directorio arriba donde se encuentra la aplicación. Por ejemplo: si coloca foo.app en la carpeta / Applcations, el directorio actual cuando inicie la aplicación será la carpeta / Aplicaciones. No el /Applications/foo.app/Contents/MacOS/ como cabría esperar.

Puede modificar su aplicación para que tenga en cuenta esto, o puede usar este pequeño script de iniciador mágico que cambiará el directorio actual e iniciará su aplicación.

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

Asegúrese de ajustar el archivo Info.plist para que CFBundleExecutable apunte al script de inicio y no al ejecutable anterior.

Ok, todo hecho ahora. Afortunadamente, una vez que conoces todo esto, lo entierras en un script de compilación.

hiperlogico
fuente
7
+1. Sin embargo, el perro zombi me asusta. ¿Podrías usar una imagen menos cicatrizante para ilustrar el infierno de DLL? (Sin mencionar que el término "infierno de DLL" no se aplica. El método preferido para distribuir bibliotecas dinámicas es a través de marcos que evitan la mayoría de los problemas. Aún así, el enfoque .dylib es útil para las bibliotecas de estilo UNIX.)
Heinrich Apfelmus
1
¿Cómo arreglamos las dependencias dobles? ¿Como un dylib hace referencia a otro dylib?
FlowUI. SimpleUITesting.com
¿No hay forma de compilar el ejecutable con las ubicaciones correctas para que no tenga que editarlo?
sincronizador
¿Funcionaría esto más en Catalina, dado lo estricta que se ha vuelto la seguridad? (Modificación de dylibs, binarios, etc.)
sincronizador
20

De hecho, encontré una herramienta muy útil que merece algo de crédito ... NO, no desarrollé esto;)

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

Resolverá todas las dependencias y "arreglará" su ejecutable y sus archivos dylib para que funcionen sin problemas en su paquete de aplicaciones.

... también comprobará las dependencias de sus bibliotecas dinámicas dependientes: D

Chris
fuente
esta es una herramienta muy útil! Gracias por compartirlo y desarrollarlo.
xpnimi
Faltan aquí las dependencias de las dependencias. ¿Reparar?
Peter
9

Lo uso en mi Makefile ... Crea un paquete de aplicaciones. Léalo y entiéndalo, porque necesitará un archivo de icono png en una carpeta macosx / junto con los archivos PkgInfo e Info.plist que incluyo aquí ...

"funciona en mi computadora" ... lo uso para múltiples aplicaciones en 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
subatómico
fuente
Puede hacer que Info.plist tenga marcadores de posición de texto y luego usar algo como sed o awk para crear el Info.plist final con los campos correctos. O tal vez incluso use plutil para crear la plist.
Mark Heath
8

La solución más simple es: cree una vez un proyecto de Xcode sin cambiar nada (es decir, mantenga la aplicación simple de una ventana que Xcode crea para usted), compile y copie el paquete que creó para usted. Luego, edite los archivos (especialmente el Info.plist) para que se adapten a su contenido y coloque su propio binario en el directorio Contenidos / MacOS /.

F'x
fuente
4
La pregunta pregunta explícitamente cómo hacerlo sin xcode. Estoy en el mismo barco y quisiera una respuesta real.
hyperlogic
5
Bueno, con esto, solo tienes que usar XCode una vez en tu vida :) o pedirle a alguien que lo haga por ti.
F'x
@ F'x ¿Puedes publicar ese .app generado como ejemplo en alguna parte?
endolito
3

Existen algunas herramientas de código abierto para ayudar a crear paquetes de aplicaciones con bibliotecas dependientes para entornos específicos, por ejemplo, py2app para aplicaciones basadas en Python. Si no encuentra uno más general, quizás pueda adaptarlo a sus necesidades.

Ned Deily
fuente
2

Ojalá hubiera encontrado esta publicación antes ...

Esta es mi forma esquemática de resolver este problema usando una Run scriptfase que se invoca cada vez que construyo una Releaseversión de mi aplicación:

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

Espero que esto pueda ser útil para alguien.

peetonn
fuente