此文适合大型C++工程,涉及到多个自定义库,多个第三方库,以及还有给第三方用户进行二次开发的需求下,应对这种复杂编译环境下的工程组织方式的一些经验介绍,希望给大型工业软件的开发者一些参考

一个大型工程,并不会单单只包括应用自身,还有此程序抽象出来的库,这些库除了自身模块化意外,还有可能是提供给第三方用户进行插件化的开发使用的,同时工程还包含了很多第三方库

一个大型工程通常由如内容组成:

  • 第三方库(例如 OCCTVTKQt
  • 自定义库
  • Gui 程序
  • 静态资源(例如脚本、图片、配置文件)

针对这些大型的工程,如果用一些简单的构建工具,是很难做到一键编译一键安装的,例如 qmake,缺少强大的安装和依赖管理功能,所以QT6已经弃用了qmke,而转用了cmake,目前来说,在C++领域,最适合进行构建管理的还是cmake,虽然cmake 有非常非常多的缺点,但它的功能暂时是最全面的

针对大型系统目前还是推荐用cmake进行构建,通过cmake,你可以做到如下这些事情:

  • 组织庞大的工程关系
  • 自动化编译第三方库
  • 按照依赖关系自动构建出整个工程的各组件
  • 自动化安装
  • 形成一个第三方方便引入的插件开发环境

这里所谓第三方方便引入的开发环境,是能让第三方可以一键引入你的库,以及你依赖的第三方库

但是cmake的缺点也是复杂,文档生涩,语法奇葩,说到底cmake就是一种高级别的宏

下面根据我的经验,介绍一下如何通过cmake组织和构建一个大型的工程,适合大型工业软件的构建,构建出来的软件能给第三方用户方便的进行二次开发,同时,结合 gitsubmodule 管理第三方库,让整个工程变得更为简洁明了

工程的目录结构

工程的顶层文件夹应该包含如下几个文件夹:

  • src 文件夹,这个文件夹用来放置你所有的源代码
  • doc 文件夹,这个文件夹用来放置你所有的文档
  • 3rdparty 文件夹,这个文件夹用来放置你所有的第三方库,这个文件夹可以放在 src 文件夹里面,也可以放在外层目录
  • 针对整个工程的 CMakeLists.txt 文档
  • cmake 文件夹,这个文件夹放置了一些封装好的 cmake 文件,用来方便你的 cmake 的集成

上面的这些文件夹和文件是一个工程比较通用的组织结构

一般而言,在工程的顶层目录下,还会有.clang-format用于规范编码,.clang-tidy.clazy用于代码检查这些按需提供,但作为一个开源的项目,还是建议提供的

因此一个相对标准的源码目录如下图所示

第三方库的管理

3rdparty 文件夹用来放置所有的第三方库的源代码,通常来讲,第三方库源代码不应该下载下来,放进 3rdparty 文件夹,而是通过 git 的 submodule 添加进去,通过 submodule 方式添加进去的源代码,可以随时更新到 github 上的最新版本,也可以指定这个第三方库是某个固定分支或者是某个 tag

例如我这里需要使用ribbon界面,添加了SARibbon作为第三方库

git submodule add https://github.com/czyt1988/SARibbon.git ./src/3rdparty/SARibbon

注意,对于使用submodule管理第三方库的方式,首次拉取项目之后,需要执行:

git submodule update --init --recursive

把所有库拉取下来

也可以clone的时候使用–recursive参数

git clone --recursive

大部分的第三方库都提供了 cmake,如果不提供的话,我会 fork 一个,写一个带有 cmake 的版本,例如 qwt库,QtPropertyBroswer库,3rdparty 文件夹下会写一个 cmake 文件,用来集中编译所有的第三方库,一般我会在 cmake 中就指定安装目录,确保第三方库的安装目录和我的程序的安装目录是一致的,这样的好处是,如果你的程序需要给其他人进行二次开发的话,能保证你程序编译出来的库和第三方库是在一个安装环境下,这样可以解决第三方库和你自身程序库的依赖问题,不需要用户在编译你的程序之前先进行大量的第三方库的编译,只需要一次统一的编译即可把所有的第三方库安装到固定目录下,最后install后,形成一个完整的开发环境

连同第三方库一起发布的开发环境bin目录

连同第三方库一起发布的开发环境lib目录

作为第三方开发者,这个完整开发环境里面包含了所有的库,第三方开发者只需知道安装目录,就可以加载所有的依赖

下面就介绍一下,如何通过cmake实现这种大型工程的组织

大型工程的cmake写法

这里不会教你如何写cmake,而是着重讲讲大型工程的cmake要注意事项,工程顶层会有个CMakeLists.txt文件,这个文件定义了整个工程的信息、可选项、总体的安装步骤等,实现整个工程的构建,顶层的CMakeLists.txt通过add_subdirectory添加子目录,一般会添加src目录,以我自己的一个仿真集成平台data-workbench举例,介绍如何通过cmake组织一个大型的工程

上述的仿真集成平台不提供业务逻辑,所有业务逻辑都是通过插件实现,插件的开发就需要依赖此集成平台和所有第三方库

首先,如前文所述,这个工程的目录结构大致如下:

data-workbench|-doc|-cmake| |-此项目用到的cmake文件|-src| |-3rdparty| |-DAUtils (utils模块,封装了通用功能)

这里的工程使用同一前缀

在进行大型工程组织之前,cmakeinstall 命令是绝对要掌握的,而且要熟知通用的安装目录结构标准,不能过度自由的发挥,install 命令可以做下面这些事情:

  1. 复制文件或者文件夹到某个固定的目录下
  2. 导出能被其它工程 cmake 正确导入的 cmake 文件,一般是4个cmake文件:{库名}Config.cmake{库名}ConfigVersion.cmake{库名}Targets.cmake{库名}Targets-debug.cmake
  3. 能给当前这个构件树下其他的模块提供依赖支持

另外 cmake 有一个很重要的功能,可以区分构建环境和安装环境进行不同的依赖引用和头文件寻址,这样就可以区分当前的构建环境亦或是未来第三方用户,进行二次开发时候的安装环境,这两个环境的头文件寻址路径以及依赖的寻址路径是不一样的

cmakeinstall用法是比较固定的,按照一个例子或者模板非常简单的就能实现自己的安装和部署,针对大型系统一个多组件的安装是必须的,类似于QT的包引入,能进行模块的划分,不需要整个QT所有库都一起引进工程里面,针对自己的大型系统也应该实现类似的引入,因此,下面将着重介绍如何进行模块化的install

规范的安装路径

使用规范的安装路径,能让你工程的库以及第三方库安装在同一个目录下,这样你的工程就很容易被第三方使用者集成起来进行二次开发,因此,安装路径尽量使用规范化的安装路径,而不是过于自由的进行定制,一般规范化的安装路径如下:

  • bin
  • lib
  • lib/cmake
  • include

常见的cmake安装路径下的文件夹如图所示

cmake中提供了GNUInstallDirs来获取这些规范的路径命名,你可以通过include(GNUInstallDirs)导入,就可以使用:

  • CMAKE_INSTALL_BINDIR
  • CMAKE_INSTALL_LIBDIR
  • CMAKE_INSTALL_INCLUDEDIR
  • CMAKE_INSTALL_DOCDIR

这些变量了提供了标准的命名

bin目录放置编译完的二进制文件在WINDOWS系统上就是dll文件

lib文件夹放置编译后的lib文件,在WINDOWS系统下,MSVC编译器编译出来就是.lib后缀的文件

一般情况下,lib文件夹下还有一个cmake子文件夹,这个文件夹放置cmake的导出文件,通常来讲,这个文件夹下的导出文件放在它自身工程名的一个文件夹里,形成如:lib/cmake/{LibName}的文件夹结构

include文件夹主要放置头文件,通常来讲头文件也是需要放在他自身工程名的一个文件夹里,形成如:include/{LibName}的文件夹结构

基本上大部分的第三方库都是按照这个目录结构进行安装,这样当你的工程包含了大量的第三方库,以及你自身的库的情况下,最终所有的dll都会安装在bin录下,所有的库文件都会安装在lib目录下,所有的头文件都会在include文件夹下面对应的自身库名的文件夹下面,所有cmake需要用的文件都在lib/cmake文件夹下对应的自身库名的文件夹下面

以这种标准化的形式构建,第三方开发者可以很方便的使用你的工程

这里举一个例子,假如你的库名叫SARibbonBar,那么它安装后在windows系统下应该生成如下结构

bin|-SARibbonBar.dllinclude|-SARibbonBar |-SARibbonBar.h |-...所有头文件都在此文件夹下lib|-SARibbonBar.lib|-cmake|-SARibbonBar |-SARibbonBarConfig.cmake |-SARibbonBarConfigVersion.cmake |-SARibbonBarTargets.cmake |-SARibbonBarTargets-debug.cmake

install命令

cmake的install是一组命令,有多个功能

复制文件install(FILES)&install(DIRECTORY)

最简单的就是拷贝,包括install(FILES),install(DIRECTORY)这两个都是拷贝用的,一个拷贝文件,一个拷贝目录

例如,SARibbon库的Cmake文件中,头文件都在${SARIBBON_HEADER_FILES}这个变量下面,把它拷贝到安装目录下的include/${SARIBBON_LIB_NAME}路径下,就如下这样写,安装路径按照上面说的标准化走

install(FILES${SARIBBON_HEADER_FILES}DESTINATION include/${SARIBBON_LIB_NAME}COMPONENT headers)

生成cmake目标文件install(TARGETS)&install(EXPORT)

install(TARGETS)&install(EXPORT)这两个命令是要配合一起用,用来生成cmake目标文件,cmake目标文件描述了整个工程所有的依赖内容,最终生成{LibName}Targets.cmake{LibName}Targets-debug.cmake文件

这两个函数写法也比较固定,这里假如你自己的库名字叫${LIB_NAME},那么install(TARGETS)&install(EXPORT)的写法基本如下:

install(TARGETS ${LIB_NAME}EXPORT ${LIB_NAME}TargetsRUNTIME DESTINATION binLIBRARY DESTINATION libARCHIVE DESTINATION libINCLUDES DESTINATION include/${LIB_NAME})install(EXPORT ${LIB_NAME}TargetsFILE ${LIB_NAME}Targets.cmakeDESTINATION lib/cmake/${LIB_NAME})

这两个结合,把你的目标依赖内容,写到了{LibName}Targets.cmake文件中,并复制到安装目录下的lib/cmake/${LIB_NAME}下,install(TARGETS)用来把你的工程信息导出到${LIB_NAME}Targets这个变量中,install(EXPORT)${LIB_NAME}Targets这个内容,生成到${LIB_NAME}Targets.cmake文件中,同时也会生成一个{LibName}Targets-debug.cmake文件

这个文件就是第三方引入你的工程的关键,这里会把target_打头的函数相关信息写入这个文件中,例如target_compile_definitionstarget_include_directories这些函数定义的信息,也会写入${LIB_NAME}Targets.cmake文件中,一些预定义的宏和头文件路径在加载${LIB_NAME}Targets.cmake后就自动加载进来了,因此,那些宏和头文件路径要暴露给第三方的,都应该使用target_xx的函数,同时也要区分构建环境还是安装环境,如果你的CMakeLists.txt作为一个子目录,那么这时属于构建环境,尤其针对include_directories,构建环境和安装环境肯定不一样的,因此要区别对待,否则作为子工程嵌入时会出错,如下区分构建环境和安装环境的路径引用

target_include_directories(${SARIBBON_LIB_NAME} PUBLIC$$)

生成config文件configure_package_config_file

cmake的find_package函数不是加载${LIB_NAME}Targets.cmake,而是加载{LibName}Config.cmake文件,{LibName}Config.cmake文件的生成,需要用到configure_package_config_file函数,一般还会配合write_basic_package_version_file,这两个函数在CMakePackageConfigHelpers里面,需要先引入

include(CMakePackageConfigHelpers)

你会留意到好多开源项目都会有个{LibName}Config.cmake.in文件在src目录下,这个文件就是用来生成{LibName}Config.cmake文件的,{LibName}Config.cmake的作用就是加载上面生成的${LIB_NAME}Targets.cmake文件,同时能加入一些自己的扩展内容,因此,{LibName}Config.cmake.in的通用写法是:

# This module defines# @PROJECT_NAME@_FOUND, if false, do not try to link to @PROJECT_NAME@# @PROJECT_NAME@_INCLUDE_DIR, where to find the headers# @PROJECT_NAME@_LIBRARIES, where to find the libs@PACKAGE_INIT@set (PackageName @YOUR_LIB_NAME@)set (@YOUR_LIB_NAME@_VERSION @YOUR_LIB_VERSION@)include ( ${CMAKE_CURRENT_LIST_DIR}/${PackageName}Targets.cmake )set_and_check ( ${PackageName}_INCLUDE_DIR ${PACKAGE_PREFIX_DIR}/@YOUR_LIB_INCLUDE_INSTALL_DIR@ )set ( ${PackageName}_LIBRARIES)list ( APPEND ${PackageName}_LIBRARIES ${PackageName})check_required_components(${PackageName})

其中@xx@是调用这个cmake.in文件的CMakeLists文件的变量,一般会传入三个变量

  • @YOUR_LIB_NAME@ 你的库名字,也可以用PROJECT_NAME替代,看习惯
  • @YOUR_LIB_VERSION@ 你的库的版本号
  • @YOUR_LIB_INCLUDE_INSTALL_DIR@ 你的库安装后的include文件位置,这个用来检查文件的完整性

${PACKAGE_PREFIX_DIR}这个变量是在@PACKAGE_INIT@里面展开的

@PACKAGE_INIT@这个变量会展开为下面这段

####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ############## Any changes to this file will be overwritten by the next CMake run ########### The input file was DAWorkbenchConfig.cmake.in########get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)macro(set_and_check _var _file)set(${_var} "${_file}")if(NOT EXISTS "${_file}")message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")endif()endmacro()####################################################################################

@PACKAGE_INIT@相当于提供了一个宏函数和一个变量,宏函数为set_and_check,用于检测文件是否存在,变量为PACKAGE_PREFIX_DIR,用于指定工程的绝对安装路径,用过这个变量可以直接指到安装路径的顶层目录

单一模块的install写法

如果你作为一个库开发者,这个库只有一个模块,那么写法相对固定,根据上面的介绍,单一模块的install写法基本就是如下步骤:

  1. 确定库名和版本号
set(YOUR_LIB_NAME YOURLibName)set(YOUR_VERSION_MAJOR 1)set(YOUR_VERSION_MINOR 0)set(YOUR_VERSION_PATCH 0)set(YOUR_VERSION "${YOUR_VERSION_MAJOR}.${YOUR_VERSION_MINOR}.${YOUR_VERSION_PATCH}")
  1. target_xx相关函数,定义宏、头文件路径等内容
  • 注意要区分安装模式还是构建模式,通过$<INSTALL_INTERFACE:指定安装模式,通过$<BUILD_INTERFACE:指定构建模式
  1. 文件复制

复制头文件

install(FILES${YOUR_HEADER_FILES}DESTINATION include/${YOUR_LIB_NAME}COMPONENT headers)

其中${YOUR_HEADER_FILES}为你头文件的列表

通过install可以复制任意内容

4.把依赖信息导出同时生成XXTargets.cmake文件

install(TARGETS ${YOUR_LIB_NAME}EXPORT ${YOUR_LIB_NAME}TargetsRUNTIME DESTINATION binLIBRARY DESTINATION libARCHIVE DESTINATION libINCLUDES DESTINATION include/${YOUR_LIB_NAME})install(EXPORT ${YOUR_LIB_NAME}TargetsFILE ${YOUR_LIB_NAME}Targets.cmakeDESTINATION lib/cmake/${YOUR_LIB_NAME})
  1. 生成config文件
include(CMakePackageConfigHelpers)set(YOUR_LIB_INCLUDE_INSTALL_DIR include/${YOUR_LIB_NAME})write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/${YOUR_LIB_NAME}ConfigVersion.cmakeVERSION ${YOUR_VERSION}COMPATIBILITY SameMajorVersion)configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/${YOUR_LIB_NAME}Config.cmake.in""${CMAKE_CURRENT_BINARY_DIR}/${YOUR_LIB_NAME}Config.cmake"INSTALL_DESTINATION lib/cmake/${YOUR_LIB_NAME}PATH_VARS YOUR_LIB_INCLUDE_INSTALL_DIR)

