第22章:Apple专属特性

Apple平台有许多独特的特点,这些特点直接影响软件的构建方式。尽管macOS的简单命令行应用程序可以与其他基于Unix的平台类似的方式构建,但那些具有图形用户界面的应用程序通常以特定的格式提供给Apple,称为应用程序包(application bundle,或简称为app bundle)。这些包不仅仅是一个可执行文件,它们是一个标准化的目录结构,包含与应用程序关联的各种文件。这些应用程序包是自包含的,能够作为一个单元移动,可以放置在用户文件系统的任何位置。

库的情况也有类似。独立的静态和动态库可以像其他基于Unix的平台上的库一样创建,但也可以作为框架的一部分构建,这在本质上相当于应用程序包的库。框架有自己的标准化目录结构,可能包含库二进制文件以外的文件。它们甚至可以在该目录结构中支持多个版本。在运行时加载的库可以构建为可加载的包,这与 Apple的CFBundle功能相对应。

捆绑包和框架是为Apple应用商店生成内容的重要组成部分。另一个关键方面是代码签名,这是验证软件完整性和来源的过程,是app store发行的一个强制性标准。代码授权也是构建过程中不可或缺的一部分,它控制着Apple使用的代码特性。这些授权是代码签名过程密封的信息,如果默认授权集(为空)不合适,则必须在构建时定义。

总之,这些特性给CMake项目带来了挑战。接下来的部分提供了用于理解和处理这些问题的工具,或者在某些情况下,突出了CMake当前的局限性。还应该指出的是,虽然CMake正式支持macOS和iOS,但对tvOS和watchOS的支持还不完整。

22.1. CMake选择生成器

用于生成框架和捆绑包的技术和工具在不断发展,Apple操作系统发布版本经常引入新特性,并围绕签名、发布等改变需求。这些流程和技术会集成到Xcode中,作为苹果希望开发人员使用的主要工具,开发人员通常希望升级到当前的Xcode版本,而不是停留在过去的版本中。资源编译、代码签名等领域是作为构建应用程序和框架会自动处理的一部分,其中许多特性是Apple生态系统所独有的。

对于CMake项目,这意味着Xcode生成器是使用Xcode工具链构建的最可靠、最方便的工具。其他生成器,如makefile或Ninja,往往缺乏Xcode生成器的一些自动化功能,或者它们对一些Xcode特性的支持方面比较落后。除了不打算通过app store发布未签名的桌面应用程序外,开发人员也会需要使用Xcode生成器支持构建。还要注意的是,Apple平台快速变化的特性,意味着开发人员通常希望最新的CMake版本跟上变化。

Xcode生成器的独特优势之一是它支持设置任意的Xcode项目属性。大多数项目设置都可以通过使用XCODE_ATTRIBUTE_XXX格式的目标属性(其中XXX是一个Xcode属性的名称)对每个目标进行修改。这些名称是在Apple文档中定义的,但是找到它们更方便的方法是打开一个Xcode项目,转到目标的构建设置,并单击感兴趣的构建设置。Quick Help助手编辑器会显示设置名称和描述。所有目标的默认值都可以通过CMAKE_XCODE_ATTRIBUTE_XXX变量设置,在定义目标时,使用这些变量对的目标属性进行初始化。下面的例子演示了如何设置一些常用的属性:

# Set the default signing identity and team ID to use for all targets
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer"
set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM XYZ123ABCD)

# Some target-specific settings
set_target_properties(myiOSApp PROPERTIES
 XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY 1,2
 XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET 10.0
)

该特性还可以用于为特定构建类型设置Xcode属性,方法是在属性名后面附加[variant=ConfigName]。对于更具体的属性设置,也可以将其他后缀类型附加到属性名中,但是并不常见。甚至[variant=…]后缀也不太需要。下面的例子给出了这个特性可用的用例:

set_target_properties(myiOSApp PROPERTIES
 XCODE_ATTRIBUTE_GCC_UNROLL_LOOPS[variant=Release] YES
 XCODE_ATTRIBUTE_ENABLE_TESTABILITY[variant=Debug] YES
)

有些项目可能需要设置相当多的属性来获得所需的Xcode行为和特性,而其他项目可能非常简单,只需要很少的设置。有些属性只在非常特定的环境中需要,而其他属性则非常常见,它们几乎(或者应该)出现在每个Apple平台的项目中。本章的其余部分将讨论其中的一些属性,包括上面例子中使用的属性。

22.2. 应用程序包

macOS的应用程序包结构与iOS、tvOS和watchOS的不同。macOS结构中不同类别的文件在不同的子目录中,看起来像以下结构(应用程序可能只有一些子目录显示):

相比之下,iOS、tvOS和watchOS的目录结构是扁平的,几乎没有定义结构:

当构建一个app bundle时,CMake在某种程度上抽象了这些结构上的差异,不管这个bundle是为macOS、iOS、tvOS还是watchOS构建的,至少有一些事情可以用相同的方式处理。然而,开发人员应该意识到,直到最近的CMake版本(资源的处理是一个特定的例子),这种抽象才能正确实现,所以强烈建议使用最新的CMake版本。

通过向add_executable()添加MACOSX_BUNDLE关键字,会将应用程序识别为一个bundle:

add_executable(myApp MACOSX_BUNDLE ...)

