用CMake构建中等规模的工程

主要涉及如下主题:

    1. 目录布局
    1. 定义全局编译选项
    1. 交叉编译
    1. 编译开发者自己的库
    1. 编译第三方软件
    1. 引用库
    1. 编译可执行程序
    1. 安装配置文件
    1. 安装启动脚本
    1. CPack打包

如下是示例工程:

git clone https://github.com/sronzheng/cmake-demo.git

1. 目录布局

目录布局包括两方面,一是源代码的布局,二是部署到目标系统后的布局。

源代码布局如下:

  • cmake是自定义的CMake脚本,3th-party是第三方软件,platform是依赖特定平台特性的库,common是平台无关的库,apps是应用程序,scripts是软件包的脚本,如启动脚本等。
  • 库的目录下:
    • include是对外开放的C++头文件,src下是只在库内部使用的C++头文件和实现文件,test是单元测试程序,example是使用库的例子。
  • apps的程序目录下:
    • src是C++头文件和实现文件, etc是配置文件。
├── cmake
├── 3th-party
│   ├── gtest
│   ├── jsoncpp
│   └── protobuf
├── platform
├── common
│   └── arithm
│       ├── include
│       ├── src
│       ├── test
│       └── example
├── apps
│   └── hello
│       ├── etc
│       └── src
└── scripts

部署后的布局为:

  • include是库的头文件,lib是库,etc是配置文件,bin是程序和启动脚本等。
├── include
├── lib
├── etc
└── bin

2. 定义全局编译选项

如果只改变单个target的编译选项,用add_definitions()就可以。如果是改变针对所有的target,更好的办法是设置CMake的变量:CMAKE_C_FLAGS和CMAKE_CXX_FLAGS,它们分别用于gcc和g++编译。

# BuildType.cmake
 
set (CMAKE_C_FLAGS           "-Wall -std=c++11 -pthread")
set (CMAKE_C_FLAGS_DEBUG     "-g -ggdb")
set (CMAKE_C_FLAGS_RELEASE   "-O2")

set (CMAKE_CXX_FLAGS         "-Wall -std=c++11 -pthread")
set (CMAKE_CXX_FLAGS_DEBUG   "-g -ggdb")
set (CMAKE_CXX_FLAGS_RELEASE "-O2")

3. 交叉编译

在嵌入式开发中几乎总是要使用交叉编译,而且还可能需要编译到不同的目标平台。切换到新的目标平台时,整个编译环境,包括编译工具链,系统库等,都会改变。

可以用CMake选项CMAKE_TOOLCHAIN_FILE指定一个文件,在这个文件中指定编译环境。

cmake -DCMAKE_TOOLCHAIN_FILE=ArmToolchain.cmake .

如下是一个CMAKE_TOOLCHAIN_FILE的例子。

# 只有指定这个变量,CMake才认为是在进行交叉编译
set (CMAKE_SYSTEM_NAME Linux) 

# 指定编译器
set (CMAKE_C_COMPILER /opt/arm/bin/arm-none-linux-gnueabi-gcc)
set (CMAKE_CXX_COMPILER /opt/arm/bin/arm-none-linux-gnueabi-g++)

# 指定目标环境的根目录
set (CMAKE_FIND_ROOT_PATH /opt/arm)

# FIND_PROGRAM()使用宿主机的程序,而不是目标环境的
set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# FIND_LIBRARY()使用目标环境的库
set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)

# FIND_PATH()和FIND_FILE() 使用目标环境的库
set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

4. 编译开发者自己的库

编译库最小的要求是指出需要哪些源文件,还有头文件目录。这样就能make得到库了。

include_directories (../include .)
add_library (arithm SHARED Arithm.cpp)

如果要支持make install或CPack打包,则还需要指出库的安装目录。这里将arithm库安装在lib下。

install (TARGETS arithm 
         DESTINATION lib)

直接在CMake语句中列出头文件目录,源代码文件或库的做法,不是很好看。更好的办法是用set()定义一组变量代表它们。

5. 编译第三方软件

