如何使用 CMake 正确添加包含目录

How to properly add include directories with CMake

本文关键字:添加 包含目 何使用 CMake      更新时间:2023-10-16

大约一年前,我问过CMake中的标头依赖关系。

我最近意识到问题似乎是CMake认为这些头文件是项目外部的。至少,在生成 Code::Blocks 项目时,头文件不会出现在项目中(源文件会出现)。因此,在我看来,CMake 认为这些标头是项目外部的,并且不会在依赖项中跟踪它们。

在 CMake 教程中快速搜索仅指向似乎没有按照我的意愿进行include_directories......

向 CMake 发出信号的正确方法是什么,表明特定目录包含要包含的标头,并且生成的 Makefile 应跟踪这些标头?

必须做两件事。

首先添加要包含的目录:

target_include_directories(test PRIVATE ${YOUR_DIRECTORY})

如果您坚持使用非常旧的CMake版本(2.8.10或更早版本)而不支持target_include_directories,您也可以改用旧版include_directories

include_directories(${YOUR_DIRECTORY})

然后,还必须将头文件添加到当前目标的源文件列表中,例如:

set(SOURCES file.cpp file2.cpp ${YOUR_DIRECTORY}/file1.h ${YOUR_DIRECTORY}/file2.h)
add_executable(test ${SOURCES})

这样,头文件将在生成文件中显示为依赖项,例如,如果生成一个依赖项,也会显示在生成的 Visual Studio 项目中。

如何将这些头文件用于多个目标:

set(HEADER_FILES ${YOUR_DIRECTORY}/file1.h ${YOUR_DIRECTORY}/file2.h)
add_library(mylib libsrc.cpp ${HEADER_FILES})
target_include_directories(mylib PRIVATE ${YOUR_DIRECTORY})
add_executable(myexec execfile.cpp ${HEADER_FILES})
target_include_directories(myexec PRIVATE ${YOUR_DIRECTORY})

首先,使用include_directories()告诉 CMake 将目录作为-I添加到编译命令行。其次,列出add_executable()add_library()调用中的标头。

例如,如果您的项目的源代码在src中,并且您需要来自include的标头,您可以这样做:

include_directories(include)
add_executable(MyExec
src/main.c
src/other_source.c
include/header1.h
include/header2.h
)

项目结构

.
├── CMakeLists.txt
├── external //We simulate that code is provided by an "external" library outside of src
│   ├── CMakeLists.txt
│   ├── conversion.cpp
│   ├── conversion.hpp
│   └── README.md
├── src
│   ├── CMakeLists.txt
│   ├── evolution   //propagates the system in a time step
│   │   ├── CMakeLists.txt
│   │   ├── evolution.cpp
│   │   └── evolution.hpp
│   ├── initial    //produces the initial state
│   │   ├── CMakeLists.txt
│   │   ├── initial.cpp
│   │   └── initial.hpp
│   ├── io   //contains a function to print a row
│   │   ├── CMakeLists.txt
│   │   ├── io.cpp
│   │   └── io.hpp
│   ├── main.cpp      //the main function
│   └── parser   //parses the command-line input
│       ├── CMakeLists.txt
│       ├── parser.cpp
│       └── parser.hpp
└── tests  //contains two unit tests using the Catch2 library
├── catch.hpp
├── CMakeLists.txt
└── test.cpp

怎么做

1. 顶级 CMakeLists.txt 与配方 1 非常相似,使用函数和宏重用代码

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(recipe-07 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
# defines targets and sources
add_subdirectory(src)
# contains an "external" library we will link to
add_subdirectory(external)
# enable testing and define tests
enable_testing()
add_subdirectory(tests)

2.目标和源在 src/CMakeList 中定义.txt(转化目标除外)

add_executable(automata main.cpp)

add_subdirectory(evolution)
add_subdirectory(initial)
add_subdirectory(io)
add_subdirectory(parser)
target_link_libraries(automata
PRIVATE
conversion
evolution
initial
io
parser
)

3.转换库在外部/CMakeLists中定义.txt

add_library(conversion "")
target_sources(conversion
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/conversion.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/conversion.hpp
)
target_include_directories(conversion
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)

4.src/CMakeLists.txt 文件添加了更多子目录,这些子目录又包含 CMakeLists.txt 文件。它们在结构上都很相似;src/evolution/CMakeLists.txt 包含以下内容:

add_library(evolution "")
target_sources(evolution
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/evolution.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/evolution.hpp
)
target_include_directories(evolution
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)

5.单元测试在测试/CMakeLists中注册.txt

add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test evolution)
add_test(
NAME
test_evolution
COMMAND
$<TARGET_FILE:cpp_test>
)