这将MACOSX_BUNDLE目标属性设置为TRUE,非Apple平台会直接忽略该属性。一个项目可以将CMAKE_MACOSX_BUNDLE变量设置为TRUE,并且所有随后定义可执行目标具有MACOSX_BUNDLE属性,可以说是清楚的描述了在add_executable()中使用MACOSX_BUNDLE关键字的特点(项目通常定义了一小部分应用程序包,通常只有一个)。

MACOSX_BUNDLE不仅适用于macOS,还适用于iOS、tvOS和watchOS。关键字先于非桌面Apple平台,因此有桌面专用的名称。而不是为其他平台创建新的关键字,现有关键字的使用扩大到覆盖所有Apple平台。这种扩展特定于OSX的关键字和变量,可以覆盖所有Apple平台的模式。在其他一些情况下也可以看到,但请注意,这种模式并不适用于所有与OSX相关的变量和属性。在本章中,我们将重点介绍这一点。

每个应用程序包至少有一个Info.plist文件和一个可执行文件(MyApp在上面的目录结构例子)。默认情况下,CMake将从一个模板文件中提供Info.plist文件。然而,在大多数情况下,项目希望提供自己这个文件,这样他们就可以完全控制应用程序的配置。当应用程序使用故事板或界面构建器文件时,提供自定义Info.plist是非常必要的,这样相关的关键条目就会出现,比如NSMainStoryboardFile。MACOSX_BUNDLE_INFO_PLIST目标属性可以设置为用作Info.plist的文件模板(适用于所有苹果平台,不只是macOS)。默认模板文件名为MacOSXBundleInfo.plist.in,并且可以在CMake的模块目录中找到。它可以作为定制模板的起点

不管目标是否使用默认Info.plist模板,CMake会将模板文件复制到app bundle中,并在此过程中执行一些特定的替换。模板文件中,如果XXX是下表中的属性之一,则表单${XXX}的任何内容都将使用XXX目标属性的值所替代。这些属性会在默认的Info.plist中映射特定键值。如果项目提供了自己的模板文件并使用了这些变量,通常应该遵循相同的映射方式。

Apple不再将CFBundleLongVersionString作为Info.plist的键值,所以项目可以选择不提供它。文档还声明NSHumanReadableCopyright已经取代了CFBundleGetInfoString,并且CFBundleIconFile已被弃用,建议使用CFBundleIconFiles或CFBundleIcons代替。如果没有设置其他选项,仍然使用CFBundleIconFile。

如果定义了多个app目标,项目可以设置与上表中的属性名称完全相同的变量,这些变量用于初始化目标属性。注意,这不同于通常的CMake约定,即在变量作为默认值的目标属性之前具有CMAKE_…前缀。

当项目提供自己的 Info.plist模板文件,不需要使用上面的目标属性。而且,硬编码值是有效的。但请注意,CFBundleVersion和CFBundleShortVersionString可能需要从CMakeLists.txt文件中指定的版本信息,因此通过MACOSX_BUNDLE_BUNDLE_VERSION和MACOSX_BUNDLE_SHORT_VERSION_STRING替换设置它们仍然是最方便的方法。随着时间的推移,Apple对版本号的要求也在不断变化,现在基本上是强制性的major.minor.patch格式(有些例外)。下面展示了一种提供满足Apple要求的版本号的映射:

add_executable(myApp MACOSX_BUNDLE ...)
set_target_properties(myApp PROPERTIES
 MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}${BUILD_SUFFIX}"
 MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
)

上面的例子中,BUILD_SUFFIX在最终将是一个空字符串,或者可以是一个或多个字母后跟一个范围在1-255间的数字。示例后缀可能是用于alpha版本的a17,或用于第二个候选版本的rc2等等。一个Info.plist文件将用于进一步包含使用的这些属性。

用合适的Info.plist文件后,可以将注意力转向要编译和链接到包中的源文件。通常除了C/C++源文件,Apple平台还支持Objective C/C++源文件。通常具有.m或.mm文件后缀,可以在add_executable()和target_sources()命令中作为源文件列出,就像普通的C/C++文件一样。大多数CMake的生成器将识别这些文件后缀并编译文件(不仅仅是Xcode生成器)。

Apple平台特有的另一组源文件是用于定义用户界面的源文件。故事板或接口构建器文件类似于源文件,但需要一些额外的处理来将它们编译为资源,并将编译后的结果放在app bundle中适当的位置。Xcode生成器实现了这种自动编译并将其复制到适当的位置,所以当应用程序包中有这些文件时,不推荐使用Makefile或Ninja生成器。故事板和接口构建器源需要作为add_executable()或target_sources()中的源列出。为了将它们自动编译并复制到bundle中的适当位置,还需要在RESOURCE目标属性中列出。例如:

set(uiFiles
 Base.lproj/Main.storyboard
 Base.lproj/LaunchScreen.storyboard
)

add_executable(MyApp MACOSX_BUNDLE
 AppDelegate.m
 AppDelegate.h
 ViewController.m
 ViewController.h
 main.m
 ${uiFiles}
)

set_target_properties(MyApp PROPERTIES
 RESOURCE "${uiFiles}"
 MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist"
)

