第16章:目标类型

CMake支持各种各样的目标类型,而不仅仅是第4章中介绍的构建简单目标的简单可执行文件和库。可以定义不同的目标类型,作为对其他实体目标的引用,而不是自己构建。它们可以用来收集传递属性和依赖关系,而无需生成二进制文件,或者可以是一种库,即对象文件的集合,而不是传统的静态或共享库。可以将许多东西抽象为目标,以隐藏平台差异、文件系统中的位置、文件名等的复杂性。本章涵盖了所有这些不同的目标类型,并讨论如何使用它们。

另一类目标是实用程序或自定义目标。这些可用于执行任意命令和定义自定义构建规则,从而允许项目实现所需的任何行为。它们有自己专用的命令和独特的行为,将在下一章深入介绍。

16.1. 可执行类型

add_executable()命令不仅仅是第4章中介绍的可用于构建简单目标。还有另外两种形式,可用于定义引用其他内容的可执行目标。所支持的方式包括:

add_executable(targetName [WIN32] [MACOSX_BUNDLE]
 [EXCLUDE_FROM_ALL]
 source1 [source2 ...])
add_executable(targetName IMPORTED [GLOBAL])
add_executable(aliasName ALIAS targetName)

IMPORTED的方式可以为现有的可执行文件创建CMake目标,而不是由项目构建的目标。通过创建一个可执行文件的目标,项目的其他部分可以将它视为对待项目本身的构建(有一些限制)。最显著的好处是,可以用于CMake自动将目标名称替换为其在磁盘上位置的上下文,比如测试或自定义任务执行时(这两个内容将在后面的章节中介绍)。与常规目标的区别之一是,导入的目标不能安装,这个主题将在第25章中讨论。

定义导入的可执行目标时,需要先设置某些目标属性,然后才能使用。大多数导入目标的相关属性的名称都以IMPORTED开头,但是对于可执行文件来说,IMPORTEDLOCATION和`IMPORTED_LOCATION`是最重要的属性。需要导入的可执行文件的位置时,CMake会首先查看配置属性,只有当没有设置时,才会查看更通用的IMPORTED_LOCATION属性。通常,位置不需要特定的配置,因此通常只设置IMPORTED_LOCATION。

没有定义GLOBAL关键字时,导入的目标只会在当前目录的范围内可见,当添加了GLOBAL属性,则目标随处可见。不过,由项目构建的可执行目标总是全局的。造成这种情况的原因,以及目标可见度降低的相关影响,16.3节中会进一步讨论。

ALIAS目标只是在CMake中引用另一个目标的只读方式,并没有使用别名创建新的构建目标。别名只能指向真实的目标(即不支持别名的别名),并且它们不能安装或导出。CMake 3.11之前,导入的目标也不能混叠,CMake 3.11开始放宽了一些限制,允许混叠导入的目标,但只允许具有全局可见性的导入目标。

16.2. 库类型

add_library()命令也有许多不同的形式。第4章介绍的基本形式,大多数开发人员都熟悉的是,建立简单的目标通常可以用于定义库,可以用来定义对象库、静态库或动态库。该命令的扩展基本形式为:

add_library(targetName [STATIC | SHARED | MODULE | OBJECT]
 [EXCLUDE_FROM_ALL]
 source1 [source2 ...])

CMake 3.12之前,对象库不能像其他库类型那样链接(即不能与target_link_libraries()一起使用),需要使用$<TARGET_OBJECTS:objLib>生成器表达式作为另一个可执行文件或库目标的部分源。由于不能链接,因此不提供对作为对象/源添加的目标的传递依赖关系。这使得对象库用起来很不方便,因为头文件搜索路径、编译器定义等必须手动到添加依赖于它们的目标上。

CMake 3.12引入了一些特性,这些特性使对象库的行为更像其他类型的库,但也有一些注意事项。CMake 3.12中,对象库可以与target_link_libraries()一起使用,要么作为添加的目标(即命令的第一个参数),要么作为添加的库之一。但因为添加的是对象文件而不是实际的库,因为要防止对象文件多次添加到目标中,所以传递特性非常受限。一个简单的解释是,对象文件只添加到直接链接到对象库的目标中,而不是传递到其他对象库中。不过,对象库的使用确实可以像普通库那样传递。

