使用 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