Makefile 学习笔记
什么是 makefile
makefile 是一个用于自动编译的文件。初学阶段,我们往往直接手写整个编译命令来编译和调试,但是这样效率低下,尤其是文件经常增减的时候。所以人们发明了 make 工具,可以写好编译脚本,实现高效地编译。
gcc 的编译流程
最简单的 Makefile 程序
1> vi sample.c
2#include <stdio.h>
3int main(int argc, char const *argv[])
4{
5 printf("ok\n");
6 return 0;
7}
8> vi makefile
9wow:
10 gcc sample.c -o sample
我们创建了一个 c 程序代码,然后创建了 makefile
文件。现在执行:
1> make
2gcc sample.c -o sample
你会发现 sample
已经被编译出来了。
makefile 的规则如下:
target ... : prerequisites ...
command
...
...
target:目标,可以是:
-
对象文件
-
执行文件
-
标签
在我们的例子中是 wow
,是一个标签。
prerequisites:前提。也即依赖的 target,可以为空表示无条件。
command:要执行的命令,可以是任意的命令。可以是多行,例如你可以写成:
1wow:
2 gcc sample.c -o sample
3 chmod +x sample
4 ./sample
执行 make,会看到
1> make
2gcc sample.c -o sample
3chmod +x sample
4./sample
5ok
直接执行了编译和运行。
多文件的 Makefile
main.c:
1#include <stdio.h>
2#include "app_version.h"
3
4int main(int argc, char const *argv[])
5{
6 const char *v = get_version();
7 printf("version: %s\n", v);
8 return 0;
9}
app_version.h:
1const char* get_version();
app_version.c
1const char* get_version(){
2 return "v1.0.0";
3}
最简单的编译方法是终端执行:
1> gcc app_version.c main.c -o my_app
2> ./my_app
3version: v1.0.0
另外,我们也可以显式地先生成 object 文件,再链接,这样做的好处是可以避免重复编译:
1> gcc -c app_version.c main.c
2# 生成了 app_version.o, main.o
3> gcc app_version.o main.o -o my_app
4# 生成了 my_app
5> ./my_app
6version: v1.0.0
将其转写为 makefile:
1my_app: main.o app_version.o
2 cc -o my_app main.o app_version.o
3
4main.o: main.c app_version.h
5 cc -c main.c
6app_version.o: app_version.c
7 cc -c app_version.c
8
9clean:
10 rm my_app *.o
然后执行
1make
将会看到编译出了 .o
文件和 my_app
可执行文件。
可以发现,执行 make
之后,定向到了第一个编译目标(my_app
),然后发现依赖 main.o app_version.o
,make 工具会帮我们按照我们所写的规则:
1main.o: main.c app_version.h
2 cc -c main.c
3app_version.o: app_version.c
4 cc -c app_version.c
自动寻找所需的两个 .o
文件。
使用变量
makefile 的变量十分暴力。常见的做法是:
1testvar = main.o app_version.o
2my_app: $(testvar)
3 cc -o my_app $(testvar)
4
5main.o: main.c app_version.h
6 cc -c main.c
7app_version.o: app_version.c
8 cc -c app_version.c
9
10clean:
11 rm my_app *.o
但是实际上这个变量更像是宏,你甚至可以这样:
1testvar = main.o app_version.o
2什么鬼 = c -o my_app $(testvar)
3my_app: main.o app_version.o
4 c$(什么鬼)
5
6main.o: main.c app_version.h
7 cc -c main.c
8app_version.o: app_version.c
9 cc -c app_version.c
10
11clean:
12 rm my_app *.o
从字符串中间断开都行 0v0
自动推导 c 文件依赖
对于涉及的 .o
文件,make 工具会自动将同名 .c
文件设置依赖。另外 cc -c xxx.c
也可以直接省略不写:
1obj = main.o app_version.o
2my_app: $(obj)
3 cc -o my_app $(obj)
4
5main.o: app_version.h
6app_version.o:
7
8clean:
9 rm my_app *.o
你还可以做得更绝,因为main.c
已经 include "app_verison.h"
,所以你可以省略:
1obj = main.o app_version.o
2my_app: $(obj)
3 cc -o my_app $(obj)
4
5main.o:
6app_version.o:
7
8clean:
9 rm my_app *.o
伪目标
1clean:
2 rm my_app *.o
这样的操作一般没啥问题,但是要是目录存在一个 clean
文件,就会出错了:
1> make clean
2make: `clean' is up to date.
这和预期不一致。解决方法:
1obj = main.o app_version.o
2my_app: $(obj)
3 cc -o my_app $(obj)
4
5main.o:
6app_version.o:
7
8.PHONY:clean # 添加 clean 为 .PHONY 的依赖
9clean:
10 rm my_app *.o
include 指令
makefile 可以引入其它文件。语法是:
1include a.mk b.mk *.make # ...
多目录的源文件
如果代码放在不同的地方,就需要使用 VPATH
变量。
另外有小写的 vpath
指令,用于搜索文件。
我们在程序目录下再创建 user
目录,其中创建 user.h
和 user.c
:
user.h
1#if !defined(__USER_H__)
2#define __USER_H__
3
4#include <stdint.h>
5#include <string.h>
6#include <stdlib.h>
7#include <stdio.h>
8
9typedef struct _user
10{
11 char name[16];
12 uint8_t age;
13} user, *const puser;
14
15// create a user
16puser new_user(char *const name, uint8_t age);
17
18// print a user's info
19void print_user(puser user);
20
21#endif // __USER_H__
user.c
1#include "user.h"
2
3// create a user
4puser new_user(char *const name, uint8_t age)
5{
6 user *const u = (user *const)malloc(sizeof(user));
7 u->age = age;
8 strcpy(u->name, name);
9 return u;
10}
11
12// print a user's info
13void print_user(puser user)
14{
15 printf("Name: %s\n", user->name);
16 printf("Name: %hhu\n-\n", user->age);
17}
makefile 变更如下:
1VPATH = ./user # 或者 VPATH = user
2
3obj = main.o app_version.o user.o
4my_app: $(obj)
5 cc -o my_app $(obj)
6
7$(obj):
8
9.PHONY:clean
10clean:
11 rm my_app *.o
编译,正常。
这样,我们每增加一个文件,只需要在对应的 obj
变量那里添加 filename.o
即可。
参考
本文基本上全文参照了:makefile介绍 — 跟我一起写Makefile 1.0 文档 (seisman.github.io)
Make 工具还有更多的脚本语法,但是个人觉得如果都那么复杂了,何不直接写个 python 脚本配合编译呢。