注意使用uiFiles变量处理接口构建器文件的方式。这个变量值可以在add_executable()的源列表中不加引号使用。这使得接口构建器文件仅作为源文件列表中的其他项出现。另一方面,RESOURCE目标属性持有一个值,该值应该是一个以分号分隔的列表。因此,RESOURCE属性要求引用uiFiles变量的值,而add_executable()不引用。

上面的例子中,Info.plist文件将包含键NSMainStoryboardFile,、NSMainNibFile或UIMainStoryboardFile(关于这些键的含义和适当使用的细节,请参阅Apple官方文档)。这样的信息会告诉操作系统在启动应用程序时使用哪个UI元素。一个简单的Info.plist看起来像以下这样:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>CFBundleDevelopmentRegion</key>
 <string>en</string>
 <key>CFBundleExecutable</key>
 <string>$(EXECUTABLE_NAME)</string>
 <key>CFBundleIconFile</key>
 <string>${MACOSX_BUNDLE_ICON_FILE}</string>
 <key>CFBundleIdentifier</key>
 <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
 <key>CFBundleInfoDictionaryVersion</key>
 <string>6.0</string>
 <key>CFBundleName</key>
 <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
 <key>CFBundlePackageType</key>
 <string>APPL</string>
 <key>CFBundleShortVersionString</key>
 <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
 <key>CFBundleVersion</key>
 <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
 <key>LSMinimumSystemVersion</key>
 <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
 <key>NSHumanReadableCopyright</key>
 <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
 <key>NSMainStoryboardFile</key>
 <string>Main</string>
 <key>NSPrincipalClass</key>
 <string>NSApplication</string>
</dict>
</plist>

在上面的例子中,NSMainStoryboardFile字段的值是Main,它指定Base.lproj/Main.storyboard UI将在应用程序启动时使用。还要注意一些字段值是如何使用${}语法作为CMake变量提供的,而CFBundleExecutable和LSMinimumSystemVersion是使用Xcode变量替换$()提供的。这两个字段将由Xcode本身根据项目文件和正在构建的方案中提供的其他信息填充。LSMinimumSystemVersion的值将来自macOS应用的CMAKE_OSX_DEPLOYMENT_TARGET变量,或为iOS目标属性设置的XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET变量(注意,CMake 3.11之后可以使用CMAKE_OSX_DEPLOYMENT_TARGET应用于所有平台)。项目可以直接硬编码一个Info.plist 文件,如果这样更方便的话。

需要包含在应用程序包的通常不是资源文件,可以使用另一种机制。这些文件仍然作为目标源列出,但不是将它们包含在资源目标属性中,而是将每个源的MACOSX_PACKAGE_LOCATION属性设置为它应该在bundle中(复制到)的位置。这些路径应该相对于bundle顶部。这可将文件复制到非资源位置,或完全控制不需要编译的资源文件目标。也可以列出一个目录作为源,并设置它的MACOSX_PACKAGE_LOCATION将目录及其内容复制到包中,但是CMake是否正式支持这一点没有文档说明(目录通常不能作为源列出)。下面的例子可以演示这些行为。

add_executable(MyApp MACOSX_BUNDLE
 AppDelegate.m
 AppDelegate.h
 ViewController.m
 ViewController.h
 main.m
 sharedConfig.xml
 nestedResource.dat
 someDir # Directory, CMake may not formally support this
)

set_source_files_properties(sharedConfig.xml PROPERTIES
 MACOSX_PACKAGE_LOCATION SharedSupport/config
)
set_source_files_properties(nestedResource.dat PROPERTIES
 MACOSX_PACKAGE_LOCATION Resources/private/other
)

# Works, but might not be formally supported
set_source_files_properties(someDir PROPERTIES
 MACOSX_PACKAGE_LOCATION Resources/lotsOfThings
)

当将MACOSX_PACKAGE_LOCATION设置为从资源起始路径,并且目标为iOS构建时,会出现一种特殊情况。因为iOS应用程序包使用扁平化的结构,CMake将剥离路径中的资源部分。CMake 3.9之前,这种行为的实现是不正确的,并不是总能将文件放到想要的位置。

22.3. 框架

框架与应用程序包有一些相似之处,也有自己的特性。框架包含一个主库,但与应用程序包不同,macOS上可能有多个版本的库。除了资源之外,框架还支持头文件,在macOS中,资源和头文件都是特定于版本。macOS框架结构的典型示例如下所示:

框架的顶层总是有一个以.framework结尾的文件,通常该顶层目录中唯一的非符号链接是Versions子目录(伞形框架是例外,不在这里的框架支持范围)。该级别的其他内容通常是指向当前版本子目录的符号链接。

版本目录中,库的每个版本都有自己的子目录,其名称为版本。大多数情况下,这些目录名只是A、B等。数字版本使用的是另一种常见约定,它与动态库在其他平台上的版本控制方式结合在一起。不管版本控制的风格如何,名为Current的符号链接都指向最新的版本,它的作用类似于框架的默认版本。每个版本都应该有一个资源目录,其中至少包含一个Info.plist文件,提供关于特定版本的配置细节(后面将进一步讨论)。还会有一个库(通常是动态库,也可以是静态的),通常还有头目录和私有头文件子目录。

相比之下,iOS、tvOS和watchOS上的结构是扁平的,通常不支持多版本:

CMake支持创建框架(macOS只支持单一版本),并提供处理版本信息的特性。也支持 Info.plist与应用程序包使用的方法类似。第一步是按照常规的方式定义一个库,然后通过设置框架属性标记为框架。大多数框架定义为动态库。在CMake 3.8中,静态库也可以构建为框架。框架属性在非Apple平台上可以忽略。仅对macOS而言,框架版本可以使用FRAMEWORK_VERSION属性指定,或者设置为默认版本。非macOS平台将忽略FRAMEWORK_VERSION属性,如果设置了,产出文件结构同样是扁平的,Xcode会为平台创建框架产生未版本化框架结构。

add_library(MyFramework SHARED foo.cpp)
set_target_properties(MyFramework PROPERTIES
 FRAMEWORK TRUE
 FRAMEWORK_VERSION 5
)

Info.plist文件模板指定的方式与应用程序包相同,除了属性为MACOSX_FRAMEWORK_INFO_PLIST(支持所有Apple平台,不仅仅是macOS):

set_target_properties(MyFramework PROPERTIES
 MACOSX_FRAMEWORK_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist"
)

至于应用程序包,如果一个框架没有显式提供Info.plist文件,该文件会自动生成一个默认的。项目是否提供自己Info.plist或依赖于默认生成的,CMake在将应用程序捆绑复制到框架中时,执行类似的替换。以下属性将替换为Info.plist文件的引用(信息期望的关联键值,也在Info.plist文件中列出):

与应用程序包不同,默认的Info.plist在很多情况下,可能已经够用了,所以项目通常只需要设置上述四个属性,然后让CMake提供适当的Info.plist文件。框架通常包含与框架库相关联的头文件。这允许将框架视为自包含的包,其他软件可以根据它进行构建。

框架头文件划分为公共和私有组,只有公共头文件可以直接包含或导入。私有头文件通常需要作为公共头的内部实现细节,而框架通常根本不包含任何私有头文件。CMake支持使用PUBLIC_HEADER和PRIVATE_HEADER属性指定公共和私有头文件。这两个属性都包含一个头文件列表,并且所有提到的文件也必须作为目标的源显式列出。PUBLIC_HEADER中列出的文件将复制到框架的Headers目录中,路径将会删除,而在PRIVATE_HEADER中列出的文件将复制到PrivateHeaders目录中,路径也将会删除。如果需要保留路径,则不能使用这些目标属性,而必须通过使用MACOSX_PACKAGE_LOCATION来添加头文件。

add_library(MyFramework SHARED
 foo.cpp
 foo.h
 foo_privateA.h
 nested/foo_privateB.h
)
set_target_properties(MyFramework PROPERTIES
 FRAMEWORK TRUE
 PUBLIC_HEADER foo.h
 PRIVATE_HEADER "foo_privateA.h;nested/foo_privateB.h"
)

上面的例子会在macOS上产生以下的目录结构:

在iOS上的相同例子会产生一个更扁平的结构:

注意,在非Apple平台上安装目标时,还要使用PUBLIC_HEADER和PRIVATE_HEADER属性。第25.2.3节“Apple特定的目标”中有更详细的介绍。

22.4. 可加载包

除了应用程序包和框架,Apple还支持macOS的可加载包。它们通常用作插件,或提供在运行时可能支持的可选特性。可加载包的结构与应用程序包的结构相同,但顶层目录通常具有.bundle或.plugin扩展名。CMake通过MODULE库类型和BUNDLE目标属性,支持创建可加载的BUNDLE。默认情况下,可加载包将放入扩展包中,也可以用BUNDLE_EXTENSION目标属性覆盖它。

add_library(MyBundle MODULE ...)
set_target_properties(MyBundle PROPERTIES
 BUNDLE TRUE
 BUNDLE_EXTENSION plugin
)

与应用程序包相关的所有属性,也可以用于可加载包。

22.5. 构建设置

当为Apple平台构建一个项目时,许多属性一起来定义为什么平台构建,并为平台指定了最低的版本。不像其他CMake生成器类型,Xcode生成器允许开发人员在构建时设置时间,这对于新手和有经验的CMake用户来说,都是一个比较难以正确处理的特性。

对于单配置生成器,目标设备在配置时是已知的,但是对于Xcode同时支持设备和模拟器。此外,其中一些设备具有多个架构。在iOS平台下,这可能意味着多达五种不同的目标平台组合。不同版本的Xcode附带不同版本的iOS SDK,一些开发人员甚至可能将旧的SDK移植到新的Xcode版本中。为了允许开发者在构建时在不同的目标设备和SDK之间切换,CMake项目必须正确地指定这些细节。

iOS、tvOS和watchOS SDK的选择,是许多在线示例中相当复杂的一个领域,经常让开发者重新运行CMake,从而在设备和模拟器构建之间进行切换。然而,对于最新版本的CMake和Xcode,指定SDK是一个非常简单的步骤,只需将CMAKE_OSX_SYSROOT变量设置为某个iphoneos、appletvos或watchos即可。然后,Xcode将为该平台选择最新的SDK,它将允许在设备和模拟器构建之间切换,而无需重新运行CMake。此外,Xcode会根据所选的SDK自动填充架构支持,因此项目不需要添加任何额外的逻辑来指定架构。这让开发人员不需要重新运行CMake,就能对他们想要构建的内容进行最大的控制。可用的SDK可以通过运行以下命令获得:

