1.交叉编译参考

  • 参考Cross Compiling for Linux
  • 凡有不清楚的地方,可通过cmake的document里搜索相关知识
  • 编译工具链必须设置在project函数之前
  • 建议把开发库也放到编译工具链SYSROOT里
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm) //类似confiugre 一步里的--host,一般与编译工具链的target目录有密切关联;可设置为aarch64

set(CMAKE_SYSROOT /home/devel/rasp-pi-rootfs) //设置的ROOTFS,find_xxx函数会从中寻找库/头文件;是目标平台的环境
set(CMAKE_STAGING_PREFIX /home/devel/stage) //用于ROOTFS无法写入时,指定安装路径

set(tools /home/devel/gcc-4.7-linaro-rpi-gnueabihf) //指定编译工具链:tarball解压后的路径
set(CMAKE_C_COMPILER ${tools}/bin/arm-linux-gnueabihf-gcc) //交叉编译的cc compiler
set(CMAKE_CXX_COMPILER ${tools}/bin/arm-linux-gnueabihf-g++)

// 说明可执行程序不在ROOTFS寻找,库/头文件等应该只在ROOTFS寻找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

2.find_library设置额外查询路径

  • 使用find_library函数查找需要的库
    • 在默认路径下,利用PATHS/HINTS指定额外路径即可
    • 不设置NO_DEFAULT_PATH:设置会阻止去额外路径查询
  • 类似makefile里的-Lpath -lxxx,当然要完成这个效果需要联动target_link_libraries
find_library(MOSQUITTO_LIBRARY NAMES mosquitto PATH ${DEPS_DIR})
  • 当然cmake也可以直接设置上述的"-Lpath -lxxx"
set(DEV_LINKER_FLAGS "-Lpath -lxxx“ )

3.链接动态库

  • 找到相关的库后,执行程序需要链接库
  • 使用target_link_libraries函数
  • 类似makefile里的-lxxx
target_link_libraries(${PROJECT_NAME} ${YOUR_LIBRARY})

4.设定rpath

  • 若生产环境,相关的库没有部署在系统默认路径
  • 此时,需要指明这些库的位置
  • 可使用set_target_properties函数设置
    • 其中,使用$ORIGIN,表示程序的运行路径
    • 从而达成去相对$ORIGIN的路径寻找相关库
  • 类似makefile里的-Wl,-rpath=xxx
set_target_properties(${PROJECT_NAME} PROPERTIES
    INSTALL_RPATH "$ORIGIN"
    BUILD_WITH_INSTALL_RPATH TRUE
)

5.设置编译的源文件

  • 使用file函数设置目录文件
  • 类似makefile里的vpath关键字
set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
file(GLOB SOURCES "${SOURCE_DIR}/*.c")

6.设置编译的输出路径

  • 使用set_target_properties函数输出目录
  • 类似makefile里mv指令
set(BUILD_BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/bin")
set(BUILD_LIB_DIR "${CMAKE_CURRENT_BINARY_DIR}/lib")
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BUILD_BIN_DIR})

7.设置编译的目标文件

  • 使用add_executable函数构建可执行程序
  • 使用add_library函数构建库文件
  • 类似gcc -o $@ $^
add_executable(${PROJECT_NAME} ${SOURCES})

8.设置自定义的编译选项

  • 使用target_compile_definitions函数设置自定义的编译选项
    • 比如-DSELF_DEFINITION
    • 比如-O0
  • 这个函数需要在有编译目标文件后,才可以设置
    • 在add_executable、add_library函数后面

9.设置子目录的CMakeLists.txt调用

  • 使用add_subdirectory函数指定要调用的子目录CMakeLists.txt
    • 传递子目录路径
      • 路径下有子目录的CMakeLists.txt
    • 构建后,顶层make会自动先构建子目录,再构建顶层目录
9.1 子目录的CMakeLists.txt
# 子目录subdir下的CMakeLists.txt
# 目录结构
# subdir
# - src
# 	- include
# - lib
cmake_minimum_required(VERSION 3.10)

# 将源文件添加到静态库
# mylib,构建时会自动补全为libmylib.a/libmylib.so
add_library(mylib STATIC src/file1.c src/file2.c)
# set_target_properties 设置库的输出路径