上面的${YOUR_LIB_NAME}Config.cmake.in是你为了生成Config.cmake文件使用的内嵌文件,具体位置视情况而定

YOUR_LIB_INCLUDE_INSTALL_DIR变量指定了安装位置,用于进行导入检查

  1. 复制cmake文件到lib/cmake
install(FILES"${CMAKE_CURRENT_BINARY_DIR}/${YOUR_LIB_NAME}Config.cmake""${CMAKE_CURRENT_BINARY_DIR}/${YOUR_LIB_NAME}ConfigVersion.cmake"DESTINATION lib/cmake/${YOUR_LIB_NAME})

上面6步完成了一个库的安装,能把库需要的东西都放到安装目录下,通过提供的cmake文件,能找到对应的内容

使用这个库仅仅需要以下步骤:

set(${YOUR_LIB_NAME}_DIR "your-lib-install-dir/lib/cmake")find_package(${YOUR_LIB_NAME})

多模块的install写法

上面介绍了单一模块的写法,多模块有一点区别

Qt就是一个多模块的例子,Qt模块的引入是这样写的:

find_package(QT NAMES Qt6 Qt5 COMPONENTS CoreGuiWidgets)

可以按需获取对应的模块

多模块的install必须先有单模块的install,其实多模块的install就是遍历了所有单模块所生成的config文件,多模块的install 也是需要一个类似单模块的config文件,只是这个config文件和单模块不太一样,它会遍历所有子模块的Target,把这些子模块需要的头文件引用路径以及依赖的库加载进来,这样只需要调用findPackage函数就可以把这个子模块所有需要的内容加载进来

