Tworzenie pakietu aplikacji OSX

90

Załóżmy, że stworzyłem aplikację osX bez użycia Xcode. Po kompilacji z GCC otrzymuję plik wykonywalny, który jest powiązany z kilkoma innymi bibliotekami. Niektóre z tych bibliotek mogą być ponownie dynamicznie połączone z innymi niestandardowymi bibliotekami systemowymi

Czy istnieje jakieś narzędzie, które tworzy pakiet aplikacji OSX, najpierw tworząc wymagane struktury katalogów, a następnie rekurencyjnie kopiując / sprawdzając / naprawiając linki, aby upewnić się, że wszystkie dynamiczne zależności znajdują się również w pakiecie aplikacji?

Chyba mogę spróbować napisać coś takiego, ale zastanawiałem się, czy coś takiego już istnieje.

Jog
źródło
4
Nie mogę używać Xcode z kilku powodów. jednym z nich jest to, że używam niestandardowego gcc. Xcode nie pozwala mi określić innego gcc. Do tworzenia plików makefile używam cmake.
Yogi
>> Niektóre z tych bibliotek mogą być ponownie dynamicznie połączone z innymi niestandardowymi bibliotekami systemowymi. << Wybrana odpowiedź nie pomaga w tym przypadku. Jak to rozwiązałeś?
FlowUI. SimpleUITesting.com

Odpowiedzi:

143

Istnieją dwa sposoby tworzenia pakietu aplikacji w systemie MacOSX: łatwy i brzydki.

Najłatwiejszym sposobem jest użycie XCode. Gotowe.

Problem polega na tym, że czasami nie możesz.

W moim przypadku tworzę aplikację, która tworzy inne aplikacje. Nie mogę założyć, że użytkownik ma zainstalowany XCode. Używam również MacPorts do tworzenia bibliotek, od których zależy moja aplikacja. Muszę się upewnić, że te dylibs zostaną dołączone do aplikacji, zanim ją rozpowszechnię.

Zastrzeżenie: nie mam żadnych kwalifikacji do napisania tego posta, wszystko, co jest, zostało przejrzane z dokumentów Apple, oddzielając istniejące aplikacje i próbę i błąd. U mnie działa, ale najprawdopodobniej jest źle. Proszę napisz do mnie, jeśli masz jakieś poprawki.

Pierwszą rzeczą, którą powinieneś wiedzieć, jest to, że pakiet aplikacji to tylko katalog.
Przeanalizujmy strukturę hipotetycznego pliku foo.app.

foo.app/
    Zawartość/
        Info.plist
        System operacyjny Mac/
            bla
        Zasoby/
            foo.icns

Info.plist to zwykły plik XML. Możesz go edytować za pomocą edytora tekstu lub aplikacji Property List Editor, która jest dostarczana w pakiecie z XCode. (Znajduje się w katalogu / Developer / Applications / Utilities /).

Najważniejsze rzeczy, które musisz uwzględnić, to:

CFBundleName - nazwa aplikacji.

CFBundleIcon - Zakłada się, że plik ikony znajduje się w katalogu Contents / Resources. Użyj aplikacji Icon Composer, aby utworzyć ikonę. (Jest również w katalogu / Developer / Applications / Utilities /) Możesz po prostu przeciągnąć i upuścić png na jego okno i powinien automatycznie wygenerować dla ciebie poziomy mip.

CFBundleExecutable - nazwa pliku wykonywalnego, który prawdopodobnie znajduje się w podfolderze Contents / MacOS /.

Opcji jest dużo więcej, te wymienione powyżej to tylko absolutne minimum. Oto dokumentacja firmy Apple dotycząca pliku Info.plist i struktury pakietu aplikacji .