如何运行它

$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .

参考: https://github.com/sun1211/cmake_with_add_subdirectory

添加include_directories("/your/path/here").

这将类似于使用-I/your/path/here/选项调用gcc

确保在路径两边加上双引号。其他人没有提到这一点,这让我卡了 2 天。所以这个答案是给那些对CMake非常陌生并且非常困惑的人的。

如果将其与其他创建Makefile的方法(例如make或qmake)进行比较,CMake更像是一种脚本语言。它不像Python那样很酷,但仍然如此。

如果在各种开源项目中查看人们如何包含目录,则没有">正确的方法"。但是有两种方法可以做到这一点。

  1. 粗include_directories会将一个目录附加到当前项目和所有其他后代项目,您将通过一系列add_subdirectory命令附加这些目录。有时人们说这种方法是遗留下来的。

  2. 更优雅的方式是使用target_include_directories。它允许为特定项目/目标附加目录,而不会(可能)不必要的继承或各种包含目录的冲突。还允许执行甚至微妙的配置,并为此命令附加以下标记之一。

专用- 仅用于此指定的构建目标

PUBLIC- 将其用于指定目标和与此项目链接的目标

接口-- 仅将其用于与当前项目链接的目标

附注:

  1. 这两个命令都允许将目录标记为 SYSTEM,以提示指定的目录将包含警告不是您的业务。

  2. 类似的答案是其他命令对target_compile_definitions/add_definitions、target_compile_options/CMAKE_C_FLAGS

我遇到了同样的问题。

我的项目目录是这样的:

--project
---Classes
----Application
-----.h and .c files
----OtherFolders
--main.cpp

以及我过去在所有这些文件夹中包含的文件:

file(GLOB source_files CONFIGURE_DEPENDS
"*.h"
"*.cpp"
"Classes/*/*.cpp"
"Classes/*/*.h"
)
add_executable(Server ${source_files})

它完全奏效了。

您有两个选择。

老:

include_directories(${PATH_TO_DIRECTORY})

和新的

target_include_directories(executable-name PRIVATE ${PATH_TO_DIRECTORY})

要使用target_include_directories,你需要定义你的可执行文件 -add_executable(executable-name sourcefiles)

所以你的代码应该看起来像

add_executable(executable-name sourcefiles)
target_include_directories(executable-name PRIVATE ${PATH_TO_DIRECTORY})

您可以在此处阅读更多信息 https://cmake.org/cmake/help/latest/command/target_include_directories.html

这对我有用:

set(SOURCE main.cpp)
add_executable(${PROJECT_NAME} ${SOURCE})
# target_include_directories must be added AFTER add_executable
target_include_directories(${PROJECT_NAME} PUBLIC ${INTERNAL_INCLUDES})

不要忘记包含${CMAKE_CURRENT_LIST_DIR}。 这就是给我带来问题的原因。

示例应如下所示:

target_include_directories(projectname
PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include"                          
)

PUBLIC 表示要包含在父项目中的依赖项。 私人的那些你没有。

在较新的CMake版本中,我们可以将包含路径限制为目标,例如:

target_include_directories(MyApp PRIVATE "${CMAKE_CURRENT_LIST_DIR}/myFolder")

我的意思是,如果CMakeLists.txt有多个目标,否则,包含路径不会与其他CMakeLists.txt脚本共享,并且执行如下操作就足够了:

include_directories("${CMAKE_CURRENT_LIST_DIR}/myFolder")

但是,也许我们可以模拟target_include_directories(...)对CMake 2.8.10或更早版本的作用,例如:

set_property(
TARGET MyApp
APPEND PROPERTY
INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/myFolder"
)

全部完成,但似乎如果您希望源文件在更改它们使用的任何头文件后重新编译,则还需要将所有这些头文件添加到每个目标中,例如:

set(SOURCES src/main.cpp)
set(HEADERS
${CMAKE_CURRENT_LIST_DIR}/myFolder/myHeaderFile.h
${CMAKE_CURRENT_LIST_DIR}/myFolder/myOtherHeader.h
)
add_executable(MyApp ${SOURCES} ${HEADERS})

