使用 cmake 管理项目

介绍

cmake 是 c/c++ 的一个项目管理工具。远古人类使用 make, 后来慢慢用 automake/autoconf/libtool 这些工具。 后来有人重新轮了几个,包括 scon, gyp , cmake。

gyp 是 google 的 JavaScript 引擎 V8 的管理工具。据说 google 自己也不用了,我没有确认过。

cmake 一开始是 kde 的项目的管理工具,后来越来越多的人使用了。

hello world

cmake 需要 CMakeLists.txt 作为输入。目录结构如下

├── CMakeLists.txt
└── hello.cpp

CMakeLists.txt 如下

project(hello C CXX)
cmake_minimum_required(VERSION 3.8)
add_executable(hello hello.cpp)

cmake 建议工程生成的文件不要和源代码混在一起,所以我们需要手工创建工程的输出目录,然后用 cmake 生成 Makefiles

% mkdir build
% cd build
% cmake ..
-- The CXX compiler identification is AppleClang 8.0.0.8000042
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/wangchunye/d/working/test_cmake/build
% ./hello
% make
Scanning dependencies of target hello
[ 50%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
% ./hello
hello world
%

hello.cpp 如下

#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{
    cout << "hello world" << endl;
    return 0;
}

生成一个库

假设我们要生成一个静态连接库 libfoo.a

foo.cpp 如下

#include <iostream>
void foo() {
    std::cout << "hello from foo library" << std::endl;
}

我们修改 CMakeLists.txt

