题 构建OSX App Bundle


假设我已经制作了一个不使用Xcode的osX应用程序。在使用GCC编译后,我得到一个可执行文件,链接到其他几个库。其中一些库可能再次动态链接到其他非标准系统库

是否有任何工具存在,它通过首先制作所需的目录结构,然后递归复制/检查/修复链接来确保所有动态依赖关系也在应用程序包中,从而制作OSX App包?

我想我可以尝试写这样的东西,但我想知道这样的事情是否已经存在。


66
2017-10-20 19:59


起源


由于几个原因,我无法使用Xcode。其中一个是因为我使用自定义gcc。 Xcode不允许我指定不同的gcc。我正在使用cmake来构建我的makefile。 - Yogi
我有类似的问题,但我落后于你。我被困在编译/链接fase。你能帮帮忙吗? - Gik
>>其中一些库可能会再次动态链接到其他非标准系统库<<选定的响应对此情况没有帮助。你是怎么解决的? - iOS Calendar View OnMyProfile


答案:


有两种方法可以在MacOSX上创建应用程序包,即Easy和Ugly。

简单的方法就是使用XCode。 完成。

问题有时你不能。

在我的情况下,我正在构建一个构建其他应用程序的应用程序。 我不能假设用户已安装XCode。 我也在用 MacPorts的 构建我的应用程序所依赖的库。 在分发之前,我需要确保这些dylib与应用程序捆绑在一起。

免责声明: 我完全没有资格写这篇文章,一切都在 已经从Apple文档中剔透,挑选现有的应用程序和 反复试验。它适用于我,但很可能是错误的。请 如果您有任何更正,请给我发电子邮件

您应该知道的第一件事是应用程序包只是一个目录。
让我们来看看假设的foo.app的结构。

foo.app/
    内容/
        的Info.plist
        苹果系统/
            FOO
        资源/
            foo.icns

Info.plist是一个纯XML文件。 您可以使用文本编辑器或与XCode捆绑在一起的Property List Editor应用程序对其进行编辑。 (它位于/ Developer / Applications / Utilities /目录中)。

您需要包含的关键事项是:

CFBundleName  - 应用程序的名称。

CFBundleIcon  - 假定在Contents / Resources目录中的Icon文件。 使用Icon Composer应用程序创建图标。 (它也在/ Developer / Applications / Utilities /目录中) 您只需将png拖放到它的窗口,就可以自动为您生成mip级别。

CFBundleExecutable  - 假定在Contents / MacOS /子文件夹中的可执行文件的名称。

还有更多选项,上面列出的选项只是最低限度。 这是关于的一些Apple文档 的Info.plist  文件和 应用包结构

另外,这是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”>
<字典>
  <键> CFBundleGetInfoString </键>
  <字符串>富</字符串>
  <键> CFBundleExecutable </键>
  <字符串> FOO </字符串>
  <键> CFBundleIdentifier </键>
  <字符串> com.your-公司name.www </字符串>
  <键> CFBundleName </键>
  <字符串> FOO </字符串>
  <键> CFBundleIconFile </键>
  <字符串> foo.icns </字符串>
  <键> CFBundleShortVersionString </键>
  <字符串> 0.01 </字符串>
  <键> CFBundleInfoDictionaryVersion </键>
  <字符串> 6.0 </字符串>
  <键> CFBundlePackageType </键>
  <字符串> APPL </字符串>
  <键> IFMajorVersion </键>
  <整数> 0 </整数>
  <键> IFMinorVersion </键>
  <整数> 1 </整数>
</字典>
</ plist中>

在一个完美的世界中,你可以将你的可执行文件放入 内容/ MacOS /目录并完成。但是,如果您的应用程序有任何 非标准的dylib依赖项不起作用。像Windows,MacOS 它带有它自己的特殊类型 DLL地狱

如果你正在使用 MacPorts的 要构建链接的库,dylib的位置将硬编码到您的可执行文件中。 如果您在具有完全相同位置的dylib的计算机上运行该应用程序,它将运行正常。 但是,大多数用户不会安装它们;当他们双击你的应用程序时它会崩溃。

在分发可执行文件之前,您需要收集它加载的所有dylib并将它们复制到应用程序包中。 您还需要编辑可执行文件,以便在正确的位置查找dylib。即你将它们复制到的地方。

