cmake 组织项目

概要

CMake使用教程 一文中我们已经了解了使用 CMake 构建工程的一般步骤,接下来我们将了解如何在 IDE(如VS、Xcode)中组织我们的项目。

默认状态下所有没有 FOLDER 属性的构建目标都会出现在 IDE 的顶层实体下。下图为 CMake使用教程 最终(步骤7)构建的 VS 解决方案目录:

默认状态下项目结构

可以看到所有生成的 12 个项目全部都在顶层解决方案下面。这在我们的项目较多时,会变得很难管理,如我们常用的 OpenCV 中共包含了 280多个项目。一般情况下,我常见的做法是使用 CMake 生成项目解决方案,然后在 IDE 中进行代码的编写和调试工作。本文我们将解决以下问题:

  1. 在生成的解决方案中包括头文件。
  2. 建立项目文件夹,将不同项目模块组织到不同文件夹中。
  3. 对于同一个项目的所有文件,同样按照文件层次组织到不同文件夹中。
  4. 安装头文件时保留文件层次关系。

本文所提到的 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经验

Listing header files in VS

组织项目到不同文件夹

正如前文所说,对于一个大型的解决方案其可能会包含很多项目。如果这些项目都包含在顶层实体下,会使项目的层次结构比较混乱。

接下来我们将整理这些项目,并按逻辑层次放置到解决方案的不同文件夹下。这里我们将会把 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")
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • CMake学习 本篇分享一下有关CMake的一些学习心得以及相关使用。 本文目录如下: [1、CMake介绍] [...
    AlphaGL阅读 12,244评论 11 79
  • 注:首发地址 1. 前言 当在做 Android NDK 开发时,如果不熟悉用 CMake 来构建,读不懂 CMa...
    cfanr阅读 24,368评论 1 53
  • 资源 本文档翻译自官方 cmake turorial 。更新日期:2018年9月27日。译者这里以 windows...
    刘亚彬92阅读 86,148评论 3 13
  • 向您的项目添加 C 和 C++ 代码 本文内容 下载 NDK 和构建工具 创建支持 C/C++ 的新项目 构建和运...
    会飞的大象_阅读 3,776评论 0 3
  • 去参加高考的孩子昨天半夜问我,“以后我该怎么走,因为有的时候我也不知道我走的如何,万一做的不好了要怎么办”。 我很...
    唐凤平凡记阅读 486评论 3 9