xcodebuild -showsdks

由于CMake执行编译器测试的方式,当针对Apple平台而不是macOS时,需要设置更多的缓存变量。CMake 3.12之前,代码签名会干扰编译器测试,这些测试并不总是使用正确的类型(例如,不会尝试创建一个包,否则它们应该创建一个包)。要解决这些问题需要设置CMAKE_MACOSX_BUNDLE和CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED变量。为了让编译器测试选择是否正确,CMAKE_OSX_SYSROOT, CMAKE_MACOSX_BUNDLE和CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED需要在配置阶段的早期设置。最好使用工具链文件,类似这样:

set(CMAKE_MACOSX_BUNDLE YES)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED NO)
set(CMAKE_OSX_SYSROOT iphoneos)

默认情况下,项目将其部署目标设置为SDK或主机系统支持的部署目标。通常不想这样,因为项目通常希望能与特定的最小OS版本兼容。对于macOS, OSX_DEPLOYMENT_TARGET属性将控制的目标支持的最低macOS版本。可以使用CMAKE_OSX_DEPLOYMENT_TARGET变量为这个属性指定默认值,不过需要在第一个使用project()命令之前设置该值。此外,如果CMakeLists.txt文件中直接设置了CMAKE_OSX_DEPLOYMENT_TARGET,那么需要一个缓存变量,否则project()命令会在执行编译器检查时将其值覆盖。另一种策略是使用一个工具链文件并在其中设置CMAKE_OSX_DEPLOYMENT_TARGET,但是在macOS构建中使用工具链文件相当少见,这个变量应该由项目来定义。还有一种方法是在cmake命令行上设置CMAKE_OSX_DEPLOYMENT_TARGET的缓存变量,但这也是让开发人员来设置它,并保证提供正确的值,从而降低了使用这种方式的积极性。

CMake 3.11之前,当目标平台不是macOS时,CMAKE_OSX_DEPLOYMENT_TARGET变量没有效果。要在CMake 3.11之前控制iOS的最小部署版本,可以使用XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET属性。这个目标属性的默认值可以使用CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET变量来设置,与macOS不同的是,这个变量可以在第一次project()调用之后设置。从CMake 3.11开始,CMAKE_OSX_DEPLOYMENT_TARGET可以用来定义任何Apple平台的最小部署版本。如果目标同时定义了OSX_DEPLOYMENT_TARGET和XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET目标属性,那么在使用Xcode生成器时,后者优先。

# Set the deployment target for macOS with any CMake version, or all Apple
# platforms when using CMake 3.11 or later
cmake_minimum_required(VERSION 3.9)

# Must be before first call to project()
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.11)
project(AppleProject)
# Set the deployment target for iOS with any CMake version.

# Set defaults for all targets added hereafter within this directory scope or below
set(CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET 9.0)

# Build an app with the deployment target explicitly set
add_executable(MyApp MACOSX_BUNDLE ...)
set_target_properties(MyApp PROPERTIES XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET 10.0)

在iOS的下,项目还可能希望指定目标设备系列。Apple具有TARGETED_DEVICE_FAMILY属性,可用于指定设备系列。对于iOS系统,有效值为iPhone的1(技术上也包括iPod touch)或iPad的2。如果应用程序同时支持iPhone和iPad,可以用逗号分隔指定两个值。如果未设置此属性,则默认为1。Xcode会使用这个值在应用程序的信息中添加一个UIDeviceFamily条目在Info.plist中,所以避免在Info.plist中设置这个条目

# An app that supports only iPad
add_executable(MyiPadApp MACOSX_BUNDLE ...)
set_target_properties(MyiPadApp PROPERTIES
 XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY 2
)

# An app that supports both iPhone and iPad
add_executable(RunEverywhereApp MACOSX_BUNDLE ...)
set_target_properties(RunEverywhereApp PROPERTIES
 XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY 1,2
)

以上介绍了大多数Apple项目需要定义与构建的主要设置。对于简单的未签名macOS应用就足够了,但是大多数项目需要进一步的配置签名来构建产品。

22.6. 代码签名

在过去几个的Xcode版本中,与代码签名相关的Xcode功能有了很大的发展。最近,代码签名和配置的自动化管理使得使用CMake构建签名应用程序变得更加容易,但是仍然需要理解签名过程,从而设置适当的属性和变量。在Xcode 8中,自动签名和配置的工作方式发生了显著的变化,留下了许多演示Xcode 7和更早版本方法的示例,不再是最佳实践。本章主要介绍当前的自动签名和配置过程。

为了使自动签名和配置,应用程序必须有一个有效的包ID和另外两个需要提供的关键信息:开发团队ID和代码签名。这些属性需要指定为Xcode属性,通过属性或CMake变量在单个目标上设置,为相应的目标属性指定默认值。由于这两个数量在整个构建过程中通常需要相同,因此建议设置为项目顶部的变量。