对于多模块,你的工程目录可能是这样的

root├src│├─module-1││ └─CMakeLists.txt│├─module-2││ └─CMakeLists.txt│...│├─module-n││└─CMakeLists.txt│├─CMakeLists.txt│└─LibConfig.cmake.in│CMakeLists.txt└PackageConfig.cmake.in

这里有两个cmake.in文件,一个是LibConfig.cmake.in这个是给各个独自模块公用的,名字不固定,如果每个模块有特殊处理,可以用自己的,这个非必须,有个通用的方便一点,写法和单一模块的config文件写法一致

但这个单一模块在构建时,应该加入命名空间,避免冲突,这时候,你的库在定义过程中应该如下:

add_library(${YOUR_LIB_NAME} SHARED${YOUR_LIB_HEADER_FILES}${YOUR_LIB_SOURCE_FILES})# 定义别名让YourNameSpace::${DA_LIB_NAME}也能获取到add_library(YourNameSpace::${YOUR_LIB_NAME} ALIAS ${YOUR_LIB_NAME})

在安装过程中,生成的XXTargets.cmake也可以加个前缀也可以不加,只要能保证不重命名即可

要实现模块化,最重要的是PackageConfig.cmake.in这个文件,这个文件作用是组织所有的模块,此cmake.in名字不固定,方便记忆即可

