Creazione del pacchetto di app OSX


90

Supponiamo di aver creato un'app osX senza utilizzare Xcode. Dopo aver compilato con GCC ottengo un eseguibile che è collegato a diverse altre librerie. Alcune di queste librerie potrebbero essere nuovamente collegate dinamicamente ad altre librerie di sistema non standard

Esiste uno strumento che crea un pacchetto di app OSX creando prima le strutture di directory richieste e quindi copiando / verificando / correggendo in modo ricorsivo i collegamenti per assicurarsi che tutte le dipendenze dinamiche siano anche nel pacchetto dell'app?

Immagino di poter provare a scrivere qualcosa di simile, ma mi chiedevo se qualcosa di simile esiste già.


4
Non riesco a usare Xcode per diversi motivi. uno dei quali è perché utilizzo gcc personalizzato. Xcode non mi consente di specificare un diverso gcc. Sto usando cmake per creare i miei makefile.
Yogi

>> Alcune di queste librerie potrebbero essere nuovamente collegate dinamicamente ad altre librerie di sistema non standard << La risposta selezionata non aiuta in questo caso. Come lo hai risolto?
Solo un programmatore il

Risposte:


144

Esistono due modi per creare un app bundle su MacOSX, Easy e the Ugly.

Il modo più semplice è usare XCode. Fatto.

Il problema è che a volte non puoi.

Nel mio caso sto costruendo un'app che crea altre app. Non posso presumere che l'utente abbia installato XCode. Sto anche usando MacPorts per creare le librerie da cui dipende la mia app. Devo assicurarmi che questi dylib vengano forniti in bundle con l'app prima di distribuirlo.

Dichiarazione di non responsabilità: non non sono assolutamente qualificato per scrivere questo post, tutto ciò che è contenuto è stato brillato dai documenti di Apple, selezionando le app esistenti e le prove ed errori. Per me funziona, ma molto probabilmente è sbagliato. Per favore mandami un'e-mail se hai correzioni.

La prima cosa che dovresti sapere è che un app bundle è solo una directory.
Esaminiamo la struttura di un ipotetico foo.app.

foo.app/
    Contenuti/
        Info.plist
        Mac OS/
            foo
        Risorse /
            foo.icns

Info.plist è un semplice file XML. Puoi modificarlo con un editor di testo o l'app Property List Editor fornita in bundle con XCode. (Si trova nella directory / Developer / Applications / Utilities /).

Le cose chiave che devi includere sono:

CFBundleName : il nome dell'app.

CFBundleIcon : si presume che un file Icon si trovi nella directory Contents / Resources. Usa l'app Icon Composer per creare l'icona. (Si trova anche nella directory / Developer / Applications / Utilities /) Puoi semplicemente trascinare e rilasciare un png nella sua finestra e dovrebbe generare automaticamente i livelli mip per te.

CFBundleExecutable : nome del file eseguibile che si presume si trovi nella sottocartella Contents / MacOS /.

Ci sono molte più opzioni, quelle sopra elencate sono solo il minimo indispensabile. Ecco della documentazione Apple sul file Info.plist e sulla struttura del bundle di app .

Inoltre, ecco un esempio di 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> 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>

In un mondo perfetto potresti semplicemente rilasciare il tuo eseguibile in Contents / MacOS / dir ed essere fatto. Tuttavia, se la tua app ha dipendenze dylib non standard, non funzionerà. Come Windows, MacOS viene fornito con il suo tipo speciale di DLL Hell .

Se utilizzi MacPort per creare librerie a cui ti colleghi, le posizioni dei dylibs saranno codificate nel tuo eseguibile. Se esegui l'app su una macchina che ha i dylibs nella stessa identica posizione, funzionerà correttamente. Tuttavia, la maggior parte degli utenti non li installa; quando fanno doppio clic sulla tua app, si bloccherà.

Prima di distribuire il tuo eseguibile, dovrai raccogliere tutti i dylibs che carica e copiarli nel bundle dell'app. Dovrai anche modificare l'eseguibile in modo che cerchi i dylibs nella posizione corretta. cioè dove li hai copiati.

Modificare manualmente un eseguibile sembra pericoloso, giusto? Fortunatamente ci sono strumenti da riga di comando per aiutare.

otool -L nome_eseguibile

Questo comando elencherà tutti i dylibs da cui dipende la tua app. Se ne vedi qualcuno che NON si trova nella cartella System / Library o usr / lib, questi sono quelli che dovrai copiare nel bundle dell'app. Copiali nella cartella / Contents / MacOS /. Successivamente dovrai modificare l'eseguibile per utilizzare i nuovi dylibs.

Innanzitutto, assicurati di eseguire il collegamento utilizzando il flag -headerpad_max_install_names. Questo fa solo in modo che se il nuovo percorso dylib è più lungo del precedente, ci sarà spazio per esso.

In secondo luogo, utilizzare install_name_tool per modificare ogni percorso di dylib.

install_name_tool -change existing_path_to_dylib @ executable_path / blah.dylib executable_name

