目录
有了之前对cmakelists写法1.0的总结(优快云),基本上对如何构建cmakelists有了了解。最近在读一些复杂工程的时候,发现其实组织cmakelists的能力其实是架构整个工程的能力,下面将从工程化的角度,进一步做cmakelists写法的进阶介绍。
1. 文件架构的设计
回顾一下1.0中的文件结构:
|——— build
|——— include
| |—— function.h
|——— src
| |—— function.cc
|——— submodule
| |—— submodule.cc
| |—— submodule.h
| |—— CMakeLists.txt
|——— test3.cc
|——— CMakeLists.txt
这里由于test3.cc也在最外层,所以最外层的CMakeLists.txt中很多写法是对主程序test3.cc的具体执行。而对于一个大型的工程来说,往往主程序的入口不在最外层,最外层的CMakeLists.txt可以更专注于整个工程的一些总体构建思想。
下面设计一个稍微复杂一点的文件组织方式:
|——— build
|——— app
| |—— x86
| |—— test4.cc
| |—— CMakeLists.txt
| |—— arm
| |—— test4.cc
| |—— CMakeLists.txt
| |—— CMakeLists.txt
|——— modules
| |—— module1
| |—— module1.cc
| |—— CMakeLists.txt
| |—— module2
| |—— module2.cc
| |—— CMakeLists.txt
| |—— CMakeLists.txt
|——— CMakeLists.txt
这个文件组织方式有点接近实际的复杂工程,具体的逻辑代码放在modules文件夹下,可执行的入口函数放在app文件夹下,并提供x86和arm两个入口,可以实现同一套代码的交叉编译。下面来看最外层的CMakeLists.txt的一些写法。
2. 顶层CMakeLists的写法
# cmake 最小版本需求
cmake_minimum_required(VERSION 3.0.0)
project(test4 VERSION 2.0)
# 增加编译选项,通过-D在外部定义,如果没有定义,就set PLATFORM 为x86
if(NOT PLATFORM)
set(PLATFORM x86)
endif()
# 增加编译选项,通过-D在外部定义,如果没有定义,就set CMAKE_BUILD_TYPE 为Release
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# STATUS表示输出信息的级别,另外还有WARNING、AUTHOR_WARNING、SEND_ERROR和FATAL_ERROR
message(STATUS "Build for platform ${PLATFORM}")
# 在编译前确定是编译x86版本还是arm版本,需要用到的编译器名称不同
if(${PLATFORM} STREQUAL "x86")
set(CMAKE_SYSTEM_PROCESSOR x86_64)
else()
set(CMAKE_SYSTEM_PROCESSOR aarch64)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
add_compile_options(-march=armv8.2)
endif()
# 设置一些基础的编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread -fmessage-length=0")
# 根据debug还是release设置更多的编译选项
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -Og -g3 -ggdb -fsanitize=address -fsanitize-recover=address")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O2 -DNDEBUG")
# 设置输出结果的目录
set(OUTPUT_ROOT ${CMAKE_SOURCE_DIR}/output)
# 连接到下一层的CMakeLists
add_subdirectory(modules)
add_subdirectory(app)
最上层的CMakeLists看着写的比较多(实际上可以扩展更多),但是因为剥离了具体的主程序编译,所以主要功能就是三类操作:
- 设置工程名
- set()为主的各类定义(包括编译平台,编译类型,编译选项,文件路径等等)
- add_subdirectory()连接到子目录层的文件夹
这里我们着重从工程思维讲一下set,其他两项在1.0中都有具体解释。上述代码中PLATFORM是由于我们想提供交叉编译的可能,所以将其定义在最上层的CMakeLists中,并且在后续代码中,通过if判断,来设定不同的编译处理器,分别对x86和arm的平台增加不同的编译选项(调用编译器的路径也是不同的,可以通过PLATFORM字段进行区分,也可以搭配启动脚本做定义,这个后面再讲)。
另外有一个CMAKE_BUILD_TYPE字段,这个我们在调用make命令的时候,能通过-DCMAKE_BUILD_TYPE来进行赋值,这样就能在make的时候,决定编译的时候用release还是debug。并且在上面代码中设置基础编译选项后,对debug和release设置一些差异化的选项。
最后还有一些文件路径等,都可以在顶层的CMakeLists中进行设置。
3. 子层CMakeLists的写法
cmake_minimum_required(VERSION 3.0.0)
# 根据平台不同,进行赋值
if(${PLATFORM} STREQUAL "x86")
set(X86 ON)
else()
set(ARM OFF)
# 如果一个平台想编译不同功能的接口,也可以在这里再进行分支的设置
set(BASIC OFF)
# 选择连接哪个子文件夹的cmakelist
if(X86)
add_subdirectory(x86)
endif()
if(ARM)
add_subdirectory(arm)
endif()
if(BASIC)
add_subdirectory(basic)
endif()
在app文件夹下的子层CMakeLists逻辑就简单很多,主要是根据平台的不同,选择不同的子文件夹路径,这里对同一个平台,比如x86平台想编译不同功能的主函数入口,也可以从这里进行分支,主要就是找到对应平台及功能主函数的路径下的底层CMakeLists.txt。
4. 底层CMakeLists的写法
cmake_minimum_required(VERSION 3.0.0)
project(x86_test VERSION 2.0)
# 因为可能有不同的分支生成结果,所以用底层工程名来做生成结果路径下的文件夹的区分
set(PROJECT_OUTPUT_ROOT ${OUTPUT_ROOT}/${PROJECT_NAME})
# 针对当前功能主函数入口可以再增加一些编译选项的区分
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE -pie")
# 除了包含子module生成的库,还可以包含一些公共库
set(LINK_LIBS
module1
module2
# commlib
)
# 设置需要编译的文件
aux_source_directory(. APP_DIR_SRCS)
# 生成编译完成后的可执行文件
add_executable(${PROJECT_NAME} ${APP_DIR_SRCS})
# 设置编译预处理阶段头文件的查找路径(关乎源文件中#include的写法)
# 用docker的时候,有些路径需要明前定义出来
target_include_directories(${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/
# ${BUILD_SYSROOT}/lib/
# ${BUILD_SYSROOT}/usr/lib/
)
# 设置链接阶段库文件的查找路径
target_link_directories(${PROJECT_NAME} PRIVATE
${LIBRARY_OUTPUT_PATH}# 这里包含子文件夹编译成库后的结果路径
)
# 设置链接阶段需要链接的库的名称
target_link_libraries(${PROJECT_NAME} PRIVATE
${LINK_LIBS} # 需要链接的库名称在上面定义好
)
#将生成的结果安装到最终的结果文件夹中
install(
DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}/etc/
DESTINATION ${PROJECT_OUTPUT_ROOT}/etc
)
最底层的cmakelists就是最接近源文件的,那当然涉及到 aux_source_directory(. APP_DIR_SRCS) 和 add_executable(${PROJECT_NAME} ${APP_DIR_SRCS})这种基本操作。但是对于最底层的cmakelists,因为涉及到具体的编译和链接,那么最重要的就是弄清楚源文件涉及到的所有头文件路径,库文件路径和库名称,这里着重强调三个命令:
- target_include_directories:设置编译预处理阶段头文件的查找路径(关乎源文件中#include的写法)
- target_link_directories:设置链接阶段库文件的查找路径
- target_link_libraries:设置链接阶段需要链接的库的名称
弄清楚这三个命令,并且结合set()将所涉及到的路径和名称打包定义好,一个工程可用的CMakeLists体系就建立起来了。另外需要注意,add_executable命令需要放在包含target的命令之前。
上面的代码中还有install和一些生成文件结果的路径设置等,都是具体工程中可以根据实际情况修改的。