ExternalProject_Add()用于引入外部软件包,它来自CMake的ExternalProject模块。它创建一个定制的target,并完成构建的一系列步骤:下载 - 更新 - 配置 - 编译 - 安装 - 测试。

如下是编译Google Test的例子。Google Test本身用CMake编译。

  • URL项指出了gtest-release-1.8.0.tar.gz的地址,这里已经把它下载到本地源代码目录下,这是避免下载,节省时间的通常做法。
  • PREFIX项指出了编译输出目录的位置;
  • CMAKE_COMMAND项指定配置工具CMake,CMAKE_ARGS项是CMake的参数;
  • BUILD_COMMAND项指定编译工具make,INSTALL_COMMAND项指定安装工具make install;
  • gtest-release-1.8.0.tar.gz将在编译目录下展开,并用in-source build的方式编译。所以BUILD_IN_SOURCE项的值是1。
# 引入ExternalProject模块
include (ExternalProject)

ExternalProject_Add (gtest
    PREFIX 3th-party/gtest
    URL ${CMAKE_SOURCE_DIR}/3th-party/gtest/src/gtest-release-1.8.0.tar.gz
    CMAKE_COMMAND cmake
    CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/3th-party/gtest
    BUILD_COMMAND make
    INSTALL_COMMAND make install
    BUILD_IN_SOURCE 1)

如果编译成功,就可以在${CMAKE_BINARY_DIR}/3th-party/gtest下找到GoogleTest的头文件和库了。

另外一个例子是ProtoBuf。ProtoBuf按照老式的configure方式配置。

  • 主要的不同是用CONFIGURE_COMMAND代替CMAKE_COMMAND;
  • 如果是交叉编译,需要手工提供configure的参数,而不是直接传递${CMAKE_TOOLCHAIN_FILE};
if (TARGET_ARCH STREQUAL "ABC")
    set (PB_CMD_ARGS_CROSS --host=arm-linux-gnueabi)
endif()

set (PB_CMD_ARGS --with-protoc=/usr/bin/protoc
                        --disable-static
                        --enable-shared)

ExternalProject_Add (protobuf
    PREFIX 3th-party/protobuf
    URL ${CMAKE_SOURCE_DIR}/3th-party/protobuf/src/protobuf-2.4.1.tar.gz
    CONFIGURE_COMMAND ./configure ${PB_CMD_ARGS_CROSS}  ${PB_CMD_ARGS} --prefix=${CMAKE_BINARY_DIR}/3th-party/protobuf
    BUILD_COMMAND make
    INSTALL_COMMAND make install
    BUILD_IN_SOURCE 1)

如下是给make install准备的脚本,将protobuf安装在lib目录下。注意并没有给GoogleTest的安装脚本,因为GoogleTest不需要安装到目标系统中。

install (DIRECTORY ${PROTOBUF_DIR}/include/google/protobuf/
         DESTINATION include/protobuf)

install (DIRECTORY ${PROTOBUF_DIR}/lib/
         DESTINATION lib)

6. 引用库

编译好库之后,下一步是让其他库或程序方便地引用它。一个好的做法,是定义一组变量表示库的头文件目录和库。

开发者自己的库是以CMAKE_SOURCE_DIR为基础的。

set (MODULE_ROOT_DIR     ${CMAKE_SOURCE_DIR})
set (COMMON_DIR          ${MODULE_ROOT_DIR}/common)

set (ARITHM_LIBRARY      arithm)
set (ARITHM_DIR          ${COMMON_DIR}/arithm)
set (ARITHM_INCLUDE_DIR  ${ARITHM_DIR}/include)

引用第三方库与引用用户自己的库类似,只是它以CMAKE_BINARY_DIR为基础,因为第三方库是在CMAKE_BINARY_DIR中展开源代码的。

set (EXTERN_MODULE_ROOT_DIR  ${CMAKE_BINARY_DIR})
set (3TH_PARTY_DIR           ${EXTERN_MODULE_ROOT_DIR}/3th-party)

set (GTEST_DIR               ${3TH_PARTY_DIR}/gtest)
set (GTEST_INCLUDE_DIR       ${GTEST_DIR}/include)
set (GTEST_LIBRARY           ${GTEST_DIR}/lib/libgtest.a)