# 设置库的安装路径:头文件、库;make install
install(DIRECTORY src/include/ DESTINATION include)
install(TARGETS lib DESTINATION lib)
9.2 顶层目录调用
add_executable(myapp main.cpp)
add_subdirectory(subdir)
target_link_libraries(myapp mylib) #不必写全路径,只要指明mylib即可;自动去subdir里找libmylib.a
# 必须有target_link_libraries,否则不会自动构建该库

10.cmake推荐范例

cmake 用于快速创建构建系统即Makefile,很多的cmake内置宏,可以在生产的Makefile里看到值
cmake 生成Makefile后,执行make即可构建。
该系统,比传统的编写Makefile多了一层更容易理解的cmake封装。

# 设置cmake版本要求

# 设置编译工具:*.cmake
# 包括基础头文件、库的寻找路径

# 设置cmake项目

# 设置该项目的源码文件路径

# 设置该项目的构建空间里的输出目录

# 设置该项目的编译目标文件:可执行文件/库

# 设置编译目标文件的属性
# 自定义的编译宏
# 依赖的动态库、静态库并设置链接: *.cmake
# 构建本地的代码库:动态库、静态库 && 设置链接 : CMakeLists.txt && 一般链接仅需要xxx库名即可
# 运行库路径

# 打印cmake编译信息
# 什么项目、源码目录、输出目录等,需要的信息

# 之后调用cmake -Sxxx -Byyy生成Makefile
# 执行make/make clean等方便完成项目的构建与清理

11.CMake宏

  • CMAKE_BINARY_DIR 顶层CMakeLists.txt对应的构建目录
    • 全局变量
    • 设置后不会改变
    • 默认CMake -Sxxx -Byyy设置
    • 各层CMakeLists.txt共用一个相同值
  • CMAKE_SOURCE_DIR 顶层CMakeLists.txt所在的源码目录
    • 全局变量
    • 设置后不会改变
    • 默认CMake -Sxxx -Byyy设置
    • 各层CMakeLists.txt共用一个相同值
  • CMAKE_CURRENT_BINARY_DIR 各层CMakeLists.txt对应的构建目录
    • 一般默认为各层所在的目录
    • 可指定,在所在目录创建子目录
    • 各CMakeLists.txt独享
  • CMAKE_CURRENT_SOURCE_DIR 各层CMakeLists.txt对应的源码目录
    • 一般默认为各层所在的目录
    • 可指定,在所在目录创建子目录
    • 各CMakeLists.txt独享

12.cmake根据编译工具判定目标平台的机器长度

  • 交叉编译里,往往根据目标平台选择适配的交叉编译工具链
  • 这样就可以基于交叉编译工具链,判定目标平台的机器长度
  • 利用cmake自带的宏 CMAKE_SIZEOF_VOID_P
  • 设置目标平台的机器长度相关预编译宏
  • 就可以编写针对不同机器长度的程序代码
# 设置项目名称和最低版本要求
cmake_minimum_required(VERSION 3.10)
project(myapp)

# 添加可执行文件
add_executable(myapp main.cpp)

# 根据目标平台的指针大小设置预编译宏
if (CMAKE_SIZEOF_VOID_P EQUAL 4)
    target_compile_definitions(myapp PUBLIC MACHINE_32_BIT)
    message(STATUS "Target is 32-bit.")
else()
    target_compile_definitions(myapp PUBLIC MACHINE_64_BIT)
    message(STATUS "Target is 64-bit.")
endif()

13.设置编译器的兼容

虽然参考第8点,可设置自定义的编译选项;但如果需要兼容不同版本的编译器尚有不足。

低版本gcc 4.9.4 libc库没有local_time_r需要增加–gnu99/c99兼容
高版本gcc 13.1.0 libc库则可以正常使用local_time_r

兼容的关键在于激活-gnu99/c99的选项,cmakel里可以这样实现:

set(CMAKE_C_FLAGS_INIT "-std=gnu99 ${CMAKE_C_FLAGS_INIT}")

14.设置编译器针对机器的微调

arm版本的机器与对应的编译器:

  • 机器,为armv8的32位兼容模式,即CPU arch=7、CPU part=0xd03
  • 编译器,支持-march=armv7-a也支持-march=armv8-a
  • 如果让编译器采用其默认配置,将构建针对armv8-a的一些指令
  • 最终导致在32位兼容模式的机器上运行,出现非法指令即"illegal instruction"

