开始之前
参与到一个项目时,往往因为需求而去快速Get某些技能,并将之应用到实际的项目中,慢慢的对这些知识越来越熟悉,有时候准备把Get到的这些知识记录下来,但静下来想想要把这些知识点写全也不太容易,而且自我感觉应该不会忘的,所以把笔记的事情没当回事。时间一天天过去,当我们参与到其他项目之中,开始了新的关键技术分析和概要设计,当初Get到的那些技能由于不经常使用而慢慢退化,当某个时间点再次需要这些技能时,开始在大脑中搜索,what fuck???除了有个概念,什么都记不起来,那些之前踩过的坑,又会再踩一遍,那些曾经困惑的知识点,又会再次感到困惑,天道好轮回,苍天绕过谁啊!!!CMake有很多人在用,现在Github上大部分工程都支持CMake编译,当我自己构建工程时还是遇到过很多问题,但能搜索到的知识点还是太少,只能去研究cmake的官方英文文档,所以这里把重要的知识点记录一下。
背景
项目工程目录介绍
工程目录树如下图所示,微服务的项目工程结构,common目录是多个服务(services)公共的依赖,services目录主要存放多个服务,包括服务1(service1)、服务2(service2)、… 服务n,每个单服务就是一个应用,意味着均可编译产生一个可执行程序。service1的子目录包含头文件目录inc和源文件目录src以及用于单元测试的test目录,inc和src的下一级子目录是针对源码的分类,包括common、controller、dao、interface、service。test目录下存放所有的测试用例。并且测试用例依赖第三方库gtest。thirdpart主要存放用于本项目的第三方源码
+-- common
| +-- inc
| -- common_guide.h
| +-- src
| -- common_guide.cpp
| -- CMakeLists.txt
+-- services
| +-- services1
| +-- inc
| +-- common
| -- cfg_common_guide.h
| +-- controller
| -- controller_guide.h
| +-- dao
| -- dao_guide.h
| +-- interface
| -- interface_guide.h
| +-- service
| -- service_guide.h
| +-- src
| +-- common
| -- cfg_common_guide.cpp
| -- CMakeLists.txt
| +-- controller
| -- controller_guide.cpp
| -- CMakeLists.txt
| +-- dao
| -- dao_guide.cpp
| -- CMakeLists.txt
| +-- interface
| -- interface_guide.cpp
| -- CMakeLists.txt
| +-- service
| -- service_guide.cpp
| -- CMakeLists.txt
| -- main_guide.cpp
| +-- test
| -- test_main.cpp
| -- test_interface.cpp
| -- CMakeLists.txt
| -- CMakeLists.txt
| +-- service2
+-- thirdpart
| +-- googletest
| -- CMakeLists.txt
-- CMakeLists.txt
部分展开后的样子
编译诉求
- 将services目录下的所有service编译成对应的可执行文件
- 一键启动编译所有services,不需要手工触发来编译
- service/service1/test目录下的CMakeLists.txt只需要创建工程,不需要连带编译,用户自己手工选择编译
依赖关系
- CMakeLists.txt依赖于services/service1/CMakeLists.txt
- services/service1/CMakeLists.txt会产生目标可执行文件,但产生目标可执行文件时会依赖services/service1/src/common/CMakeLists.txt、services/service1/src/controller/CMakeLists.txt、services/service1/src/dao/CMakeLists.txt、services/service1/src/interface/CMakeLists.txt、services/service1/src/service/CMakeLists.txt所产生的静态库
- services/service1/src/common/CMakeLists.txt、services/service1/src/controller/CMakeLists.txt、services/service1/src/dao/CMakeLists.txt、services/service1/src/interface/CMakeLists.txt、services/service1/src/service/CMakeLists.txt这些CMake文件主要职责就是将当前目录下的所有源文件打包成静态库,这些静态库会对common/src/CMakeLists.txt依赖
CMake构建
CMakeLists.txt
cmake_minimum_required (VERSION 3.14)
set(PROJECT_NAME "services")
project(${PROJECT_NAME})
add_subdirectory(common/src)
add_subdirectory(services/services1)
add_subdirectory(services/services2)
add_subdirectory(thirdpart/googletest)
add_custom_target(${PROJECT_NAME}
DEPENDS services1
DEPENDS services2)
简要说明:设置cmake最低的版本和设置工程名称就不再说了,主要说一下add_subdirctory和add_custom_target
- add_subdircetory 解释说明:
增加子目录,我的理解是cmake构建时,发现有这条命令,就进入对应的子目录,然后根据子目录下的CMakeLists.txt进行构建。我这里说的构建,不同于编译,他可能仅仅只是跳到子目录中,然后根据子目录的CMakeLists.txt自动生成Makefile文件。但并不会自动针对子目录进行编译。 - 我们为什么要在这里添加add_subdirectory?
在我们编译开始之前,我们的目标需要依赖于下级子目录的目标,所以cmake必须先行构建子目录,才能识别你依赖的目标在子目录中存不存在,名称是否匹配,这个我们需要依赖/common/src,services/service1,service/service2,以及thirdpart/googletest,有同学会问,我们为什么需要第三方库的googletest呢?这是因为我们自己的test工程需要依赖gtest的库,如果在这里不去将googletest的cmake加入进来的话,那么我们自己的test工程中依赖的"gtest"这个名称是无法识别的。在各级目录中只有把你需要用到的子目录加入进来,那么在产生目标时,所依赖的目标才能被正确识别,否则无法找到。 - add_custom_target 解释说明:
增加定制化的目标,这里的目标不会自动被创建,仅仅只是一个称谓。比如这里的${PROJECT_NAME},并不会在你的输出目录中生成一个${PROJECT_NAME}名称的可执行程序文件。利用这个特性,我们可以来创建编译的入口,编译就从这里开始,由于依赖项为service1和service2,而在这之前我们已经add_subdirectory(services/services1)和add_subdirectory(services/services2),相当于把serivice1和service2的名称以及记住了,所以能够很好的理解我们的依赖项在什么位置了,该跳转到哪个目录去执行了。
services/service1/CMakeLists.txt
cmake_minimum_required (VERSION 3.14)
set(PROJECT_NAME "services1")
project(${PROJECT_NAME})
set(PUBLIC_COMMON_PATH ${CMAKE_HOME_DIRECTORY}/common/inc)
set(INNER_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/inc)
set(OUTPUT_PATH ${CMAKE_CURRENT_BINNARY_DIR})
set(DEPENDS_HEADER_PATH)
list(APPEND DEPENDS_HEADER_PATH ${PUBLIC_COMMON_PATH})
list(APPEND DEPENDS_HEADER_PATH ${PUBLIC_COMMON_PATH}/common)
list(APPEND DEPENDS_HEADER_PATH ${PUBLIC_COMMON_PATH}/controller)
list(APPEND DEPENDS_HEADER_PATH ${PUBLIC_COMMON_PATH}/dao)
list(APPEND DEPENDS_HEADER_PATH ${PUBLIC_COMMON_PATH}/interface)
list(APPEND DEPENDS_HEADER_PATH ${PUBLIC_COMMON_PATH}/service)
macro(include_headers)
foreach (HEADER_PATH ${DEPENDS_HEADER_PATH})
include_directories(${HEADER_PATH})
endforeach ()
endmacro()
macro(compile_module MODULE)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_PATH}/lib)
include_headers()
aux_source_directory(. DIR_SRCS)
add_library(${MODULE} ${DIR_SRCS})
endmacro()
macro(compile_target MODULE)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_PATH}/bin)
include_headers()
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src DIR_SRCS)
add_executable(${MODULE} ${DIR_SRCS})
add_dependencies(${MODULE} interface controller service cfg_common common dao)
target_link_libraries(${MODULE} interface controller service cfg_common common dao)
endmacro()
add_subdirectory(src/common)
add_subdirectory(src/controller)
add_subdirectory(src/services)
add_subdirectory(src/dao)
add_subdirectory(src/interface)
add_subdirectory(test)
compile_target(${PROJECT_NAME})
- PUBLIC_COMMON_PATH
实际路径为common/inc,这个的CMAKE_HOME_DIRECTORY变量是最外层目录,也是工程的入口目录。这个定义PUBLIC_COMMON_PATH这个变量主要是为了方便后面宏内代码使用,不用每次都去写全路径 - INNER_INCLUDE_PATH,OUTPUT_PATH
INNER_INCLUDE_PATH是指service1内部的inc目录,为了方便后面宏内代码使用,不用每次都去写全路径。OUTPUT为输出路径,即编译产生的静态库或者可执行文件的输出路径。这里需要注意的是CMAKE_CURRENT_SOURCE_DIR以及CMAKE_CURRENT_BINNARY_DIR的区别。CMAKE_CURRENT_SOURCE_DIR目录为源码目录,就是当前CMakefile所在的目录。而CMAKE_CURRENT_BINNARY_DIR目录是二进制的目录,有的同学说,这有什么区别?我举个例子,一般我们通过cmake编译时都不会直接在源码目录下进行编译,这样会将我们的源码目录污染,产生很多工程相关的文件,我们一般会在源码目录中创建一个build目录,然后在build目录运行cmake,这样子cmake会将所有工程相关的文件拷贝过去,但是这个build目录下仅仅包括工程相关的文件(比如Makefile),并不包括源码。那么CMAKE_CURRENT_BINNARY_DIR这个目录就是build目录下对应CMakeLists.txt的位置。所以我们在引用头文件时映射的目录是源码目录,而编译输出的目标为二进制目录。 - 宏include_headers
由于在构建静态库或可执行程序时,均需要引用相关的头文件目录,为了代码简洁,直接采用宏的方式,宏内部采用for循环逐个执行include_directories来包含依赖的头文件。 - 宏compile_module
主要实现的功能是编译静态库。修改变量CMAKE_ARCHIVE_OUTPUT_DIRECTORY来变更静态库的输出目录,这里我们把所有的静态库都输出lib目录下。aux_source_directory(. DIR_SRCS)表示查找当前目录的所有源文件,这里需要注意的时,他并不会循环搜索子目录中的源文件,如果你需要搜索子目录的源文件,请加上子目录的路径。这里由于是编译库文件,所以没有增加对应的依赖项,都是打库,所以并不care是否有对应的实现,只要有头文件即可。 - 宏compile_target
主要实现的功能是编译可执行文件。首先设置可执行文件的输出路径,这里使用变量CMAKE_RUNTIME_OUTPUT_DIRECTORY来修改你的输出路径,不同于静态库哦!!!我们的编译目标所依赖的源码不再是当前目录下了,而是当前目录下的子目录src,所以我们这样来使用aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src DIR_SRCS)。最后两个点就是add_dependencies和target_link_libraries了,首先添加依赖是由于我们的在编译可执行程序之前,还没有编译各个静态库,所以如果你不添加依赖库,第一次编译时会导致编译失败。添加了依赖之后,就相当于告诉cmake,我所依赖的库文件有这些,如果他们不存在的话,则先去编译静态库吧。target_link_libraries则表明我编译目标可执行文件时需要链接哪些静态库,虽然前面有了依赖关系,但是并没有告诉cmake链接的时候要链接哪个库啊,所以才有了target_link_libraries。也正是有了这两项,才能使cmake自动run起来,知道先去哪个后去哪。 - add_subdirectory
添加子工程,虽然上面好像已经在编译,在链接了,但仅仅只是宏定义,并没有实际调用,所以在实际调用这些宏之前,需要先把个子目录的工程文件加载进来。这个test工程加载进来了,但是我们的目标并没有对test工程进行依赖,所以他不会自动编译出自动化用例的程序,需要你自己手工点击编译才会编译。这也符合我们的诉求。 - 调用compile_target
最后我们调用compile_target来执行编译动作。
services/service1/src/common/CMakeLists.txt
set(MODULE "cfg_common")
compile_module(${MODULE})
services/service1/src/controller/CMakeLists.txt
set(MODULE "controller")
compile_module(${MODULE})
services/service1/src/dao/CMakeLists.txt
set(MODULE "dao")
compile_module(${MODULE})
services/service1/src/interface/CMakeLists.txt
set(MODULE "interface")
compile_module(${MODULE})
services/service1/src/service/CMakeLists.txt
set(MODULE "service")
compile_module(${MODULE})
services/service1/test/CMakeLists.txt
set(PROJECT_NAME "test_sample")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_PATH}/bin)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g -Wall -Wno-deprecated")
include_headers()
include_directories(${CMAKE_HOME_DIRECTORY}/thirdpart/googletest/googletest/include)
aux_source_directory(. DIR_SRCS)
add_executable(${PROJECT_NAME} ${DIR_SRCS})
add_dependencies(${PROJECT_NAME} gtest interface controller service cfg_common common dao)
target_link_libraries(${PROJECT_NAME} gtest interface controller service cfg_common common dao)
这里我们的测试用例需要用到gtest库,而gtest库需要c++11,所以我们在编译选项增加了c++11的标识。另外依赖于链接时增加了gtest库,这里为什么能够识别gtest这个名字呢?因为一开始我们就将thirdpart/googletest的cmake加载进来了,所以这里他能够识别gtest这个目标名称。
common/src/CMakeLists.txt
aux_source_directory(. DIR_SRCS)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../lib)
include_directories(${CMAKE_HOME_DIRECTORY}/common/inc)
add_library(common ${DIR_SRCS})
其他说明
- 工程名不能重复,cmake 工程中,工程名称不能相同
- 上级CMakeLists.txt定义的变量或者宏,子目录中可以直接使用