project(hello CXX)
cmake_minimum_required(VERSION 3.2)
add_executable(hello hello.cpp)
add_library(foo foo.cpp)
% make
Scanning dependencies of target foo
[ 25%] Building CXX object CMakeFiles/foo.dir/foo.cpp.o
[ 50%] Linking CXX static library libfoo.a
[ 50%] Built target foo
Scanning dependencies of target hello
[ 75%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
% ls -la
total 96
drwxr-xr-x   8 wangchunye  staff    272 Mar 18 11:43 .
drwxr-xr-x   6 wangchunye  staff    204 Mar 18 11:43 ..
-rw-r--r--   1 wangchunye  staff  11732 Mar 18 11:43 CMakeCache.txt
drwxr-xr-x  15 wangchunye  staff    510 Mar 18 11:43 CMakeFiles
-rw-r--r--   1 wangchunye  staff   5756 Mar 18 11:43 Makefile
-rw-r--r--   1 wangchunye  staff   1254 Mar 18 11:43 cmake_install.cmake
-rwxr-xr-x   1 wangchunye  staff  15204 Mar 18 11:43 hello
-rw-r--r--   1 wangchunye  staff   7968 Mar 18 11:43 libfoo.a

使用一个库

修改 hello.cpp 如下

#include <iostream>
using namespace std;
extern void foo();
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

在 CMakeLists.txt 里面添加一行

target_link_libraries(hello foo)

然后就 hello.cpp 就可以使用外部的库了

指定特定的头文件搜索目录

c++ 编译的时候需要搜索头文件,例如,有的时候,我们需要添加搜索头文件目录。 我们修改 hello.cpp 如下

#include <iostream>
using namespace std;
#include "foo.h"
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

创建 include/foo.h 如下

#pragma once
extern void foo();

修改 CMakeLists.txt ,增加搜索目录

include_directories(${CMAKE_SOURCE_DIR}/include)

这里注意到我们使用了 ${CMAKE_SOURCE_DIR} 这个变量。还有很多这样的变量。

  • CMAKE_BINARY_DIR 工程的输出目录
  • CMAKE_CURRENT_BINARY_DIR 当前的 CMakeList.txt 的输出目录, 因为可以包含子工程,这个就是子工程的输出目录。
  • CMAKE_CURRENT_LIST_FILE 当前 CMakeList.txt 的全路径名称
  • CMAKE_CURRENT_LIST_DIR 当前 CMakeList.txt 所在目录
  • CMAKE_CURRENT_SOURCE_DIR 当前的项目源代码目录

太多了,https://cmake.org/Wiki/CMake_Useful_Variables 下有详细的说明

添加第三方库依赖

通常,c/c++ 添加第三方库需要解决两个问题

  • 头文件在哪里?
  • 库文件在哪里?

我们先手工添加一个库。

假设 OpenCV 的头文件安装在了

/usr/local/Cellar/opencv/2.4.13.2/include/opencv/

库文件在

/usr/local/Cellar/opencv/2.4.13.2/lib

那么我们写一个简单的 OpenCV 的程序。

// cv1.cpp
#include "opencv2/highgui/highgui.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main( int argc, const char** argv )
{
     Mat img = imread(argv[1], CV_LOAD_IMAGE_UNCHANGED); //read the image data in the file "MyPic.JPG" and store it in 'img'

     if (img.empty()) //check whether the image is loaded or not
     {
          cout << "Error : Image cannot be loaded..!!" << endl;
          //system("pause"); //wait for a key press
          return -1;
     }

     namedWindow("MyWindow", CV_WINDOW_AUTOSIZE); //create a window with the name "MyWindow"
     imshow("MyWindow", img); //display the image which is stored in the 'img' in the "MyWindow" window

     waitKey(0); //wait infinite time for a keypress

     destroyWindow("MyWindow"); //destroy the window with the name, "MyWindow"

     return 0;
}

修改 CMakeLists.txt

include_directories("/usr/local/Cellar/opencv/2.4.13.2/include")
link_directories("/usr/local/Cellar/opencv/2.4.13.2/lib")
add_executable(cv1 cv1.cpp)
target_link_libraries(cv1
  -lopencv_calib3d
  -lopencv_contrib
  -lopencv_core
  -lopencv_features2d
  -lopencv_flann
  -lopencv_gpu
  -lopencv_highgui
  -lopencv_imgproc
  -lopencv_legacy
  -lopencv_ml
  -lopencv_nonfree
  -lopencv_objdetect
  -lopencv_ocl
  -lopencv_photo
  -lopencv_stitching
  -lopencv_superres
  -lopencv_ts
  -lopencv_video
  -lopencv_videostab
  )

然后 make 就可以了。

cmake 不建议这样手工的方式添加第三方依赖,很明显,如果第三方库放在其他目录下,就找不到了。而且一长串 -l 也是很累人的。

按照 http://docs.opencv.org/2.4/doc/tutorials/introduction/linux_gcc_cmake/linux_gcc_cmake.html 的描述,建议的方法是

FIND_PACKAGE( OpenCV REQUIRED )
IF(OpenCV_FOUND)
   MESSAGE("Found OpenCV")
   MESSAGE("Includes: " ${OpenCV_INCLUDE_DIRS})
ENDIF(OpenCV_FOUND)
ADD_EXECUTABLE(cv1 cv1.cpp)
TARGET_LINK_LIBRARIES(cv1 ${OpenCV_LIBS})

但是,我们运行 cmake ... 的时候,会报错。

% mkdir build ; cd build; cmake ..
Re-run cmake no build system arguments
-- The CXX compiler identification is AppleClang 8.0.0.8000042
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Error at CMakeLists.txt:10 (find_package):
  By not providing "FindOpenCV.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "OpenCV", but
  CMake did not find one.

  Could not find a package configuration file provided by "OpenCV" with any
  of the following names:

    OpenCVConfig.cmake
    opencv-config.cmake

  Add the installation prefix of "OpenCV" to CMAKE_PREFIX_PATH or set
  "OpenCV_DIR" to a directory containing one of the above files.  If "OpenCV"
  provides a separate development package or SDK, be sure it has been
  installed.


-- Configuring incomplete, errors occurred!
See also "/Users/wangchunye/d/working/test_cmake/build/CMakeFiles/CMakeOutput.log".

提示的很明确,在 CMAKE_MODULE_PATH 中,我们没有 找到 OpenCVConfig.cmake 。 这里可以看到 cmake find_package 的机制, 他实际上是找 <LIBNAME>Config.cmake 然后运行里面的脚本。 cmake 自带了很多这样的脚本, 通常在 /usr/local/share/cmake/Modules 下面。 可惜 OpenCV 并不包含在里面, OpenCVConfig.cmake 在 OpenCV 自己的目录下。

/usr/local/Cellar/opencv/2.4.13.2/share/OpenCV/OpenCVConfig.cmake

所以我们运行下面的命令可以解决这个问题

cmake  -DOpenCV_DIR=/usr/local/Cellar/opencv/2.4.13.2/share/OpenCV ..

OpenCVConfig.cmake 文件的开头有文档,说明了使用方法,例如,哪些变量可以使用。

再来一个使用 boost 的例子。 下面这些代码,也是在 FindBoost.cmake 的开头可以找到。

FIND_PACKAGE(Boost 1.36.0 COMPONENTS locale)
IF(Boost_FOUND)
  MESSAGE("Found Boost")
  MESSAGE("BOOST Includes: " ${Boost_INCLUDE_DIRS})
  MESSAGE("BOOST Libraries directories: " ${Boost_LIBRARY_DIRS})
  MESSAGE("BOOST Libraries: " ${Boost_LIBRARIES})
ENDIF(Boost_FOUND)
INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIRS})
ADD_EXECUTABLE(hello_boost hello_boost.cpp)
TARGET_LINK_LIBRARIES(hello_boost ${Boost_LIBRARIES})
// hello_boost.cpp
//
//  Copyright (c) 2009-2011 Artyom Beilis (Tonkikh)
//
//  Distributed under the Boost Software License, Version 1.0. (See
//  accompanying file LICENSE_1_0.txt or copy at
//  http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/locale.hpp>
#include <iostream>

