(四)构建简单目标

构建简单目标

前一章节展示了如何通过 add_executable() 命令,指定 targetname 和 源文件列表来构建一个可执行文件示例,看起来非常简单。

但实际情况并不总是这样简单,有可能开发者想在苹果平台上构建应用软件集,或者一款 Winodws GUI 应用。

因此我们这一章节来讨论一下如何通过 add_executable() 命令的其他可选项来具体指定这些细节。CMake 支持静态库、动态库、模块和框架。同时提供了非常强大的功能来管理不同目标之间的依赖,以及如何链接库。这些功能和特性的基础用法大概展示了 CMake 中与库和目标文件相关的功能。

可执行程序

完整的 add_executable() 命令如下所示:

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

与之前相比只是多了一些可选项。

WIN32

当在 windows 平台进行构建时,该选项指导 CMake 构建为 Winodws GUI 程序。这意味着,在实际过程中会以 WinMain() 作为程序入口,而不是 main(),同时会链接 /SUBSYSTEM:WINDOWS 选项。在其他平台上该选项会被忽略。

ACOSX_BUNDLE

EXCLUDE_FROM_ALL

有时项目中定义了多个构建目标,但默认只有其中个别几个会被构建。当未指定具体的构建目标时,默认会构建所有(ALL),取决于使用的生成器,名称会有所区别:比如 Xcode 为 ALL_BUILD,Makefiles 和 Ninjia 为 all。如果一个可执行目标使用 EXCLUDE_FROM_ALL 进行定义,它会被排除在默认的 ALL 选项之外。除非其被构建命令明确指定或被其他模块引用。典型场景是当该可执行目标为一个特定情况下才会用到的工具时,会使用该选项进行定义。

除以上之外,还有其他 add_executable() 命令形式,会生成一个其他可执行程序或目标的引用而不是一个新的可执行目标,这在后续章节进行介绍。

示例:

# Main GUI app, built as part of the default "ALL" target
add_executable(MyApp WIN32 MACOSX_BUNDLE
  main.cpp
  widgets.cpp
)
# Helpful command-line tools, not built by default
add_executable(checker checker.cpp EXCLUDE_FROM_ALL)
add_executable(reporter reporter.cpp EXCLUDE_FROM_ALL)

以下命令只会构建 MyApp:

cmake --build /path/to/build

构建一个帮助工具:

cmake --build /path/to/build --target checker

定义库

创建一个可执行程序是所有的项目基本诉求,但对于大型项目来说,使用库文件来管理项目也是必需的。CMake 提供了多种类型库文件,兼容了不同平台之间的区别的同时,支持它们独特的本地化特性。

定义库使用 add_library() 命令,同样它也有多种形式,其中最基本的形式如下:

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

同 add_executable() 命令类似,targetname 和 EXCLUDE_FROM_ALL 具有同样的意义和作用,不同的是库有三种类型:STATIC、SHARED、MODULE。

STATIC:表示静态库和文档。Winodws 中库文件名称为 targetname.lib,类 Unix 平台中为 libtargetname.a。
SHARED:表示共享库或动态库。Winodws 中文件名为 targetname.dll,MacOS 中为 libtargetname.dylib,类 Unix 系统中为 libtargetname.so;
MODULE:表示为一种共享库,但是主要是用来在运行时动态加载,而不是链接到其他库或可执行程序。一般为典型的插件或组件,用户可以选择是否加载。在 Windows 平台上,不会为该动态链接库创建导入库。

对于构建来说,不指定库的类型也是可以的,推荐的做法是不进行指定,而留给项目整体构建时通过环境变量指定其为静态库或动态库。比如设置 BUILD_SHARED_LIBS 为 true,则这些库为共享库,否则为静态库。关于环境变量会在后续章节讲解,目前可以在构建命令中使用 -D 选项指定:

cmake -DBUILD_SHARED_LIBS=YES -B /path/to/build

当然也可以在 CMakeLists.txt 文件中 add_library() 之前加入以下命令来设置,但是当需要改变其类型时也就需要手动进行变更:

set(BUILD_SHARED_LIBS YES)

正如对于可执行文件一样,库目标也可以被定义为指向某些已存在的二进制文件或目标,而非由项目自行构建。此外,还支持另一种伪库类型,用于将目标文件组合在一起,但不会像创建静态库那样深入处理。

链接

当一个项目包含多个目标文件且不同文件之间存在某种关系时,开发人员常规会考虑到 A 依赖于 B 这种类似形式,但实际情况会很复杂,不同库之间可能存在多种不同的依赖关系:

PRIVATE

PRIVATE表明库 A 在其自身的内部实现中使用了库 B。任何其它链接到 A 的文件都无需关心 B,因为 B 仅仅是 A 的内部实现而已。

PUBLIC
PUBLIC 的意思是不仅库 A 在内部使用库 B,而且在其对外接口中也使用了 B。这意味着如果要使用 A 中某个与 B 相关的功能就必须要依赖于 B,包括其他要使用该功能的模块。典型案例就是 A 中定义了函数,但入参的定义在 B 中,所以要在外部调用该函数,必须同时依赖于 A 和 B。

