Cmake 使用教程

发布于:2025-05-21 ⋅ 阅读:(33) ⋅ 点赞:(0)

介绍

CMake 是一个开源、跨平台的构建系统,主要用于软件的构建、测试和打包。CMake 使用平台无关的配置文件 CMakeLists.txt 来控制软件的编译过程,并生成适用于不同编译器环境的项目文件。例如,它可以生成 Unix 系统的 Makefile Windows 下 的 Visual Studio 项目文件或 Mac Xcode 工程文件,从而简化了跨平台和交叉编译的工作流程。CMake 并不直接构建软件,而是产生标准的构建文件,然后使用这些文件在各自的构建环境中构建软件。
CMake 有以下几个特点:
  • 开放源代码:使⽤类 BSD 许可发布
  • 跨平台:并可⽣成编译配置⽂件,在 Linux/Unix 平台,⽣成 makefile;在苹果平台,可以生成 xcode;在 Windows 平台,可以⽣成 MSVC 的工程文件
  • 能够管理⼤型项⽬:KDE4 就是最好的证明
  • 简化编译构建过程和编译过程:Cmake 的⼯具链⾮常简单:cmake+make
  • ⾼效率:按照 KDE 官⽅说法,CMake 构建 KDE4 kdelibs 要⽐使⽤autotools来构建 KDE3.5.6 kdelibs 40%,主要是因为 Cmake 在⼯具链中没有 libtool
  • 可扩展:可以为 cmake 编写特定功能的模块,扩充 cmake 功能

安装

Ubuntu 下安装 cmake

sudo apt update
sudo apt install cmake
确定 cmake 是否安装成功
cmake --version

入门样例 - Hello-world 工程

创建 hello 目录, 并在其目录下创建 main.cpp 源文件和 CMakeLists.txt 文件。
main.cpp文件内容如下
#include <iostream>
using namespace std;
int main()
{
    std::cout << "hello world" << std::endl;
    return 0;
}
CMakeLists.txt 文件内容如下
# 声明所需的cmake版本
cmake_minimum_required(VERSION 3.0)
# 定义项目工程名称
project(hello)
# 设置生成目标
add_executable(main main.cpp)
  • cmake_minimum_required:指定使用的 cmake 的最低版本。可选,如果不加会有警告。
  • project:定义工程名称, 并可指定工程的版本、工程描述、web 主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。
  • add_executable:定义工程会生成一个可执行程序,这里的可执行程序名和 project 中的项目名没有任何关系,源文件名可以是一个也可以是多个,如有多个可用空格或;间隔
# 样式 1 
add_executable(app test1.c test2.c test3.c)
# 样式 2 
add_executable(app test1.c;test2.c;test3.c)
此时我们可以使用 cmake 来构建这个工程,生成 makefile, 从而编译代码。
cmake .
make

我们可以看到如果在 CMakeLists.txt 文件所在目录执行了 cmake 命令之后就会生成一些目录和文件,如果再基于 makefile 文件执行 make 命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样会导致整个项目目录看起来很混乱,不太容易管理和维护。

这其实被称为内部构建,但 CMake 强烈推荐的做法是外部构建。此时我们可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比如将这个目录命名为 build,这就叫做外部构建。

mkdir build
cd build
cmake ..
make

使用

定义变量

假如我们的项目中存在多个源文件,并且这些源文件需要被反复使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就可以定义一个变量,将文件名对应的字符串存储起来,在 cmake 里定义变量需要使用 set 指令。
 
# [] 中的参数为可选项, 如不需要可以不写,VAR 表示变量名, VALUE 表示变量值。
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
样例
set(SRC_LIST test1.c test2.c test3.c)
add_executable(app ${SRC_LIST})
注意:变量使⽤ ${} ⽅式取值,但是在 if 控制语句中是直接使⽤变量名

预定义变量

预定义变量名称 备注
CMAKE_CXX_STANDARD
c++特性标准
CMAKE_CURRENT_BINARY_DIR
cmake 执行命令时所在的工作路径
CMAKE_CURRENT_SOURCE_DIR
CMakeLists.txt 所在目录
CMAKE_INSTALL_PREFIX
默认安装路径
样例
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)

包含头文件

在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在 CMake 中设置头文件路径也很简单,通过命令 include_directories 就可以搞定了。
include_directories(headpath)

添加生成目标

add_executable(target srcfiles1 srcfile2 ...)

链接动态库/静态库

在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake 中也为我们提供了相关的加载静态库 / 动态库的命令。

链接静态库

cmake 中,链接静态库的命令如下:
link_libraries(<static lib> [<static lib>...])
参数为指定要链接的静态库的名字,可以是全名, 也可以是去掉 lib .a 之后的名字。如果该静态库是自己制作或者使用第三方提供的静态库,可能出现静态库找不到的情况,此时需要将静态库的路径也指定出来:
link_directories(<lib path>)