set (PROTOBUF_DIR            ${3TH_PARTY_DIR}/protobuf)
set (PROTOBUF_INCLUDE_DIR    ${PROTOBUF_DIR}/include)
set (PROTOBUF_LIBRARY        ${PROTOBUF_DIR}/lib/libprotobuf.so)

7. 编译可执行程序

如下编译可执行程序,这里

  • 使用set()定义了一组变量让脚本看起来更整齐;
  • 使用了前面定义的库变量ARITHM_INCLUDE_DIR和ARITHM_LIBRARY。
set (TARGET hello)

set (INC_DIRS 
    ${ARITHM_INCLUDE_DIR}
    . )

set (LIBS
    ${ARITHM_LIBRARY} )

set (SRCS
    Hello.cpp )

include_directories (${INC_DIRS})
add_executable (${TARGET} ${SRCS})
target_link_libraries (${TARGET} ${LIBS})

如下是给make install准备的脚本,将demo安装在bin目录下:

install (TARGETS ${TARGET} 
         DESTINATION bin)

8. 安装配置文件

很多程序可以依赖配置文件提供初始设置。一般把程序的配置文件放在一个目录下,如etc,让make install安装整个目录:

install (DIRECTORY etc/
         DESTINATION etc)

9. 安装启动脚本

软件包一般会需要一个启动脚本和一个停止脚本,把它们放在scripts目录下。需要make install安装它们,安装到bin目录下。

set (SCRIPT
     start.sh
     stop.sh)

install (PROGRAMS ${SCRIPT} 
         DESTINATION bin)

10. CPack打包

CPack模块是CMake提供的工具,用于配置生成各种安装包,包括二进制安装包和源码安装包。

  • InstallRequiredSystemLibraries模块是CPack所依赖的,所以要先导入;
  • CPack变量CPACK_PACKAGE_VERSION_XXX指出安装包的版本信息,用户要给出它们的值;
  • 安装包的名字格式为<pkg_name>.<version_major>.<version_minor>.<version_patch>.tar.gz,比如demo.1.22.3333.tar.gz。
include (InstallRequiredSystemLibraries)

# 安装包名字,如果不指定,则使用project()指定的名字
set (CPACK_PACKAGE_NAME demo)

# 版本信息
set (CPACK_PACKAGE_VERSION_MAJOR "${VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${VERSION_MINOR}")
set (CPACK_PACKAGE_VERSION_PATCH "${VERSION_PATCH}")
include (CPack)

可以在一个C++头文件Version.hpp指定版本信息。这样各个模块可以用#include直接引用,并在启动时打印这些信息;同时CMake也可以从文件解析版本信息,并传递给CPack。

 // Version.hpp

#define DEMO_VERSION_MAJOR 1
#define DEMO_VERSION_MINOR 0
#define DEMO_VERSION_PATCH 0

用file()从Version.hpp读入版本信息,并用string()解析出将要传递给CPack的变量。

file (READ Version.hpp VERSION_STR)

string (REGEX REPLACE ".*#define +VERSION_MAJOR +([0-9]+).*" "\\1" VERSION_MAJOR "${VERSION_STR}")
string (REGEX REPLACE ".*#define +VERSION_MINOR +([0-9]+).*" "\\1" VERSION_MINOR "${VERSION_STR}")
string (REGEX REPLACE ".*#define +VERSION_PATCH +([0-9]+).*" "\\1" VERSION_PATCH "${VERSION_STR}")

使用如下命令生成二进制安装包。

cpack -C CPackConfig.cmake

相关链接

CMake 常用法
用CMake构建中等规模的工程
在CMake工程中使用NDK独立工具链 (一)
在CMake工程中使用NDK独立工具链 (二)

参考资料

CMake 入门实战
http://www.hahack.com/codes/cmake/

CMake交叉编译配置
https://blog.csdn.net/bytxl/article/details/50635788

ExternalProject
https://cmake.org/cmake/help/v3.0/module/ExternalProject.html

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

推荐阅读更多精彩内容