解决方式,就是明确编译器-march=armv7-a -mtune=cortex-a8,这样向下兼容。默认可能-march=armv8-a -mtune=cortex-a53
cmake的设置方式如下:

target_compile_options(${PROJECT_NAME} PRIVATE -march=armv7-a -mtune=cortex-a8)

15. 动态生成version.h

为了更好地定位故障,在项目每次构建时将仓库的版本git describe写入version.h文件,从而获取实时版本信息

  • CMakeLists.txt 的工作:
    • 执行git describe --tags --always --dirty命令获取版本信息
    • 将信息写入VERSION
    • 每次构建执行目标generate_version_h:会先删除version.h,再生成version.h
    • 添加version.h进入构建系统
  • 创建version.h.in模板
    • 注意"@VERSION@" 应该与CMake里的VERSION值一致
# Generate version.h from version.h.in
execute_process(
    COMMAND git describe --tags --always --dirty
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE VERSION
    OUTPUT_STRIP_TRAILING_WHITESPACE
)

# Custom command to delete existing version.h and generate a new one
add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/version.h
    COMMAND ${CMAKE_COMMAND} -E remove -f ${CMAKE_BINARY_DIR}/version.h
    COMMAND ${CMAKE_COMMAND} -E echo "#define VERSION \"${VERSION}\"" > ${CMAKE_BINARY_DIR}/version.h
    DEPENDS ${CMAKE_SOURCE_DIR}/version.h.in
    COMMENT "Generating version.h from version.h.in"
    VERBATIM
)

# Add a custom target to ensure version.h is generated before the build
add_custom_target(generate_version_h
    DEPENDS ${CMAKE_BINARY_DIR}/version.h
)

# Add a pre-build step to ensure version.h is generated before building the executable
add_dependencies(mosquitto_simulator generate_version_h)

# Include the generated version.h in the build
target_include_directories(mosquitto_simulator PRIVATE ${CMAKE_BINARY_DIR})

16.构建项目的子模块(本地代码库)

可以把项目的全部源文件,一次性构建出目标程序或库

  • 简单快速
  • 有时不利于定位构建过程里的问题
    可以将构建拆分为子模块(库)/目标程序
  • 可定位构建过程里的问题
  • 构建更为清晰

子模块(本地代码库)构建的例子

# 定义项目名称

# 设置项目源文件

# 设置项目执行文件(欲构建的目标程序)

# 设置项目build-type

# 设置项目的系统/第三方依赖库

# 构建项目子模块(本地代码库)
# 构建本地依赖库
set(SUBMOD_SOURCE_DIR "${SOURCE_DIR}/abc")
add_subdirectory(${SUBMOD_SOURCE_DIR})
target_link_libraries(${PROJECT_NAME} abc)

# 设置构建的输出目录
...

17.设置Release/Debug版本的不同编译检查

  • Release版本使用CMake宏,CMAKE_C_FLAGS_RELEASE
  • Debug版本使用CMake宏CMAKE_C_FLAGS_DEBUG
  • 主流c编译器gcc,新出的流行编译器clang,对编译选项的设置有所差异,可以分别设置
if(CMAKE_BUILD_TYPE STREQUAL "Release")
    if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
        # GCC专用选项
        set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -Wmaybe-uninitialized -Werror=uninitialized")
    elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
        # Clang等效选项  
        set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -Wsometimes-uninitialized -Werror=uninitialized")
    endif()
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG)
    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wunused-variable")
endif()

18.链接阶段报错的排查

  • cmake将make的细节隐藏起来,让开发人员能专注于项目的构建逻辑而非细节
  • 可一旦构建失败,就需要进入被隐藏的地方进行排查
  • cmake隐藏的细节,藏在Cmakefiles目录下面
  • 针对链接阶段的报错,我们查看其中的link.txt文件即可
    • link.txt 记录了具体的链接指令
    • 这些指令可手动执行
    • 通过手动执行就可以进一步排查问题
  • 比如链接阶段报没有main入口的问题
    • 常见的原因是源码文件没有设置main函数
    • 不常见的原因是link.txt里的命令被错误拆为多行:设置-Lpath -lxxx是错误的没有使用\完成折叠
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