此文件用来生成整个模块包的Config.cmake文件,效果和单一模块写法类似,但它是遍历加载所有模块的{LibName}Targets.cmake文件,cmake的find_package原理就是找到对应的xxConfig.cmake文件并加载,如果是下面这段命令

find_package(Qt5 COMPONENTS CoreGuiWidgets)

它加载的是Qt5Config.cmake,因此,看看Qt5Config.cmake是如何实现的,就知道如何写package相关的Config.cmake文件

Qt5Config.cmake的主要工作代码段是:

foreach(module ${Qt5_FIND_COMPONENTS})find_package(Qt5${module}${_Qt5_FIND_PARTS_QUIET}${_Qt5_FIND_PARTS_REQUIRED}PATHS ${_qt5_module_paths} NO_DEFAULT_PATH)if (NOT Qt5${module}_FOUND)string(CONFIGURE ${_qt5_module_location_template} _expected_module_location @ONLY)if (Qt5_FIND_REQUIRED_${module})set(_Qt5_NOTFOUND_MESSAGE "${_Qt5_NOTFOUND_MESSAGE}Failed to find Qt5 component \"${module}\" config file at \"${_expected_module_location}\"\n")elseif(NOT Qt5_FIND_QUIETLY)message(WARNING "Failed to find Qt5 component \"${module}\" config file at \"${_expected_module_location}\"")endif()unset(_expected_module_location)endif()endforeach()

