Makefile 学习笔记

什么是 makefile

makefile 是一个用于自动编译的文件。初学阶段,我们往往直接手写整个编译命令来编译和调试,但是这样效率低下,尤其是文件经常增减的时候。所以人们发明了 make 工具,可以写好编译脚本,实现高效地编译。

gcc 的编译流程

1

最简单的 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.omake 工具会帮我们按照我们所写的规则:

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.huser.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 脚本配合编译呢。