链接动态库

cmake 中链接动态库的命令如下 :
target_link_libraries(
        <target>
        <PRIVATE|PUBLIC|INTERFACE> <item>...
        [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • target:指定要加载动态库文件的名字
  • PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为 PUBLIC。如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可
  1. PUBLIC:在 public 后面的库会被 Link 到前面的 target 中,并且里面的符号也会被导出,提供给第三方使用
  2. PRIVATE:在 private 后面的库仅被 link 到前面的 target 中,并且终结掉,第三方不能感知你调了啥库
  3. INTERFACE:在 interface 后面引入的库不会被链接到前面的 target 中,只会导出符号
动态库的链接和静态库是完全不同的:
  • 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
  • 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存。
因此,在 cmake 中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后:
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 添加并指定最终生成的可执行程序名
add_executable(app ${SRC_LIST})
# 指定可执行程序要链接的动态库名字
target_link_libraries(app pthread)
target_link_libraries(app pthread) 中: app 表示最终生成的可执行程序的名字;pthread 表示可执行程序要加载的动态库, 全名为 libpthread.so , 在指定的时候一般会掐头(lib )去尾( .so )。
有些时候,当我们去链接第三方的动态库的时候, 如果不指定链接路径,会报错找不到动态库。此时,我们在生成可执行程序之前,通过命令指定出要链接的动态库的位置:
link_directories(path)
通过 link_directories 指定了动态库的路径之后,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题了。

搜索文件

如果一个项目里边的源文件很多,在编写 CMakeLists.txt 文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以在 CMake 中为我们提供了搜索文件的命令:

aux_source_directory

aux_source_directory(< dir > < variable >)
  • dir 表示要搜索的目录
  • variable 表示将从 dir 目录下搜索到的源文件列表存储到该变量中

file

file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
  • GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中
  • 递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中
样例
file(GLOB SRC_LISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB HEAD_LISTS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的路径

message 指令

CMake 中可以使用命令打印消息,该命令的名字为 message
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
第一个参数通常不设置, 表示重要消息。
  • STATUS :非重要消息
  • WARNINGCMake 警告, 会继续执行
  • AUTHOR_WARNINGCMake 警告 (dev), 会继续执行
  • SEND_ERRORCMake 错误, 继续执行,但是会跳过生成的步骤
  • FATAL_ERRORCMake 错误, 终止所有处理过程
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")

判断文件是否存在

if (NOT EXISTS file)
endif()

循环遍历

foreach(val vals)
endforeach()

执行外部指令

add_custom_command(
    PRE_BUILD 表示在所有其他步骤之前执行自定义命令
    COMMAND 要执行的指令名称
    ARGS 要执行的指令运行参数选项
    DEPENDS 指定命令的依赖项
    OUTPUT 指定要生成的目标名称
    COMMENT 执行命令时要打印的内容
)

install 指令

install 指令⽤于定义安装规则,安装的内容可以包括⽬标⼆进制、动态库、静态库以及⽂件、⽬录、脚本等。
INSTALL(TARGETS targets... [[ARCHIVE|LIBRARY|RUNTIME] [DESTINATION][PERMISSIONS permissions...] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT ] [OPTIONAL] ] [...])
  • 参数中的 TARGETS 后⾯跟的就是我们通过 ADD_EXECUTABLE 或者 ADD_LIBRARY 定义的⽬标⽂件,可能是可执⾏⼆进制、动态库、静态库。
  • ⽬标类型也就相对应的有三种,ARCHIVE 特指静态库,LIBRARY 特指动态库,RUNTIME 特指可执⾏⽬标⼆进制。
  • DESTINATION 定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候 CMAKE_INSTALL_PREFIX 其实就⽆效了。如果你希望使⽤ CMAKE_INSTALL_PREFIX 来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/...
例子:
INSTALL(TARGETS myrun mylib mystaticlib RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION libstatic)
上⾯的例⼦会将:
  • 可执⾏⼆进制 myrun 安装到${CMAKE_INSTALL_PREFIX}/bin 目录
  • 动态库 libmylib 安装到${CMAKE_INSTALL_PREFIX}/lib 目录
  • 静态库 libmystaticlib 安装到${CMAKE_INSTALL_PREFIX}/libstatic 目录

嵌套的 CMake

如果项目很大,或者项目中有很多的源码目录,在通过 CMake 管理项目的时候如果只使用一个 CMakeLists.txt ,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个 CMakeLists.txt 文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。
嵌套的 CMake 是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:
  • 根节点 CMakeLists.txt 中的变量全局有效
  • 父节点 CMakeLists.txt 中的变量可以在子节点中使用
  • 子节点 CMakeLists.txt 中的变量只能在当前节点中使用
我们还需要知道在 CMake 中父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir:指定了 CMakeLists.txt 源文件和代码文件的位置,其实就是指定子目录
  • binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可。
  • EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的 ALL 目标里,并且也会被排除在 IDE 工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式 CMakeLists.txt 文件之间的父子关系就被构建出来了。

案例

我们以微服务架构为例,在一个 test_odb 目录下为综合的大项目,创建一个根 CMakeLists.txt 文件,其下有一个 test 目录,为大项目中的一个小服务,目录结构如下:

根 CMakeLists.txt 文件如下

# 1. 添加cmake版本说明
cmake_minimum_required(VERSION 3.1)
# 2. 声明工程名称
project(all-test)
# 3. 添加子目录
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/test)
# 4. 设置安装目录
set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR})