对于"似乎",我的意思是,如果需要,CMake 可以自动检测此类头文件,因为它无论如何都会解析项目的 C/C++ 文件。

在这个问题的所有答案中,有很多"如何"(得到你想要的东西),以及宝贵的"为什么"(挖掘激发问题的问题以及提问者可能误解了不同类型的工具(如 IDE 和构建工具)相互交互和不交互和共享信息的方式, 以及 CMake 传递/需要传递给这些工具的信息)。

这个问题很烦人,因为它是由特定 IDE 代码::Blocks) 和 CMake 的特定行为引起的,但随后提出了一个与该 IDE 无关的问题,而是关于 Makefiles 和 CMake 的问题,假设他们对 CMake 做了一些错误,导致 Makefiles 出现问题,从而导致他们的 IDE 出现问题。

TL;DR CMake 和 Makefiles 有自己的方法来跟踪标头依赖项,给定包括目录和源文件。CMake 如何配置 Code::Blocks IDE 是一个完全独立的故事。

什么是 CMake 中的"外部"标头?(没有这样的事情)

我最近意识到问题似乎是CMake认为这些头文件是项目外部的。[...] 因此,在我看来,CMake 认为这些标头是项目外部的,并且不会在依赖项中跟踪它们

据我所知,当涉及到CMake时,没有官方或有用的"外部标头"定义。我还没有在文档中看到过这个短语。另请注意,"项目"一词是一个相当超载的术语。CMake 生成的每个构建系统都包含一个顶级项目,可能包括其他外部或子目录项目。每个项目可以包含多个目标(库、可执行文件等)。CMake 所指的目标有时可转换为 IDE 所称的项目 (Ix.Visual Studio,可能还有Code::Blocks)。如果你必须给这样的短语一个含义,这对我来说是有意义的:

如果问题指的是某些 IDE 对"项目"一词(CMake 称之为"目标")的含义,则项目外部的头文件将是那些不打算通过目标的任何包含目录访问的文件(例如,包含来自链接到相关目标的目标的目录)。

如果问题指的是CMake对"项目"一词的含义:目标要么是项目的一部分(通过调用project()命令定义/创建,并由生成的构建系统构建),要么是IMPORTED的一部分(不是由生成的构建系统构建的,并且预期已经存在,或者由添加到生成的构建系统中的某个自定义步骤构建, 如通过ExternalProject_Add)。包含IMPORTED目标的目录将是相关 CMake 项目外部的标头,包含非IMPORTED目标的目录将是项目"的一部分"的标头。

