使用Xcode和SDK 4+构建包含设备和模拟器的大型静态库

290

理论上,我们可以构建一个单一的静态库,包括模拟器和iPhone和iPad。

然而,苹果没有相关文档,且Xcode的默认模板未配置实现此功能。

我正在寻找一种简单、便携、可重用的技术,在Xcode中完成。

历史:

  • 2008年,我们曾经能够制作同时包含模拟器和设备的单个静态库。但苹果已经禁止这样做。
  • 在2009年期间,我们制作了一对静态库 - 一个用于模拟器,一个用于设备。但现在苹果也已经禁止了这一做法。

参考资料:

  1. 这是一个很好的想法,是一个卓越的方法,但它不起作用:http://www.drobnik.com/touch/2010/04/universal-static-libraries/

    • 他的脚本存在一些错误,导致只能在他的机器上工作 - 他应该使用BUILT_PRODUCTS_DIR和/或BUILD_DIR而不是“估计”它们)
    • 由于Xcode处理目标的方式(已记录),苹果的最新Xcode阻止您执行他所做的操作 - 它根本不会起作用。
  2. 另一个SO问题提问者询问如何在没有Xcode的情况下实现它,并得到了关注arm6与arm7部分但忽略i386部分的回答:How do i compile a static library (fat) for armv6, armv7 and i386

    • 由于苹果的最新更改,模拟器部分已经不再与arm6 / arm7差异相同 - 这是一个不同的问题,详见上文)

3
在95%的实际情况下,库的“大小”并不重要-对于我们大多数人来说,库通常很小,尤其是与仅显示一个UIImageView相比。 - Adam
1
@Cawas - 同时,这里的价值在于您使其他人使用/重复使用您的库变得更加容易。它变成了一个一阶段的拖放过程。 - Adam
5
@Cawas - 最后,一个出人意料的有价值的好处是:很容易不小心向某人发送“错误”的编译库- XCode不进行任何检查,并会愉快地将“错误”的架构编译到您认为是“正确”的文件中。苹果公司在这个领域一直在破坏Xcode-每个新版本都有变化,这意味着“昨天您按的按钮来正确编译您的库,今天可能会出错”。除非苹果公司停止困扰我们所有人,否则我们需要对他们糟糕的用户界面进行傻瓜式的保护:) - Adam
1
那真的太好了!因为现在,我们只是不能依赖模拟器处理稍微复杂一点的事情。 - cregox
请回答这个相关的问题:lipo是否增加最终二进制文件的大小? - ma11hew28
显示剩余2条评论
10个回答

275

替代方案:

最新版本的简单复制粘贴(但安装说明可能会改变-请参见下文!)

Karl's library 需要更多的设置工作,但是长期解决方案更好(它将您的库转换为框架)。

使用此方法,然后调整以添加支持档案构建 - c.f. @Frederik在下面关于他使用的更改,使其与档案模式良好配合。


最近的更改: 1. 添加了对iOS 10.x的支持(同时保持支持早期平台)

  1. 有关如何在嵌入在另一个项目中的项目中使用此脚本的信息(尽管我强烈建议永远不这样做-从Xcode 3.x到Xcode 4.6.x,Apple有几个无法解决的Xcode错误,如果您将项目嵌入彼此之间)

  2. 让您自动包含包(即包括PNG文件,PLIST文件等从您的库中!)的奖励脚本-请参见下文(滚动到底部)

  3. 现在支持iPhone5(使用Apple的lipo错误修复方法)。注意:安装说明已更改(可能可以通过将来更改脚本来简化此过程,但现在不想冒险)

  4. “复制标头”部分现在尊重公共标头位置的构建设置(感谢Frederik Wallner)

  5. 添加了SYNROOT的显式设置(也许还需要设置OBJROOT?),感谢Doug Dickinson


脚本(这是您需要复制/粘贴的内容)

有关用法/安装说明,请参见下文

