构建OSX应用程序包

106

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

是否存在任何工具,可以首先创建所需的目录结构,然后递归地复制/检查/修复链接,以确保所有动态依赖项也在应用程序包中?

我想我可以尝试编写类似的东西,但我想知道是否已经存在这样的工具。


4
由于多种原因,我无法使用Xcode。其中之一是因为我使用自定义的gcc,而Xcode不允许我指定不同的gcc。我正在使用cmake来构建我的makefile。 - Yogi
一些这些库可能会再次动态链接到其他非标准系统库。 - Just a coder
7个回答

168
在MacOSX上创建应用程序包有两种方法,一种简单的方法是使用XCode,搞定。但问题是有时候你不能这么做。例如,我正在构建一个可以构建其他应用程序的应用程序,我不能假设用户已经安装了XCode。同时,我也在使用MacPorts来构建我的应用程序依赖库,我需要确保这些dylibs在分发应用程序之前被捆绑到应用程序中。免责声明:我完全没有资格撰写此文章,其中所有内容都来自于苹果文档、分析现有应用程序以及试错。它对我有效,但很可能是错误的。如果您有任何更正,请给我发电子邮件。
首先你需要知道的是,应用程序包只是一个目录。
让我们来看一下一个假想的foo.app的结构。 Info.plist是一个普通的XML文件。 你可以使用文本编辑器或随XCode捆绑的Property List Editor应用程序进行编辑。 (它在/Developer/Applications/Utilities/目录中)。
你需要包括的关键内容是: CFBundleName - 应用程序的名称。 CFBundleIcon - 假定在Contents/Resources目录中的图标文件。 使用Icon Composer应用程序创建图标。(它也在/Developer/Applications/Utilities/目录中) 你可以将png文件拖放到其窗口上,它应该会自动为您生成mip级别。
CFBundleExecutable - 假定在Contents/MacOS子文件夹中的可执行文件的名称。
还有很多选项,上面列出的只是最基本的部分。 这里是一些苹果文档 Info.plist 文件和 App bundle structure
此外,这是一个示例Info.plist。
在完美的世界里,你只需要将可执行文件放入Contents/MacOS/目录中即可完成。 但是,如果你的应用程序有任何非标准dylib依赖项,它就不起作用了。 像Windows一样,MacOS也有自己特殊的DLL Hell
如果你正在使用MacPorts构建库以链接,那么dylibs的位置将被硬编码到你的可执行文件中。 如果在具有相同位置的dylibs的机器上运行应用程序,则它将正常运行。 但是,大多数用户都没有安装它们;当他们双击你的应用程序时,它会崩溃。
在分发可执行文件之前,您需要收集其加载的所有dylibs并将它们复制到应用程序包中。 您还需要编辑可执行文件,以便它将在正确的位置查找dylibs。 即你复制它们的地方。
手动编辑可执行文件听起来很危险? 幸运的是,有命令行工具可以帮助。
otool -L executable_name 此命令将列出您的应用程序所依赖的所有dylibs。 如果您看到任何不在System/Library或usr/lib文件夹中的内容,则需要将它们复制到应用程序包中。 将它们复制到/Contents/MacOS/文件夹中。 接下来,您需要编辑可执行文件以使用新的dylibs。
首先,您需要确保使用-headerpad_max_install_names标志进行链接。 这只是确保如果新的dylib路径比以前的长,就会有足够的空间。
其次,使用install_name_tool更改每个dylib路径。
作为一个实际的例子,假设您的应用程序使用libSDL,otool将其位置列为“/opt/local/lib/libSDL-1.2.0.dylib”。
首先将其复制到应用程序包中。
然后编辑可执行文件以使用新位置(注意:确保您使用了-headerpad_max_install_names标志构建它)
哇,我们快要完成了。 现在有一个小问题是当前工作目录。
当你启动应用程序时,当前目录将是应用程序所在目录的上一级目录。 例如:如果你将foo.app放在/Applications文件夹中,则启动应用程序时的当前目录将是/Applications文件夹。 而不是/Applications/foo.app/Contents/MacOS/,正如你可能期望的那样。
你可以改变你的应用程序来解决这个问题,或者你可以使用这个神奇的小启动脚本来改变当前目录并启动你的应用程序。
确保你调整Info.plist文件,使CFBundleExecutable</

7
+1. 然而,这只僵尸狗让我感到不安。你能否使用一张没有那么吓人的图片来说明DLL地狱?(更不用说“DLL地狱”这个术语不再适用了。分发动态库的首选方法是通过框架,可以避免大多数问题。但对于类Unix类型的库来说,.dylib方法仍然很有用。) - Heinrich Apfelmus
1
我们如何解决双重依赖?比如一个dylib引用了另一个dylib? - Just a coder
有没有办法在正确的位置编译可执行文件,以便您不必编辑它? - synchronizer
在Catalina上,由于安全性变得更加严格,这个还能正常工作吗?(修改dylibs、二进制文件等) - synchronizer
你是否仍然可以以任何方式找到应用程序所依赖的dylibs?显然,OSX 11中的更改使得otool对于此目的无用,因为它通常只会将您指向不存在的usr/lib/libSystem.B.dylib。 - P...
这里提到的图标工具似乎不见了,但是可以使用iconutil来代替它:https://dev59.com/Smct5IYBdhLWcg3wKqQd#20703594 - rsethc

24

我实际上找到了一款非常方便的工具,值得一提......NO - 我没开发出这款工具 ;)

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

它将解决所有依赖关系,并“修复”您的可执行文件以及dylib文件,使它们在应用程序包中平稳运行。

... 还会检查您的相关动态库的依赖关系:D


这是一个非常有用的工具!感谢您的分享和开发。 - xpnimi
这里缺少了依赖项的依赖关系。需要修复吗? - Peter
@Chris 好人 :) - undefined

10

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

"它在我的电脑上工作"......我在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

你可以让 Info.plist 拥有文本占位符,然后使用 sed 或 awk 等工具创建带有正确字段的最终 Info.plist。或者甚至可以使用 plutil 来创建 plist。 - silicontrip

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

4
问题明确要求不使用Xcode来完成。我也面临同样的问题,希望得到一个真正的答案。 - hyperlogic
5
好的,您只需要在一生中使用一次XCode :) 或者要求别人替您完成。 - F'x
@F'x,您能否在某个地方发布生成的.app文件以作为示例呢? - endolith

3

有一些开源工具可以帮助构建应用程序包,并针对特定环境使用相关的库,例如基于Python的应用程序可以使用py2app工具。如果你没有找到更通用的工具,也许可以根据自己的需求进行适当的调整。


2

希望我早些时候找到这篇文章...

这是我解决问题的简略方法,使用一个“运行脚本”阶段,在每次构建我的应用程序的“发布”版本时调用:

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

希望这对某些人有所帮助。

0

在 Mac 上使用 wxWidget 代码使菜单正常工作的解决方法是:

  1. 像往常一样在终端中启动程序(./appname)
  2. 程序 GUI 正常启动,然后点击终端以使应用程序失去焦点
  3. 点击 GUI 以恢复焦点,此时菜单项将起作用。

我同意在 Mac 上构建程序的正确方式是使用 app bundle。这只是一个简单的解决方法,可帮助调试。

编辑:这是在 Mac Catalina 上,使用 wxWidgets 3.1.4 和 g++ 4.2.1(2020 年 11 月)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接