Qt5_FIND_COMPONENTS是调用find_package(Qt5 COMPONENTS Core Gui Widgets)命令COMPONENTS后面的内容列表

其实就是遍历这些components,逐个find_package

find_package(Qt5 COMPONENTS Core Gui Widgets)这段最后相当于执行了

find_package(Qt5Core)find_package(Qt5Gui)find_package(Qt5Widgets)

一般没有哪个库有Qt如此大的规模,因此我们可以适当简化,这里给出一个简单的模块化cmake写法

对于模块化的cmake,首先要有个总的进入文件,以YourPackage命名,像Qt5就叫Qt5Config.cmake,自己模块就叫{YourPackageName}Config.cmake

一般{YourPackageName}Config.cmake会通过{YourPackageName}Config.cmake.in模板生成,一个相对通用的写法如下:

@PACKAGE_INIT@include(CMakeFindDependencyMacro)# 这里PROJECT_NAME就作为包名set(_package_name @PROJECT_NAME@)# 这里要修改为所支持的模块名set(_${_package_name}_supported_components Module1 Module2 Module3 ... ModuleN)# 遍历所有要导入的模块foreach(_component ${${_package_name}_FIND_COMPONENTS})# 首先判断是否在所支持列表中if(_component IN_LIST _${_package_name}_supported_components)set(__target ${_package_name}::${_component})if(TARGET ${__target})# 避免重复加载continue()else()find_package(${_component} REQUIRED PATHS ${CMAKE_CURRENT_LIST_DIR})endif()else()set(${_package_name}_FOUND FALSE)set(${_package_name}_NOT_FOUND_MESSAGE "Unknown component: ${__target}.")break()endif()endforeach()