##########################################
#
# c.f. https://dev59.com/anA75IYBdhLWcg3wAUF9
#
# Version 2.82
#
# Latest Change:
# - MORE tweaks to get the iOS 10+ and 9- working
# - Support iOS 10+
# - Corrected typo for iOS 1-10+ (thanks @stuikomma)
# 
# Purpose:
#   Automatically create a Universal static library for iPhone + iPad + iPhone Simulator from within XCode
#
# Author: Adam Martin - http://twitter.com/redglassesapps
# Based on: original script from Eonil (main changes: Eonil's script WILL NOT WORK in Xcode GUI - it WILL CRASH YOUR COMPUTER)
#

set -e
set -o pipefail

#################[ Tests: helps workaround any future bugs in Xcode ]########
#
DEBUG_THIS_SCRIPT="false"

if [ $DEBUG_THIS_SCRIPT = "true" ]
then
echo "########### TESTS #############"
echo "Use the following variables when debugging this script; note that they may change on recursions"
echo "BUILD_DIR = $BUILD_DIR"
echo "BUILD_ROOT = $BUILD_ROOT"
echo "CONFIGURATION_BUILD_DIR = $CONFIGURATION_BUILD_DIR"
echo "BUILT_PRODUCTS_DIR = $BUILT_PRODUCTS_DIR"
echo "CONFIGURATION_TEMP_DIR = $CONFIGURATION_TEMP_DIR"
echo "TARGET_BUILD_DIR = $TARGET_BUILD_DIR"
fi

#####################[ part 1 ]##################
# First, work out the BASESDK version number (NB: Apple ought to report this, but they hide it)
#    (incidental: searching for substrings in sh is a nightmare! Sob)

SDK_VERSION=$(echo ${SDK_NAME} | grep -o '\d\{1,2\}\.\d\{1,2\}$')

# Next, work out if we're in SIM or DEVICE

if [ ${PLATFORM_NAME} = "iphonesimulator" ]
then
OTHER_SDK_TO_BUILD=iphoneos${SDK_VERSION}
else
OTHER_SDK_TO_BUILD=iphonesimulator${SDK_VERSION}
fi

echo "XCode has selected SDK: ${PLATFORM_NAME} with version: ${SDK_VERSION} (although back-targetting: ${IPHONEOS_DEPLOYMENT_TARGET})"
echo "...therefore, OTHER_SDK_TO_BUILD = ${OTHER_SDK_TO_BUILD}"
#
#####################[ end of part 1 ]##################

#####################[ part 2 ]##################
#
# IF this is the original invocation, invoke WHATEVER other builds are required
#
# Xcode is already building ONE target...
#
# ...but this is a LIBRARY, so Apple is wrong to set it to build just one.
# ...we need to build ALL targets
# ...we MUST NOT re-build the target that is ALREADY being built: Xcode WILL CRASH YOUR COMPUTER if you try this (infinite recursion!)
#
#
# So: build ONLY the missing platforms/configurations.

if [ "true" == ${ALREADYINVOKED:-false} ]
then
echo "RECURSION: I am NOT the root invocation, so I'm NOT going to recurse"
else
# CRITICAL:
# Prevent infinite recursion (Xcode sucks)
export ALREADYINVOKED="true"

echo "RECURSION: I am the root ... recursing all missing build targets NOW..."
echo "RECURSION: ...about to invoke: xcodebuild -configuration \"${CONFIGURATION}\" -project \"${PROJECT_NAME}.xcodeproj\" -target \"${TARGET_NAME}\" -sdk \"${OTHER_SDK_TO_BUILD}\" ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO" BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" SYMROOT=\"${SYMROOT}\"

xcodebuild -configuration "${CONFIGURATION}" -project "${PROJECT_NAME}.xcodeproj" -target "${TARGET_NAME}" -sdk "${OTHER_SDK_TO_BUILD}" ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" SYMROOT="${SYMROOT}"

ACTION="build"

#Merge all platform binaries as a fat binary for each configurations.

# Calculate where the (multiple) built files are coming from:
CURRENTCONFIG_DEVICE_DIR=${SYMROOT}/${CONFIGURATION}-iphoneos
CURRENTCONFIG_SIMULATOR_DIR=${SYMROOT}/${CONFIGURATION}-iphonesimulator

echo "Taking device build from: ${CURRENTCONFIG_DEVICE_DIR}"
echo "Taking simulator build from: ${CURRENTCONFIG_SIMULATOR_DIR}"

