升级构建工具,从Makefile到CMake

Posted 易水南风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了升级构建工具,从Makefile到CMake相关的知识,希望对你有一定的参考价值。

更多博文,请看音视频系统学习的浪漫马车之总目录

上一篇文章一篇文章入门C/C++自动构建利器之Makefile介绍了构建项目的脚本工具Makefile,相比于手动执行gcc命令来说,已经方便了太多了。不过如果只用Makefile构建项目,会有一个问题,那就是不支持跨平台的问题,因为Makefile脚本中的命令是和具体平台绑定的,比如上篇文章就是用GNU的gcc命令,那如果项目要移植到其他不能使用gcc命令的平台,那又要重新写一套Makefile,这可不符合懒惰的程序员群体,所以支持跨平台语法又更简洁的CMake就应运而生了。

本文算是CMake的入门,阅读本文前建议先将前面2篇文章看完,至少也要看完一篇文章入门C/C++自动构建利器之Makefile,不然就会因为缺少对项目构建的整体认识而容易感到雾里看花。

CMake介绍

CMake官网开头已经把CMake的作用说得很清楚了:

CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. The suite of CMake tools were created by Kitware in response to the need for a powerful, cross-platform build environment for open-source projects such as ITK and VTK.

上面有一段文字,一句话概括就是CMake是一段跨平台的构建脚本,可以根据具体平台上生成对应的makefile,所以CMake的本质还是生成makefile,然后还是通过makefile来构建项目,CMake本身不构建项目。

以下就从简单到复杂项目来谈一谈CMake如何使用,所用平台依旧是Ubuntu。

单目录单文件

首先在Demo1目录下创建源文件main.cpp:

#include <iostream>
  
int main() 
    std::cout << "Hello, World!" << std::endl;
    return 0;


然后创建CMakeLists文件(根据规范,CMakeLists.txt这个名字是固定的):

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo$ cd Demo1/
ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo1$ ls
main.cpp
#创建CMakeLists文件(名字固定)
ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo1$ vim CMakeLists.txt

编写CMakeLists.txt:

#指定cmake的版本号
cmake_minimum_required(VERSION 3.16)
#工程名称
project(CmakeDemo)
#指定C++版本
set(CMAKE_CXX_STANDARD 14)
#指定生成可执行文件名称和依赖的源文件(可以多个)
add_executable(CmakeDemo main.cpp)

可以看到,cmake脚本相比Makefile更加简洁,直接指定源文件和最终可执行文件即可(终于不用为那些gcc命令和中间的汇编、目标文件什么的操碎心了)。通过一句“add_executable”,仿佛就是一句“我用main.cpp生成可执行文件CmakeDemo ”,然后cmake命令就把对应的Makfile生成了。

cmake 命令格式:

cmake [选项] <现有构建路径>

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo1$ cmake .
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /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: /home/ubuntu/study/projects/CmakeDemo/Demo1

此时cmake指令生成了很多文件,注意到熟悉的Makefile文件已经生成:

对于Makefile,那当然就是执行make指令去构建项目:

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo1$ make 
Scanning dependencies of target CmakeDemo
[ 50%] Building CXX object CMakeFiles/CmakeDemo.dir/main.cpp.o
[100%] Linking CXX executable CmakeDemo
[100%] Built target CmakeDemo

可以很开心地看到可执行文件已经生成:

运行下:

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo1$ ./CmakeDemo 
Hello, World!

没问题~~

单目录多文件

现在在原来的基础上进行调整,增加了Dog类:

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo2$ ls
CMakeLists.txt  Dog.cpp  Dog.h  main.cpp

Dog.h:

#ifndef CMAKEDEMO_DOG_H
#define CMAKEDEMO_DOG_H


class Dog 
public:
    void shut();

;


#endif //CMAKEDEMO_DOG_H

Dog.cpp:

#include "Dog.h"
#include <iostream>

void Dog::shut() 
    std::cout << "I am dog!" << std::endl;

main.cpp:

#include "Dog.h"

int main() 
    Dog dog;
    dog.shut();
    return 0;

对应的CMakeLists.txt改动很小,只需要在add_executable中增加Dog类相关文件即可:

#指定cmake的版本号
cmake_minimum_required(VERSION 3.16)
#工程可执行文件名称
project(CmakeDemo1:)
#指定C++版本
set(CMAKE_CXX_STANDARD 14)
#指定生成可执行文件的源文件
add_executable(CmakeDemo main.cpp Dog.cpp Dog.h)

这样每次增加类都要修改CMakeLists.txt,想起Makefile有变量和通配符,那CMake有没有呢?