在 test 目录结构如下,其有一个子节点 CMakeLists.txt 文件,有 entity 目录存放odb映射文件,source 目录存放源文件。

student.hxx 文件如下

#pragma once
#include <string>
#include <cstddef> // std::size_t
#include <odb/nullable.hxx>
#include <odb/core.hxx>
#include <iostream>

#pragma db object
class Student
{
public:
    Student() {}
    Student(unsigned long sn, const std::string &name, unsigned short age, unsigned long cid) : _sn(sn), _name(name), _age(age), _classes_id(cid) {}
    void sn(unsigned long num) { _sn = num; }
    unsigned long sn() { return _sn; }

    void name(const std::string &name) { _name = name; }
    std::string name() { return _name; }

    void age(unsigned short num) { _age = num; }
    odb::nullable<unsigned short> age() { return _age; }

    void classes_id(unsigned long cid) { _classes_id = cid; }
    unsigned long classes_id() { return _classes_id; }

private:
    // 将 odb::access 类作为 person 类的朋友。
    // 这是使数据库支持代码可访问默认构造函数和数据成员所必需的。
    // 如果类具有公共默认构造函数和公共数据成员或数据成员的公共访问器和修饰符,则不需要友元声明
    friend class odb::access;
#pragma db id auto
    unsigned long _id;
#pragma db unique
    unsigned long _sn;
    std::string _name;
    odb::nullable<unsigned short> _age;
#pragma db index
    unsigned long _classes_id;
};

#pragma db object
class Classes
{
public:
    Classes() {}
    Classes(const std::string &name) : _name(name) {}
    void name(const std::string &name) { _name = name; }
    std::string name() { return _name; }

private:
    friend class odb::access;
#pragma db id auto
    unsigned long _id;
    std::string _name;
};

// 查询所有的学生信息,并显示班级名称
#pragma db view object(Student)                                      \
    object(Classes = classes : Student::_classes_id == classes::_id) \
        query((?))
struct classes_student
{
#pragma db column(Student::_id)
    unsigned long id;
#pragma db column(Student::_sn)
    unsigned long sn;
#pragma db column(Student::_name)
    std::string name;
#pragma db column(Student::_age)
    odb::nullable<unsigned short> age;
#pragma db column(classes::_name)
    std::string classes_name;
};

// 只查询学生姓名
#pragma db view object (Student)\
    query((?))
struct all_name
{
    #pragma db column(Student::_name)
    std::string name;
};

main.cc 文件如下

#include <odb/database.hxx>
#include <odb/mysql/database.hxx>
#include "student.hxx"
#include "student-odb.hxx"
#include <gflags/gflags.h>

DEFINE_string(host, "127.0.0.1", "这是Mysql服务器地址");
DEFINE_int32(port, 3306, "这是Mysql服务器端口");
DEFINE_string(db, "TestDB", "数据库默认库名称");
DEFINE_string(user, "root", "这是Mysql用户名");
DEFINE_string(pswd, "2162627569", "这是Mysql密码");
DEFINE_string(cset, "utf8", "这是Mysql客户端字符集");
DEFINE_int32(max_pool, 3, "这是Mysql连接池最大连接数量");

void insert_classes(odb::mysql::database &db)
{
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        Classes c1("一年级一班");
        Classes c2("一年级二班");
        db.persist(c1);
        db.persist(c2);
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "插入数据出错:" << e.what() << std::endl;
    }
}

void insert_student(odb::mysql::database &db)
{
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        Student s1(1, "张三", 18, 1);
        Student s2(2, "李四", 19, 1);
        Student s3(3, "王五", 18, 1);
        Student s4(4, "赵六", 15, 2);
        Student s5(5, "刘七", 18, 2);
        Student s6(6, "孙八", 23, 2);
        db.persist(s1);
        db.persist(s2);
        db.persist(s3);
        db.persist(s4);
        db.persist(s5);
        db.persist(s6);
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "插入学生数据出错:" << e.what() << std::endl;
    }
}