CREATING_UNIVERSAL_DIR=${SYMROOT}/${CONFIGURATION}-universal
echo "...I will output a universal build to: ${CREATING_UNIVERSAL_DIR}"

# ... remove the products of previous runs of this script
#      NB: this directory is ONLY created by this script - it should be safe to delete!

rm -rf "${CREATING_UNIVERSAL_DIR}"
mkdir "${CREATING_UNIVERSAL_DIR}"

#
echo "lipo: for current configuration (${CONFIGURATION}) creating output file: ${CREATING_UNIVERSAL_DIR}/${EXECUTABLE_NAME}"
xcrun -sdk iphoneos lipo -create -output "${CREATING_UNIVERSAL_DIR}/${EXECUTABLE_NAME}" "${CURRENTCONFIG_DEVICE_DIR}/${EXECUTABLE_NAME}" "${CURRENTCONFIG_SIMULATOR_DIR}/${EXECUTABLE_NAME}"

#########
#
# Added: StackOverflow suggestion to also copy "include" files
#    (untested, but should work OK)
#
echo "Fetching headers from ${PUBLIC_HEADERS_FOLDER_PATH}"
echo "  (if you embed your library project in another project, you will need to add"
echo "   a "User Search Headers" build setting of: (NB INCLUDE THE DOUBLE QUOTES BELOW!)"
echo '        "$(TARGET_BUILD_DIR)/usr/local/include/"'
if [ -d "${CURRENTCONFIG_DEVICE_DIR}${PUBLIC_HEADERS_FOLDER_PATH}" ]
then
mkdir -p "${CREATING_UNIVERSAL_DIR}${PUBLIC_HEADERS_FOLDER_PATH}"
# * needs to be outside the double quotes?
cp -r "${CURRENTCONFIG_DEVICE_DIR}${PUBLIC_HEADERS_FOLDER_PATH}"* "${CREATING_UNIVERSAL_DIR}${PUBLIC_HEADERS_FOLDER_PATH}"
fi
fi

安装说明

  1. 创建静态库项目
  2. 选择目标
  3. 在“构建设置”选项卡中,将“仅构建活动架构”设置为“NO”(对于所有项目)
  4. 在“构建阶段”选项卡中,选择“添加…新的构建阶段…新的运行脚本构建阶段”
  5. 将脚本(上面的)复制/粘贴到框中

…额外的可选用法:

  1. 可选:如果您的库中有标题,请将它们添加到“复制标题”阶段中
  2. 可选:…并将它们从“项目”部分拖放到“公共”部分
  3. 可选:…它们每次构建应用程序时都会自动导出到“debug-universal”目录的子目录中(它们将位于usr/local/include中)
  4. 可选:注意:如果您还尝试将项目拖放到另一个Xcode项目中,这会暴露Xcode 4中的错误,如果您的拖放项目中有公共标题,则无法创建.IPA文件。解决方法:不要嵌入xcode项目(Apple代码中有太多的错误!)

如果找不到输出文件,可以使用以下解决方法:

  1. 将以下代码添加到脚本的最末尾(由Frederik Wallner提供):open "${CREATING_UNIVERSAL_DIR}"

  2. Apple在200行后删除所有输出。选择目标,在“运行脚本”阶段中,您必须取消选中:“在构建日志中显示环境变量”

  3. 如果您正在为XCode4使用自定义的“构建输出”目录,则XCode会将所有“意外”的文件放置在错误的位置。

    1. 构建项目
    2. 单击Xcode4左上角的最右边的图标。
    3. 选择顶部项目(这是您的“最近的构建”)。Apple应该自动选择它,但他们没有考虑到这一点。
    4. 在主窗口中,向下滚动。最后一行应该是:lipo: for current configuration (Debug) creating output file: /Users/blah/Library/Developer/Xcode/DerivedData/AppName-ashwnbutvodmoleijzlncudsekyf/Build/Products/Debug-universal/libTargetName.a

    …那就是您通用版的位置。