INTERFACE
INTERFACE 是指为了使用 A,必须依赖于 B 的部分功能。不同于 PUBLIC 的地方是,A 不是在内部依赖于 B,而只是在接口中使用了 B。

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)

示例中,Collector 以 PUBLIC 的形式链接了 UI 库,即使 MyApp 只链接了 Collector,但是由于 PUBLIC 的原因也会链接 Ui 库。
反之,Algo 和 Engine 库以 PRIVATE 的形式链接到 Collector,MyApp 将不会链接它们。

链接非目标单位

绝对路径

CMake 会将库文件添加到链接器命令中。如果库文件发生更改,CMake 会检测到这一变化并重新链接目标文件。请注意,从 CMake 3.3 版本开始,链接器命令始终使用指定的完整路径,但在 3.3 版本之前,存在一些情况,CMake可能会要求链接器去查找该库(例如,将 /usr/lib/libfoo.so 替换为 -lfoo)。具体可参考 CMP0060.

简单库名

如果只有库名没有路径,链接命令会在系统范围内查找该文件,对于系统文件来说这种很通用。

链接标识

特殊情况,如被链接对象以 “-” 开头,则会被识别为链接标识。CMake 警告这种情况应该只在 PRIVATE 时使用,因为如果是 PUBLIC 或 INTERFACE 可能导致安全问题。

早期形式

由于历史原因,target_link_libraries() 命令中任意被指定的链接项前都可以添加 debug、optimized 或 general 中的任意一项作为修饰。这些关键字的作用是进一步使当其后面的项是否应该被链接依赖于构建配置是否为 debug 时显得更加优雅。

如果一个链接项只是前面是 debug,那么它只会在 debug 构建中被添加,如果是 optimized,则只有在非 debug 构建中才会被添加。而 general 是指在所有构建配置中都会被添加,也是默认值,即如果未指定则默认为 general。

新项目中应该尽量避免使用这三个关键字,在新版 CMake 中有更清晰、更灵活、更稳定的方式。

target_link_libraries() 命令在 2.8.11 之前还有其他形式,这里作为讨论或对理解旧项目作为参考,但不建议在新项目中继续使用。之前所展示的包含 PRIVATE、PUBLIC 和 INTERFACE 的完整形式应当被优先采用,因为这种方式能更准确地体现依赖关系的本质。

target_link_libraries(targetName item [item...])

上述形式通常等同于被定义为 PUBLIC 的项目中的各项内容,但在某些情况下,它们可能会被当作“私有”来处理。特别是,如果一个项目定义了一条包含旧式和新式命令形式的库依赖链,那么旧式形式通常会被视为“私有”。

另一种被支持但已过时的形式如下所示:

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

这与上述所介绍的新形式中的 INTERFACE 关键字具有一定的关联性,但 CMake 文档建议不要使用它。其行为可能会影响不同的目标属性,而具体的策略设置则会控制这种行为。这可能会给开发人员带来困惑,而通过使用新的 INTERFACE 形式可以避免这种情况。

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

与之前的旧式形式类似,此形式也是新式形式中 PRIVATE 和 PUBLIC 关键字版本的前身。同样,旧式形式在涉及哪些目标属性会受到影响方面存在混淆,因此对于新项目应优先使用 PRIVATE/PUBLIC 关键字形式。

建议

链接目标无需与项目名相关联,很多教程和示例中经常看到项目名和可执行程序名使用同一名称:

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

这仅适用于最基础的项目,而且还会助长一些不良习惯的形成。根据构建目标功能来选择名称,而非依据其所属的项目来定,因为有可能项目中会包含多个构建目标,尤其是在大型项目中更应该如此。

在为库指定名称时,切勿将名称的开头或结尾设定为“lib”。在许多平台上(即除了 Windows 之外几乎所有的平台)在构建实际的库名称时,会自动在名称前加上“lib”,以符合该平台通常的命名规范。如果目标名称已经以“lib”开头,那么库文件的名称最终会变成“liblibsomething…”这样的形式,人们往往认为这是个错误。

除非有充分的理由,否则在确定确实需要该库之前,尽量不要为其指定 PUBLIC 或 SHARED 关键字。这样可以为整个项目范围内的静态或动态库选择策略提供更大的灵活性。可以使用 BUILD_SHARED_LIBS 变量在一处更改默认设置,而无需对每个调用的 add_library() 语句进行修改。

在调用 target_link_libraries() 命令时,务必始终明确指定 PRIVATE、PUBLIC 和/或 INTERFACE 关键字,而不要遵循旧式的 CMake 语法(该语法假定一切都是 PUBLIC)。随着项目的复杂性增加,这三个关键字对处理目标之间的依赖关系有着更显著的影响。从项目一开始就使用它们,还能促使开发人员思考目标之间的依赖关系,这有助于更早地发现项目中的结构问题。

posted @ 2025-06-16 10:15  凉皮也是菜  阅读(5)  评论(0)    收藏  举报  来源