Oto przykładowa 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">
<dykt>
  <key> CFBundleGetInfoString </key>
  <string> Foo </string>
  <key> CFBundleExecutable </key>
  <string> foo </string>
  <key> CFBundleIdentifier </key>
  <string> com.nazwa-twojej-firmy.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 </entry>
  <key> IFMajorVersion </key>
  <integer> 0 </integer>
  <key> IFMinorVersion </key>
  <integer> 1 </integer>
</dict>
</plist>

W idealnym świecie możesz po prostu wrzucić swój plik wykonywalny do Contents / MacOS / dir i gotowe. Jeśli jednak Twoja aplikacja ma niestandardowe zależności dylib, nie będzie działać. Podobnie jak Windows, MacOS jest wyposażony w specjalny rodzaj biblioteki DLL Hell .

Jeśli używasz MacPorts do budowania bibliotek, z którymi łączysz się, lokalizacje dylibs zostaną zakodowane na stałe w pliku wykonywalnym. Jeśli uruchomisz aplikację na komputerze, który ma dylibs w dokładnie tej samej lokalizacji, będzie działać poprawnie. Jednak większość użytkowników nie będzie ich instalować; po dwukrotnym kliknięciu aplikacji po prostu ulegnie awarii.

Zanim rozpowszechnisz swój plik wykonywalny, musisz zebrać wszystkie pliki dylibs, które ładuje, i skopiować je do pakietu aplikacji. Będziesz także musiał edytować plik wykonywalny, aby szukał dylib we właściwym miejscu. tj. gdzie je skopiowałeś.

Ręczna edycja pliku wykonywalnego brzmi niebezpiecznie, prawda? Na szczęście istnieją narzędzia wiersza poleceń, które mogą pomóc.

otool -L nazwa_pliku

To polecenie wyświetli listę wszystkich dylibs, od których zależy Twoja aplikacja. Jeśli zobaczysz takie, które NIE znajdują się w folderze System / Library lub usr / lib, to te, które musisz skopiować do pakietu aplikacji. Skopiuj je do folderu / Contents / MacOS /. Następnie musisz edytować plik wykonywalny, aby używał nowych dylibs.

Po pierwsze, musisz upewnić się, że łączysz się za pomocą flagi -headerpad_max_install_names. To tylko zapewnia, że ​​jeśli nowa ścieżka dylib jest dłuższa niż poprzednia, będzie na nią miejsce.

Po drugie, użyj narzędzia install_name_tool, aby zmienić każdą ścieżkę dylib.

install_name_tool -change existing_path_to_dylib @ executable_path / blah.dylib executable_name

Jako praktyczny przykład, powiedzmy, że Twoja aplikacja używa libSDL , a otool wyświetla swoją lokalizację jako „/opt/local/lib/libSDL-1.2.0.dylib”.

Najpierw skopiuj go do pakietu aplikacji.

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

Następnie edytuj plik wykonywalny, aby używał nowej lokalizacji (UWAGA: upewnij się, że zbudowałeś go z 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

Uff, prawie skończyliśmy. Teraz jest mały problem z bieżącym katalogiem roboczym.

Po uruchomieniu aplikacji bieżącym katalogiem będzie katalog powyżej, w którym znajduje się aplikacja. Na przykład: jeśli umieścisz foo.app w folderze / Applcations, wówczas bieżącym katalogiem po uruchomieniu aplikacji będzie folder / Applications. Nie /Applications/foo.app/Contents/MacOS/, jak można się spodziewać.

Możesz zmienić swoją aplikację, aby to uwzględnić, lub możesz użyć tego magicznego małego skryptu uruchamiania, który zmieni bieżący katalog i uruchomi Twoją aplikację.

#! / bin / bash
cd „$ {0% / *}”
./bla

Upewnij się, że dostosowałeś plik Info.plist tak, aby CFBundleExecutable wskazywało na skrypt uruchamiania, a nie na poprzedni plik wykonywalny.

OK, wszystko gotowe. Na szczęście, kiedy już znasz te wszystkie rzeczy, zakopujesz to w skrypcie kompilacji.

hiperlogika
źródło
7
+1. Jednak pies zombie mnie przeraża. Czy możesz użyć mniej bliznowego obrazu, aby zilustrować piekło DLL? (Nie wspominając, że termin „piekło DLL” nie ma zastosowania. Preferowaną metodą dystrybucji bibliotek dynamicznych są frameworki, które pozwalają uniknąć większości problemów. Mimo to podejście .dylib jest przydatne w przypadku bibliotek w stylu UNIX).
Heinrich Apfelmus
1
Jak naprawić podwójne zależności? Tak jak dylib odnosi się do innego dylib?
FlowUI. SimpleUITesting.com
Czy nie ma sposobu, aby skompilować plik wykonywalny z poprawnymi lokalizacjami, więc nie musisz go edytować?
synchronizator
Czy to w ogóle zadziała na Catalinie, biorąc pod uwagę, jak rygorystyczne stały się zabezpieczenia? (modyfikowanie dylibów, plików binarnych itp.)
synchronizator
20

Właściwie znalazłem bardzo przydatne narzędzie, które zasługuje na uznanie ... NIE - nie rozwinąłem tego;)

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