手编辑可执行文件听起来很危险吗? 幸运的是,有命令行工具可以提供帮助。

otool -L executable_name

此命令将列出您的应用所依赖的所有dylib。 如果您看到System / Library或usr / lib文件夹中没有任何内容,则需要将这些文件复制到应用程序包中。 将它们复制到/ Contents / MacOS /文件夹中。 接下来,您需要编辑可执行文件以使用新的dylib。

首先,您需要确保使用-headerpad_max_install_names标志进行链接。 这只是确保如果新的dylib路径比前一个路径长,那么它将有空间。

其次,使用install_name_tool更改每个dylib路径。

install_name_tool -change existing_path_to_dylib @ executable_path / blah.dylib executable_name

作为一个实际的例子,假设您的应用使用了 libSDL,otool将其位置列为“/opt/local/lib/libSDL-1.2.0.dylib”。

首先将其复制到应用程序包中。

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

然后编辑可执行文件以使用新位置(注意:确保使用-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

哇,我们差不多完成了。 现在,当前工作目录存在一个小问题。

当您启动应用程序时,当前目录将是应用程序所在的目录。 例如:如果将foo.app放在/ Applcations文件夹中,则启动应用程序时的当前目录将是/ Applications文件夹。 正如您所料,不是/Applications/foo.app/Contents/MacOS/。

您可以更改您的应用程序以解决此问题,或者您可以使用这个神奇的小启动器脚本来更改当前目录并启动您的应用程序。

#!/斌/庆典
cd“$ {0%/ *}”
./foo

确保您调整Info.plist文件 CFBundleExecutable 指向启动脚本而不是前一个可执行文件。

好的,现在都完成了。 幸运的是,一旦你知道所有这些东西,你就把它埋在一个构建脚本中。


113
2017-07-14 23:15



+1。然而,僵尸狗吓坏了我。你能用一张不那么疤痕的图片来说明DLL地狱吗? (更不用说术语“DLL hell”不适用。分发动态库的首选方法是通过避免大多数问题的框架。但是,.dylib方法对UNIX风格的库很有用。) - Heinrich Apfelmus
“僵尸狗吓坏了我。” - 僵尸狗很可爱,它在哪里? - boj
我们如何修复双重依赖?像dylib引用另一个dylib? - iOS Calendar View OnMyProfile


我实际上找到了一个非常方便的工具,值得一些信任......不 - 我没有开发这个;)

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

它将解析所有依赖项并“修复”您的可执行文件以及您的dylib文件,以便在您的应用程序包中顺利运行。

...它还将检查依赖动态库的依赖关系:D


14
2018-02-19 20:25





捆绑包真的没有什么神奇之处 - 只需阅读Apple的文档,并模仿它。在基础上,您需要Info.plist,Contents / MacOS / binary和图标。


6
2017-10-21 10:52



添加文档的链接,它将不那么神奇。 - Wyatt8740


最简单的解决方案是:创建一次Xcode项目而不更改任何内容(即保留Xcode为您创建的简单单窗口应用程序),构建它,并复制它为您创建的包。然后,编辑文件(特别是Info.plist)以适合您的内容,并将您自己的二进制文件放在Contents / MacOS /目录中。


6
2018-03-10 15:34



这个问题明确地询问如何在没有xcode的情况下做到这一点。我在同一条船上,想要一个真正的答案。 - hyperlogic
好吧,有了这个,你只需要在你的生活中使用一次XCode :)或者让别人为你做这件事。 - F'x


我在Makefile中使用它...它创建了一个应用程序包。阅读并理解它,因为你需要一个macosx /文件夹中的png图标文件以及我在这里包含的PkgInfo和Info.plist文件......

“它适用于我的电脑”......我在小牛队的多个应用程序中使用它...

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

6
2018-05-27 20:16





有一些开源工具可帮助构建具有特定环境的依赖库的应用程序包,例如, py2app 用于基于Python的应用程序。如果你找不到更通用的,也许你可以根据自己的需要进行调整。


2
2017-10-20 20:50





我希望我早些时候发现这篇文章......

这是我使用a解决这个问题的粗略方法 Run script 每次构建一个时调用的阶段 Release 我的应用版本:

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

希望这可能对某人有用。


1
2017-07-29 23:38