概要
在 CMake使用教程 一文中我们已经了解了使用 CMake 构建工程的一般步骤,接下来我们将了解如何在 IDE(如VS、Xcode)中组织我们的项目。
默认状态下所有没有 FOLDER 属性的构建目标都会出现在 IDE 的顶层实体下。下图为 CMake使用教程 最终(步骤7)构建的 VS 解决方案目录:
默认状态下项目结构可以看到所有生成的 12 个项目全部都在顶层解决方案下面。这在我们的项目较多时,会变得很难管理,如我们常用的 OpenCV 中共包含了 280多个项目。一般情况下,我常见的做法是使用 CMake 生成项目解决方案,然后在 IDE 中进行代码的编写和调试工作。本文我们将解决以下问题:
- 在生成的解决方案中包括头文件。
- 建立项目文件夹,将不同项目模块组织到不同文件夹中。
- 对于同一个项目的所有文件,同样按照文件层次组织到不同文件夹中。
- 安装头文件时保留文件层次关系。
本文所提到的 VS 项目(project)和 CMake 中的目标(Target)是同一个概念。以下出现这两个概念时,我们可以认为二者完全相同。
包括头文件
在之前的教程中,我们生成的解决方案只包括源文件并不会列出头文件,如果需要在 IDE 中列出头文件,只需要在执行 add_executable 或者 add_library 时添加上头文件即可。如:
add_executable (Tutorial tutorial.cxx tutorial.h)
本文我们以 CMake使用教程 最后步骤(步骤7)使用的代码为例,介绍如何在 IDE 中组织代码,该代码可以在 CMake 源码中找到。复制步骤7代码并重命名为 step8,本项目中手动编写的头文件只有 MathFunctions.h ,所以只需要添加该文件即可。这里修改 MathFunctions 文件夹下的 CMakeLists.txt 文件内容如下:
add_library(MathFunctions mysqrt.cxx MathFunctions.h)
如果我们目标文件较多且包含在多级目录下,手动的指定这些文件就会变的比较麻烦。下面代码同样能实现上面的功能,其使用的方法为递归的收集当前目录下所有文件,并去除不需要的文件:
file(GLOB_RECURSE MathFiles *.cxx *.h) # 收集当前目录下所有文件
foreach(rmFile ${MathFiles})
string(REGEX MATCH ".*/MakeTable.cxx" needRemoveFile ${rmFile})
if(needRemoveFile)
list(REMOVE_ITEM MathFiles ${needRemoveFile})
endif(needRemoveFile)
endforeach(rmFile ${MathFiles})
message(STATUS ${MathFiles}) # 打印文件列表
add_library(MathFunctions ${MathFiles})
这里首先使用 FILE 命令递归的收集所有的头文件和源文件。然后依次遍历这些文件,通过 STRING 的正则匹配去除不想收集的文件。最后在 add_library()
命令中引用这些文件。这里在引用文件之前使用 message()
命令打印出文件列表,用以验证收集的文件是否正确,该命令在调试时是否有用,调试完毕后可以删除。
参考资料:
组织项目到不同文件夹
正如前文所说,对于一个大型的解决方案其可能会包含很多项目。如果这些项目都包含在顶层实体下,会使项目的层次结构比较混乱。
接下来我们将整理这些项目,并按逻辑层次放置到解决方案的不同文件夹下。这里我们将会把 CMake 预定义的目标(如:INSTALL,PACKAGE,RUN_TESTS等等)放置到 CMakeTargets 文件夹下。将表盘工具项目放置到 CTestDashboardTargets 文件夹下。将 MakeTable,MathFunctions 放置到 MathFunctions 文件夹下。Tutorial 保留在顶层目录下。
由于并不是所有的人都需要组织项目,例如 OpenCV 项目我只关心生成的库文件,并不在意其在 IDE 中的组织形式,我甚至不会用 IDE 去打开它,所以为了加快编译速度我更希望不组织项目代码。所以这里我们将组织项目设置为可选项,在我们的顶层 CMakeLists.txt 文件夹下添加如下选项:
option(USE_SOLUTION_FOLDERS "使用资源管理器文件夹" ON)
# ----------------------------------------------------------------------------
# Solution folders:
# ----------------------------------------------------------------------------
if(USE_SOLUTION_FOLDERS)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMakeTargets")
endif()
如果我们使用资源管理器文件夹,那么我们会使用 set_property 命令设置 USE_FOLDERS 为 ON,表示使用文件夹组织我们的目标层次结构。另外设置 PREDEFINED_TARGETS_FOLDER 为 CMakeTargets,表示预定义的目标(INSTALL,PACKAGE,RUN_TESTS)会放在 CMakeTargets 文件夹中,同时表盘工具相关的预定义项目此时也会自动组织到 CTestDashboardTargets 文件夹下。
注意:必须要在顶层 CMakeLists.txt 中使用
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
命令来开启目标层次结构,才可以组织我们的项目层次。
接下来我们设置 MakeTable,MathFunctions 目标到 MathFunctions 文件夹下,修改 MathFunctions 文件夹下的 CMakeLists.txt 文件内容如下:
add_executable(MakeTable MakeTable.cxx)
set_target_properties(MakeTable PROPERTIES FOLDER "MathFunctions")
#...
add_library(MathFunctions ${MathFiles})
set_property(TARGET MathFunctions PROPERTY FOLDER "MathFunctions")
设置过程简单明了,我们只需要设置生成目标的 FOLDER 属性即可。设置的方式有两种:使用 set_target_properties 命令设置和使用 set_property 命令设置。
由于 Tutorial 目标默认文件夹为顶层解决方案,所以这里无需设置。
组织同一项目的文件结构
接下来我们将了解如何将同一项目中的文件放置在不同的文件夹中。默认状态下,CMake 将所有源文件都放在项目的 Source Files 和 Header Files 文件夹中。如果我们的项目包含多个文件则需要对我们的源文件进行分组。文件的分组使用 source_group 命令,根据个人的习惯通常有两种组织文件的方式:一种是 VS 中默认的组织形式,源文件都放到 SourFiles 文件夹中,头文件都放在Header Files 中,不同的文件目录对应的在这两个文件夹中建立;另一种是将 VS 中的文件目录和实际文件的目录对应。这里我们同样提供一个选项来根据需要进行选择,在顶层 CMakeLists.txt 中加入以下代码:
option(GROUP_BY_EXPLORER ON) # 文件分组是否和资源管理器对应
为了演示在如何进行文件分组,这里在顶层目录下添加一个 printmodel,其文件目录如下:
-step8
|- printmodel
| |- subsrc
| | |- print1.cpp
| | |- print1.h
| | |- print2.cpp
| | |- print2.h
| |- printall.cpp
| |- printall.h
|- tutorial.cxx
这里我们只关系顶层的 tutorial 目标,所以没有列出 MathFunctions 文件夹。printmodel 非常简单其任务就是打印信息。各个文件的内容如下:
print1.cpp 文件:
# include "printmodel/subsrc/print1.h"
void print1(void)
{
std::cout<<"this is print1."<<std::endl;
}
print1.h 文件:
#ifndef _PRINT1_H
#define _PRINT1_H
#include <iostream>
void print1(void);
#endif
print2.cpp 及 print2.h 文件和上面两个文件对应只是文件中的 1 替换为 2.
printall.cpp 文件:
#include "printmodel/printall.h"
void printAll(void)
{
print1();
print2();
std::cout<<"this is print all."<<std::endl;
}
printall.h 文件:
#ifndef _PRINTALL_H
#define _PRINTALL_H
#include "printmodel/subsrc/print1.h"
#include "printmodel/subsrc/print2.h"
void printAll(void);
#endif
在 tutorial.cxx 文件中加入以下内容:
#include "printmodel/printall.h"
//...
printAll();
注意到我们所有文件的包含路径都是从源文件根目录开始,所以需要在顶层项目的 CMakeLists.txt 文件中加入以下语句以添加包含路径:
include_directories(${PROJECT_SOURCE_DIR})
源文件和头文件分开组织
下面的代码实现了第一种组织文件的形式(源文件和头文件在不同文件夹中):
file(GLOB_RECURSE printmodel_SRC printmodel/*.cpp)
file(GLOB_RECURSE printmodel_HEADERS printmodel/*.h)
SET(printmodel_ALL ${printmodel_HEADERS} ${printmodel_SRC}) # 拼接获得 print model 所有文件
if (USE_SOLUTION_FOLDERS AND (NOT GROUP_BY_EXPLORER)) # 在 IDE 中对文件进行分组,源文件和头文件分开
foreach(FILE ${printmodel_ALL})
# Get the directory of the source file
get_filename_component(PARENT_DIR "${FILE}" DIRECTORY)
# Remove common directory prefix to make the group
string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "" GROUP "${PARENT_DIR}")
# Make sure we are using windows slashes
string(REPLACE "/" "\\" GROUP "${GROUP}")
# Group into "Source Files" and "Header Files"
if ("${FILE}" MATCHES ".*\\.cpp")
set(GROUP "Source Files${GROUP}")
elseif("${FILE}" MATCHES ".*\\.h")
set(GROUP "Header Files${GROUP}")
endif()
source_group("${GROUP}" FILES "${FILE}")
endforeach()
endif()
将上面代码添加到顶层 CMakeLists.txt 文件中后,生成的文件结构如下图所示:
源文件和头文件分开组织可以看到 Tutorial 项目中的源文件和头文件,按照对应的目录结构分别放置到 Source Files 和 Header Files 文件夹中了。对于 MathFunctions 由于只有一个头文件和一个源文件所以这里不需要使用 source_group
命令组织了。
上面的代码中首先使用了 FILE 命令收集了 printmodel 的源文件和头文件,然后使用 SET 命令将文件进行拼接,这里也可以直接用 FILE 命令收集全部文件。接下来根据我们所定义选项的值判断是否执行分组。当需要进行分组时执行我们这里的分组代码,分组代码参考了 这里 的回答。代码中已经给出了相关注释,如果对使用的 CMake 命令不太了解的话,可以查看官方文档:foreach ,get_filename_componet ,string ,if 。
由于之前我们将 GROUP_BY_EXPLORER
选项的值默认设置为 ON
,所以如果我们想得到上面的分组结果的话需要在 CMake 执行的过程中将该选项设置为 OFF
。我这里 CMake 执行的完整命令如下:
cmake -G "Visual Studio 15 2017 Win64" -DGROUP_BY_EXPLORER=OFF -DCMAKE_INSTALL_PREFIX=D:\project\cpp\cmake\tutorial\step8\install ..
源文件和头文件组织在同一文件夹下
有时候我们可能希望将源文件在项目中的组织结构和实际的文件结构对应,即头文件和源文件在同一目录下。下面代码实现了这个功能:
if (USE_SOLUTION_FOLDERS AND (GROUP_BY_EXPLORER)) # 在 IDE 中对文件进行分组,源文件和头文件不分开
MACRO(SOURCE_GROUP_BY_FOLDER target) # 将源文件和头文件进行分组
SET(SOURCE_GROUP_DELIMITER "/")
SET(last_dir "")
SET(files "")
FOREACH(file ${${target}_SRC} ${${target}_HEADERS})
file(RELATIVE_PATH relative_file "${PROJECT_SOURCE_DIR}" ${file})
GET_FILENAME_COMPONENT(dir "${relative_file}" PATH)
IF (NOT "${dir}" STREQUAL "${last_dir}")
IF (files)
SOURCE_GROUP("${last_dir}" FILES ${files})
ENDIF (files)
SET(files "")
ENDIF (NOT "${dir}" STREQUAL "${last_dir}")
SET(files ${files} ${file})
SET(last_dir "${dir}")
ENDFOREACH(file)
IF (files)
SOURCE_GROUP("${last_dir}" FILES ${files})
ENDIF (files)
ENDMACRO(SOURCE_GROUP_BY_FOLDER)
set(PRINT_MODEL "printmodel")
SOURCE_GROUP_BY_FOLDER(${PRINT_MODEL})
file(GLOB root_All "${PROJECT_SOURCE_DIR}" *.cxx *.h)
SOURCE_GROUP("" FILES ${root_All})
endif()
该代码生成的文件目录结构如下:
源文件和头文件一起组织此时我们的头文件和源文件都组织到了同一个文件夹中,和实际的文件结构相同。由于 GROUP_BY_EXPLORER
的选项默认为 ON,所以这里默认为这种文件组织的方式。
上面代码主要参考了 这里 ,首先我们定义了一个宏 SOURCE_GROUP_BY_FOLDER
,这个宏的功能是:指定一个当前源文件下的一个子模块(这里为 printmodel),该宏自动将该模块中的文件放置到对应的目录结构中。该宏调用需要满足以下条件:
- 输入模块的的名称,这里
set(PRINT_MODEL "printmodel")
定义了模块的名称。 - 调用模块前,已经存在
${target}_SRC
变量保存了该模块所有的源文件,在前一章节中我们已经使用file(GLOB_RECURSE printmodel_SRC printmodel/*.cpp)
收集了 printmodel 模块的所有源码文件。 - 调用模块前,已经存在
${target}_HEADERS}
变量保存了该模块所有的头文件,同样前一章节我们已经定义了该变量。
最后的 file(GLOB root_All "${PROJECT_SOURCE_DIR}" *.cxx *.h)
指令和 SOURCE_GROUP("" FILES ${root_All})
指令用于收集根目录下的文件(这里为 tutorial.cxx)并将其放置到项目根目录下。
对于 MathFunctions 文件夹中的 MakeTable 项目和 MathFunctions 项目,我们同样可以将其组织形式进行修改。由于这两个项目没有子文件夹,我们只需要收集项目根目录下的文件即可。在 MathFunctions 中的 CMakeLists.txt 文件夹下添加以下内容:
if (USE_SOLUTION_FOLDERS AND (GROUP_BY_EXPLORER))
source_group("" FILES MakeTable.cxx)
endif()
#...
if (USE_SOLUTION_FOLDERS AND (GROUP_BY_EXPLORER))
source_group("" FILES ${MathFiles})
endif()
安装头文件时保留文件层次关系
最后需要提到的一点是,因为我们的源码在包含头文件的时候使用的是相对顶层目录的路径,因此为了让安装后的文件能正确的搜索到头文件,我们设置的安装规则要保证头文件在安装后依然保留文件的层次关系。要实现这一点也非常简单只需要使用 install(DIRECTORY) 命令即可。在顶层 CMakeLists.txt 文件中添加下面命令将安装 printmodel 中的头文件到 include 文件夹中。
install(DIRECTORY printmodel DESTINATION include
FILES_MATCHING PATTERN "*.h")
网友评论