第4章:构建目标

如前一章所示,在CMake中构建可执行文件不会很难。给出的例子需要为可执行文件定义目标名称,并列出要编译的源文件:

add_executable(myApp main.cpp)

假设开发者想要构建一个控制台可执行文件(当然CMake也允许开发者定义其他类型的可执行文件),比如苹果平台上的应用程序包和Windows GUI应用程序。本章只讨论add_executable()提供的其他选项。

除了可执行文件之外,开发人员还需要构建和链接库。CMake支持几种不同类型的库,包括静态、动态、模块和框架。CMake提供了强大的特性来管理目标间的依赖关系,以及如何链接库,以及如何在CMake中使用它们。还会介绍了一些基本变量和属性的使用,以说明这些CMake特性是如何与库和目标相关联的。

4.1. 可执行文件

add_executable()命令的完整形式如下:

add_executable(targetName [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               source1 [source2 ...]
)

与之前的区别是有新的可选关键字:

WIN32

在Windows平台上构建可执行文件时,此选项表示CMake将该可执行文件构建为Windows GUI应用程序。实际上,会使用WinMain()创建入口,而不是main(),并且会添加链接选项/SUBSYSTEM:WINDOWS。其他平台会忽略WIN32选项。

MACOSX_BUNDLE

它会引导CMake在苹果平台上构建应用程序包。与选项名称不同,不仅适用于macOS,也适用于其他苹果平台,如iOS。此选项的确切效果在不同平台间略有不同。例如,在macOS上,app bundle有一个非常特定的目录结构,而iOS的目录结构是扁平的。CMake还会生成一个 Info.plist文件。这些细节将在22.2节中详细地介绍。非Apple平台上,会忽略MACOSX_BUNDLE关键字。

EXCLUDE_FROM_ALL

项目定义了许多目标,但默认情况下只需要构建一部分。当在构建没有指定目标时,将构建默认的ALL目标(根据使用CMake生成器的不同,名称略有不同,比如:Xcode的ALL_BUILD)。如果可执行文件用EXCLUDE_FROM_ALL定义,就不会包含在默认的ALL目标中。然后,只有当构建命令显式地请求该可执行文件,或者作为默认构建对其有依赖时,可执行文件才会构建。

除此之外,还有其他形式的add_executable(),它们对现有可执行文件或目标是一种引用,而非要构建的新文件。这些别名可执行文件将在第16章中详细介绍。

4.2. 定义库

创建可执行文件是任何构建系统的基本需求。对于许多大型项目,创建和使用库也是保持项目可管理性的关键。CMake支持构建不同类型的库,可以处理平台差异,同时支持库的本地特性。库目标使用add_library()命令构建,命令中有许多参数。其中基本参数如下所示:

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

使用add_executable()可以构建可执行文件。CMakeLists.txt文件中使用targetName来引用库,默认构建库的名称源于targetNameEXCLUDE_FROM_ALL关键字具有与add_executable()相同的效果,避免库包含在默认ALL目标中。构建库的类型由其余的三个关键字指定:STATICSHAREDMODULE

STATIC

指定为静态库。在Windows上构建的库为targetName.lib,而在类Unix平台上,通常是libtargetName.a。

SHARED

指定动态库。在Windows上的库名称是targetName.dll,在Apple平台上是libtargetName.dylib,在类Unix平台上,通常是libtargetName.so。Apple平台上,动态库也可以标记为框架,这个将在第22.3节中介绍。

MODULE

这种方式类似于动态库,不同的是在运行时动态加载。通常会作为插件或可选组件,用户可以选择加载或不加载。Windows平台上,不会为DLL创建导入库。

其实可以省略要构建的库类型的关键字,除非项目特别需要特定类型的库,并让开发人员在构建项目时进行选择。这样,库可以静态的,也可以是动态的,由名为BUILD_SHARED_LIBS的CMake变量的值决定。如果BUILD_SHARED_LIBS设置为true,库将构建为动态库,否则将构建为静态库。与变量的设置方法相同,可以通过在cmake命令行执行时,使用-D选项进行定义:

cmake -DBUILD_SHARED_LIBS=YES /path/to/source

也可以在CMakeLists.txt文件中直接设置,只要在add_library()命令之前设置以下内容就可以了。但如果开发者想要改变它,就需要修改CMakeLists.txt(即灵活性较差):

set(BUILD_SHARED_LIBS YES)

与可执行文件一样,库目标也可以引用已有的二进制文件或目标,而不必是由项目构建出来的。另外还支持另一种类型的伪库,用于收集obj文件,而非创建静态库。这些将在第16章目标类型中详细讨论。

4.3. 目标连接

考虑搭建项目的目标时,开发人员通常习惯于考虑A库依赖于B库,因此将A链接到B。这是处理库的传统方式,而在实际中,库之间存在几种不同类型的依赖关系:

PRIVATE

私有依赖项指定A库自己使用B库。其他链接到A库的目标都不需要知道B库,因为它只影响A库的内部实现。

PUBLIC

公共依赖关系指定A库不仅在内部使用B库,还在其接口中使用B库。这样,其他任何使用A库的目标也会依赖B库。例如,在A库中使用的函数,至少有一个在B库中定义和实现,因此代码需要依赖B库。

INTERFACE

接口依赖规定,为了使用A库,也必须使用B库的一部分。当依赖于纯头文件库时,在add_library()中使用INTERFACE就非常有有用了。

CMake通过target_link_libraries()命令,可以获得更丰富的依赖关系,该命令的一般形式为:

target_link_libraries(targetName
     <PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
     [<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
     ...
)

允许项目定义一个库如何依赖于其他库。然后,CMake负责管理以这种方式的链接。

add_library(collector src1.cpp)
add_library(algo src2.cpp)
add_library(engine src3.cpp)
add_library(ui src4.cpp)
add_executable(myApp main.cpp)
target_link_libraries(collector
 PUBLIC ui
 PRIVATE algo engine
)
target_link_libraries(myApp PRIVATE collector)

上例中,ui库PUBLIC链接到collector,因此即使myApp只直接链接到collector,由于这种关系,myApp也将链接到ui。另一方面,algo和engine作为PRIVATE链接到collector,所以myApp不会直接链接。第16.2节会讨论静态库的其他行为,这些行为可能会进行更多的链接,以满足依赖关系,包括循环依赖关系。

后面的章节将介绍其他target_…()系列的命令,这些命令增强了目标之间的依赖。通过target_link_libraries()连接时,允许编译器/链接器选项和头文件搜索路径从一个目标执行到另一个目标。这些特性从CMake 2.8.11到3.2渐进式添加,从而让CMakeLists.txt更加简单。

后面的章节还会讨论了更复杂的源目录层次。target_link_libraries()所使用的targetName,必须在add_executable()add_library()命令之后调用,所连接的库目标需要在同一目录中进行定义。

4.4. 非目标连接

上一节中,所有的链接项都是当前CMake的目标,但target_link_libraries()命令要灵活很多。除了CMake目标之外,target_link_libraries()也可以通过以下内容指定:

库文件的完整路径

CMake将把库文件添加到链接器命令中。如果库文件更改,CMake将检测更改并重新链接目标。CMake 3.3版本中,链接器命令总是使用完整路径,但在3.3版本之前,某些情况下,CMake可能会要求链接器搜索库(例如:使用/usr/lib/libfoo.so替换-lfoo)。3.3版本之前的行为大多是历史遗留的,但对于感兴趣的读者来说,可以在CMP0060策略下的CMake文档看到完整的信息。

库名

如果只给出库的名称而没有路径,链接器将搜索该库(例如:foo变成-lfoo或foo.lib,具体取决于平台)。这对于系统提供的库来说很常见。

连接标识

有种特殊情况,需要以-l或-framework以外的连字符开头的形式添加到链接器命令中的标志。CMake会警告,因为这些应该只用于PRIVATE构建,因为如果定义为PUBLICINTERFACE,会携带其他目标,这可能并不安全。

除了上述内容之外,由于历史原因,任何配置项前面都可以有一个关键字debugoptimizedgeneral。这些关键字的作用是,根据构建确定是否配置为调试,从而进一步细化应该在什么时候包含后面的配置项(参见第13章)。如果某个项前面有debug关键字,只有在生成为调试时才会添加该项。如果项前面有optimized的关键字,则只有在构建时(不是Debug构建)才会添加。general关键字为所有构建配置添加选项,如果没有使用任何关键字,general就是默认行为。对于新项目,应该避免使用debugoptimizedgeneral的关键字,因为现代CMake有更清晰、更灵活和更健壮的特性可以实现同样的效果。

4.5. 老式CMake

target_link_libraries()还有其他一些形式,这些形式早在2.8.11版本之前就已是CMake的一部分。这些选项有助于理解理解老式CMake项目,但不推荐在新项目中使用。应该在命令中使用PRIVATEPUBLICINTERFACE,从而更准确地表达依赖关系。

target_link_libraries(targetName item [item...])

上述形式通常等价于使用了PUBLIC,但在某些情况下,可能视为PRIVATE。如果项目混合使用新旧命令形式定义库的依赖关系,那么老式的方式通常会视为PRIVATE

另一种不推荐的形式如下:

target_link_libraries(targetName
 LINK_INTERFACE_LIBRARIES item [item...]
)

这是上面新形式INTERFACE关键字的一个前导,但是CMake文档不推荐这样使用。它的行为可以影响不同的目标属性,并使用相应的策略设置控制其行为。对于开发人员来说,这会产生一些疑惑,使用新的参数形式可以避免这种疑惑。

target_link_libraries(targetName
 <LINK_PRIVATE|LINK_PUBLIC> lib [lib...]
 [<LINK_PRIVATE|LINK_PUBLIC> lib [lib...]]
)

与老式参数类似,参数表是新形式PRIVATEPUBLIC关键字的前导。同样,老式参数对于目标属性同样会产生疑惑,PRIVATE/PUBLIC关键字会更适合于新项目。

4.6. 总结

目标名称与项目名称无关。常见的教程和例子使用同一个变量命名,也用于项目名称和可执行的目标,像这样:

# Poor practice, but very common
set(projectName MyExample)
project(${projectName})
add_executable(${projectName} ...)

这只适用于比较简单的项目。最好将项目名称和可执行文件名称分开,即使一开始是相同的。直接设置项目名称(而不是通过变量),根据目标所做的事情(而不是它所属的项目)选择目标名称,并假定项目将定义多个目标。这能有助于好习惯的养成,在处理复杂项目时会很重要。

命名库的目标时,不要用lib作为名称的开头或结尾。许多平台上(除了Windows之外的所有平台),构建实际的库名称时,lib会自动成为前缀,以使其符合平台的使用习惯。如果目标名称已经以lib开头,则生成的库文件名将以liblibsomething…为名称(通常会认为这是一个错误)。

除非有硬性原因需要这样做,否则尽量避免直接将库指定STATICSHARED。这在选择静态库或动态库时有更大的灵活性,并可以作为整个项目范围的策略。可以通过BUILD_SHARED_LIBS变量修改默认值,而不必修改对add_library()的每次使用。

目标在调用target_link_libraries()时需要指定PRIVATEPUBLIC和/或INTERFACE。随着项目越来越复杂,这三个关键字对如何处理目标间的依赖关系有更大的影响。项目开始时这样使用会让开发人员考虑目标之间的依赖关系,这有助于更早地暴露项目结构的问题。

Last updated