cmake 学习笔记1

Geant4、ROOT 等项目使用 cmake 来作为构建工具,对项目管理能力具有相当的优越性,只要简单的几句话就能代替一长篇的 Makefile,其实我最早看到了 Geant4 的 CmakeLists.txt,被它的便捷性所吸引了,以致我先会使用 cmake 来管理项目之后才对 Makefile 有进一步的了解。

CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的 makefile 或者 project 文件,能测试编译器所支持的 C++ 特性,类似 UNIX 下的 automake。只是 CMake 的组态档取名为 CmakeLists.txt。Cmake 并不直接建构出最终的软件,而是产生标准的建构档(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依一般的建构方式使用。这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件,这种可以使用各平台的原生建构系统的能力是 CMake 和 SCons 等其他类似系统的区别之处。

参考了网上一些博客,对 cmake 常用语句有了了解,对理工科的我们学会简单使用就行,下面是参考了一些博客写出来的,只要这几个语句学会基本就够用了,其实只要花几天学习下,做好一个自己的通用模板基本就一劳永逸了。

| 例子 | 说明 | | :——-: | :——-: | | 例子一 | 单个源文件 main.c | | 例子二 | ==>分解成多个 main.c hello.h hello.c | | 例子三 | ==>先生成一个静态库,链接该库 | | 例子四 | ==>将源文件放置到不同的目录 | | 例子五 | ==>控制生成的程序和库所在的目录 |


例子一

用 cmake 来构建一个经典的 C++ 程序:

//main.cpp
#include <iostream>
int main()
{
    std::cout<<"Hello World!"<<std::endl;
    return 0;
}

编写一个 CMakeList.txt 文件(可看做cmake的工程文件):

project(HELLO)
set(CODE_LIST main.cpp)
add_executable(hello ${CODE_LIST})

然后,建立一个任意目录(习惯在本目录下创建一个 build 子目录),在该 build 目录下调用 cmake

注意:为了简单起见,我们从一开始就采用cmake的 out-of-source 方式来构建(即生成中间产物与源代码分离),并始终坚持这种方法,这也就是此处为什么单独创建一个目录,然后在该目录下执行 cmake 的原因。

cmake ..
make 

即可生成可执行程序 hello。 目录结构:

+
| 
+--- main.cpp
+--- CMakeList.txt
|
/--+ build/
   |
   +--- hello

CMakeList.txt

第一行 project 不是强制性的,但最好始终都加上。这一行会引入两个变量:

HELLO_BINARY_DIR 和 HELLO_SOURCE_DIR

同时,cmake自动定义了两个等价的变量:

PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR

因为是 out-of-source 方式构建,所以我们要时刻区分这两个变量对应的目录

可以通过 message 来输出变量的值:

message(${PROJECT_SOURCE_DIR})
  • set 命令用来设置变量

  • add_exectuable 告诉工程生成一个可执行文件。

  • add_library 则告诉生成一个库文件。 注意:CMakeList.txt 文件中,命令名字是不区分大小写的,而参数和变量是大小写相关的。

cmake命令

cmake 命令后跟一个路径(..),用来指出 CMakeList.txt 所在的位置。由于系统中可能有多套构建环境,我们可以通过 -G 来制定生成哪种工程文件,通过 cmake -h 可得到详细信息。

要显示执行构建过程中详细的信息(比如为了得到更详细的出错信息),可以在 CMakeList.txt 内加入:

SET( CMAKE_VERBOSE_MAKEFILE on )

或者执行 make 时

$ make VERBOSE=1

或者

$ export VERBOSE=1
$ make

对单一文件 cmake 可能看不出其无比优越性,那就继续往下呗。


例子二

一个源文件的例子一似乎没什么意思,拆成 3 个文件再试试看:

  • hello.h 头文件

#ifndef _HELLO_
#define _HELLO_
void hello(const char* name);
#endif //_HELLO_
  • hello.cpp

#include <stdio.h>
#include "hello.h"
void hello(const char * name)
{
    printf ("Hello %s!/n", name);
}
  • main.cpp

#include "hello.h"
int main()
{
    hello("World");
    return 0;
}
  • 准备好 CMakeList.txt 文件

project(HELLO)
set(CODE_LIST main.cpp hello.cpp)
add_executable(hello ${CODE_LIST})

执行 cmake 的过程同上, 目录结构

+
| 
+--- main.cpp
+--- hello.h
+--- hello.cpp
+--- CMakeList.txt
|
/--+ build/
   |
   +--- hello

例子三

如果先将 helio.cpp 生成一个库,然后再使用呢? 修改一下前面的 CMakeList.txt 文件试试:

project(HELLO)
set(LIB_SRC hello.cpp)
set(APP_SRC main.cpp)
add_library(libhello ${LIB_SRC})
add_executable(hello ${APP_SRC})
target_link_libraries(hello libhello)

和前面相比,我们添加了一个新的目标 libhello,并将其链接进 hello 程序。 同前面一样运行 cmake,结果: 目录结构

+
| 
+--- main.cpp
+--- hello.h
+--- hello.cpp
+--- CMakeList.txt
|
/--+ build/
   |
   +--- hello
   +--- liblibhello.so

因为我的可执行程序(add_executable)占据了 hello 这个名字,所以 add_library 就不能使用这个名字了,所以,我们才取了个 libhello 的名字,这将导致生成的库为 liblibhello.so(或 liblibhello.a),很不爽,想生成 hello.so(或hello.a) 怎么办? 添加一行:

set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

例子四

在前面,我们成功地使用了库,可是源代码放在同一个路径下,不大规范,于是下面就是分开放了。 我们想要以下这样一种结构:

+
|
+--- CMakeList.txt
+--+ src/
|  |
|  +--- main.cpp
|  /--- CMakeList.txt
|
+--+ libhello/
|  |
|  +--- hello.h
|  +--- hello.cpp
|  /--- CMakeList.txt
|
/--+ build/

现在需要 3 个 CMakeList.txt 文件了,每个源文件目录都需要一个,每一个都不是太复杂。

  • 顶层的CMakeList.txt 文件

project(HELLO)
add_subdirectory(src)
add_subdirectory(libhello)
  • src 中的 CMakeList.txt 文件

include_directories(${PROJECT_SOURCE_DIR}/libhello)
set(APP_SRC main.cpp)
add_executable(hello ${APP_SRC})
target_link_libraries(hello libhello)
  • libhello 中的 CMakeList.txt 文件

set(LIB_SRC hello.cpp)
add_library(libhello ${LIB_SRC})
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

和前面一样,建立一个 build 目录,在其内运行 cmake,然后可以得到 - build/src/hello - build/libhello/hello.so 顶层的 CMakeList.txt 文件中使用 add_subdirectory 告诉 cmake 去子目录寻找新的 CMakeList.txt 子文件。在 src 的 CMakeList.txt 文件中,新增加了include_directories,用来指明头文件所在的路径。


例子五

前面还是有一点不爽:如果想让可执行文件在 bin 目录,库文件在 lib 目录怎么办? 想要以下的目录结构:

   + build/
   |
   +--+ bin/
   |  |
   |  /--- hello.exe
   |
   /--+ lib/
      |
      /--- hello.lib
  • 一种办法:修改顶级的 CMakeList.txt 文件

project(HELLO)
add_subdirectory(src bin)
add_subdirectory(libhello lib)

这样一来:build/src 就成了 build/bin 了,可是除了 hello,中间产物也进来了。还不是我们最想要的。

  • 另一种方法:不修改顶级的文件,修改其他两个文件

    • src/CMakeList.txt 文件

    include_directories(${PROJECT_SOURCE_DIR}/libhello)
    #link_directories(${PROJECT_BINARY_DIR}/lib)
    set(APP_SRC main.cpp)
    set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
    add_executable(hello ${APP_SRC})
    target_link_libraries(hello libhello)
    
    • libhello/CMakeList.txt 文件

    set(LIB_SRC hello.cpp)
    add_library(libhello ${LIB_SRC})
    set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
    set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")