CMake 是否跟踪标头依赖项?(视情况而定!

[...]CMake 认为这些标头是项目外部的,并且不会在依赖项中跟踪它们

我对 CMake 的历史或构建工具中的标头依赖项跟踪不是很熟悉,但这是我从对该主题所做的搜索中收集到的内容。

CMake 本身与与 implmentation 文件/翻译单元的标头/包含依赖项相关的任何信息没有太大关系。该信息对 CMake 很重要的唯一方法是,如果 CMake 需要告诉生成的构建系统这些依赖项是什么。生成的构建系统想要跟踪头文件依赖项中的更改,以避免任何不必要的重新编译。特别是对于Unix Makefiles生成器,在CMake 3.20之前,CMake会扫描标头/包含依赖项,以告诉Makefiles构建系统这些依赖项。从编译器支持的 v3.20 开始,默认情况下,CMake 将该责任委托给编译器。请参阅可用于还原该行为的选项 此处.

标头/包含依赖项扫描如何因每个受支持的 CMake 生成器而异的确切详细信息。例如,您可以在他们的手册上找到一些关于 Ninja 功能/方法的高级描述。由于这个问题只是关于 Makefiles,所以我不会尝试详细介绍其他生成器。

注意如何获取构建系统的标头/包含依赖项信息,你只需要给 CMake 一个目标的包含目录列表,以及一个要编译的实现源文件的列表?无需为其提供头文件列表,因为可以扫描该信息(通过 CMake 或编译器)。

IDE 是否通过扫描获取有关目标标头的信息?(他们可以)

每个 IDE 都可以以它想要的任何方式显示信息。像 IDE 不显示标题这样的问题通常只发生在文件系统布局以外的项目布局的 IDE 显示格式上(项目头文件通常与实现文件位于同一项目目录中)。例如,这种非文件系统布局视图在Visual Studio和Code::Blocks中可用。

每个 IDE 都可以以它选择的任何方式获取标头信息。据我所知(但我可能是错误的Visual Studio),Visual Studio和Code::Blocks都希望项目头列表明确列在IDE项目配置文件中。还有其他可能的方法(例如标头依赖项扫描),但似乎许多 IDE 选择显式列表方法。我的猜测是因为它在实现方面很简单。

为什么 IDE 查找与目标关联的头文件会让扫描变得繁重?(注意:这有点猜测,因为我不是任何此类工具的维护者,只使用了其中的几个)IDE 可以实现文件扫描(这本身就是一项复杂的任务),但要知道哪些标头"在"目标中,它们要么需要从构建系统获取有关如何编译目标的翻译单元的信息,这是假设所有"不在目标中"标头包含路径都使用类似"系统"的标志指定, 不一定是这样。或者,它可以尝试从元构建系统获取该信息,这里是CMake。或者它可以尝试执行 CMake 现在执行的操作,并尝试调用选定的编译器来扫描依赖项。但无论哪种情况,他们都必须做出一些艰难的决定,决定支持哪些构建系统、元构建系统和/或编译器,然后做一项艰巨的工作,从这些工具存储该信息的任何格式中提取这些信息,可能没有任何保证这些格式在未来的工具版本中是相同的(支持在较新的工具版本中更改格式可能类似于必须完全支持单独的工具)。IDE 可以完成所有这些工作,也可以只要求您为其提供属于每个目标的标头列表。如您所见,C/C++生态系统的工具多样性存在缺点。也有优点,但这超出了这个问题的范围。

从好的方面来说,CMake 实际上确实有一种机制来尝试将一些工作从你的肩膀上拿走。对于具有非文件系统视图的此类 IDE,它确实实现了一个简单的启发式方法来尝试查找与源文件关联的头文件...

头文件发现如何为 CMake 的代码::块 IDE 生成器工作?

至少,在生成 Code::Blocks 项目时,头文件不会出现在项目中(源文件会出现)。

这里有一些有趣的事情:Code::Blocks编辑器具有源文件和头文件的概念,它们是项目的一部分,并且由于CMake不希望/要求其用户告诉它项目中的每个头文件(它只需要知道哪些包含目录应该与目标相关联), 它尝试使用某种启发式方法来发现与实现文件关联的头文件。这种启发式是非常基本的:获取项目中每个源文件的路径,并尝试将扩展更改为通常提供给头文件的扩展,并查看是否存在任何此类文件。请参阅:/Source/cmExtraCodeBlocksGenerator.cxx中的cmExtraCodeBlocksGenerator::CreateNewProjectFile成员函数。

在"干草叉布局"术语中,可以说启发式假设项目使用"合并标头"放置而不是"拆分标头"放置,其中有单独的src/include/目录。因此,如果您不使用合并标头布局,或者有任何不符合该启发式的目标标头(例如实用程序标头文件),则需要显式告知 CMake 这些文件(例如,使用target_sources),以便它将该知识传递给它生成的 IDE 配置。

延伸阅读:

  • 这是关于其代码的CMake文档::块生成器(与手头主题相关的信息不多,但无论如何都可以链接)。
  • 这是代码::Blocks在其"项目视图"上的文档。下面是.cpbxml 架构文档(尤其请参阅Unit元素)。
  • 如果要读取执行相关标头检测的 CMake 代码,可以在 Source/cmExtraCodeBlocksGenerator.cxx 文件的cmExtraCodeBlocksGenerator::CreateNewProjectFile函数中找到它。

结语

我敢肯定,有很多人比我更了解这些工具。如果您是这些人中的一员,并且注意到我犯了一个错误,请在评论或聊天中亲切地纠正我,或者只是编辑这篇文章。

请注意,虽然构建工件的安装是许多项目生命周期的重要组成部分,因此被纳入大多数 C/C++ 构建系统的设计中,但由于这个问题没有明确询问配置安装部分,我选择将其排除在这个答案之外,因为它本身并不是一个要涵盖的微不足道的主题(只需查看"掌握 CMake"书中的相关章节有多长: 关于安装的章节,以及关于导入和导出的章节)。