开发人员会发现对象库更自然。然而,通常情况下,如果有选择的话,静态库在CMake项目中通常是更方便的选择。在依赖于CMake 3.12中对象库的扩展特性之前,请考虑静态库是否更合适。

和可执行文件一样,库也可以定义为导入的目标。打包过程中创建的配置文件或Find模块实现中大量使用了这些工具。他们不定义构建库的目标,而是作为外部提供的引用库(例如:已经存在的系统,是由当前CMake以外的项目的部分提供)。

add_library(targetName (STATIC | SHARED | MODULE | OBJECT | UNKNOWN)
 IMPORTED [GLOBAL]
)

库类型需要在targetName后给出。如果新目标将引用库的类型是已知的,则应该这样指定。这将允许CMake在各种情况下将导入的目标,可以视为命名类型的库目标。该类型只能设置为CMake 3.9或更高版本的OBJECT(该版本之前不支持导入的对象库)。如果库类型是未知的,UNKNOWN类型显式给出,在这种情况下,CMake只能使用库的完整路径。这意味着更少的检查,并且在Windows环境下构建时,不处理DLL导入库。

除了对象库之外,导入的目标所代表的文件系统的位置需要由IMPORTEDLOCATION和/或`IMPORTED_LOCATION属性来指定(即与导入的可执行文件相同)。Windows平台上,两个属性都应该设置:IMPORTED_LOCATION应持有DLL的位置,IMPORTED_IMPLIB应持有相关导入库的位置,通常是lib文件的位置(…_`的方式也可以设置这些属性,应该优先考虑)。对于对象库,必须将IMPORTED_OBJECTS属性设置为导入的目标的对象文件列表,而不是上面的那些位置属性。

导入库还支持许多其他目标属性,其中大多数属性可以由CMake自动设置。需要手动编写配置包的开发人员应该参考CMake参考文档,以了解其他可能的情况,和相关的IMPORTED_…目标属性。大多数项目会依赖CMake生成这样的文件,所以这样的操作时不多见的。

默认情况下,导入的库定义为本地目标,这意味着只在当前目录范围内可见。可以使用GLOBAL关键字使它们具有全局可见性。创建库时可能没有GLOBAL关键字,但随后会将其提升为全局可见性,这是16.3节中讨论的主题。

# Windows-specific example of imported library
add_library(myWindowsLib SHARED IMPORTED)
set_target_properties(myWindowsLib PROPERTIES
 IMPORTED_LOCATION /some/path/bin/foo.dll
 IMPORTED_IMPLIB /some/path/lib/foo.lib
)
# Assume FOO_LIB holds the location of the library but its type is unknown
add_library(mysteryLib UNKNOWN IMPORTED)
set_target_properties(mysteryLib PROPERTIES
 IMPORTED_LOCATION ${FOO_LIB}
)
# Imported object library, Windows example shown
add_library(myObjLib OBJECT IMPORTED)
set_target_properties(myObjLib PROPERTIES
 IMPORTED_OBJECTS /some/path/obj1.obj # These .obj files would be .o
 /some/path/obj2.obj # on most other platforms
)

# Regular executable target using imported object library.
# Platform differences are already handled by myObjLib.
add_executable(myExe $<TARGET_SOURCES:myObjLib>)

另一种形式的add_library()命令允许定义接口库。通常不表示物理库,主要收集应用于链接到它们的目标使用需求和依赖关系。使用它们的示例是仅用于纯头文件库,其中没有需要链接的物理库,但是头文件搜索路径、编译器定义等需要传递到使用头文件库的地方。

add_library(targetName INTERFACE [IMPORTED [GLOBAL]])

不同的target_…()命令都可以与INTERFACE关键字一起使用,以定义接口库将的使用需求。也可以直接使用setproperty()或set_target_properties()设置相关的`INTERFACE属性,但是使用target_…()`命令更安全、更容易。

add_library(myHeaderOnlyToolkit INTERFACE)
target_include_directories(myHeaderOnlyToolkit
 INTERFACE /some/path/include
)
target_compile_definitions(myHeaderOnlyToolkit
 INTERFACE COOL_FEATURE=1
 $<$<COMPILE_FEATURES:cxx_std_11>:HAVE_CXX11>
)
add_executable(myApp ...)
target_link_libraries(myApp PRIVATE myHeaderOnlyToolkit)

