- 构建工具 CMake 介绍
- 官方入门教程翻译
- 后续增加部分高级功能实现
CMake
背景
- 你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。
- CMake 就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeLists.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等 [1]。
流程
- 编写 CMake 配置文件 CMakeLists.txt 。
- 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile 。其中, PATH 是 CMakeLists.txt 所在的目录。
- 使用 make 命令进行编译。
实例
Step 1. 基本的CMakeLists.txt
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
double outputValue = sqrt(inputValue);
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}
- 添加版本号,配置头文件 CMakeLists.txt
# Basic two lines.(cmake version & project name)
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
# Add the version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
# Configure a header file to pass some of the CMake settings to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)
# Add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")
# add the executable
add_executable(Tutorial tutorial.cxx)
- 因为配置文件会被写入到生成路径(binary tree) 中,所以我们必须将该文件夹添加到头文件搜索路径中。接下来我们在源码中创建一个包含以下内容的 TutorialConfig.h.in 文件
// Define the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
- 当 CMake 配置这个头文件的时候,
@Tutorial_VERSION_MAJOR@
和 @Tutorial_VERSION_MINOR@
就会用 CMakeLists.txt 文件中对应的值替换。接下来我们修改 tutorial.cxx 源码包含配置头文件并使用版本号,修改后的源码如下:
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
int main (int argc, char *argv[])
{
// Print the version info stored in configuration file.
if (argc < 2)
{
fprintf(stdout,"%s Version %d.%d\n",
argv[0],
Tutorial_VERSION_MAJOR,
Tutorial_VERSION_MINOR);
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
double outputValue = sqrt(inputValue);
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}
Step 2. 导入库文件
- 现在我们将会给我们的项目添加一个库文件。这个库文件包含了我们自己实现的开方运算。可执行文件使用这个库替代编译器提供的标准开方运算。本教程中我们将其放到 MathFunctions 文件夹下,该文件夹下还有一个包含下面一行代码的 CMakeLists.txt 文件。
add_library(MathFunctions mysqrt.cxx)
- mysqrt.cxx 文件只有一个名为 mysqrt 的函数,其提供了和标准库 sqrt 相同的功能。内容如下
#include "MathFunctions.h"
#include <stdio.h>
// a hack square root calculation using simple operations
double mysqrt(double x)
{
if (x <= 0)
{
return 0;
}
double result;
double delta;
result = x;
// do ten iterations
int i;
for (i = 0; i < 10; ++i)
{
if (result <= 0)
{
result = 0.1;
}
delta = x - (result * result);
result = result + 0.5 * delta / result;
fprintf(stdout, "Computing sqrt of %g to be %g\n", x, result);
}
return result;
}
- 对应的头文件为 MathFunction.h,其内容如下:
double mysqrt(double x);
- 为了构建并使用新的库文件,我们需要在顶层 CMakeList.txt 文件添加
add_subdirectory
语句。我们需要添加额外的头文件包含路径,以便将包含函数原型的 MathFunctions/MathFunctions.h 头文件包含进来。最后我们还需要给可执行文件添加库。最终顶层 CMakeList.txt 文件的最后几行如下所示:
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial MathFunctions)
- 现在我们考虑将
MathFunctions
库作为一个可选项。虽然在这里并没有什么必要,但是如果库文件很大或者库文件依赖第三方库你可能就希望这么做了。首先先在顶层 CMakeLists.txt 文件添加一个选项:
# should we use our own math functions?
option (USE_MYMATH "Use tutorial provided math implementation" ON)
- 这个选项会在 CMake GUI 中显示并会将默认值设置为 ON,用户可以根据需求修改该值。这个设置会本保存下来,所以用户无需在每次运行
CMake
时都去设置。接下来就是将构建和连接 MathFunctions
设置为可选项。修改顶层的 CMakeLists.txt 文件如下所示:
# add the MathFunctions library?
if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial ${EXTRA_LIBS})
- 这里使用
USE_MYMATH
来决定是否编译并使用 MathFunctions
。注意收集可执行文件的可选连接库所使用的变量(这里为 EXTRA_LIBS
)的使用方法。这种方法在保持有许多可选组件的大型项目整洁时经常使用。对应的我们修改源码如下:
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif
int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"%s Version %d.%d\n", argv[0], Tutorial_VERSION_MAJOR, Tutorial_VERSION_MINOR);
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
#ifdef USE_MYMATH
double outputValue = mysqrt(inputValue);
#else
double outputValue = sqrt(inputValue);
#endif
fprintf(stdout,"The square root of %g is %g\n", inputValue, outputValue);
return 0;
}
- 在源码中我们同样使用了 USE_MYMATH 宏。这个宏由 CMake 通过在配置文件 TutorialConfig.h 添加以下代码传递给源码:
#cmakedefine USE_MYMATH
Step 3. 安装和测试
- 下一步我们将给我们的项目添加安装规则和测试。安装规则简单明了。对于
MathFunctions
库的安装,我们通过在 MathFunction
的 CMakeLists.txt 文件中添加以下两行来设置其库和头文件的安装。
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)
- 对于本文这个应用通过在顶层 CMakeLists.txt 添加以下内容来安装可执行文件和配置的头文件:
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h" DESTINATION include)
- 以上就是安装的全部步骤。现在你应该可以编译本教程了。输入
make install
(或在 IDE 中编译 install 项目),对应的头文件、库文件和可执行文件就会被安装。CMake
的 CMAKE_INSTALL_PREFIX
参数可以指定安装文件的根目录(之前还可以加上 -D
参数,具体意义可以参考 what does the parameter "-D" mean
)。添加测试过程同样简单明了。在顶层 CMakeLists.txt 文件的最后我们可以添加一个基础测试数据来验证该应用程序是否正常运行。
include(CTest)
# does the application run
add_test (TutorialRuns Tutorial 25)
# does it sqrt of 25
add_test (TutorialComp25 Tutorial 25)
set_tests_properties (TutorialComp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")
# does it handle negative numbers
add_test (TutorialNegative Tutorial -25)
set_tests_properties (TutorialNegative PROPERTIES PASS_REGULAR_EXPRESSION "-25 is 0")
# does it handle small numbers
add_test (TutorialSmall Tutorial 0.0001)
set_tests_properties (TutorialSmall PROPERTIES PASS_REGULAR_EXPRESSION "0.0001 is 0.01")
# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")
- 在编译完成之后,我们可以运行
ctest
命令行工具来执行测试。第一个测试简单的验证了程序是否工作,是否有严重错误并且返回0.这是 Ctest 测试的基础。接下来的一些测试都使用了 PASS_REGULAR_EXPRESSION
测试属性(正则表达式)来验证输出中是否包含了特定的字符串。这里验证开方是否正确并且在计算错误时输出输出对应信息。如果你希望添加很多的测试来测试不同的输入值,你可以考虑定义一个像下面这样的宏:
#define a macro to simplify adding tests, then use it
macro (do_test arg result)
add_test (TutorialComp${arg} Tutorial ${arg})
set_tests_properties (TutorialComp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)
# do a bunch of result based tests
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
Step 4. 添加系统自检
- 接下来我们考虑给我们的项目添加一些取决于目标平台是否有一些特性的代码。这里我们将添加一些取决于目标平台是否有
log
和 exp
函数的代码。当然对于大多数平台都会有这些函数,但这里我们认为这并不常见。如果平台有 log
函数我们将在 mysqrt
函数中使用它计算平方根。我们首先在顶层 CMakeLists.txt 文件中使用 CheckFunctionExists
宏测试这些函数是否可用,代码如下:
# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)
- 接下来我们修改 TutorialConfig.h.in 文件定义一些宏以表示
CMake
是否在平台上找到这些函数:
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
- 一定要在使用
configure_file
生成 TutorialConfig.h 之前测试 log
和 exp
。因为 configure_file
命令会立刻使用当前 CMake 的设置配置文件。最后根据 log
和 exp
是否在我们的平台上可用我们给 mysqrt 函数提供一个可选的实现,代码如下:
// if we have both log and exp then use them
#if defined (HAVE_LOG) && defined (HAVE_EXP)
result = exp(log(x)*0.5);
#else // otherwise use an iterative approach
. . .
Step 5. 添加一个生成的文件和生成器
- 在这一章节我们将会展示如何在构建一个应用的过程中添加一个生成的源文件。在本例中我们将创建一个预先计算的平方根表作为构建过程的一部分,然后将其编译到我们的应用中。为了做到这一点我们首先需要一个能产生这张表的程序。在
MathFunctions
文件夹下创建一个新的名为 MakeTable.cxx 的文件,内容如下:
// A simple program that builds a sqrt table
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main (int argc, char *argv[])
{
int i;
double result;
// make sure we have enough arguments
if (argc < 2)
{
return 1;
}
// open the output file
FILE *fout = fopen(argv[1],"w");
if (!fout)
{
return 1;
}
// create a source file with a table of square roots
fprintf(fout,"double sqrtTable[] = {\n");
for (i = 0; i < 10; ++i)
{
result = sqrt(static_cast<double>(i));
fprintf(fout,"%g,\n",result);
}
// close the table with a zero
fprintf(fout,"0};\n");
fclose(fout);
return 0;
}
- 注意到这张表使用 C++ 代码生成且文件的名字通过输入参数指定。下一步通过在
MathFunctions
的 CMakeLists.txt 中添加合适的代码来构建 MakeTable
可执行文件,并将它作为构建的一部分运行。只需要一点代码就能实现这个功能:
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)
# add the binary tree directory to the search path for
# include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )
# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h )
- 首先添加的
MakeTable
可执行文件和其它可执行文件相同。接下来我们添加一个自定义的命令来指定如何通过运行 MakeTable
生成 Table.h 文件。接下来我们必须让 CMake
知道 mysqrt.cxx 依赖于生成的 Table.h 文件。这一点通过将生成的 Table.h 文件添加到 MathFunctions
库的源文件列表实现。我们同样必须将当前二进制文件路径添加到包含路径中,以保证 Table.h 文件被找到并被 mysqrt.cxx 包含。该项目在构建时会首先构建 MakeTable
可执行文件。接下来会运行该可执行文件并生成 Table.h 文件。最后它将会编译包含 Table.h 的 mysqrt.cxx 文件并生成 MathFunctions
库。此时包含了所有我们添加的特性的顶层 CMakeLists.txt 文件应该像下面这样:
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
include(CTest)
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
# check the function exists? return bool value named HAVE_LOG/HAVE_EXP
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)
# should we use our own math functions
option(USE_MYMATH "Use tutorial provided math implementation" ON)
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories ("${PROJECT_BINARY_DIR}")
# add the MathFunctions library?
if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial ${EXTRA_LIBS})
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include)
# does the application run
add_test (TutorialRuns Tutorial 25)
# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
PROPERTIES
PASS_REGULAR_EXPRESSION "Usage:.*number"
)
#define a macro to simplify adding tests
macro (do_test arg result)
add_test (TutorialComp${arg} Tutorial ${arg})
set_tests_properties (TutorialComp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endmacro (do_test)
# do a bunch of result based tests
do_test (4 "4 is 2")
do_test (9 "9 is 3")
do_test (5 "5 is 2.236")
do_test (7 "7 is 2.645")
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
do_test (0.0001 "0.0001 is 0.01")
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
- 最后
MathFunctions
的 CMakeLists.txt 文件如下:
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
# add the binary tree directory to the search path
# for include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )
# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)
Step 6. 构造一个 Installer
- 接下来假设我们想将我们的项目发布给其他人以便供他们使用。我们想提供在不同平台上的二进制文件和源码的发布版本。这一点和我们在之前安装和测试章节(步骤3)略有不同,步骤三安装的二进制文件是我们从源码构建的。这里我们将构建一个支持二进制文件安装的安装包和可以在
cygwin
,debian
,RPMs
等中被找到的安装管理特性。为了实现这一点我们将使用 CPack
来创建在 Packaging with CPack
章节中介绍过的平台特定安装器(platform specific installers)。我们需要在顶层 CMakeLists.txt 文件添加以下几行内容:
# build a CPack driven installer package
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE
"${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include (CPack)
- 首先我们添加了
InstallRequiredSystemLibraries
。该模块会包含我们项目在当前平台所需的所有运行时库(runtime libraries)。接下来我们设置了一些 CPack
变量来指定我们项目的许可文件和版本信息。版本信息使用我们在之前设置的内容。最后我们包含 CPack
模块,它会使用这些变量和其它你安装一个应用程序所需的系统属性。
- 接下来就是正常编译你的项目然后使用
CPack
运行它,为了编译二进制发布版本你需要运行:
cpack --config CPackConfig.cmake
cpack --config CPackSourceConfig.cmake
Step 7. 添加 Dashboard 支持
- 添加将我们测试结果提交到仪表盘的功能非常简单。在本教程的之前步骤中我们已经给我们的项目定义了一些测试。我们只需要运行这些测试然后提交到仪表盘即可。为了支持仪表盘功能我们需要在顶层 CMakeLists.txt 文件中增加
CTest
模块。
# enable dashboard scripting
include (CTest)
- 我们同样可以创建一个
CTestConfig.cmake
文件来在表盘工具中指定本项目的名字。
set (CTEST_PROJECT_NAME "Tutorial")
CTest
会在运行时读取该文件。你可以在你的项目上运行 CMake
来创建一个简单的仪表盘,切换目录到二进制文件夹下,然后运行 ctest -DExperimental
。你仪表盘的运行结果会上传到 Kitware 的公共仪表盘上。
实例
1.编译生成可执行文件
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo)
add_compile_options( -std=c++11 )
include_directories(/c++/C-Example/linux/install/usr/local/include)
link_directories(/c++/C-Example/linux/install/usr/local/lib64)
# 指定生成目标
add_executable(Demo object_storage.cpp)
target_link_libraries(Demo -laws-cpp-sdk-core -laws-cpp-sdk-s3)
2.动态库/静态库 编译
[root@ecs-sn3-medium-2-linux-20200222090658 s3util]# tree
.
├── CMakeLists.txt
├── include
│ ├── S3Util.h
├── Makefile
└── src
├── CMakeLists.txt
└── S3Util.cpp
项目根目录下的 CMakeLists.txt
#CMake最低版本号要求
cmake_minimum_required(VERSION 2.8)
#指定项目名称
project(s3util)
#指定版本信息
set(CMAKE_SYSTEM_VERSION 1)
#若是需要指定编译器路径
#set(CROSS_TOOLCHAIN_PREFIX "/path/arm-linux-")
#指定编译器
#set(CMAKE_C_COMPILER "${CROSS_TOOLCHAIN_PREFIX}gcc")
#set(CMAKE_CXX_COMPILER "${CROSS_TOOLCHAIN_PREFIX}g++")
#使用默认路径的g++指定编译器
#set(CMAKE_CXX_COMPILER "g++")
#指定编译选项
set(CMAKE_BUILD_TYPE Debug )
#指定编译目录
set(PROJECT_BINARY_DIR ${PROJECT_SOURCE_DIR}/build)
#添加子目录,这样进入源码文件src目录可以继续构建
add_subdirectory(${PROJECT_SOURCE_DIR}/src)
src 目录下的 CMakeLists.txt
#查找当前目录下的所有源文件,
#并将名称保存到DIR_LIB_SRCS目录
#aux_source_directory(. DIR_LIB_SRCS)
#指定头文件目录,PROJECT_SOURCE_DIR为工程的根目录
include_directories(${PROJECT_SOURCE_DIR}/include)
add_compile_options( -std=c++11 )
include_directories(/c++/C-Example/linux/install/usr/local/include)
link_directories(/c++/C-Example/linux/install/usr/local/lib64)
#指定可执行文件的输出目录,输出到bin下面
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
#生成动态库
add_library(s3_shared_demo SHARED S3Util.cpp)
#设置库输出名为 shared => libshared.so
set_target_properties(s3_shared_demo PROPERTIES OUTPUT_NAME "s3shared")
#生成静态库
add_library(s3_static_demo STATIC S3Util.cpp)
#设置输库出名为 static => libstatic.a
set_target_properties(s3_static_demo PROPERTIES OUTPUT_NAME "s3static")
#指定库文件输出路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
#在指定目录下查找库,并保存在LIBPATH变量中
find_library(LIBPATHS shared ${PROJECT_SOURCE_DIR}/lib)
#指定生成目标
#add_executable(main main.cpp)
#链接共享库
#target_link_libraries(main ${LIBPATHS})
#target_link_libraries(s3util -laws-cpp-sdk-core -laws-cpp-sdk-s3)
参考文献