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