Fork me on GitHub

Makefile以及cmake

  make通常被视为一种软件构建工具,主要经由读取一种名为makefile 或 Makefile的文件来实现软件的自动化建构。它会通过一种被称之为target概念来检查相关文件之间的依赖关系,这种依赖关系的检查系统非常简单,主要通过对比文件的修改时间来实现。在大多数情况下,我们主要用它来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。

  特别是在类Unix系统下的软件编译,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的makeVisual C++的nmakeLinux下的GNU make

  Makefile基本格式如下

1
2
3
4
5
6
7
target ... : prerequisites ...
command
......
......
###target - 目标文件, 可以是 Object File, 也可以是可执行文件
###prerequisites - 生成 target 所需要的文件或者目标
###command - make需要执行的命令 (任意的shell命令), 必须以[tab]开头

  Makefile基本规则

1
2
3
4
5
显示规则 :: 说明如何生成一个或多个目标文件(包括 生成的文件, 文件的依赖文件, 生成的命令)
隐晦规则 :: make的自动推导功能所执行的规则
变量定义 :: Makefile中定义的变量
文件指示 :: Makefile中引用其他Makefile; 指定Makefile中有效部分; 定义一个多行命令
注释 :: Makefile只有行注释 "#", 如果要使用或者输出"#"字符, 需要进行转义, "\#"

  make基本的工作方式

1
2
3
4
5
6
7
1.读入主Makefile (主Makefile中可以引用其他Makefile)
2.读入被include的其他Makefile
3.初始化文件中的变量
4.推导隐晦规则, 并分析所有规则
5.为所有的目标文件创建依赖关系链
6.根据依赖关系, 决定哪些目标要重新生成
7.执行生成命令

  示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
[free@hurd]$ vim foo1.c
#include <stdio.h>
void foo1()
{
printf("This is foo1!\n");
}
[free@hurd]$ vim foo2.c
#include <stdio.h>
void foo2()
{
printf("This is foo2!\n");
}
[free@hurd]$ vim main.c
#include <stdio.h>

int main(void)
{
foo1();
foo2();
printf("This is main!\n");
return 0;
}
[free@hurd]$ vim Makefile
main: foo1.c foo2.c main.c
gcc foo1.c foo2.c main.c -o main
###这是一个最简单的Makefile
###生成目标main依赖foo1.c foo2.c main.c
###执行命令gcc foo1.c foo2.c main.c -o main;完成目标
[free@hurd]$ make
gcc foo1.c foo2.c main.c -o main
[free@hurd]$ ls
foo1.c foo2.c main main.c Makefile
[free@hurd]$ ./main
This is foo1!
This is foo2!
This is main!
###成功执行,现在改进Makefile
[free@hurd]$ vim Makefile
main: main.o foo1.o foo2.o
gcc main.o foo1.o foo2.o -o main
main.o: main.c
gcc -c main.c
foo1.o: foo1.c
gcc -c foo1.c
foo2.o: foo2.c
gcc -c foo2.c
###改写后各种依赖更清楚
###只要有一项更改,重新make只重新编译更新的部分,其他不变
[free@hurd]$ make
gcc -c main.c
gcc -c foo1.c
gcc -c foo2.c
gcc main.o foo1.o foo2.o -o main
[free@hurd]$ ./main
This is foo1!
This is foo2!
This is main!
###成功执行,继续改进Makefile
[free@hurd]$ vim Makefile
target= main
src= $(shell find . -name "*.c")
obj= $(src:%.c=%.o)

$(target): $(obj)
gcc $^ -o $@

%.o: %.c
gcc -c $< -o $@
###这里引入了变量,其实和C语言中的宏类似
###$(shell ****)使用了shell命令,将搜索到的结果传给src
###$(src:%.c=%.o)是进行字符替换,将所有的*.c字符串替换成*.o字符串,传给obj
###$^:所有依赖目标的集合
###$@:目标集合
###$<:第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标
###改进后,更加精炼清晰,而且方便以后拓展构建
[free@hurd]$ make
gcc -c foo2.c -o foo2.o
gcc -c main.c -o main.o
gcc -c foo1.c -o foo1.o
gcc foo2.o main.o foo1.o -o main
[free@hurd]$ ./main
This is foo1!
This is foo2!
This is main!
###成功执行,进一步改进Makefile
[free@hurd]$ vim Makefile
CC = gcc
CFLAGS = -Wall
CXXFLAGS =
CPPFLAGS =
LDFLAGS =
target = main
src = $(shell find . -name "*.c")
obj = $(src:%.c=%.o)

