Construindo OSX App Bundle

90

Suponha que eu tenha feito um aplicativo osX sem usar o Xcode. Após compilar com o GCC, recebo um executável que está vinculado a várias outras bibliotecas. Algumas dessas bibliotecas podem ser novamente vinculadas dinamicamente a outras bibliotecas do sistema não padrão

Existe alguma ferramenta que faz um pacote OSX App criando primeiro as estruturas de diretório necessárias e, em seguida, copiando / verificando / corrigindo recursivamente os links para garantir que todas as dependências dinâmicas também estejam no pacote do app?

Acho que posso tentar escrever algo assim, mas gostaria de saber se algo assim já existe.

Iogue
fonte
4
Não consigo usar o Xcode por vários motivos. uma delas é porque eu uso o gcc personalizado. O Xcode não me permite especificar um gcc diferente. Estou usando o cmake para construir meus makefiles.
Yogi
>> Algumas dessas bibliotecas podem ser novamente vinculadas dinamicamente a outras bibliotecas do sistema não padrão << A resposta selecionada não ajuda neste caso. Como você resolveu isso?
Apenas um codificador

Respostas:

144

Existem duas maneiras de criar um pacote de aplicativos no MacOSX, o Easy e o Ugly.

A maneira mais fácil é usar o XCode. Feito.

O problema é que às vezes você não consegue.

No meu caso, estou construindo um aplicativo que cria outros aplicativos. Não posso presumir que o usuário tenha o XCode instalado. Também estou usando MacPorts para construir as bibliotecas das quais meu aplicativo depende. Preciso ter certeza de que esses dylibs sejam incluídos no aplicativo antes de distribuí-lo.

Isenção de responsabilidade: eu não estou totalmente qualificado para escrever este post, tudo nele foi mostrado nos documentos da Apple, separando aplicativos existentes e tentativa e erro. Funciona para mim, mas provavelmente está errado. Por favor, me envie um email se você tiver alguma correção.

A primeira coisa que você deve saber é que um pacote de aplicativos é apenas um diretório.
Vamos examinar a estrutura de um foo.app hipotético.

foo.app/
    Conteúdo/
        Info.plist
        Mac OS/
            foo
        Recursos/
            foo.icns

Info.plist é um arquivo XML simples. Você pode editá-lo com um editor de texto ou o aplicativo Property List Editor que vem com o XCode. (Está no diretório / Desenvolvedor / Aplicativos / Utilitários /).

Os principais itens que você precisa incluir são:

CFBundleName - O nome do aplicativo.

CFBundleIcon - Um arquivo de ícone que se presume estar no diretório Conteúdo / Recursos. Use o aplicativo Icon Composer para criar o ícone. (Também está no diretório / Developer / Applications / Utilities /). Você pode simplesmente arrastar e soltar um png em sua janela e deve gerar automaticamente os níveis de mip para você.

CFBundleExecutable - Nome do arquivo executável que se presume estar em Contents / MacOS / subpasta.

Existem muito mais opções, as listadas acima são apenas o mínimo. Aqui estão algumas documentações da Apple sobre o arquivo Info.plist e a estrutura do pacote de aplicativos .

Além disso, aqui está um exemplo 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">
<dict>
  <key> CFBundleGetInfoString </key>
  <string> Foo </string>
  <key> CFBundleExecutable </key>
  <string> foo </string>
  <key> CFBundleIdentifier </key>
  <string> com.nome-da-sua-empresa.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>

Em um mundo perfeito, você poderia simplesmente soltar seu executável no diretório Contents / MacOS / e pronto. No entanto, se seu aplicativo tiver dependências dylib fora do padrão, ele não funcionará. Como o Windows, o MacOS vem com seu próprio tipo especial de DLL Hell .

Se você estiver usando MacPorts para construir bibliotecas às quais vincular, os locais dos dylibs serão embutidos em código em seu executável. Se você executar o aplicativo em uma máquina que tenha os dylibs exatamente no mesmo local, ele funcionará bem. No entanto, a maioria dos usuários não os instalará; quando eles clicarem duas vezes em seu aplicativo, ele simplesmente travará.

Antes de distribuir seu executável, você precisará coletar todos os dylibs que carrega e copiá-los para o pacote de aplicativos. Você também precisará editar o executável para que procure os dylibs no local correto. ou seja, para onde você os copiou.

Editar manualmente um executável parece perigoso, certo? Felizmente, existem ferramentas de linha de comando para ajudar.

otool -L executable_name

Este comando listará todos os dylibs dos quais seu aplicativo depende. Se você vir algum que NÃO esteja na pasta System / Library ou usr / lib, esses são os que você precisa copiar para o pacote de aplicativos. Copie-os para a pasta / Contents / MacOS /. Em seguida, você precisará editar o executável para usar os novos dylibs.

Primeiro, você precisa ter certeza de vincular usando o sinalizador -headerpad_max_install_names. Isso apenas garante que, se o novo caminho dylib for mais longo que o anterior, haverá espaço para ele.

Em segundo lugar, use o install_name_tool para alterar cada caminho dylib.

install_name_tool -change existing_path_to_dylib @ executable_path / blah.dylib executable_name