应该将XCODE_ATTRIBUTE_DEVELOPMENT_TEAM目属性或相应的CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM变量设置为开发团队ID,这是通常是一个约10个字符的短字符串。最方便的方法通常是在最顶层的CMakeLists.txt文件中的第一个project()命令之后设置CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM变量。根据项目的不同,开发人员可能需要更改此值的能力。例如,如果项目是商业软件,并由一名员工构建,那么团队ID很可能永远不会改变,而对公众开放的开源项目,肯定是由开发人员使用自己的开发团队ID构建的。团队ID应该永远不会改变的情况下,只需要将CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM定义为一个普通的变量是足够了,但是当开发人员需要更改它时,应该将它定义为一个缓存变量,这样就可以给出一个默认值,开发人员可以在不编辑CMakeLists.txt文件的情况下进行覆盖。

类似地,XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY属性或相应的CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY变量可以指定签名标识。在Xcode 8中,这应该始终是字符串Mac Developer for macOS applications或iPhone Developer for iOS, tvOS或watchOS applications。这些值将引导Xcode为指定的开发团队选择最合适的签名标识。特殊情况下,签名标识可以设置为一个字符串,该字符串专门标识开发人员密钥链中的特定代码签名标识,但是开发人员有责任确保该标识属于指定的开发团队。

下面的例子展示了CMakeLists.txt是如何为macOS应用程序构建的,该应用程序允许开发者更改团队ID和签名身份:

cmake_minimum_required(VERSION 3.9)
project(macOSexample)

set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "ABC12345DE" CACHE STRING "")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Mac Developer" CACHE STRING "")

对于一个iOS应用程序,团队ID预计不会改变,但开发者可能仍然想要控制签名身份,只有身份需要缓存变量:

cmake_minimum_required(VERSION 3.9)
project(iOSexample)

set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "ABC12345DE")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer" CACHE STRING "")

当如上所述配置时,Xcode将自动选择适当的配置文件。如果合适的配置文件不存在,Xcode IDE可以自动创建一个(该功能是IDE的一个特性,不能用命令行构建)。这种自动配置是对早期Xcode版本的一个重大改进,早期版本中,配置文件必须通过开发人员手动创建。

在前面的小节中,CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED没有在iOS的工具链文件中设置,但该变量在CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY设置时忽略。移动代码签名细节在工具链文件中要避免CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY的设置,但请注意,这将意味着try-compile测试会作为目的执行的第一个project()命令的一部分,将需要一个有效的配置文件,所以需要有效的包ID。通常不希望在团队帐户中创建这样的包ID和配置文件。try-compile测试不需要执行代码签名,因此不应该使用工具链文件来启用全局签名。

Apple应用程序也有一组相关的权利。操作系统的这些控制功能将允许应用程序使用,比如Siri,推送通知等功能。在Xcode IDE的项目设置中,用户可以转到app目标的Capabilities选项卡,打开所需的功能。启用相关的授权会自动生成到plist文件中,目标会链接到任何需要的框架,并将该功能添加到团队账户中应用的ID中。对于CMake生成的项目,这个Capabilities选项卡将会绕过。如果默认的权利不够,CMake项目应该直接提供它自己的plist文件。项目本身必须处理所有需要的框架链接,并且app ID不做任何更改。实际上,对于许多应用程序来说,这些限制相当温和,只是框架链接出现了一些问题。

通过将XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS目标属性指定一个合适的权限文件的名称,如下所示:

set_target_properties(myApp PROPERTIES
 XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS
 ${CMAKE_CURRENT_LIST_DIR}/myApp.entitlements
)

举个例子,将Siri添加到默认权限中的权限文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>com.apple.developer.siri</key>
 <true/>
</dict>
</plist>

如果应用程序链接到共享框架,而这些框架也是由项目构建的,不要为这些框架启用代码签名。推荐的方法是通过Xcode的Embed frameworks构建阶段,启用Code sign on copy选项,但是不幸的是CMake并不直接支持这个(关于CMake支持框架的限制的讨论,请参阅22.8节“限制”)。

22.7. 创建和导出归档

为了通过app Store、企业发布门户或临时发布来发布应用程序,首先需要创建一个归档文件。虽然CMake不为创建这样的归档文件创建构建目标,但xcodebuild工具可以用CMake生成的项目来完成该任务。归档构建操作只需要几个选项,就可以构建发布所需的目标并创建归档。有几种方法可以指定要归档的内容,但一个相当简单的方法是命名项目、方案和输出的名称:

xcodebuild archive \
 -project MyProject.xcodeproj \
 -scheme MyApp \
 -archivePath MyApp.xcarchive

使用Xcode生成器时,CMake会创建.xcodeproj文件。CMake 3.9之前,用户必须在Xcode IDE中加载项目来创建构建方案。这对于无法访问IDE的持续集成构建来说是一个问题,为了解决这个问题,CMake 3.9引入了CMAKE_XCODE_GENERATE_SCHEME变量作为一个实验特性。当这个变量设置为true时,CMake还会构建生成模式文件,然后允许为-scheme选项指定app目标的名称,归档任务就有了所有需要的信息。上面的命令将为所有支持的架构构建发布配置的MyApp目标,签署它(仍然使用开发人员签名身份),然后创建一个名为MyApp的归档文件,存档在当前目录中。