如何在项目中包含“非源代码”文件(PNG、PLIST、XML等)

  1. 完成以上步骤,并检查其是否有效
  2. 创建一个接在第一个步骤后的新运行脚本阶段(复制/粘贴下面的代码)
  3. 在Xcode中创建一个新的类型为“bundle”的目标
  4. 在您的主项目中,在“构建阶段”中,将新的bundle添加为它“依赖”的东西(顶部部分,点击加号按钮,向下滚动,在产品中找到“.bundle”文件)
  5. 在您的新捆绑目标中,在“构建阶段”中,添加一个“复制捆绑资源”部分,并将所有PNG文件等拖放到其中

自动将构建的bundle(s)复制到与您的FAT静态库相同的文件夹中的脚本:

echo "RunScript2:"
echo "Autocopying any bundles into the 'universal' output folder created by RunScript1"
CREATING_UNIVERSAL_DIR=${SYMROOT}/${CONFIGURATION}-universal
cp -r "${BUILT_PRODUCTS_DIR}/"*.bundle "${CREATING_UNIVERSAL_DIR}"

2
我现在已经在几个项目中使用了这个,而且还将使用它来构建库并将其发布到应用商店。所有的工作都完美无误,所以我现在会继续使用它(直到 Xcode 4 或许出现)。 - Adam
2
有人能确认这个方法是否适用于 XCode 4.5 吗?我正在尝试编译一个静态库并将其用于我的主项目。我能在设备上运行此项,但无法在模拟器上运行。这是我得到的错误:missing required architecture i386 in file /Users/alex/Documents/iphone/production/iphone/mymedia/libMyUnrar4iOS.a (2 slices)。 - Alex1987
2
有没有想法如何在XCode 5和ARM64上使其工作?如果我将架构设置为标准,它会像预期的那样使用armv7、armvs7和i386生成库。如果我将架构设置为包括64位的标准,则库仅包含“cputype 16777223”。我使用otool -h命令检查.a文件以验证其中的内容。 - Roger Binns
1
XCode5让添加运行脚本构建阶段变得更加棘手。请查看以下链接:http://www.runscriptbuildphase.com/ - Fabio Napodano
1
XCode7:目前仍可使用,但苹果暗示他们将在Xcode 7.1或Xcode 8中永久性地破坏它。目前,您通常需要禁用新的(可选且实现不佳的)“位代码”功能,因为它存在错误,并在合并文件时导致其他苹果后台工具冲突。通过重写脚本中间的大块内容可以解决此问题 - 如果有人想尝试使这个位代码兼容,请fork gist并ping我,我会帮助测试。 - Adam
显示剩余47条评论

89

我花了许多时间尝试构建一个适用于armv7、armv7s和模拟器的静态库。最终找到了解决方法

要点是分别构建两个库(一个用于设备,一个用于模拟器),将它们重命名以区别于彼此,然后使用lipo -create将它们合并为一个库。

lipo -create libPhone.a libSimulator.a -output libUniversal.a

我尝试过它,它有效!


4
我建议您阅读被接受的答案。您可能会发现,这个问题已经在2年前得到了解决。 - Adam
2
我阅读了它,使用了脚本,但对于armv7s,它对我来说无法工作。 - g_low
2
lipo命令在脚本上不起作用,但手动操作很好!10x - Dima
11
这真的是我所需要的,而不是一个庞大的“制作框架”的脚本。 - CodeSmile

74

我已经制作了一个 XCode 4 项目模板,使得您可以像创建常规库一样轻松地创建通用框架。


无法使用iOS 4.3目标构建。出现以下错误:-stdlib=libc++的部署目标无效(需要iOS 5.0或更高版本)。 - Alex1987
我希望我能为这个答案提供更多的声望点数……比使用CMake创建静态库要容易得多。非常感谢你做到了这一点! - iwasrobbed
对我来说,它也适用于iOS 6。但可能是因为我的库非常简单,没有任何依赖和资源。 - Paulius Vindzigelskis
这个解决方案存在一个大问题:其他想要使用由此解决方案创建的框架的人(该解决方案建议在Xcode中安装框架模板)必须将此模板安装到他们的Xcode中!!! - evya
你只需要为真正的框架安装模板。虚假的框架在未经修改的Xcode中也可以运行良好。 - Karl

