Linux 与 Cmake (一)——了解CMake

  • Post author:
  • Post category:linux


我使用的是

Ubuntu20.04.1

进行学习。

一、

那就先使用Linux编写一个C开始吧:Hello World!

(1)在Terminal中编写C语言程序,进行编译调试。首先先熟悉一下Linux的基本操作命令吧:

Linux命令的名称 作用
mkdir 新建文件夹
cd 更改当前的工作目录
ls 列出文件夹下包含的文件信息
pwd 查看当前工作目录
cp 拷贝文件
rm 删除文件或文件夹
mv 移动文件
cat 查看文件内容
touch 创建文件或更新文件时间

(2)熟悉vi编辑器

vi编辑器分为一般模式、插入模式、底行指令模式:

一般模式:进入vi时,当前为插入模式下按ESC即可到一般模式。

插入模式:按下A,I,O进入(编辑模式,AIO都是有区别的,看你的效率选择)

底行指令模式:使用

: , / ?

进行访问

插入模式用于编辑代码的内容,一般模式用来复制、粘贴、删除等操作,底行模式主要用来保存文件、退出、查找文本内容等。

对于一般模式下:

[N]dd 剪切行内容
[N]yy 复制行内容
[N]x 剪切从光标开始处的N个字符
p或P 粘贴当前行内容,小写p是向下粘贴,大写P是向上粘贴
u 撤销上一步的操作
h,,j,k,l 分别表示向左、向下、向上、向右
1G或G 分别指光标移动到首行和最末行
y0,y$ 表示复制光标到行首部分的内容和光标到行尾部的内容
d0,d$ 表示剪切同上

其中[N]是表示光标处向下N行的内容。

对底行指令模式下:

:w 保存内容
:q! 强制退出
:q 退出未修改的文件
😡 保存并退出(也可以用:wq)
:w 文件名 另存为文件名的文件夹
:r 文件名 读入文件呢诶人插入到当前光标位置
:N 光标移动到第N行
:set nu 显示行号(:set nonu不显示行号)
/string 查找字符串
:s/string1/string2 将字符串string1替换为string2 可以是起始行,结束行:s/string1/string2来将某一范围内的字符串进行替换,在末尾加/g表示所有找到的字符都替换
:!+Linux 可执行Linux命令,执行后可再次回到编辑界面

对于复制粘贴,可以使用起始行,结束行 + y(或者d)的方式对某一部分进行复制(剪切)。

  • 新建文件目录编写程序

mkdir ~/Ctest
  • 进入文件目录进行编写C

cd Ctest
  • 使用vi编辑

vi HelloWold.c
  • 保存并退出

按下ESC键输入:wq退出。

  • 编译并运行

对于gcc-c,是将后缀为.c的文件编译为.o的文件。对于Windows系统中的编译器来说,目标文件后缀为.obj,对于GCC编译器,目标文件的后缀名为.o。

对于gcc-c helloworld.c -o test是生成文件的名字是test.o,-o表示自定义目标文件的意思。

gcc-c helloworld.c -o test
./helloworld.c	#运行

以上是gcc编译和链接分开的方式。下面看看gcc的编译和链接一次性过程:

$ cd Ctest	#源文件所在目录
$ touch test.c #新建空白的源文件
$ gedit test.c #编辑源文件
$ gcc test.c  2022-03-13 21-21-12 的屏幕截图#生成可执行程序
$ ./a.out  #运行可执行程序

最后总结一下

gcc的编译过程

Gcc是GNU编译器套件,包括C、C++、Objective-C、Fortran、Java、Ada和Go语言的前端等。

  • 预处理,生成,i文件

  • 预处理后的文件转换成汇编语言,生成.s文件

  • 汇编变为目标代码(机器代码),生成.o的文件

  • 链接目标代码,生成可执行程序a.out

二、

回到Cmake

cmake官方网站:

www.cmake.org

  • 为什么要使用Cmake?

    为了管理和组织大型的项目代码,使项目代码更清晰可读,也容易维护。决定代码的组织方式及其编译方式,也是程序设计的一部分。

  • 那为什么不使用makefile?

    makefile依赖于当前的编译平台,而且makefile的工作量比较大,解决依赖关系时也容易出错。因此对于大多数的项目,应当考虑使用一些更自动化一些的cmake或者autotools来生成makefile,而不是自己去动手编写。对于大多数的IDE,比如Delphi的make,Visual C++的nmake,Linux下GNU的make等等很多make工具,他们都是带着不同的规范和标准,所执行的makefile格式也是千差万别的。那么回到最初,如果软件想跨平台,必须要保证能够在不同的平台上面编译,那就需要cmake来解决这个问题,它允许开发者编写一种平台无关的CMakeList.txt文件来定制整个编译流程,然后根据目标用户的平台进一步生成所需的本地化Makefile和工程文件,如Unix的Makefile或Windows的Visual Studio工程。实现“write once,run everywhere”。

  • Cmake的主要特点

    1. 开发源码

    2. 跨平台

    3. 能够管理大型项目

    4. 简化编译构建过程和编译过程。Cmake的工具链非常简单:cmake+make。

    5. 可扩展,可编写特定功能的模块。

cmake下载:

Download | CMake

  • 安装Cmake:

sudo apt install cmake
  • 查看Cmake版本:
cmake -version
  • 解压从cmake官网下下来的资料
tar -zxvf cmake-3.23.0-rc3.tar.gz -C ./

-C表示指定目录,./表示当前目录。

  • 进入解压目录

cd cmake-3.23.0-rc3
  • 执行命令安装:
./bootstrap && make && make install

那继续从helloworld开始吧:

按照上面创建helloworld的流程一样,在Hello文件中创建一个helloworld.c的文档。

之后创建一个CMakeLists.txt,他就是Cmake所处理的代码。

在CMakeListtxt中加入这几行:

#规定运行此配置文件所需的CMake的最低版本
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
#参数值是HELLO 表示项目的名称为HELLO
PROJECT(HELLO)
#将名为helloworld.c的源文件编译成一个名称为
ADD_EXECUTABLE(hello helloworld.c)

整个hello项目就已经构建完毕了,下一步进行编译。

在hello目录下新建一个目录build,用于存放生成的所有中间文件和可执行文件。整体项目架构为:

hello/
|-CMakeLists.txt
|-build /
|-helloworld.c

在Windows下的cmake提供了图形界面,设定hello为source目录,build为二进制目录,然后点击configure即可以开始构建,之后进入build目录运行make命令编译。

在Linux下,首先进入目录build,然后运行命令。该命令会使cmake检测编译环境,并生成相应的makefile。接着,运行命令make进行编译。编译后,生成的所有中间文件和可执行文件都会在build目录下:

打开要编译文件所在的文件夹,输入:

cmake .

就可以看到成功生成了Makefile,还有一些cmake运行时自动生成的文件。这个时候生成了Makefile还有可执行文件hello。

然后在终端make并回车就会生成这句:[100%] Built target hello

之后运行hello:


三、对存在错误的Cmake进行改进,深入了解Cmake

这次要更改的是关于lightssdp的一个文件,其中存在有Makefiles以及CMakeList.txt,其中项目的源程序是没有错误的,CMakeList.txt是有很多错误的,那么就开始吧!

使用以上的命令进行编译:

这是一份关于SSDP的程序源码,他要使用Cmake进行编译,先好奇的学一下SSDP吧:

#CMake最低版本号要求
cmake_minimum_required(VERSION 2.8)

#项目信息
project(Demo2)

#指定生成目标
add_executable(Demo main.c MathFuntions.c)

以上程序对应的框架为:

./Demo2
|-main.c
|-MathFunctions.c

那么如果我们一个项目里面他有许多.c文件怎么办?使用

aux_source_directory( <dir> <variable>)

命令,这个命令会去查找指定目录下的所有源文件,然后将其结果存进指定变量名。

那么对于CMakeList.txt的修改即可如下:

# CMake最低版本号要求
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

#项目信息
PROJECT(DEMO 2)

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

#指定生成目标
ADD_EXECUTABLE(DEMO ${DIR_SRCS})
  • 那如果有多个目录,多个源文件的话,需要怎么做?

./DEMO 3
|-MAIN.C
|-MATH/
	|-MATHFUNCTIONS.C
	|-MATHFUNCTIONS.H

在多个目录的情况下,要在每一个目录各编写一个CMakeList.txt文件。

使用

ADD_SUBDIRECTORY

指明项目包含一个子目录,这样子目录中的CmakeList.txt和源代码也会进行处理。

使用

TARGET_LINK_LIBRARIES

指明可执行文件main需要链接一个名为MATHFUNCTIONS的链接库。

那么对于根目录的CMakeList.txt:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

PROJECT(DEMO3)

AUX_SOURCE_DIRECTORY(.DIR_SRCS)

