98

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

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

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

4

7 回答 7

155

在 MacOSX 上创建 app bundle 有两种方法,Easy 和 Ugly。

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

问题是有时你不能。

就我而言,我正在构建一个构建其他应用程序的应用程序。我不能假设用户安装了 XCode。我还使用MacPorts来构建我的应用程序所依赖的库。在分发之前,我需要确保这些 dylib 与应用程序捆绑在一起。

免责声明:我完全没有资格写这篇文章,其中的所有内容都来自 Apple 文档,挑选现有的应用程序和反复试验。它对我有用,但很可能是错误的。如果您有任何更正,请给我发电子邮件。

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

foo.app/
    内容/
        信息列表
        苹果系统/
            富
        资源/
            foo.icns

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的一些 Apple 文档 。

此外,这是一个示例 Info.plist。

<?xml 版本="1.0" 编码="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist 版本="1.0">
<字典>
  <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>
  <整数>0</整数>
  <key>IFMinorVersion</key>
  <整数>1</整数>
</dict>
</plist>

在一个完美的世界中,您可以将您的可执行文件放入 Contents/MacOS/ 目录并完成。但是,如果您的应用程序有任何非标准的 dylib 依赖项,它将无法工作。与 Windows 一样,MacOS 也带有它自己特殊的DLL Hell类型。

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

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

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

otool -L 可执行文件名

此命令将列出您的应用所依赖的所有 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/ 。

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

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

确保调整 Info.plist 文件,使CFBundleExecutable指向启动脚本,而不是之前的可执行文件。

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

于 2010-07-14T23:15:50.940 回答
20

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

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

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

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

于 2011-02-19T20:25:13.250 回答
10

我在我的 Makefile 中使用它......它创建了一个应用程序包。阅读并理解它,因为您需要在 macosx/ 文件夹中的 png 图标文件以及我在此处包含的 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

信息列表

<?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>

包信息

APPLMyAp
于 2014-05-27T20:16:51.130 回答
8

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

于 2010-03-10T15:34:38.513 回答
3

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

于 2009-10-20T20:50:14.107 回答
2

我希望我早点找到这个帖子......

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)

希望这可能对某人有用。

于 2015-07-29T23:38:08.233 回答
0

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

  1. 像往常一样在终端中启动程序(./appname)
  2. 程序 GUI 正常启动,单击终端使应用失去焦点
  3. 单击 GUI 重新获得焦点,菜单项将起作用。

我同意 app bundle 是在 Mac 上构建程序的正确方法。这只是在调试期间提供帮助的简单解决方法。

编辑:这是在 Mac Catalina 上,wxWidgets 3.1.4,带有 g++ 4.2.1(2020 年 11 月)

于 2020-11-15T14:55:46.740 回答