30

有一个命令行工具xcodebuild,你可以在xcode中运行shell命令。 因此,如果您不介意使用定制脚本,此脚本可能会对您有所帮助。

#Configurations.
#This script designed for Mac OS X command-line, so does not use Xcode build variables.
#But you can use it freely if you want.

TARGET=sns
ACTION="clean build"
FILE_NAME=libsns.a

DEVICE=iphoneos3.2
SIMULATOR=iphonesimulator3.2






#Build for all platforms/configurations.

xcodebuild -configuration Debug -target ${TARGET} -sdk ${DEVICE} ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO
xcodebuild -configuration Debug -target ${TARGET} -sdk ${SIMULATOR} ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO
xcodebuild -configuration Release -target ${TARGET} -sdk ${DEVICE} ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO
xcodebuild -configuration Release -target ${TARGET} -sdk ${SIMULATOR} ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO







#Merge all platform binaries as a fat binary for each configurations.

DEBUG_DEVICE_DIR=${SYMROOT}/Debug-iphoneos
DEBUG_SIMULATOR_DIR=${SYMROOT}/Debug-iphonesimulator
DEBUG_UNIVERSAL_DIR=${SYMROOT}/Debug-universal

RELEASE_DEVICE_DIR=${SYMROOT}/Release-iphoneos
RELEASE_SIMULATOR_DIR=${SYMROOT}/Release-iphonesimulator
RELEASE_UNIVERSAL_DIR=${SYMROOT}/Release-universal

rm -rf "${DEBUG_UNIVERSAL_DIR}"
rm -rf "${RELEASE_UNIVERSAL_DIR}"
mkdir "${DEBUG_UNIVERSAL_DIR}"
mkdir "${RELEASE_UNIVERSAL_DIR}"

lipo -create -output "${DEBUG_UNIVERSAL_DIR}/${FILE_NAME}" "${DEBUG_DEVICE_DIR}/${FILE_NAME}" "${DEBUG_SIMULATOR_DIR}/${FILE_NAME}"
lipo -create -output "${RELEASE_UNIVERSAL_DIR}/${FILE_NAME}" "${RELEASE_DEVICE_DIR}/${FILE_NAME}" "${RELEASE_SIMULATOR_DIR}/${FILE_NAME}"

也许看起来不太高效(我不擅长编写shell脚本),但易于理解。我仅使用此脚本配置了一个新的目标。该脚本是为命令行设计的,但未在其中进行测试 :)

核心概念是xcodebuildlipo

我尝试了许多Xcode UI中的配置,但都没有成功。因为这是一种批量处理,所以命令行设计更加合适,因此苹果逐渐从Xcode中移除了批量构建功能。因此,我不希望他们会在未来提供基于UI的批量构建功能。


谢谢,有趣的是底层的“简单”命令似乎仍然有效 - 只是苹果公司在GUI方面出了问题。看起来我可以制作一个完全自定义的项目模板,以“不糟糕”的方式修复苹果公司破坏的东西,通过预先创建所有目标,并使用xcode构建变量连接此脚本。我会在我的下一个项目中尝试它 :) - Adam
1
我使用了类似于这个脚本的脚本,并将其放在一个新目标下,该目标仅包含shell脚本。上面的递归构建脚本非常聪明,但是不必要地令人困惑。 - benzado
1
我更喜欢使用Shell脚本来处理这类事情,以下是我的做法 https://gist.github.com/3178578 - slf
@benzado 是的,我故意避免了复杂性,因为我认为 shell 脚本必须易于阅读和修改。 - eonil
lipo:无法打开输入文件:/Debug-iphoneos/ - Dima
需要注意的是,从Xcode 5开始,-sdk参数的值应为iphoneos或iphonesimulator,不带版本号,全部小写。 - aeskreis

11

我需要一个适用于JsonKit的静态库,因此在Xcode中创建了一个静态库项目,然后在项目目录中运行了这个bash脚本。只要你已经配置了xcode项目并关闭了“仅构建活动配置”,你就可以在一个库中获得所有架构。