上面的示例中,myApp目标链接指向myHeaderOnlyToolkit接口库。当编译myApp源代码时,将使用/some/path/include作为头文件搜索路径,并在编译器命令行中提供一个编译时定义COOL_FEATURE=1。如果构建myApp目标时支持C++11,那就和定义了HAVE_CXX11一样。然后,myHeaderOnlyToolkit中的头文件可以使用这个符号来确定它们声明和定义的内容,而不是依赖C++提供的__cplusplus符号(__cplusplus的值对于编译器来说通常是不可靠的)。

接口库的另一个用途是为链接更大的库提供方便,其中封装了应该在集合中的库。例如:

# Regular library targets
add_library(algo_fast ...)
add_library(algo_accurate ...)
add_library(algo_beta ...)

# Convenience interface library
add_library(algo_all INTERFACE)
target_link_libraries(algo_all INTERFACE
 algo_fast
 algo_accurate
 $<$<BOOL:${ENABLE_ALGO_BETA}>:algo_beta>
)

# Other targets link to the interface library
# instead of each of the real libraries
add_executable(myApp ...)
target_link_libraries(myApp PRIVATE algo_all)

如果CMake选项变量ENABLE_ALGO_BETA为真,那么上面的库列表中将只包含algo_beta。其他目标简单地链接到algo_all, algo_beta的条件链接由接口库处理。这是一个使用接口库来抽象实际要链接、定义什么的细节的例子,这样针对它们链接的目标就不必为实现这些细节。这样在不同的平台上,就可以用来做一些完全不同的抽象库结构,开关库的实现基于某些条件(变量、生成器表达式等),提供一个旧库目标名称(库结构进行了重构)(例如:分成单独的库)等等。

虽然INTERFACE库的用例通常很容易理解,但是添加IMPORTED关键字来生成INTERFACE IMPORTED库有时会引起混淆。当导出或安装INTERFACE库以便在项目外部使用时,通常会出现这种组合。当另一个项目使用时,它仍然起到INTERFACE库的作用。但是添加为IMPORTED以表明库来自其他地方时,这样做的效果是将库的可见性限制为当前目录范围,而不是全局可见性。除了下面讨论的一个例外,添加GLOBAL关键字将在库中生成INTERFACE IMPORTED GLOBAL关键字组合,与单独使用INTERFACE相比,实际差别不大。所以,没必要使用(实际上也禁止)INTERFACE IMPORTED库来设置IMPORTED_LOCATION。

CMake 3.11之前,target_…()命令都不能用于在任何类型的IMPORTED库上设置INTERFACE_…属性。但是,可以使用setproperty()或set_target_properties()设置这些属性。CMake 3.11删除了使用`target…()`命令来设置这些属性的限制,因此尽管过去的INTERFACE IMPORTED非常类似于普通IMPORTED库,但在CMake 3.11中,它们现在在限制方面更接近于普通的INTERFACE库。

下表总结了各种关键字组合所支持的功能:

关键字

可见性

导入位置信息

设置接口属性

可安装性

INTERFACE

全局

禁止

任何形式

Yes

IMPORTED

本地

需要

受限$^*$

No

IMPORTED GLOBAL

全局

需要

受限$^*$

No

INTERFACE IMPORTED

本地

禁止

受限$^*$

No

INTERFACE IMPORTED GLOBAL

全局

禁止

受限$^*$

No

$^*$只有在使用CMake 3.11或更高版本时,才能使用target_…()命令来设置INTERFACE_…属性。任何版本的CMake中,INTERFACE_…都可以用set_property()或set_target_properties()进行设置。

人们可能会认为不同的接口和导入的库的组合过于复杂和混乱。然而,对于大多数开发人员来说,导入的目标通常是在幕后创建的,它们看起来和常规的目标无异。上表中的所有组合,只有普通INTERFACE目标通常由项目直接定义。第25章,将介绍其他组合的动机和机制。

add_library()命令的最后一种形式用于定义别名库:

add_library(aliasName ALIAS otherTarget)

库别名类似于可执行别名。它作为引用另一个库的只读方式,但不创建新的构建目标。无法安装库别名,也无法将其定义为另一个别名。CMake 3.11之前,不能为导入的目标创建别名库,但是在CMake 3.11中对导入的目标进行了更改,这个限制放宽了,现在可以为全局可见的导入目标创建别名了。