Come esempio pratico, supponiamo che la tua app utilizzi libSDL e otool elenchi la sua posizione come "/opt/local/lib/libSDL-1.2.0.dylib".

Prima copialo nel bundle dell'app.

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

Quindi modifica l'eseguibile per utilizzare la nuova posizione (NOTA: assicurati di averlo creato con il flag -headerpad_max_install_names)

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

Accidenti, abbiamo quasi finito. Ora c'è un piccolo problema con la directory di lavoro corrente.

Quando avvii la tua app, la directory corrente sarà la directory sopra dove si trova l'applicazione. Ad esempio: se metti foo.app nella cartella / Applcations, la directory corrente quando avvii l'app sarà la cartella / Applicazioni. Non /Applications/foo.app/Contents/MacOS/ come ci si potrebbe aspettare.

Puoi modificare la tua app per tenerne conto, oppure puoi usare questo magico piccolo script di avvio che cambierà la directory corrente e avvierà la tua app.

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

Assicurati di modificare il file Info.plist in modo che CFBundleExecutable punti allo script di avvio e non all'eseguibile precedente.

Ok, tutto fatto adesso. Fortunatamente, una volta che conosci tutte queste cose, le seppellisci in uno script di build.


7
+1. Tuttavia, il cane zombi mi fa impazzire. Potresti usare un'immagine meno sfregiata per illustrare l'inferno della DLL? (Senza contare che il termine "DLL hell" non si applica. Il metodo preferito per la distribuzione di librerie dinamiche è tramite framework che evitano la maggior parte dei problemi. Tuttavia, l'approccio .dylib è utile per le librerie in stile UNIX.)
Heinrich Apfelmus

1
Come risolviamo le doppie dipendenze? Come un dylib fa riferimento a un altro dylib?
Solo un programmatore il

Non c'è modo di compilare l'eseguibile con le posizioni corrette in modo da non doverlo modificare?
sincronizzatore

Funzionerebbe più anche su Catalina, visto quanto è diventata severa la sicurezza? (modifica dylibs, binari, ecc.)
sincronizzatore

20

In realtà ho trovato uno strumento molto utile che merita un po 'di credito ... NO - Non l'ho sviluppato io;)

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

Risolverà tutte le dipendenze e "aggiusterà" il tuo eseguibile così come i tuoi file dylib per funzionare senza problemi nel tuo app bundle.

... controllerà anche le dipendenze delle librerie dinamiche dipendenti: D


questo è uno strumento molto utile! Grazie per averlo condiviso e sviluppato.
xpnimi

Mancano qui le dipendenze delle dipendenze. Fix?
Peter

9

Lo uso nel mio Makefile ... Crea un app bundle. Leggilo e comprendilo, perché avrai bisogno di un file icona png in una cartella macosx / insieme ai file PkgInfo e Info.plist che includo qui ...

"funziona sul mio computer" ... lo uso per più app su Mavericks ...

Info.plist

PkgInfo


Potresti fare in modo che Info.plist abbia dei segnaposto di testo e quindi utilizzare qualcosa come sed o awk per creare il file Info.plist finale con i campi corretti. O forse anche usare plutil per creare il plist.
silicontrip

8

La soluzione più semplice è: crea una volta un progetto Xcode senza cambiare nulla (cioè mantieni la semplice app a una finestra che Xcode crea per te), costruiscilo e copia il pacchetto che ha creato per te. Quindi, modifica i file (in particolare Info.plist) per adattarli al tuo contenuto e metti il ​​tuo binario nella directory Contents / MacOS /.


4
La domanda chiede esplicitamente come farlo senza xcode. Sono sulla stessa barca e vorrei una vera risposta.
hyperlogic

5
Bene, con questo, devi usare XCode solo una volta nella vita :) o chiedere a qualcuno di farlo per te.
F'x

@ F'x Puoi semplicemente pubblicare quel .app generato come esempio da qualche parte?
endolith

3

Esistono alcuni strumenti open source per aiutare a creare bundle di app con librerie dipendenti per ambienti specifici, ad esempio py2app per applicazioni basate su Python. Se non ne trovi uno più generale, forse puoi adattarlo alle tue esigenze.


2

Vorrei aver trovato questo post prima ...

Ecco il mio modo abbozzato di risolvere questo problema utilizzando una Run scriptfase che viene richiamata ogni volta che creo una Releaseversione della mia app:

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

Spero che questo possa essere utile a qualcuno.


0

Una soluzione alternativa per far funzionare il menu su Mac con il codice wxWidget è semplicemente:

  1. Avvia il programma in un terminale come al solito (./appname)
  2. La GUI del programma si avvia normalmente e fai clic sul terminale per far perdere il focus all'app
  3. Fare clic sulla GUI per ripristinare lo stato attivo e le voci di menu funzionano.

Sono d'accordo che un app bundle è il modo corretto per creare un programma su un Mac. Questa è solo una semplice soluzione alternativa per assistenza durante il debug.

Modifica: questo è su Mac Catalina, wxWidgets 3.1.4, con g ++ 4.2.1 (novembre 2020)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.