Como um exemplo prático, digamos que seu aplicativo usa libSDL e otool lista sua localização como "/opt/local/lib/libSDL-1.2.0.dylib".

Primeiro, copie-o para o pacote de aplicativos.

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

Em seguida, edite o executável para usar o novo local (NOTA: certifique-se de criá-lo com o sinalizador -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

Ufa, estamos quase terminando. Agora há um pequeno problema com o diretório de trabalho atual.

Ao iniciar seu aplicativo, o diretório atual será o diretório acima de onde o aplicativo está localizado. Por exemplo: Se você colocar o foo.app na pasta / Applcations, o diretório atual ao iniciar o aplicativo será a pasta / Applications. Não o /Applications/foo.app/Contents/MacOS/ como você poderia esperar.

Você pode alterar seu aplicativo para dar conta disso, ou pode usar este pequeno script de iniciador mágico que mudará o diretório atual e iniciará seu aplicativo.

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

Certifique-se de ajustar o arquivo Info.plist de forma que CFBundleExecutable aponte para o script de inicialização e não para o executável anterior.

Ok, tudo feito agora. Felizmente, depois de saber todas essas coisas, você as enterra em um script de construção.

hiperlógico
fonte
7
+1. No entanto, o cachorro zumbi me assusta. Você poderia usar uma imagem menos cicatrizante para ilustrar o inferno do DLL? (Sem mencionar que o termo "DLL inferno" não se aplica. O método preferido para distribuir bibliotecas dinâmicas é por meio de estruturas que evitam a maioria dos problemas. Ainda assim, a abordagem .dylib é útil para bibliotecas de estilo UNIX.)
Heinrich Apfelmus
1
Como corrigimos dependências duplas? Como um dylib faz referência a outro dylib?
Apenas um codificador
Não há como compilar o executável com os locais corretos para que você não precise editá-lo?
sincronizador
Isso ainda funcionaria em Catalina, dado o quão rigorosa a segurança se tornou? (modfying dylibs, binários, etc.)
sincronizador
20

Na verdade, encontrei uma ferramenta muito útil que merece algum crédito ... NÃO - eu não desenvolvi isso;)

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

Ele resolverá todas as dependências e "consertará" seu executável, bem como seus arquivos dylib para funcionarem perfeitamente em seu pacote de aplicativos.

... também verificará as dependências de suas bibliotecas dinâmicas dependentes: D

Chris
fonte
esta é uma ferramenta muito útil! Obrigado por compartilhar e desenvolvê-lo.
xpnimi
Falta aqui as dependências das dependências. Consertar?
Peter,
9

Eu uso isso no meu Makefile ... Ele cria um pacote de aplicativos. Leia e entenda, porque você precisará de um arquivo de ícone png em uma pasta / macosx junto com os arquivos PkgInfo e Info.plist que incluo aqui ...

"funciona no meu computador" ... Eu uso isso para vários aplicativos no Mavericks ...

Info.plist

PkgInfo

subatômico
fonte
Você poderia fazer o Info.plist ter espaços reservados para texto e então usar algo como sed ou awk para criar o Info.plist final com os campos corretos. Ou talvez até use plutil para criar o plist.
silicone
8

A solução mais simples é: crie uma vez um projeto Xcode sem alterar nada (ou seja, mantenha o aplicativo simples de uma janela que o Xcode cria para você), construa-o e copie o pacote que ele criou para você. Em seguida, edite os arquivos (notavelmente o Info.plist) para se adequar ao seu conteúdo e coloque seu próprio binário no diretório Contents / MacOS /.

F'x
fonte
4
A questão pergunta explicitamente como fazer isso sem o xcode. Estou no mesmo barco e gostaria de uma resposta real.
hiperlógica de
5
Bem, com isso, você só precisa usar o XCode uma vez na vida :) ou pedir a alguém para fazer isso por você.
F'x de
@ F'x Você pode publicar esse .app gerado como exemplo em algum lugar?
endolith
3

Existem algumas ferramentas de código aberto para ajudar a construir pacotes de aplicativos com bibliotecas dependentes para ambientes específicos, por exemplo, py2app para aplicativos baseados em Python. Se você não encontrar um mais geral, talvez possa adaptá-lo às suas necessidades.

Ned Deily
fonte
2

Eu gostaria de ter encontrado este post antes ....

Esta é minha maneira superficial de resolver esse problema usando uma Run scriptfase que é invocada toda vez que eu crio uma Releaseversão do meu aplicativo:

# 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 isso possa ser útil para alguém.

Peetonn
fonte
0

Uma solução alternativa para que o menu funcione no Mac com o código wxWidget é simplesmente:

  1. Inicie o programa em um terminal normalmente (./appname)
  2. A GUI do programa inicia normalmente e clique no terminal para que o aplicativo perca o foco
  3. Clique na GUI para recuperar o foco e os itens do menu funcionarão.

Concordo que um pacote de aplicativos é a maneira correta de construir um programa em um Mac. Esta é apenas uma solução simples para obter ajuda durante a depuração.

Editar: no Mac Catalina, wxWidgets 3.1.4, com g ++ 4.2.1 (novembro de 2020)

eugenedakin
fonte