#!/bin/bash
xcodebuild -sdk iphoneos
xcodebuild -sdk iphonesimulator
lipo -create -output libJsonKit.a build/Release-iphoneos/libJsonKit.a build/Release-iphonesimulator/libJsonKit.a

7

IOS 10更新:

我在使用iphoneos10.0构建fatlib时遇到了问题,因为脚本中的正则表达式只能匹配9.x及以下版本,并且对于ios 10.0返回0.0。

要解决这个问题,只需替换

SDK_VERSION=$(echo ${SDK_NAME} | grep -o '.\{3\}$')

使用

SDK_VERSION=$(echo ${SDK_NAME} | grep -o '[\\.0-9]\{3,4\}$')

谢谢。今天早上我做了类似的更改,但使用了\d。我认为这是我们想要的(比你的更好还是更差?)...grep -o '\d{1,2}.\d{2}$' - Adam
我认为我的更可靠,因为它只考虑数字。 - ben
1
不是的,你的方法只适用于一种数字书写方式。鉴于苹果历史上对美化字符和文本(例如文件名)的支持和使用,我预计你的专有数字选择会更不可靠。 - Adam
1
好的,也许你是对的。至少我的代码让我的项目正常运行了,我们可以在接下来的89个iOS版本中保持安全。 - ben
@ben 的解决方案对我有效,Adam 的正则表达式 '[\.0-9]{3,4}$' 给出错误代码 2。 - Zee

4

我已经将这个东西做成了一个Xcode 4 模板,与Karl的静态框架模板类似。

我发现使用静态框架(而不是普通的静态库)会因为LLVM的一个显然的链接器错误导致随机崩溃 - 所以,我想静态库仍然是有用的!


嗨,Michael,我尝试了你的静态库模板,但是我可以编译模拟器版本,但无法编译设备版本,这里是错误信息:** BUILD FAILED **以下构建命令失败: ProcessPCH /var/folders/qy/ncy6fkpn6677qt876ljrc54m0000gn/C/com.apple.Xcode.501/SharedPrecompiledHeaders/MenuBarUniversal-Prefix-gwxxzpanxyudmfgryorafazokagi/MenuBarUniversal-Prefix.pch.pth MenuBarUniversal/MenuBarUniversal-Prefix.pch normal armv7 objective-c com.apple.compilers.llvm.clang.1_0.compiler (1个失败) 仅显示前200个通知 /bin/sh 命令以退出代码65失败。 - Kappe

3

XCode 12 更新:

如果您在不使用-arch参数的情况下运行xcodebuild,XCode 12将默认构建模拟器库的架构为 "arm64 x86_64"。

然后运行xcrun-sdk iphoneos lipo-create-output将会出现冲突,因为模拟器和设备库都存在arm64架构。

我从Adam git上fork脚本并进行了修改


欢迎提供解决方案的链接,但请确保您的答案即使没有链接也是有用的:在链接周围添加上下文,以便其他用户了解它的内容和原因,然后引用您链接的页面中最相关的部分,以防目标页面无法访问。仅仅提供链接的答案可能会被删除。 - Yunnosch

2

干得好!我曾经也拼凑出了类似的东西,但必须单独运行它。将其作为构建过程的一部分使它变得更加简单。

需要注意的一点是,我注意到它不会复制任何标记为公共的包含文件。我已经将我的脚本改编成了你的,并且效果还不错。将以下内容粘贴到你的脚本末尾即可。

if [ -d "${CURRENTCONFIG_DEVICE_DIR}/usr/local/include" ]
then
  mkdir -p "${CURRENTCONFIG_UNIVERSAL_DIR}/usr/local/include"
  cp "${CURRENTCONFIG_DEVICE_DIR}"/usr/local/include/* "${CURRENTCONFIG_UNIVERSAL_DIR}/usr/local/include"
fi

1
好的,我已经将其添加到上面的答案中了。(还没有测试的机会,但在我看来应该是正确的) - Adam

1
我实际上只是为了这个目的编写了自己的脚本。它不使用Xcode。(它基于Gambit Scheme项目中的类似脚本。)
基本上,它运行./configure和make三次(对于i386、armv7和armv7s),并将每个结果库组合成一个fat lib。

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