示例程序中都只有一个.c
源文件,非常简单。所以,编译这些示例代码其实都非常简单,直接使用 GCC
编译器编译即可,连
Makefile
都不需要。但是,在实际的项目中,并非如此简单,一个工程中可能包含几十、成百甚至上千个源文件,这些源文件按照其类型、功能、模块分别放置在不同的目录中;面对这样的一个工程,通常会使用 make
工具进行管理、编译,
make 工具依赖于 Makefile 文件,通过 Makefile
文件来定义整个工程的编译规则,使用
make
工具来解析
Makefile
所定义的编译规则。
Makefile 带来的好处就是——“自动化编译”,一旦写好,只需要一个
make
命令,整个工程完全按照 Makefile 文件定义的编译规则进行自动编译,极大的提高了软件开发的效率。大都数的
IDE
都有这个工具, 譬如 Visual C++
的
nmake
、
linux
下的
GNU make
、
Qt
的
qmake
等等,这些
make
工具遵循着不同的规范和 标准,对应的 Makefile
文件其语法、格式也不相同,这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台下编译,而如果使用上面的 make
工具,就得为每一种标准写一次 Makefile
,这将是一件让人抓狂的工作。
而 cmake
就是针对这个问题所诞生,允许开发者编写一种与平台无关的
CMakeLists.txt
文件来制定整个工程的编译流程,再根据具体的编译平台,生成本地化的 Makefile
和工程文件,最后执行
make
编译。 因此,对于大多数项目,我们应当考虑使用更自动化一些的 cmake
或者
autotools
来生成
Makefile
,而不是直接动手编写 Makefile
。
cmake 简介
cmake 是一个跨平台的自动构建工具,前面导语部分也已经给大家介绍了,
cmake
的诞生主要是为了解决直接使用 make+Makefile
这种方式无法实现跨平台的问题,所以
cmake
是可以实现跨平台的编译工具,这是它最大的特点
语法规则简单。Makefile
语法规则比较复杂,对于一个初学者来说,通常并不那么友好,并且 Makefile 语法规则在不同平台下往往是不一样的;而
cmake 依赖的是 CMakeLists.txt
文件,该文件的语法规则与平台无关,并且语法规则简单、容易理解!cmake
工具通过解析
CMakeLists.txt
自动帮我们生成 Makefile
,这样就不需要我们自己手动编写
Makefile
了。
//main.c
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
CMakeLists.txt
project(HELLO)
add_executable(hello ./main.c)
⚫
第一行
project(HELLO)
project 是一个命令,命令的使用方式有点类似于
C
语言中的函数,因为命令后面需要提供一对括号, 并且通常需要我们提供参数,多个参数使用空格分隔而不是逗号“,”。 project 命令用于设置工程的名称,括号中的参数
HELLO
便是我们要设置的工程名称;设置工程名称并不是强制性的,但是最好加上。
⚫
第二行
add_executable(hello ./main.c)
add_executable 同样也是一个命令,用于生成一个可执行文件,在本例中传入了两个参数,第一个参数表示生成的可执行文件对应的文件名,第二个参数表示对应的源文件;所以 add_executable(hello ./main.c)
表示需要生成一个名为 hello
的可执行文件,所需源文件为当前目录下的
main.c
。
使用 out-of-source 方式构建
在上面的例子中,cmake
生成的文件以及最终的可执行文件
hello
与工程的源码文件
main.c
混在了一起,这使得工程看起来非常乱,当我们需要清理 cmake
产生的文件时将变得非常麻烦,这不是我们想看到的;我们需要将构建过程生成的文件与源文件分离开来,不让它们混杂在一起,也就是使用 out-of-source
方式构建。
新建build文件夹然后进入到 build 目录下执行 cmake :cd build/cmake ../make
示例二:多个源文件
⚫
hello.h
文件内容
#ifndef __TEST_HELLO_
#define __TEST_HELLO_
void hello(const char *name);
#endif //__TEST_HELLO_
⚫
hello.c
文件内容
#include <stdio.h>
#include "hello.h"
void hello(const char *name)
{
printf("Hello %s!\n", name);
}
⚫
main.c
文件内容
#include "hello.h"
int main(void)
{
hello("World");
return 0;
}
⚫ CMakeLists.txt 文件
project(HELLO)
set(SRC_LIST main.c hello.c)
add_executable(hello ${SRC_LIST})
在本例子中,CMakeLists.txt
文件中使用到了
set
命令,
set 命令用于设置变量,如果变量不存在则创建
该变量并设置它;在本例中,我们定义了一个
SRC_LIST
变量,
SRC_LIST
变量是一个源文件列表,记录生成可执行文件 hello
所需的源文件
main.c
和
hello.c
,而在
add_executable
命令引用了该变量;当然我们也可以不去定义 SRC_LIST
变量,直接将源文件列表写在
add_executable
命令中,如下:
add_executable(hello main.c hello.c)
示例三:生成库文件 (静态库 / 动态库)
在本例中,除了生成可执行文件 hello
之外,我们还需要将
hello.c
编译为静态库文件或者动态库文件, 在示例二的基础上对 CMakeLists.txt
文件进行修改,如下所示:
project(HELLO)add_library(libhello hello.c)add_executable(hello main.c)target_link_libraries(hello libhello)
本例中我们使用到了 add_library
命令和
target_link_libraries
命令。
add_library 命令用于生成库文件,在本例中我们传入了两个参数,第一个参数表示库文件的名字,需要注意的是,这个名字是不包含前缀和后缀的名字;在 Linux
系统中,库文件的前缀是
lib
,动态库文件的后缀是.so
,而静态库文件的后缀是.a
;所以,意味着最终生成的库文件对应的名字会自动添加上前缀和后缀。
第二个参数表示库文件对应的源文件
动态库文件的后缀是.so
,而静态库文件的后缀是.a
本例中,add_library
命令生成了一个静态库文件
liblibhello.a
,如果要生成动态库文件,可以这样做:
add_library(libhello SHARED hello.c) # 生成动态库文件add_library(libhello STATIC hello.c) # 生成静态库文件
target_link_libraries 命令为目标指定依赖库,在本例中,
hello.c
被编译为库文件,并将其链接进
hello
程序。
进入到 build 目录下,执行
cmake
、再执行
make
编译工程,编译完成之后,在
build
目录下就会生成可执行文件 hello
和库文件,如下所示:
![](https://img-blog.csdnimg.cn/ed6fcc930dd84a20b6fd309be27144e5.png)
修改生成的库文件名字
本例中有一点非常不爽,生成的库为 liblibhello.a
,名字非常不好看;如果想生成
libhello.a 该怎么办?只需要在 CMakeLists.txt文件中添加下面这条命令即可:
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
set_target_properties 用于设置目标的属性,这里通过
set_target_properties 命令对 libhello 目标的 OUTPUT_NAME 属性进行了设置,将其设置为 hello
。
cmake_minimum_required(VERSION 3.5)project(HELLO)add_library(libhello SHARED hello.c)set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")add_executable(hello main.c)target_link_libraries(hello libhello)
除了添加 set_target_properties
命令之外,我们还加入了
cmake_minimum_required 命令,该命令用于设置当前工程的 cmake
最低版本号要求,当然这个并不是强制性的,但是最好还是加上。进入到
build
目录下,使用 cmake+make
编译整个工程,编译完成之后会发现,生成的库文件为
libhello.a
,而不是
liblibhello.a
。
示例四:将源文件组织到不同的目录
上面的示例中,我们已经加入了多个源文件,但是这些源文件都是放在同一个目录下,这样还是不太正规,我们应该将这些源文件按照类型、功能、模块给它们放置到不同的目录下,于是笔者将工程源码进行了整理,当前目录结构如下所示:
![](https://img-blog.csdnimg.cn/13d756a787e648cd87ac561f0bb7edf6.png)
在工程目录下,我们创建了 src 和 libhello 目录,并将 hello.c 和 hello.h 文件移动到 libhello 目录下,将 main.c 文件移动到 src 目录下,并且在顶层目录、libhello 目录以及 src 目录下都有一个 CMakeLists.txt 文件。 CMakeLists.txt 文件的数量从 1 个一下变成了 3 个,顿时感觉到有点触不及防!还好每一个都不复杂!我们来看看每一个 CMakeLists.txt 文件的内容。
⚫
顶层
CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(HELLO)
add_subdirectory(libhello)
add_subdirectory(src)
⚫
src
目录下的
CMakeLists.txt
include_directories(${PROJECT_SOURCE_DIR}/libhello)
add_executable(hello main.c)
target_link_libraries(hello libhello)
⚫
libhello
目录下的
CMakeLists.txt
add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
顶层 CMakeLists.txt
中使用了
add_subdirectory 命令,该命令告诉 cmake
去子目录中寻找新的CMakeLists.txt 文件并解析它;而在
src
的
CMakeList.txt
文件中,新增加了
include_directories
命令用来指明头文件所在的路径,并且使用到了 PROJECT_SOURCE_DIR
变量,该变量指向了一个路径,从命名上可知,该变量表示工程源码的目录。
示例五:将生成的可执行文件和库文件放置到单独的目录下
前面还有一点不爽,在默认情况下,make
编译生成的可执行文件和库文件会与
cmake
命令产生的中间文件(CMakeCache.txt
、
CmakeFiles
、
cmake_install.cmake
以及
Makefile
等)混在一起,也就是它们在同一个目录下;如果我想让可执行文件单独放置在 bin
目录下,而库文件单独放置在
lib
目录下,就像下面这样:
![](https://img-blog.csdnimg.cn/6932a4a918414f0995e042da36847984.png)
将库文件存放在 build
目录下的
lib
目录中,而将可执行文件存放在
build
目录下的
bin
目录中,这个时候又该怎么做呢?这个时候我们可以通过两个变量来实现,将 src
目录下的
CMakeList.txt
文件进行修改, 如下所示:
⚫
src
目录下的
CMakeLists.txt
include_directories(${PROJECT_SOURCE_DIR}/libhello)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
add_executable(hello main.c)
target_link_libraries(hello libhello)
⚫ libhello 目录下的 CMakeLists.txt
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
修改完成之后,再次按照步骤对工程进行构建、编译,此时便会按照我们的要求将生成的可执行文件 hello 放置在
build/bin 目录下、库文件 libhello.a 放置在 build/lib
目录下
其实实现这个需求非常简单,通过对 LIBRARY_OUTPUT_PATH
和
EXECUTABLE_OUTPUT_PATH 变 量 进 行 设 置 即 可 完 成 ; EXECUTABLE_OUTPUT_PATH
变量控制可执行文件的输出路径,而 LIBRARY_OUTPUT_PATH 变量控制库文件的输出路径。
设置交叉编译
使用的交叉编译器如下:
arm-poky-linux-gnueabi-gcc #C 编译器
arm-poky-linux-gnueabi-g++ #C++编译器
其实配置交叉编译非常简单,只需要设置几个变量即可,如下所示:
# 配置 ARM 交叉编译
set(CMAKE_SYSTEM_NAME Linux) #设置目标系统名字
set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构
# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器 arm-gcc 和 arm-g++
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/armpoky-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linuxgnueabi/arm-poky-linux-gnueabi-g++)
# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
CMAKE_SYSTEM_NAME 变量在前面给大家介绍过,表示目标主机(譬如
ARM
开发板)的操作系统名称,这里将其设置为 Linux
,表示目标操作系统是
Linux
系统。
CMAKE_SYSTEM_PROCESSOR 变量表示目标架构名称。
CMAKE_SYSROOT 变量前面也给大家介绍过,该变量的值会传递给
gcc
编译器的
--sysroot
选项,也就是--sysroot=${CMAKE_SYSROOT}
,
--sysroot
选项指定了编译器的
sysroot
目录,也就是编译器的系统根目录,编译过程中需要链接的库、头文件等,就会去该目录下寻找,譬如标准 C
库、标准
C
头文件这些。
CMAKE_C_COMPILER 变量指定了 C
语言编译器
gcc
,由于是交叉编译,所以应该指定为
arm-gcc
。
CMAKE_CXX_COMPILER 变量指定了
C++
语言编译器
g++
,由于是交叉编译,所以应该指定为
arm-g++。
CMAKE_C_FLAGS 变量为
gcc
编译器添加编译选项,
CMAKE_CXX_FLAGS
变量为
g++
编译器添加编译选项。
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY 和 CMAKE_FIND_ROOT_PATH_MODE_INCLUDE
被设置为 ONLY
;
CMAKE_FIND_ROOT_PATH_MODE_INCLUDE
变量控制
CMAKE_SYSROOT
中的路径是否被 find_file()
和
find_path()
使用。如果设置为
ONLY
,则只会搜索
CMAKE_SYSROOT
中的路径,如果设置为 NEVER
,则
CMAKE_SYSROOT
中的路径将被忽略并且仅使用主机系统路径。如果设置为
BOTH
,则将搜索主机系统路径和 CMAKE_SYSROOT
中的路径。
同理,CMAKE_FIND_ROOT_PATH_MODE_LIBRARY
变量控制
CMAKE_SYSROOT
中的路径是否被 find_library()使用,如果设置为
ONLY
,则只会搜索
CMAKE_SYSROOT
中的路径,如果设置为
NEVER
, 则 CMAKE_SYSROOT
中的路径将被忽略并且仅使用主机系统路径。如果设置为
BOTH
,则将搜索主机系统路径和 CMAKE_SYSROOT
中的路径。
CMAKE_SYSROOT、
CMAKE_C_COMPILER、CMAKE_CXX_COMPILER 这些变量涉及到交叉编译工具的安装路径,需要根据自己的实际安装路径来确定。
接着我们进行测试,譬如工程目录结构如下所示:
![](https://img-blog.csdnimg.cn/fbf53199acb4481e80dd484830c00d4d.png)
main.c 源文件中调用了 printf()函数打印了“Hello World!”字符串,CMakeLists.txt 文件内容如下:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
##################################
# 配置 ARM 交叉编译
#################################
set(CMAKE_SYSTEM_NAME Linux) #设置目标系统名字
set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构
# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器 arm-linux-gcc 和 arm-linux-g++
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/armpoky-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linuxgnueabi/arm-poky-linux-gnueabi-g++)
# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
#################################
# end
##################################
project(HELLO) #设置工程名称
add_executable(main main.c)
这里要注意,配置 ARM 交叉编译的这些代码需要放置在 project()命令之前,否则不会生效!
![](https://img-blog.csdnimg.cn/1ceafcbf98a34192a6b22a5a8ddca72f.png)