答案是肯定的。

#指定cmake的版本号
cmake_minimum_required(VERSION 3.16)
#工程可执行文件名称
project(CmakeDemo1:)
#指定C++版本
set(CMAKE_CXX_STANDARD 14)


# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
#指定生成可执行文件的源文件
add_executable(CmakeDemo $DIT_SRCS)

只要增加aux_source_directory就可以得到目录下的所有文件,然后将所有文件赋值给一个变量DIR_SRCS,传给add_executable即可。

cmake有大量内置变量,有些是用来获取当前环境信息的,比如获取系统版本的CMAKE_SYSTEM_VERSION,有些用来获取当前工程信息的,比如获取工程目录的PROJECT_SOURCE_DIR,有些是用来改变构建过程的,比如编译 C++ 文件时的选项CMAKE_CXX_FLAGS。

官方变量文档就介绍了这些内置变量:cmake-variables

多目录多文件

还是刚才的工程源文件,不过将Dog类放到lib目录下,lib目录下创建一个CMakeLists.txt文件,用将Dog类打包为一个动态链接库,然后交给main.cpp链接为一个可执行文件。

lib中的CMakeLists.txt:

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)

# 指定生成 Animal链接库,SHARED 表示生成动态链接库
add_library (Animal SHARED $DIR_LIB_SRCS)

其实非常简单,用add_library 生成一个动态链接库libAnimal.so(前缀lib和后缀.so是系统自动会加上去的)

Demo3中的CMakeLists.txt:

#指定cmake的版本号
cmake_minimum_required(VERSION 3.16)
#工程可执行文件名称
project(Demo)
#指定C++版本
set(CMAKE_CXX_STANDARD 14)
# 添加一个子目录并构建该子目录。
add_subdirectory(lib)
#添加头文件目录,因为当前根目录的main.cpp需要引用到Dog.h头文件
include_directories(lib)
# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
#打印出DIR_SRCS变量用于调试
Message("DIR_SRCS = $DIR_SRCS")
#指定生成可执行文件的源文件
add_executable(Demo $DIR_SRCS)
# 添加链接库
target_link_libraries(Demo Animal)

多目录这里重点有三个:

1.添加头文件目录:
因为当前根目录的main.cpp需要引用到Dog.h头文件,所以需要通过include_directories引用头文件目录,注意CMakeLists的目录是相对于当前CMakeLists文件而言的。

2.add_subdirectory,官方的说明是

Add a subdirectory to the build.

就是要让当前的CMakeLists可以执行到执指定子目录的CMakeLists进而构建子目录中的源文件,在这里就是通过子目录构建处对应的动态链接库libAnimal.so。

3.链接操作:

target_link_libraries(Demo Animal)

将生成的Demo可执行文件和动态链接库进行链接(当然这里还不是真正的链接,因为动态链接库是在运行时链接的)

多目录多文件标准化

上面的工程结构不太规范,比如CMake产生的文件和源文件放在一起,导致这一部分文件不方便统一处理,现在一般标准的工程结构是这样的:


一个build目录专门用于存放CMake产生的文件,工程源文件和库源文件分开存放在src和lib目录,工程根目录有个CMakeLists.txt用于管理全局的CMakeLists文件。

根目录的CMakeLists.txt:

CMAKE_MINIMUM_REQUIRED(VERSION 3.16)
  
PROJECT(Demo)
#引入2个子目录
ADD_SUBDIRECTORY(lib)
ADD_SUBDIRECTORY(src)

非常简单,将src和lib目录加入构建就好。

src的CMakeLists.txt:

#指定可执行文件输出路径为执行CMake的目录下的/scr/bin(/home/ubuntu/study/projects/CmakeDemo/Demo4/build/src/bin)
SET(EXECUTABLE_OUTPUT_PATH $PROJECT_BINARY_DIR/bin)
# 添加 lib 子目录
include_directories(../lib)
#添加头文件目录
include_directories(../lib)
# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
Message("DIR_SRCS = $DIR_SRCS")
Message("PROJECT_SOURCE_DIR = $PROJECT_SOURCE_DIR")
Message("PROJECT_BINARY_DIR = $PROJECT_BINARY_DIR")

#指定生成可执行文件的源文件
add_executable(Demo $DIR_SRCS)
# 添加链接库
target_link_libraries(Demo Animal)

lib的CMakeLists.txt:

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)
#指定库的输出路径为lib(/home/ubuntu/study/projects/CmakeDemo/Demo4/build/lib)
SET(LIBRARY_OUTPUT_PATH $PROJECT_BINARY_DIR/lib)
# 指定生成 Animal链接库
add_library (Animal SHARED $DIR_LIB_SRCS)