Rozwiąże wszystkie zależności i „naprawi” plik wykonywalny, a także pliki dylib, aby działały płynnie w pakiecie aplikacji.

... sprawdzi również zależności zależnych dynamicznych bibliotek: D

Chris
źródło
to całkiem przydatne narzędzie! Dziękuję za udostępnienie i rozwój.
xpnimi
Brakuje tutaj zależności zależności. Naprawić?
Peter
9

Używam tego w moim Makefile ... Tworzy pakiet aplikacji. Przeczytaj to i zrozum, ponieważ będziesz potrzebować pliku ikony png w folderze macosx / wraz z plikami PkgInfo i Info.plist, które tutaj dołączam ...

„to działa na moim komputerze”… Używam tego do wielu aplikacji na 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
subatomowy
źródło
Możesz sprawić, że Info.plist będzie zawierał tekst zastępczy, a następnie użyj czegoś takiego jak sed lub awk, aby utworzyć ostateczną listę Info.plist z odpowiednimi polami. A może nawet użyj plutil do stworzenia plist.
Mark Heath
8

Najprostszym rozwiązaniem jest: utwórz raz projekt Xcode bez zmiany czegokolwiek (tj. Zachowaj prostą aplikację z jednym oknem, którą tworzy dla Ciebie Xcode), skompiluj ją i skopiuj pakiet, który utworzył dla Ciebie. Następnie edytuj pliki (w szczególności Info.plist) tak, aby odpowiadały Twojej zawartości i umieść swój własny plik binarny w katalogu Contents / MacOS /.

F'x
źródło
4
Pytanie wyraźnie dotyczy tego, jak to zrobić bez xcode. Jestem na tej samej łodzi i chcę prawdziwej odpowiedzi.
hyperlogic
5
Cóż, dzięki temu XCode musisz użyć tylko raz w życiu :) lub poprosić kogoś, aby zrobił to za Ciebie.
F'x
@ F'x Czy możesz po prostu opublikować gdzieś wygenerowaną aplikację jako przykład?
endolith
3

Istnieje kilka narzędzi typu open source, które pomagają w tworzeniu pakietów aplikacji z zależnymi bibliotekami dla określonych środowisk, na przykład py2app dla aplikacji opartych na języku Python. Jeśli nie znajdziesz bardziej ogólnego, być może dostosujesz go do swoich potrzeb.

Ned Deily
źródło
2

Żałuję, że nie znalazłem tego posta wcześniej ....

Oto mój szkicowy sposób rozwiązania tego problemu przy użyciu Run scriptfazy, która jest wywoływana za każdym razem, gdy tworzę Releasewersję mojej aplikacji:

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

Mam nadzieję, że to może być przydatne dla kogoś.

peetonn
źródło