$(target): $(obj)
$(CC) $^ $(CFLAGS) -o $@

%.o : %.c
$(CC) -c $< $(CFLAGS) $(CPPFALGS) -o $@

.PHONY:clean
clean:
-rm -rf $(obj) $(target)

install:
mkdir target
mv main target

test:
@echo $(src)
@echo $(obj)
###改进后基本看不到源文件的影子,慢慢走向通用化更易于拓展
###此处还添加了伪目标
###.PHONY意思表示clean是一个"伪目标";而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——"clean从来都是放在文件的最后"
###"伪目标"并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了
###为了避免和文件重名的这种情况,可以使用一个特殊的标记".PHONY"来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是"伪目标"

  Makefile 命令前缀

1
2
3
4
不用前缀  :: 输出执行的命令以及命令执行的结果, 出错的话停止执行
前缀 @ :: 只输出命令执行的结果, 出错的话停止执行
前缀 - :: 命令执行有错的话, 忽略错误, 继续执行
###例如上面的-rm、@echo

  make 退出码

1
2
3
0 :: 表示成功执行
1 :: 表示make命令出现了错误
2 :: 使用了 "-q" 选项, 并且make使得一些目标不需要更新

  make 常用参数

参数 含义
–debug[=] 输出make的调试信息, options 可以是 a, b, v
-j –jobs 同时运行的命令的个数, 也就是多线程执行 Makefile,比如常用的make -j8
-r –no-builtin-rules 禁止使用任何隐含规则
-R –no-builtin-variabes 禁止使用任何作用于变量上的隐含规则
-B –always-make 假设所有目标都有更新, 即强制重编译

   命令变量 和 命令参数变量

变量名 含义
RM rm -f
AR ar
CC cc
CXX g++
ARFLAGS AR命令的参数
CFLAGS C语言编译器的参数
CXXFLAGS C++语言编译器的参数
CPPFLAGS 预处理阶段的参数
LDFLAGS 传递给连接器的参数
LIBS 链接库的参数
1
2
3
4
5
6
7
8
9
10
11
[free@hurd]$ vim Makefile
all:
@echo $(RM)
@echo $(AR)
@echo $(CC)
@echo $(CXX)
[free@hurd]$ make
rm -f
ar
cc
g++

  自动变量

自动变量 含义
$@ 目标集合
$% 当目标是函数库文件时, 表示其中的目标文件名
$< 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标
$? 比目标新的依赖目标的集合
$^ 所有依赖目标的集合, 会去除重复的依赖目标
$+ 所有依赖目标的集合, 不会去除重复的依赖目标
$* 这个是GNU make特有的, 其它的make不一定支持

  Makefile中一些约定俗成的伪目标

伪目标 含义
all 所有目标的目标,其功能一般是编译所有的目标
clean 删除所有被make创建的文件
install 安装已编译好的程序,其实就是把目标可执行文件拷贝到指定的目录中去
print 列出改变过的源文件
tar 把源程序打包备份. 也就是一个tar文件
dist 创建一个压缩文件, 一般是把tar文件压成Z文件. 或是gz文件
TAGS 更新所有的目标, 以备完整地重编译使用
check 或 test 一般用来测试makefile的流程

  以上只是简单介绍了 Makefile,足以应付一些小项目,但当遇到一些工程级的大项目手写 Makefile 变得十分困难繁琐,而且不难以跨平台,我们需要其他的工具来生成 Makefile。

  CMake是一个比make更高级的编译配置工具,它可以根据不同平台、不同的编译器,生成相应的Makefile或者vcproj项目。通过编写CMakeLists.txt,可以控制生成的Makefile,从而控制编译过程。CMake自动生成的Makefile不仅可以通过make命令构建项目生成目标文件,还支持安装(make install)、测试安装的程序是否能正确执行(make test,或者ctest)、生成当前平台的安装包(make package)、生成源码包(make package_source)、产生Dashboard显示数据并上传等高级功能,只要在CMakeLists.txt中简单配置,就可以完成很多复杂的功能,包括写测试用例。如果有嵌套目录,子目录下可以有自己的CMakeLists.txt。

  在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

  1. 编写 CMake 配置文件 CMakeLists.txt
  2. 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile 。其中, PATH 是 CMakeLists.txt 所在的目录
  3. 使用 make 命令进行编译。

  示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