如果没有适当地设置某些安装属性,归档可能会失败。Apple developer文档包含一些故障诊断指南,这些指南有助于解决更常见的问题,其中一些相关的情况是确保目标类型正确地设置了目标的INSTALL_PATH和SKIP_INSTALL属性。在CMake项目中,目标的XCODE_ATTRIBUTE_SKIP_INSTALL属性对于库和嵌入式框架必须设置为YES,对于应用程序必须设置为NO。将其设置为NO的地方,还必须提供XCODE_ATTRIBUTE_INSTALL_PATH,通常应该为$(LOCAL_APPS_DIR)。如果不遵循这个建议,归档步骤通常会生成一个通用归档,而不是应用程序归档。

# Apps must have install step enabled
set_target_properties(macOSApp PROPERTIES
 XCODE_ATTRIBUTE_SKIP_INSTALL NO
 XCODE_ATTRIBUTE_INSTALL_PATH "$(LOCAL_APPS_DIR)"
)

创建应用程序存档之后,需要将其导出以便分发。这是通过对xcodebuild工具的另一次调用实现,这次提供了刚刚创建的存档、一个选项plist文件和写入输出的位置。该命令的基本形式如下:

xcodebuild -exportArchive \
 -archivePath myApp.xcarchive \
 -exportOptionsPlist exportOptions.plist \
 -exportPath Products

-archivepath选项指向由前面的xcodebuild调用创建的归档文件,而-exportPath选项指定创建最终输出文件的目录。关于导出步骤的其他内容,由-exportOptionsPlist指向的选项plist文件定义。支持的全部密钥可以在工具的帮助文档(xcodebuild -help)中找到,简单的plist文件可能是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>method</key>
 <string>app-store</string>
</dict>
</plist>

该方法指定了预期的发布渠道,并期望是以下渠道之一:

  • app-store

  • ad-hoc

  • enterprise

  • development

  • developer-id

  • package

默认情况下是development,但更有可能的主要方法是app-store、enterprise或ad-hoc。导出归档文件时,该工具将重新对应用程序进行签名,并根据所选方法选择合适的分发签名标识。开发人员应该已经创建/下载了适当的发行版签名身份和配置文件(很容易在Xcode IDE中完成,但也可以为持续集成服务器等手工完成)。

22.8. 限制

至少在3.11版本之前,CMake对框架的支持有一些缺点。这在很大程度上是由于框架(甚至是常规库)合并到Xcode项目中,而不是在库构建阶段定义的链接文件,CMake将链接直接硬编码到另一个链接器标志的目标属性中。这与CMake在使用其他生成器类型时链接库和框架的方式相匹配,但是这些生成器不具有Xcode所具有的附加框架处理和代码签名特性。CMake确实会尝试检测它要链接的库是否是一个框架,使用-framework someLib而不是-lsomeLib或/path/to/someLib。对于那些识别为框架的,在链接器命令行中使用dylib,但是这并不能让Xcode了解除了链接之外的任何框架。

对于静态框架,CMake的实现在很大程度上仍然可以工作,但是对于动态框架,则存在一些问题。通过将信息直接嵌入到链接器标志中,Xcode无法完全了解这个框架,在创建应用程序存档或执行代码签名时也无法正确处理它。因为没有定义关联的复制文件构建阶段,所以框架不会与目标链接一起安装,而嵌入式框架的代码签名通常是在复制期间执行的。

项目可用的选择受到当前CMake行为的限制。可以避免使用任何非系统动态框架,但这有明显的缺点。项目可能需要开发人员在运行CMake手动添加框架后执行一些手工更改,这显然是脆弱的,并排除了在无头文件环境中构建,例如在持续集成系统中。一个更可行的方法是在CMake运行后定义一个脚本来修改Xcode项目文件,或者在CMake项目中定义自定义命令或后构建步骤来模拟Xcode项目在了解嵌入式框架的情况下通常会做的事情。这些选项没有一个是特别令人满意的,而且所有这些都违背了CMake的本质。即使是自定义脚本或后构建步骤的方法也很有可能在未来与CMake的改进发生冲突,在那里这些缺点可能最终会解决。

CMake对权利的处理也相当简单。它不如Xcode IDE在Capabilities target properties选项卡中提供的自动化处理方式,在这个选项卡中,打开一个特定的功能还需要添加所需的框架,并自动更新应用ID信息。CMake的支持仍然允许指定所有权利,但整个过程完全是手动的。这个项目负责以原始的plist格式定义权利,而且还必须手动链接到任何框架,这一点CMake没有很好地处理。尽管如此,在没有变通方法或让步骤变得过于繁琐的情况下,权利的处理至少是可能的。授权所需要的任何框架都是系统提供的,因此它们不需要嵌入到应用程序中,从而避免了大多数框架的处理缺陷。

对于不那么明显的CMake行为,使用Xcode生成器,当CMake编写Xcode项目时,会创建一个名为ZERO_CHECK的实用程序目标。项目中的大多数其他目标都依赖于ZERO_CHECK,它的唯一目的是在执行剩余构建之前确定是否需要重新运行CMake。不幸的是,如果由ZERO_CHECK重新运行CMake,该构建的其余部分仍然使用旧的项目信息,这可能会导致错误,因为目标是用“过时的设置”构建的。重建一次应该确保正确的目标重建,但这个目标很容易忽略。开发人员可能需要显式地构建ZERO_CHECK目标,或者修改后重新运行CMake第一CMakeLists.txt文件,或让CMake会自动重新运行,或简单地构建两次。