void update_student(odb::mysql::database &db, Student &stu)
{
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        db.update(stu);
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "更新学生数据出错:" << e.what() << std::endl;
    }
}
Student select_student(odb::mysql::database &db)
{
    Student res;
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        typedef odb::query<Student> query;
        typedef odb::result<Student> result;
        result r(db.query<Student>(query::name == "张三"));
        if (r.size() != 1)
        {
            std::cout << "数据量不对!\n";
            return Student();
        }
        res = *r.begin();
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "更新学生数据出错:" << e.what() << std::endl;
    }
    return res;
}

void remove_student(odb::mysql::database &db)
{
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        typedef odb::query<Student> query;
        db.erase_query<Student>(query::classes_id == 2);
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "更新学生数据出错:" << e.what() << std::endl;
    }
}

void classes_student(odb::mysql::database &db)
{
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        typedef odb::query<struct classes_student> query;
        typedef odb::result<struct classes_student> result;
        result r(db.query<struct classes_student>(query::classes::id == 1));
        for (auto it = r.begin(); it != r.end(); ++it)
        {
            std::cout << it->id << std::endl;
            std::cout << it->sn << std::endl;
            std::cout << it->name << std::endl;
            std::cout << *it->age << std::endl;
            std::cout << it->classes_name << std::endl;
        }
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "更新学生数据出错:" << e.what() << std::endl;
    }
}

void all_student(odb::mysql::database &db)
{
    try
    {
        // 获取事务对象开启事务
        odb::transaction trans(db.begin());
        typedef odb::query<Student> query;
        typedef odb::result<struct all_name> result;
        result r(db.query<struct all_name>(query::id > 0));
        for (auto it = r.begin(); it != r.end(); ++it)
        {
            std::cout << it->name << std::endl;
        }
        // 5. 提交事务
        trans.commit();
    }
    catch (std::exception &e)
    {
        std::cout << "查询所有学生姓名数据出错:" << e.what() << std::endl;
    }
}

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    // 1. 构造连接池工厂配置对象
    std::unique_ptr<odb::mysql::connection_pool_factory> cpf(
        new odb::mysql::connection_pool_factory(FLAGS_max_pool, 0));
    // 2. 构造数据库操作对象
    odb::mysql::database db(
        FLAGS_user, FLAGS_pswd, FLAGS_db,
        FLAGS_host, FLAGS_port, "", FLAGS_cset,
        0, std::move(cpf));
    // 4. 数据操作
    // insert_classes(db);
    // insert_student(db);
    //  auto stu = select_student(db);
    //  std::cout << stu.sn() << std::endl;
    //  std::cout << stu.name() << std::endl;
    //  if (stu.age()) std::cout << *stu.age() << std::endl;
    //  std::cout << stu.classes_id() << std::endl;

    // stu.age(15);
    // update_student(db, stu);
    // remove_student(db);
    // classes_student(db);
    all_student(db);
    return 0;
}
子节点 CMakeLists.txt 文件如下
# 1. 添加cmake版本说明
cmake_minimum_required(VERSION 3.1)
# 2. 声明工程名称
project(odb-test)

# 3. 检测并生成ODB框架代码
# 3.1 添加所需的odb映射代码文件名称
set(odb_path ${CMAKE_CURRENT_SOURCE_DIR}/entity)
# 在该测试中只有一个测试代码文件
set(odb_files student.hxx)
# 3.2 检测代码文件是否生成
set(odb_hxx "")
set(odb_cxx "")
set(odb_srcs "")
foreach(odb_file ${odb_files})
    #3.3 如果没有生成则预定义生成指令
    string(REPLACE ".hxx" "-odb.hxx" odb_hxx ${odb_file})
    string(REPLACE ".hxx" "-odb.cxx" odb_cxx ${odb_file})
    if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${odb_cxx})
        add_custom_command(
            PRE_BUILD 
            COMMAND odb
            ARGS -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time ${odb_path}/${odb_file}
            DEPENDS ${odb_path}/${odb_file}
            OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx}
            COMMENT "生成ODB框架代码文件:" ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx}
        )
    endif()
    # 3.4 将所有生成的框架源码文件保存起来
    list(APPEND odb_srcs ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx})
endforeach()

# 4. 获取源码目录下的所有源码文件
set(src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)
# 5. 声明目标及依赖
add_executable(main ${src_files} ${odb_srcs})
# 6. 设置头文件默认搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/entity)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# 7. 设置连接的库
target_link_libraries(main -lodb -lodb-mysql -lodb-boost -lgflags)
# 8. 设置安装路径
INSTALL(TARGETS main RUNTIME DESTINATION bin)

然后进入 test_odb/build 目录下,在这里构建编译

cmake ..
make

就会在该目录下生成很多目录和文件,其中可执行程序 main 就位于 test 目录下

这样编译构建生成的代码就可以与源码完美隔离开。


网站公告

今日签到

点亮在社区的每一天
去签到