Makefile 学习笔记

什么是 makefile

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

gcc 的编译流程

1

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

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