如果项目包含对project()命令的多个调用,则存在一个与ZERO_CHECK相关的微妙问题。第二个或以后的project()调用下面定义的目标,可能没有正确地设置对ZERO_CHECK的依赖关系。可以将CMAKE_XCODE_GENERATE_TOP_LEVEL_PROJECT_ONLY变量设置为TRUE来避免这个问题,它还会加速CMake阶段,而CMake 3.11只添加了对该变量的支持。

22.9. 推荐

CMake能够处理针对Apple平台的项目,但需要仔细考虑其局限性。如果必须对应用程序进行签名,那么使用任何非系统共享框架都需要手动编写脚本和自定义构建步骤,以获得所需的最终结果。如果不需要动态框架,那么CMake的功能应该足够了,只要使用Xcode生成器,通常可以不费太多力气地自动完成这个过程。其他生成器(如Makefiles或Ninja)对于构建未签名的macOS应用程序没什么问题,但对于其他平台或签名的应用程序,这些生成器通常缺乏一些能够轻松生成用于发布的最终包所需的特性。除了未签名macOS应用程序开发,强烈建议使用Xcode生成器进行Apple平台的开发。

当谈到在Apple平台上使用CMake时,在线教程和示例中的很多信息相对来说已经过时了。特别是,在iOS中看到相当复杂的工具链文件是很常见的,但是这些工具链文件中包含的大部分逻辑不是没有必要,就是应该转移到项目本身。对于Xcode 8或更高版本,如果可能项目应该致力于利用自动签名和配置,这极大地简化了签名过程。为最小的工具链文件只需要设置CMAKE_MACOSX_BUNDLE、CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED和CMAKE_OSX_SYSROOT就可以得到一个支持代码签名和分发的工作构建。其他与Xcode项目设置、特定于设备或平台的配置等相关的逻辑应该放在项目中。

教程和示例经常做的一件事是通过设置CMAKE_OSX_ARCHITECTURES变量来指定目标架构。针对iOS、watchOS或tvOS的项目中使用Xcode生成器时,这是不受欢迎的,因为这会妨碍开发人员在设备和模拟器构建之间自由切换。当在Xcode IDE中工作或在命令行构建时,目标架构是可选择的。因此,项目通常应该避免设置CMAKE_OSX_ARCHITECTURES,而是让Xcode根据所选的SDK提供标准的框架集。SDK由CMAKE_OSX_SYSROOT确定,但当选择设备SDK时,Xcode能够识别匹配的模拟器。例如,通过将CMAKE_OSX_SYSROOT设置为类似iphoneos的东西,开发人员就可以使用设备和模拟器构建。此外,虽然可以将SDK版本指定为CMAKE_OSX_SYSROOT的值的一部分,但通常没有理由这样做。更有可能的是部署目标应设置通过MACOSX_DEPLOYMENT_TARGET或XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET设置SDK的版本。最终决定应用程序是否能够在目标上运行的是部署目标,这与用于构建它的SDK无关(当然,假设SDK支持该部署目标)。由于默认情况下将使用最新的SDK可用版本,因此要求构建使用特定的SDK版本几乎不会有什么好处,甚至可能是有害的。当指定了一个特定的SDK版本时,并不是所有的开发人员机器都可以使用它,因为这将取决于使用的是哪个Xcode版本。一些开发人员将旧的SDK移植到新的Xcode版本中,试图解决这个问题,但这是没必要的。

一些示例还将CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH设置为TRUE,以便只在Xcode IDE中构建当前选择的架构。同样,这是一个通常应该由开发人员在构建时决定,而不是由CMake强制执行的。有时,它也用于脚本构建,因为已知只应该构建一个特定的平台,架构可以指定为命令行选项。

如果项目包含的目标链接到不提供胖二进制文件的库或框架(即它们只针对单一目标平台构建),那么将构建限制为一个架构是有意义的。这种情况下,由于这些库或框架只支持单个平台,因此只能为该平台构建项目。类似地,使用find_library()或find_package()(下一章将介绍)时,这些命令本质上假设是为单个平台构建的,因此不尝试以支持在多个目标平台之间切换的方式,对平台进行定义。

有些项目可能会选择使用CMake的安装功能,而不是假设Xcode在构建时为一个可分发包完成所有需要的工作。对于这种情况,可以将IOS_INSTALL_COMBINED目标属性设置为TRUE,以构建目标的设备和模拟器版本,并在安装步骤中将它们合并为单个胖二进制文件。如果由于某些原因不希望使用Xcode生成器,或者项目的结构遵循了CMake更不依赖于平台的构建-安装模型,那么这可能是另一种选择。

Xcode的构建输出可能非常冗长,因此开发人员可能会选择使用像xcpretty这样的工具来隐藏大部分细节(在脚本构建中更常见,以减少日志大小)。不幸的是,这个特殊的工具通常会隐藏任何CMake的自定义后构建步骤的输出,即使这些自定义步骤会导致构建错误。因此,当这类自定义步骤失败时,很难找出失败的原因,因此建议避免使用此工具,或者至少让它在脚本中易于关闭,以帮助诊断构建问题。xcodebuild命令的-quiet选项可以在不隐藏警告或错误的情况下减少日志输出。

Last updated