#include <ctime>

int main()
{
    using namespace boost::locale;
    using namespace std;
    generator gen;
    locale loc=gen("");
    // Create system default locale

    locale::global(loc);
    // Make it system global

    cout.imbue(loc);
    // Set as default locale for output

    cout <<format("Today {1,date} at {1,time} we had run our first localization example") % time(0)
          <<endl;

    cout<<"This is how we show numbers in this locale "<<as::number << 103.34 <<endl;
    cout<<"This is how we show currency in this locale "<<as::currency << 103.34 <<endl;
    cout<<"This is typical date in the locale "<<as::date << std::time(0) <<endl;
    cout<<"This is typical time in the locale "<<as::time << std::time(0) <<endl;
    cout<<"This is upper case "<<to_upper("Hello World!")<<endl;
    cout<<"This is lower case "<<to_lower("Hello World!")<<endl;
    cout<<"This is title case "<<to_title("Hello World!")<<endl;
    cout<<"This is fold case "<<fold_case("Hello World!")<<endl;

}

// vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4

运行的时候需要指定 BOOST_ROOT

-DBOOST_ROOT=/usr/local/Cellar/boost/1.63.0/

使用 googletest 做测试

首先安装 googletest

% mkdir $HOME/opt
5 cd $HOME/opt
% git clone https://github.com/google/googletest.git
% cd googletest
% mdkir build
% cmake -DCMAKE_INSTALL_PREFIX=/usr/local/googletest ..
% make
5 make install

可以看到 CMAKE_INSTALL_PREFIX 用于指定安装目录, make install 用于安装。

类似的,我们可以找 FindGTest.cmake ,文件开头有文档,介绍怎么使用。

ENABLE_TESTING()
FIND_PACKAGE(GTest REQUIRED)
ADD_EXECUTABLE(test1 test1.cpp)
TARGET_LINK_LIBRARIES(test1 GTest::GTest GTest::Main)
ADD_TEST(all_tests test1)

这里 ENALBE_TESTING() 启动 cmake 的集成测试功能。 ADD_TEST 用来添加一个测试用例。

// test1.cpp
#include <iostream>
#include <cmath>
using namespace std;
#include "gtest/gtest.h"
double square_root (const double x)
{
    return sqrt(x);
}
TEST (SQUARE_ROOT_TEST, PositiveNos) {
    ASSERT_NEAR (18.0, square_root (324.0), 1.0e-4);
    ASSERT_NEAR (25.4, square_root (645.16), 1.0e-4);
    ASSERT_NEAR (50.3321, square_root (2533.310224),1.0e-4);
}

运行上面的例子

% cmake -DGTEST_ROOT=/usr/local/googletest ..