#添加MATH子目录
ADD_SUBDIRECTORY(MATH)

ADD_EXECUTABLE(DEMO MAIN.C)

#添加链接库
TARGET_LINK_LIBRARIES(DEMO MATHFUNCTIONS)

对于子目录的CMakeList.txt:

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

# 生成链接库
ADD_LIBRARY(MATHFUNCTIONS ${DIR_LIBZ_SRCS})
  • 自定义编译选项

CMake允许为项目增加编译选项,从而可以根据用户的环境和需求选择最合适的编译方案。比如说,我们需要调用一个可选的数学库MATHFUNCTIONS.C,那么就要将该选项定义为ON,否则就调用标准库中的数学函数库。

使用

configure_file

命令用于加入一个配置头文件config.h,这个文件由Cmake从config.h生成,通过这样的机制,可以通过预定义一些参数和变量来控制代码的生成。

使用

option

命令添加一个USE_MYMATH,并且默认值为ON。

使用USE_MYMATH变量的值来决定是否使用我们自己编写的MathFunctions库。

那么对应的CMakeList.txt可以这么修改:

# CMake 最低版本要求
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

# 项目信息
PRJECT(DEMO 4)

# 加入一个配置头文件,用于处理CMake对源码的设置
CONFIGURE_FILE(
	"${PROJECT_SOURCE_DIR}/config.h.in"
	"${PROJECT_BINARY_DIR}/config.h"
)

# 是否使用自己的MathFunctions库
OPTION(USE_MYMATH
		"Use provided math implemengtation" ON)
		
# 是否加入MathFunctions库
IF(USE_MYMATH)
	INCLUDE_DIRECTORIES("${PROJECT_SOURCE_DIR}/math")
	ADD_SUBDIRECTORY(MATH)
	SET(EXTRA_LIBS ${EXTRA_LIBS} MATHFUNCTIONS)
ENDIF(USE_MYMATH)

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

# 指定生成目标
ADD_EXECUTABLE(DEMO ${DIR_SRCS})
TARGET_LINK_LIBRARIES(DEMO ${EXTRA_LIBS})

之后修改所在的main.c函数,让其根据USE_MYMATH的预定义来决定是否调用标准库还是MathFunctions库:

#include <stdio.h>
#include <stdlib.h>
#include <config.h>

#ifdef USE_MYMATH
	#include "math/MathFunctions.h"
#else
	#include <math.h>
#endif

int main(int argc,char *argv[])
{
	...
#ifdef USE_MYMATH
	printf("Now we use our own Math library.\n");
	...
#else
	printf("Now we use the standard library.\n");
	...
#endif
	...
	return 0;
}


config.h.in文件到底在干什么?

configure.h文件

预定义了USE_MYMATH的值

,但是我们并不直接编写这个文件。我们的操作是编写一个config.h.in文件,使configure.h方便预定义:

在configure.h.in中编写:

#cmakedefine USE_MYMATH

编译项目

ccmake 或者是 cmake -i(会提供一个交互式配置界面)

交互式界面可以更改变量USE_MYMATH的值,当如果我们设置这个值为ON时:

在config.h的值为:

#define USE_MYMATH

当设置这个值为OFF时:

在config.h的值为:

/* #undef USE_MYMATH */
  • 安装与测试

CMake也可以指定安装规则,以及添加测试。这两个功能分别可以通过在产生Makefile后使用

make install



make test

来执行。


定制安装规则

在math/CMakeLists.txt的文件中添加:

#指定MathFunctions库的安装路径
install(TARGETS MathFunctions DESTNATION bin)
install(FILES MathFunctions.h DESTNATION include)

要指明MathFunctions库的安装路径。之后同样修改根目录的CMakeLists.txt文件,在末尾添加:

# 指定安装路径
install (TARGETS Demo DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/config.h"
		DESTINATION include)

生成的Demo文件和MathFunctions函数库libMathFunctions.o文件会被复制到

/usr/local/bin

中,而MathFunctions.h和生成的config.h文件则会被复制到

/usr/local/include

中。其中的/usr/local是默认安装到根目录。如果需要指定文件应该拷贝到哪个根目录下的话,可以使用

CMAKE_INSTALL_PREFIX

变量。


添加测试

CMake提供了一个称为CTest的测试工具。只需要在项目的根目录的CMakeLists文件中调用一系列的ADD_TEST命令。

如果需要了解了解关于CTest更详细的用法可以在终端中输入:

man 1 ctest

使用

test_run

用来测试程序是否成功并返回0值。

使用

PASS_REGULAR_EXPRESSION

用来测试输出是否包含后面跟着的字符串。

# 启用测试
enable_testing()

# 测试程序是否成功运行
add_test(test_run Demo 5 2)

# 测试帮助信息是否可以正常提示
add_test(test_usage Demo)
set_tests_properties(test_usage
 PROPERTIES PASS_REGULAR_EXPRESSION "	Usage : .* base exponent")

# 测试5的平方
add_test(test_5_2 Demo 5 2)
set_tests_properties(test_5_2
PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")

如果要测试很多数据的话,就需要一个一个往里面添加,很繁琐,可以封装为下面这个函数:

# 定义一个宏,用来简化测试工作
macro(do_test arg1 arg2 result)
	add_test(test_${arg1}_${arg2} Demo ${arg1} ${arg2})
	set_tests_properties(test_${arg1}_${arg2}
	  	PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro(do_test)

#使用一系列宏进行一系列的数据测试
do_test(5 2 "is 25")
  • 支持gdb

让CMake支持gdb的设置只需要指定Debug模式下开启-g选项。

set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -00 -Wall -g -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -03 -Wall")
  • 添加环境检查

适用于检查系统环境或者是使用一个平台相关特效的时候。比如会去检查系统是否自带我们需要的pow函数,有则调用,无则使用我们定义的pow。


添加CHECKFUNCTIONSEXISTS宏

首先在顶层CMakeLists文件中添加

CheckFuntionExists.cmake

宏,并调用

check_function_exists

命令测试链接器是否能够在链接阶段找到pow函数。

# 检查系统是否支持pow函数
include (${CMAKE_ROOT}/Modules/CheckFuntionExists.cmake)
check_function_exists(pow HAVE_POW)

要把上面这段代码放在

configure_file

命令前。


预定义相关宏变量

修改config.h.in文件,预定义相关的宏变量:

#cmakedefine HAVE_POW


在代码中使用宏和函数

#ifdef HAVE_POW
	printf("Now we use the standard library .\n");
	double result = pow(base, exponent);
#else
	printf("Now we use our own Math library .\n");
	double result = power(base,exponent);
#endif
  • 添加版本号

给项目添加和维护版本号有利于用户了解每个版本的维护情况,并及时了解当前所用的版本是否过时,还是说会出现可能不兼容的情况。

首先修改顶层CMakeLists文件,在

project

命令之后加入如下两行:

set(Demo_VERSION_MAJOR 1)
set(Demo_VERSION_MINOR 0)

分别指定当前的项目的主版本号和副版本号。

为了之后在代码中获取版本信息,我们可以修改config.h.in文件,添加两个预定义的变量:

#define Demo_VERSION_MAJOR @Demo_VERSION_MAJOR@
#define Demo_VERSION_MINOR @Demo_VERSION_MINOR@

这样就可以直接在代码中打印版本信息了:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#include "math/MathFunctions.h"

int main(int argc, char *argv[])
{
    if (argc < 3){
        // print version info
        printf("%s Version %d.%d\n",
            argv[0],
            Demo_VERSION_MAJOR,
            Demo_VERSION_MINOR);
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    
#if defined (HAVE_POW)
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#else
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#endif
    
    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}
  • 生成安装包

安装包包括二进制安装包和源码安装包,使用CPack,他也是CMake提供的一个工具,专门用于打包。

需要在CMakeLists.txt文件尾部添加下面几行:

# 构建一个CPack安装包
# 导入InstallRequiredSystemLibraries模块,以便之后导入CPack
include (InstallRequiredSystemLibraries)
#设置一些CPack相关变量,包括版权信息和版本信息
set(CPACK_RESOURCE_FILE_LICENSE
	"${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Demo_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Demo_VERSION_MINOR}")
#导入CPack模块
include (CPack)

之后使用命令就可以了:

生成二进制安装包:

cpack -C CPackConfig.cmake

生成源码安装包

cpack -C CPackSourceConfig.cmake

之后执行:

cpack -C CPackConfig.cmake

就可以得到三个二进制文件,文件的内容都是一样的,可以执行其中一个,就会出现由CPack自动生成的交互式安装界面。

关于CPack的更详细用法可以通过

man 1 cpack

参考CPack的文档。

引用文档:

CMake 入门实战 | HaHack



版权声明:本文为Alkaid2000原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。