由于需要在build下生成构建文件,所以需要在build中执行CMake:

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo4/build$ cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
DIR_SRCS = ./main.cpp
PROJECT_SOURCE_DIR = /home/ubuntu/study/projects/CmakeDemo/Demo4/src
PROJECT_BINARY_DIR = /home/ubuntu/study/projects/CmakeDemo/Demo4/build/src
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/study/projects/CmakeDemo/Demo4/build

此时build文件已经生成构建文件Makefile和对应的构建结果文件lib和src,并且lib和src已经分别生成了各自对应的Makefile文件。

在build下执行make:

此时可执行文件和动态链接库已经生成,执行可执行文件:

完美~~

总结下,这里主要就是创建了一个根目录作为统领的CMakeLists.txt,并且修改原来src和lib的CMakeLists.txt一些产出路径,使得它们CMake产生的文件放在build下对应的目录,即工程的放在build目录,src和lib的分别放在build/src和build/lib目录。

最后的目录结构为:

ubuntu@VM-20-7-ubuntu:~/study/projects/CmakeDemo/Demo4$ tree
.
├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   │   ├── 3.16.3
│   │   │   ├── CMakeCCompiler.cmake
│   │   │   ├── CMakeCXXCompiler.cmake
│   │   │   ├── CMakeDetermineCompilerABI_C.bin
│   │   │   ├── CMakeDetermineCompilerABI_CXX.bin
│   │   │   ├── CMakeSystem.cmake
│   │   │   ├── CompilerIdC
│   │   │   │   ├── a.out
│   │   │   │   ├── CMakeCCompilerId.c
│   │   │   │   └── tmp
│   │   │   └── CompilerIdCXX
│   │   │       ├── a.out
│   │   │       ├── CMakeCXXCompilerId.cpp
│   │   │       └── tmp
│   │   ├── cmake.check_cache
│   │   ├── CMakeDirectoryInformation.cmake
│   │   ├── CMakeOutput.log
│   │   ├── CMakeTmp
│   │   ├── Makefile2
│   │   ├── Makefile.cmake
│   │   ├── progress.marks
│   │   └── TargetDirectories.txt
│   ├── cmake_install.cmake
│   ├── lib
│   │   ├── CMakeFiles
│   │   │   ├── Animal.dir
│   │   │   │   ├── build.make
│   │   │   │   ├── cmake_clean.cmake
│   │   │   │   ├── CXX.includecache
│   │   │   │   ├── DependInfo.cmake
│   │   │   │   ├── depend.internal
│   │   │   │   ├── depend.make
│   │   │   │   ├── Dog.cpp.o
│   │   │   │   ├── flags.make
│   │   │   │   ├── link.txt
│   │   │   │   └── progress.make
│   │   │   ├── CMakeDirectoryInformation.cmake
│   │   │   └── progress.marks
│   │   ├── cmake_install.cmake
│   │   ├── libAnimal.so
│   │   └── Makefile
│   ├── Makefile
│   └── src
│       ├── bin
│       │   └── Demo
│       ├── CMakeFiles
│       │   ├── CMakeDirectoryInformation.cmake
│       │   ├── Demo.dir
│       │   │   ├── build.make
│       │   │   ├── cmake_clean.cmake
│       │   │   ├── CXX.includecache
│       │   │   ├── DependInfo.cmake
│       │   │   ├── depend.internal
│       │   │   ├── depend.make
│       │   │   ├── flags.make
│       │   │   ├── link.txt
│       │   │   ├── main.cpp.o
│       │   │   └── progress.make
│       │   └── progress.marks
│       ├── cmake_install.cmake
│       └── Makefile
├── CMakeLists.txt
├── lib
│   ├── CMakeLists.txt
│   ├── Dog.cpp
│   └── Dog.h
└── src
    ├── CMakeLists.txt
    └── main.cpp

总结

本文算是对CMake的入门,就作为一种抛砖引玉的作用吧,CMake本身也比较简单,详细的用法一般可以通过查CMake官方文档或者百度谷歌解决,有了CMake的基础,那么下一步就可以稳步向ndk进军啦。

如果觉得本文有帮助,别忘了点赞关注哦~

以上是关于升级构建工具,从Makefile到CMake的主要内容,如果未能解决你的问题,请参考以下文章

升级构建工具,从Makefile到CMake

项目构建工具CMake、GYP、GN

初见 cmake

cmake和makefile区别和cmake指定编译器(cmake -G)

初探ndk的世界

初探ndk的世界