添加源代码依赖关系

由于某种原因,需要添加第三方的源代码依赖。我们可以用 ExternalProject_Add 来完成这个功能。

假设我们使用 protobuf 的第三方库。

INCLUDE(ExternalProject)
ExternalProject_Add(
  googleprotobuf_proj
  GIT_REPOSITORY "http://gitlab.i.deephi.tech/wangchunye/protobuf.git"
  SOURCE_SUBDIR "cmake"
  CMAKE_ARGS "-Dprotobuf_BUILD_TESTS=OFF"
  GIT_TAG "v3.2.0"
  UPDATE_COMMAND ""
  INSTALL_COMMAND ""
)
ExternalProject_Get_Property(googleprotobuf_proj SOURCE_DIR)
ExternalProject_Get_Property(googleprotobuf_proj BINARY_DIR)
include_directories("${SOURCE_DIR}/src")
link_directories("${BINARY_DIR}")
set(Protobuf_PROTOC_EXECUTABLE ${BINARY_DIR}/protoc)
  • INCLUDE(ExternalProject) 引入 cmake 脚本。 否则 ExternalProject_Add 不能使用
  • ExternalProject_Add 引入外部第三库
  • googleprotobuf_proj 是第三方库的项目名称,我们自己随便取的的,建议不要和现有的库名称混,否则名字冲突,例如 protobuf 就不行。
  • GIT_TAG 这个指明 sha1 ,branch name 或者 tag name 。 的锁定特定的软件版本。
  • SOURCE_SUBDIR 一般不需要指定,因为 CMakeLists.txt 一般都在项目的根目录里面,但是 probobuf 是放在了 cmake 的子目录下,所以需要指定这个目录。这个功能貌似只有 cmake 3.2 以上的版本才支持。
  • CMAKE_ARGS 指明在子项目中运行 cmake 时,cmake 的命令行参数。
  • -Dprotobuf_BUILD_TESTS=off 关闭测试。否则 protobuf 需要依赖 gmock 无法编译成功,除非安装 google mock 。但是一般我们也不想运行测试。
  • ExternalProject_Get_Property 临时引入变量。
  • include_directories("${SOURCE_DIR}") 使用引入的变量
  • set 设置我们自己的变量

我们定义自己的项目,使用 protobuf

ADD_CUSTOM_COMMAND(
  OUTPUT
  "${CMAKE_CURRENT_BINARY_DIR}/msg.pb.cc"
  "${CMAKE_CURRENT_BINARY_DIR}/msg.pb.h"
  COMMAND  ${Protobuf_PROTOC_EXECUTABLE}
  ARGS --cpp_out  ${CMAKE_CURRENT_BINARY_DIR} --proto_path ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/msg.proto
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/msg.proto
  COMMENT "Running C++ protocol buffer compiler on ${FIL}"
  VERBATIM )
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR})
ADD_EXECUTABLE(hello_proto hello_proto.cpp "${CMAKE_CURRENT_BINARY_DIR}/msg.pb.cc")
TARGET_LINK_LIBRARIES(hello_proto  -lprotobuf)
add_dependencies(hello_proto googleprotobuf_proj)
  • ADD_CUSTOM_COMMAND 是底层 cmake 命令, 自定义 makefile 的规则,用于从 proto 文件生成 cpp 源码。
  • 注意生成的源代码被放在了 --cpp_out ${CMAKE_CURRENT_BINARY_DIR} 下面,这是一个好习惯,不要让中间文件污染源代码目录了。
  • add_dependencies 比较重要,这个表明需要先下载编译 protobuf ,然后才能编译 hello_proto

其他常用功能

  • 调试 cmake
% export VERBOSE=1
% make

这样可以输出详细的编译命令,我们可以检查编译选项是否正确。

  • 添加自定义宏
add_definitions("-DFOO=\" foo libray name \"")

对应的,修改 foo.cpp

#include <iostream>

void foo() {
 std::cout << "hello from " << FOO << "library" <<  std::endl;
}
% make
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/wangchunye/d/working/test_cmake/build
[ 50%] Built target foo
Scanning dependencies of target hello
[ 75%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
% ./hello
hello from foo library