库别名的一个常见的用法与CMake 3.0中引入的特性有关。对于将要安装或打包的每个库,创建匹配的别名库,其名称为projNamespace::originalTargetName。一个项目中的所有这些别名通常都共享相同的projNamespace。例如:

# Any sort of real library (SHARED, STATIC, MODULE
# or possibly OBJECT)
add_library(myRealThings SHARED src1.cpp ...)
add_library(otherThings STATIC srcA.cpp ...)

# Aliases to the above with special names
add_library(BagOfBeans::myRealThings ALIAS myRealThings)
add_library(BagOfBeans::otherThings ALIAS otherThings)

项目本身中,其他目标将链接到实际目标或命名空间目标(两者具有相同的效果)。使用别名的动机来自于,安装项目和其他连接到由安装/打包的配置文件创建的导入目标的内容。这些配置文件将定义导入库的名称空间名称,而不只是原始名称。然后,项目将链接到带有命名空间的名称上。例如:

# Pull in imported targets from an installed package.
# See details in Chapter 23: Finding Things
find_package(BagOfBeans REQUIRED)

# Define an executable that links to the imported
# library from the installed package
add_executable(eatLunch main.cpp ...)
target_link_libraries(eatLunch PRIVATE
 BagOfBeans::myRealThings
)

如果在某个时候,上面的项目想要将BagOfBeans项目直接合并到它自己的构建中,而不是寻找一个已安装的包,它可以这样做而不改变它的链接关系,因为BagOfBeans项目为命名空间名称提供了一个别名:

# Add BagOfBeans directly to this project, making
# all of its targets directly available
add_subdirectory(BagOfBeans)

# Same definition of linking relationship still works
add_executable(eatLunch main.cpp ...)
target_link_libraries(eatLunch PRIVATE
 BagOfBeans::myRealThings
)

CMake将始终将具有双冒号(::)的名称,视为别名或导入目标的名称。对不同的目标类型,使用这样的名称都会导致错误。也许,当目标名称用作target_link_library()调用的一部分时,如果CMake不知道该名称中的目标时,将在生成时报错。将其与普通名称进行比较,如果CMake不知道由该名称提供的目标,则将其视为假定由系统提供的库。这会导致错误在构建时变得很明显。

add_executable(main main.cpp)
add_library(bar STATIC ...)
add_library(foo::bar ALIAS bar)

# Typo in name being linked to, CMake will assume a
# library called "bart" will be provided by the
# system at link time and won't issue an error.
target_link_libraries(main PRIVATE bart)

# Typo in name being linked to, CMake flags an error
# at generation time because a namespaced name must
# be a CMake target.
target_link_libraries(main PRIVATE foo::bart)

因此,链接到带有名称空间的名称会更加健壮。强烈建议项目至少为要安装/打包的所有目标,定义带有名称空间的别名。这种带有名称空间的别名甚至可以在项目本身中使用,而不仅仅是将其作为预构建包或子项目在其他项目中使用。

16.3. 快速导入目标

不使用GLOBAL关键字进行定义时,导入的目标只在创建它们的目录作用域中(或子作用域中)可见,这种行为源于它们作为查找模块或包配置文件的一部分。Find模块或包配置文件定义的任何内容通常都具有局部可见性,因此通常不应该添加全局可见的目标。这允许项目层次结构的不同部分,可以以不同的设置拉入相同的包和模块,而不相互干扰。

然而,在某些情况下,需要创建具有全局可见性的导入目标,例如确保在整个项目中始终如一地使用特定包的相同版本或实例。创建导入库时添加GLOBAL关键字可以实现这一点,但项目可能无法控制执行创建的命令。为了给项目提供解决这种情况的方法,CMake 3.11引入了通过将目标的IMPORTED_GLOBAL属性设置为true来提升导入目标的全局可见性的能力。请注意,这是单向转换,不能将全局目标降级为局部可见性。

# Imported library created with local visibility.
# This could be in an external file brought in
# by an include() call rather than in the same
# file as the lines further below.
add_library(builtElsewhere STATIC IMPORTED)
set_target_properties(builtElsewhere PROPERTIES
 IMPORTED_LOCATION /path/to/libSomething.a
)