###这是一个简单的C快速排序程序
[free@hurd]$ vim test.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void swap(int *x, int *y)
{
int tmp;
tmp = *x;
*x = *y;
*y = tmp;

return ;
}

void display_arr(int *arr, int len)
{
int i;
for(i = 0; i < len; i++)
{
printf("%d\t", arr[i]);
if((i + 1) % 10 == 0)
printf("\n");
}
printf("\n");
return ;
}

void genereate(int *arr, int len, int maxnum)
{
int i;
srand(time(NULL));

for(i = 0; i < len; i++)
{
arr[i] = rand()%maxnum;
}

return ;
}

void sort(int *arr, int left, int right)
{
if(left < right)
{
int i, j;
i = left + 1;
j = right;
while(i < j)
{
if(arr[i] > arr[left])
{
swap(&arr[i], &arr[j]);
j--;
}
else
i++;
}

if(arr[i] >= arr[left])
i--;
swap(&arr[left], &arr[i]);

sort(arr, left, i);
sort(arr, j, right);
}
}

int main(int argc, char *argv[])
{
if(argc != 3)
{
fprintf(stderr,"\e[1;31mUsage: ./a.out len maxnum\e[0m");
exit(1);
}

int len, maxnum;
len = atoi(argv[1]);
maxnum = atoi(argv[2]);
if(len <= 0)
{
fprintf(stderr,"\e[1;31mThe array of length error\e[0m\n");
exit(1);
}
if(maxnum <= 0)
{
fprintf(stderr, "\e[1;31mThe maxnum is error\e[0m\n");
exit(1);
}
int arr[len];
printf("\e[1;32m排序前的随机数组: \e[0m\n");
genereate(arr, len, maxnum);
display_arr(arr, len);
printf("\e[1;32m排序后的有序数组: \e[0m\n");
sort(arr, 0, len - 1);
display_arr(arr, len);
exit(0);
}
###编写CMakeLists.txt
[free@hurd]$ vim CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (cmake_test)
# 指定生成目标
add_executable(test test.c)

###CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的。符号 # 后面的内容被认为是注释。命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔
###cmake_minimum_required:指定运行此配置文件所需的 CMake 的最低版本;
###project:参数值是 cmake_test,该命令表示项目的名称是 cmake_test
###add_executable: 将名为 test.c 的源文件编译成一个名称为 test 的可执行文件
[free@hurd]$ cmake .
-- The C compiler identification is GNU 4.8.4
-- The CXX compiler identification is GNU 4.8.4
-- 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
-- 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
-- Configuring done
-- Generating done
-- Build files have been written to: /home/free/cmake

###得到生成的Makefile,并执行make
[free@hurd]$ make
Scanning dependencies of target test
[100%] Building C object CMakeFiles/test.dir/test.c.o
Linking C executable test
[100%] Built target test

[free@hurd]$ ./test 20 255
排序前的随机数组:
71 130 200 244 31 149 206 241 4 34
127 11 251 167 233 68 250 231 247 14

排序后的有序数组:
4 11 14 31 34 68 71 127 130 149
167 200 206 231 233 241 244 247 250 251
###下面对test.c进行拆分,将单独的函数抽取出来
[free@hurd]$ ls
CMakeCache.txt CMakeFiles cmake_install.cmake CMakeLists.txt display_arr.c genereate.c Makefile sort.c swap.c test test.c
[free@hurd]$ vim CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (cmake_test)
# 指定生成目标
add_executable(test test.c display_arr.c genereate.c sort.c swap.c)
###唯一的改动只是在 add_executable 命令中增加了其他的 .c源文件
###如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作;更省事的方法是使用 aux_source_directory 命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名
###可以做如下修改
[free@hurd]$ vim CMakeLists.txt
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (cmake_test)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 指定生成目标
add_executable(test ${DIR_SRCS})

###CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRCS ,再指示变量 DIR_SRCS 中的源文件需要编译成一个名称为 test 的可执行文件
[free@hurd]$ cmake .
-- Configuring done
-- Generating done
-- Build files have been written to: /home/free/cmake
[free@hurd]$ make
Scanning dependencies of target test
[ 20%] Building C object CMakeFiles/test.dir/display_arr.c.o
[ 40%] Building C object CMakeFiles/test.dir/test.c.o
[ 60%] Building C object CMakeFiles/test.dir/swap.c.o
[ 80%] Building C object CMakeFiles/test.dir/genereate.c.o
[100%] Building C object CMakeFiles/test.dir/sort.c.o
Linking C executable test
[100%] Built target test
------ 本文结束 ------