在你的工程的顶层目录的CMakeLists里,对此config文件进行生成即可

因此,对于多模块的install写法,总结步骤如下

  1. 各自单一模块实现各自的install,参考单一模块install写法,各个子模块的安装路径为lib/cmake/${TOP_PROJECT_NAME}目录

${TOP_PROJECT_NAME}是顶层CMakeLists文件的project名称,按实际工程需求传递到子模块的CMakeLists中,可以通过变量或者固定

  1. 编写{YourPackageName}Config.cmake.in模板,模板内容参考上文描述

  2. 在工程的顶层目录的CMakeLists里,生成模块的{YourPackageName}Config.cmake文件:

include(CMakePackageConfigHelpers)write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"VERSION ${PROJECT_VERSION}COMPATIBILITY AnyNewerVersion)configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake.in""${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"INSTALL_DESTINATION lib/cmake/${PROJECT_NAME}NO_CHECK_REQUIRED_COMPONENTS_MACRO)

这里${PROJECT_NAME}是工程名称,也可以使用自定义的名字

  1. 把生成的{YourPackageName}Config.cmake文件复制到lib/cmake/${PROJECT_NAME}目录下,和子模块的路径一致
install(FILES"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake""${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"DESTINATION lib/cmake/${PROJECT_NAME})