# Promote the imported target to global visibility
set_target_properties(builtElsewhere PROPERTIES
 IMPORTED_GLOBAL TRUE
)

必须注意的是,导入的目标只有在定义与在相应的作用域中才能提升。父或子范围中定义的导入目标时,不能进行提升。include()命令没有引入新的目录作用域,find_package()调用也没有引入,因此可以通过提升导入到构建中定义的目标。事实上,促进导入目标的能力的主要用例。还应该注意的是,一旦导入的目标提升为全局可见,就能够支持创建该目标的别名。

16.4. 推荐

CMake 3.0版本对项目管理目标之间的依赖关系和需求的方式进行了重大更改,而不是通过变量指定大多数事情,然后手动管理项目,或目录级命令将适用于所有目标目录,每个目标有在自己的属性上获得必要信息的能力。这种转变焦点的靶点驱动(target-centric)模式,可让伪目标类型更好灵活和准确的表达内部目标间的关系。特别是,开发人员应该熟悉接口库,因为它们为捕获和表达关系提供了很大帮助,而无需创建或引用物理文件。它们对表示头文件库、资源集合和许多其他场景的详细信息很有用,强烈推荐使用它们,而不是试图使用变量或目录级命令来实现相同的结果。

当项目开始使用外部构建的包,或者引用Find模块找到的文件系统中的工具,就会遇到导入的目标情况。开发人员应该习惯使用导入目标,但是通常没有必要了解它们如何定义的所有细节,除非需要编写Find模块或手动创建一个包的配置文件。第25章中讨论了一些具体的情况,在安装时开发人员可能会遇到导入目标的某些限制,但是这样的情况并不常见。

许多旧的CMake模块只提供变量来引用导入的实体。从CMake 3.0开始,这些模块正在逐步更新,以提供适当的导入目标。对于那些需要引用外部工具或库的项目,如果可以,最好通过导入目标来引用。这些通常可以更好地抽象出诸如平台差异、选择依赖选项的工具等内容。更重要的是,使用需求随后可由CMake处理。如果可以选择导入库或引用相同内容的变量,请尽可能选择导入库。

比起对象库,定义静态库更好一些。静态库更简单,从早期的CMake版本得到了更完整和健壮的支持,并且对于大多数开发人员很好地理解。对象库有其用途,但是灵活性不如静态库。特别是,对象库不能链接(CMake 3.12之前),因此不支持传递依赖关系。这迫使项目手动应用此类依赖关系,从而增加了出现错误和遗漏的机会,还减少了库目标通常提供的封装。甚至名称也会在开发人员中引起一些误会,因为对象库不是真正的库,而只是一组未组合的对象文件,但开发人员有时希望它表现得像一个真正的库。CMake 3.12的变化模糊了这一区别,但剩下的差异仍然为出现意外的结果留下了空间。

命名目标时,不要使用过于通用的目标名称。全局可见的目标名称必须是唯一的,并且当在较大的层次结构安排中使用时,名称可能与其他项目的目标冲突。另外,考虑为不是项目私有的每个目标(即可能最终被安装或打包的每个目标)添加一个别名namespace::…target。这允许项目链接到带有命名空间的目标名称,而不是真正的目标名称,这种技术使项目能够相对容易地在构建子项目本身和使用预构建安装的项目之间进行切换。虽然,这最初看起来是没有多少收获的额外工作,但它正在成为CMake社区中预期的标准实践,特别是对于那些需要花费大量时间来构建的项目。这个模式将在第25.3节中进一步讨论。

不可避免的是,在某些情况下,重命名或重构一个库可能是可取的,但是可能存在现有库目标可以链接到的外部项目。这中情况下,可以使用别名目标为重命名的目标提供旧名称,以便那些外部项目可以在方便的时候继续构建和更新。分割库时,使用旧的目标名称定义接口库,同时定义库的链接依赖关系。例如:

# Old library previously defined like this:
add_library(deepCompute SHARED ...)
# Now the library has been split in two, so define
# an interface library with the old name to effectively
# forward on the link dependency to the new libraries
add_library(computeAlgoA SHARED ...)
add_library(computeAlgoB SHARED ...)

add_library(deepCompute INTERFACE)
target_link_libraries(deepCompute INTERFACE
 computeAlgoA
 computeAlgoB
)

Last updated