最终子模块的Config.cmake和包的Config.cmake都在lib/cmake/${PROJECT_NAME}

工程的组织

至此,单模块和多模块的安装都已介绍完成,大型工程的组织和安装就是这两者的组合

工程各个模块安装到固定目录下,连同第三方库指定同一个安装路径,最终形成一个完整的开发环境

这里以实际例子举例,例子源码位于:

github:https://github.com/czyt1988/data-workbench

gitee镜像:https://gitee.com/czyt1988/data-workbench

源码目录结构(这里为了便于显示,文件夹用[]扩起):

[root]├[src]│ ├─[3rdparty]│ │ ├─[spdlog]│ │ ...│ │ ├─[SARibbon]│ │ └─CMakeLists.txt(用于构建和安装第三方库)│ ├─[DAUtils]│ │ └─CMakeLists.txt│ ├─[DAGui]│ │ └─CMakeLists.txt│ ...│ ├─[APP]│ │└─CMakeLists.txt│ ├─CMakeLists.txt│ └─DALibConfig.cmake.in(用于给各个子模块生成Config.cmake文件)├─CMakeLists.txt└─DAWorkbenchConfig.cmake.in(用于生成总包的Config.cmake文件)
  1. 指定统一的安装目录

这一步可以使得第三方库和工程安装的位置一致,对于linux有比较规范的安装路径,但windows不一样,默认是在C:\Program Files\xxx这样的位置,没有统一放lib的地方,因此,windows下,个人习惯指定工程的自身目录下建立一个安装目录,以bin_{Debug/Release}_{x32/x64}的方式命名,如果有Qt,还会加上Qt的版本以作区分,如下所示:

# 获取qt版本find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)# 平台判断if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")set(my_platform_name "x86")else()set(my_platform_name "x64")endif()# 生成安装目录名称set(my_install_dir_name bin_qt${QT_VERSION}_${CMAKE_BUILD_TYPE}_${my_platform_name})# 设置固定的安装目录路径,具体位置具体设置,这里设置为当前cmake文件所在目录set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_LIST_DIR}/${my_install_dir_name}")
  1. 第三方库

如前文所述,第三方库都在src/3rdparty下面,首先需要的是对第三方库的编译,3rdparty有个CMakeLists.txt文件夹用于编译安装所有第三方库,个人习惯不把3rdparty下的CMakeLists.txt纳入顶层工程的subdirectory中,因为不保证所有第三方库的cmake写的都正常,第三方库的CMakeLists.txt指定了CMAKE_INSTALL_PREFIX和顶层工程一致,确保安装路径一致

  1. 组织顶层工程

顶层工程CMakeLists主要负责做以下事情:

  • 定义option
  • 定义工程名称
  • 做全局的编译设置,如c++版本要求,编译环境的POSTFIX设置
  • 通过add_subdirectory完成整个工程的组织
  • 工程模块化的安装(见多模块的install写法)
  • 工程的完整安装

第三方用户引入的方式

对于第三方插件开发者来说,首先需要clone你的工程,并进行编译,先编译第三方库,并进行安装(install),再编译工程,并进行安装(install),这时候,第三方开发者就可以有一个完整的开发环境了