makefile 基础、进阶及常用 makefile

makefile 语法

目标:依赖
(tab)命令

如:add.o:add.c
(一个tab缩进)gcc –Wall –g –c add.c –o add.o

目标:要生成的目标文件
依赖:目标文件由哪些文件生成
命令:通过执行该命令由依赖文件生成目标

makefile 工作原理

1、若想生成目标,检查规则中的依赖条件是否存在,如不存在,则寻找是否有规则用来生成该依赖文件

2、检查规则中的目标是否需要更新,必须先检查它的所有依赖,依赖中有任一个被更新,则目标必须更新

  • 分析各个目标和依赖之间的关系
  • 根据依赖关系自底向上执行命令
  • 根据修改时间比目标新,确定更新
  • 如果目标不依赖任何条件,则执行对应命令,以示更新

一个最简单的 makefile

main:main.c
    gcc main.c -o main

该 makefile 生成目标为 main 的文件,依赖 main.c,所需命令是 gcc main.c -o main,注意前面的 (tab)。

联合编译 makefile

上面的例子只是一个最简单的 makefile 的使用方法,但实际项目里面不可能只有一个文件,实际可能是多个 .c .h 组成,像这样的项目,我们该如何通过 makefile 管理呢?我们来看下面这个项目的目录。

mycode@vmware:~/Desktop/code/makefile$ tree
.
├── add.c
├── main.c
├── makefile
├── mul.c
└── sub.c

目录中有 main.c 是项目的 mian 函数入口代码,里面用到了三个函数,分别是 add()、sub()、mul(),他们都再不同的 .c 文件中,如果我们手动编译这个项目需要用到如下命令:

gcc main.c add.c sub.c mul.c -o app

执行上面命令后会执行联合编译,直接生成可执行文件 app。而有些时候你会发现,这样编译带来的问题是如果我只改动了其中一个文件的代码,执行这个编译语法的时候,会重新编译所有的代码,小工程也就算了,如果是个大项目那肯定会很慢,所以这不是上上之选。正常的做法应该是先使用 -c 参数生成每个文件的 .o 文件,然后联合编译所有的 .o 文件,当某个 .c 文件修改后,只重新编译这个 .c.o,然后再执行联合编译,这样就减少了多余代码编译的过程,方法如下:

gcc -c main.c add.c sub.c mul.c

这样就生成了 4 个 .o 文件。

mycode@vmware:~/Desktop/code/makefile$ tree
.
├── add.c
├── add.o
├── main.c
├── main.o
├── makefile
├── mul.c
├── mul.o
├── sub.c
└── sub.o

然后对 4 个 .o 文件执行联合编译,最终生成可执行的 app 文件。

gcc main.o add.o sub.o mul.o -o app

以上是我们通过手动敲命令的方式执行编译,这种联合编译,我们如何通过 makefile 来处理呢?先来分析一下,我们把手动执行编译的过程逆向思考一下,想生成目标为可执行的 app 文件,需要依赖 4 个 .o 文件的支持,main.o add.o sub.o mul.o。,所需执行的命令gcc main.o add.o sub.o mul.o -o app,那么下面的 makefile 语法应运而生了。

app:main.o add.o sub.o mul.o
    gcc main.o add.o sub.o mul.o -o app

但这里你会发现,如果我们项目目录是空的,只有 .c.h 文件时,根本没有 .o 文件,makefile 去哪找这些 .o 文件呢?所以我们还需要制作一些其他的目标文件,那就是这些 .o 文件。语法如下:

main.o:main.c
    gcc -c main.c

add.o:add.c
    gcc -c add.c

sub.o:sub.c
    gcc -c sub.c

mul.o:mul.c
    gcc -c mul.c

组合后的 makefile 如下:

app:main.o add.o sub.o mul.o
    gcc main.o add.o sub.o mul.o -o app

main.o:main.c
    gcc -c main.c

add.o:add.c
    gcc -c add.c

sub.o:sub.c
    gcc -c sub.c

mul.o:mul.c
    gcc -c mul.c

以上就是一个算是能实现功能的 makefile 了,但是还不算完美,后面我们再引入其他 makefile 的特性,先在这个项目目录下执行一次 make 命令,看看编译的效果如何。

mycode@vmware:~/Desktop/code/makefile$ make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -c mul.c
gcc main.o add.o sub.o mul.o -o app

执行 make 命令后我们可以看到,首先对每个 .c 文件进行编译,生成 .o 文件,然后对几个 .o 文件联合编译生成可执行的 app 文件。并且如果你修改了其中一个 .c 文件的情况下,重新执行 make 命令,make 只会重新编译你修改过的源文件,并不会重新编译所有。

mycode@vmware:~/Desktop/code/makefile$ vi main.c
mycode@vmware:~/Desktop/code/makefile$ make
gcc -c main.c
gcc main.o add.o sub.o mul.o -o app

makefile 变量

接下来我们引入 makefile 变量机制,来修改一下上面的 makefile 文件。

obj = main.o add.o sub.o mul.o

app:$(obj)
    gcc $(obj) -o app

main.o:main.c
    gcc -c main.c

add.o:add.c
    gcc -c add.c

sub.o:sub.c
    gcc -c sub.c

mul.o:mul.c
    gcc -c mul.c

clean:
    rm -rf $(obj) app

执行后,同样可以达到效果,我们引入了一个 obj 的变量,注意变量在使用的时候要加 $(),中间变量名字。并且增加了一个 clean 目标,他不依赖任何东西,执行一条清除所有 .o 文件和 app 文件的命令。用来帮助我们清理项目目录。使用方法就是在 make 命令后加 clean 参数即可,以下是执行后的效果:

mycode@vmware:~/Desktop/code/makefile$ make clean
rm -rf main.o add.o sub.o mul.o app
mycode@vmware:~/Desktop/code/makefile$ ls
add.c  main.c  makefile  mul.c  sub.c

makefile 自动变量

makefile 中有一些预定义的变量,你可以理解它像是 C 语言中的一些关键字,分别有不同的意义,我们列举几个常用的自动变量(其他还有很多),通过上面的 makefile 文件做修改来演示他们的用法。

$@:在命令中使用,表示规则中的目标
$<:在命令中使用,表示规则中的第一个条件
$^:在命令中使用,表示规则中的所有条件,组成一个列表,以空格隔开,如果这个列表中有重复的项则消除重复项。

他们的使用方法如下:

obj = main.o add.o sub.o mul.o

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

main.o:main.c
    gcc -c $< -o $@

add.o:add.c
    gcc -c $< -o $@

sub.o:sub.c
    gcc -c $< -o $@

mul.o:mul.c
    gcc -c $< -o $@

clean:
    rm -rf $(obj) app

我们分别把这三个自动变量放到了不同的位置,用来表示他们的作用。可以一目了然。执行 make 命令后,可以达到同样的效果。

makefile 模式规则

再分析一下上面的 makefile 代码,对于每个要生成的 .o 文件,我们都要给他写一条规则,如果有很多怎么办?难道要一条一条的写吗?当然不会,makefile 提供了一种模式规则,使用 % 符号来匹配任意字符串达到通配的作用,先来看改造后的代码,然后我们来分析其执行流程。

obj = main.o add.o sub.o mul.o

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

%.o:%.c
    gcc -c $< -o $@

clean:
    rm -rf $(obj) app

上面的 makefile 经过改造后,只剩下这么一点内容了,但别看内容少,一样可以达到我们的需求。其执行流程是要生成的最终目标为 appapp 需要 4 个 .o 文件的支持,这 4 个文件我们用了一个变量 obj 来表示。当去找 main.o 文件时,发现没有,则去下面找是否有匹配生成这个 .o 文件的规则,因为 %.o 可以匹配 main.o,所以找到了下面这条规则:

%.o:%.c
    gcc -c $< -o $@

规则中的 %.c 的 % 值,取决于目标 %.o,而此时 %.o% 的值是上面生成 app 所需的 main.o,所以解释以后的代码相当于下面这样:

main.o:main.c
    gcc -c $< -o $@

根据这条规则,生成出了 main.o 文件,其他 3 个 .o 文件也依次类推。这样就实现自动化的去匹配生成规则。

mekfile 函数

如果你认为上面的 makefile 已经很完美了,那你就大错特错了,做一个假设,如果你在项目中新增了一个 .c 的文件后,你还是需要修改 makefile 增加一个所依赖的 .o 文件。想自动化实现这个步骤,如果你自己写脚本,你是不是应该考虑,有多少个 .c 文件就生成多少个 .o 文件,而且每个 .o 文件的名字都与 .c 一样,所以我们可以获取一份 .c 文件的列表,根据这份列表把所有后缀改为 .o 再形成一个新的列表,这份列表就作为生成最终 app 的依赖。想实现这样的功能我们就需要用到 makefile 中的函数!如下所示:

# 获取所有 .c 文件的列表赋值给 src 变量
src = $(wildcard *.c)
# 根据 src 变量获取 .o 文件列表存放到 obj 变量中
obj = $(patsubst %.c, %.o, $(src))

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

%.o:%.c
    gcc -c $< -o $@

clean:
    rm -rf $(obj) app

这次无论你增加多少个 .c 文件,makefile 都会自动遍历到并生成 .o 文件了。我们也可以添加一些自定义的变量,让以后 makefile 维护起来更方便:

# 获取所有 .c 文件的列表赋值给 src 变量
src = $(wildcard *.c)
# 根据 src 变量获取 .o 文件列表存放到 obj 变量中
obj = $(patsubst %.c, %.o, $(src))

# 方便后面更换编译器
CC = gcc
# 一些通用的编译参数
CFLAGS = -Wall -g

app:$(obj)
    $(CC) $^ -o $@ 

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

clean:
    rm -rf $(obj) app

我们在生成 .o 文件时增加了 -Wall-g 参数,这样我们就可以看到编译时是不是有警告信息了,因为我这个项目没有引入头文件告诉编译器去哪里找实现函数,所以就会出现一些警告信息,当然我们目的并不是要处理这些警告,而是来描述 makefile 的使用,看这篇文章的人,你可以自己根据需求修改。

makefile 中的 all

因为 makefile 的执行流程是找到第一个目标作为最终生成的目标,如果顺序错乱了,makefile 就可能报错,all 方法就是解决这个问题而存在的,并且,all 方法可以让一个 makefile 生成多个目标。示例如下:

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

CC = gcc
CFLAGS = -Wall -g

all:app main

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

app:$(obj)
    $(CC) $^ -o $@ 

main:$(obj)
    $(CC) $^ -o $@ 

clean:
    rm -rf $(obj) app

makefile clean 方法优化

make clean 命令是用来清除目录下临时文件的,执行 clean 这个目标时,不需要任何依赖项,也就意味着,如果目录下有一个文件名为 clean 的话,执行 make clean 命令时会判断这个目标所依赖的内容是否有变化,如有变化则重新生成,无变化则跳过,而恰恰我们这个 clean 没写依赖规则!这将导致 clean 无论如何都不会被执行。解决这个问题的办法就是将 clean 方法声明为一个伪目标,做就就是让 clean 无论如何都更新,同样我们生成的 all 目标也可能会出现这种情况,所以我们将它们两个都声明为伪目标,方法如下:

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

CC = gcc
CFLAGS = -Wall -g

all:app main

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

app:$(obj)
    $(CC) $^ -o $@ 

main:$(obj)
    $(CC) $^ -o $@ 

clean:
    -rm -rf $(obj) main app 

.PHONY:clean all

这样即使目录下有名为 cleanall 的文件时,也不影响 make clean 的执行。同时注意看代码的人可能也发现了,rm -rf $(obj) main app 命令前增加了一个 - 符号,这个符号的目的就是如果这条命令执行失败了继续执行,不影响后续命令的执行。

至此 makefile 的功能说明到此为止一,下面就是收集的一些常用做测试用的 makefile 代码。

常用 makefile

将目录下所有 .c 文件都分别生成为可执行文件,比如有 sort.c、readfile.c、writefile.c,他们都分别有自己的 main 函数,我们希望生成时生成 3 个可执行文件,分别叫 sort、readfile、writefile,那使用以下 makefile 即可实现。

src = $(wildcard *.c)
dsc = $(patsubst %.c, %, $(src))

CC = gcc
CFLAGS = -Wall -g

all:$(dsc)

%:%.c
    $(CC) $^ -o $@ 

clean:
    -rm -rf $(dsc) 

.PHONY:clean all

一个有目录规则的 makefile 代码,我们的目录如下所示:

mycode@vmware:~/Desktop/code/project$ tree
.
├── bin
├── inc
│   └── head.h
├── makefile
├── obj
└── src
    ├── add.c
    ├── mian.c
    ├── mul.c
    └── sub.c

4 directories, 6 files

bin 是生成最终目标文件的目录
inc 是存放头文件的目录
obj 是生成的 .o 文件目录
src 是源文件目录

makefile 如下:

src = $(wildcard ./src/*.c)
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src))
inc = ./inc/

CC = gcc
CFLAGS = -Wall -g
CPPFLAGS = -I

all:./bin/app

./bin/app:$(obj)
    $(CC) $^ -o $@ $(CFLAGS)

./obj/%.o:./src/%.c
    $(CC) -c $< -o $@ $(CFLAGS) $(CPPFLAGS) $(inc)

clean:
    -rm -rf $(obj) ./bin/app

.PHONY:clean all

执行后的结果

mycode@vmware:~/Desktop/code/project$ make
gcc -c src/add.c -o obj/add.o -Wall -g -I ./inc/
gcc -c src/mian.c -o obj/mian.o -Wall -g -I ./inc/
gcc -c src/mul.c -o obj/mul.o -Wall -g -I ./inc/
gcc -c src/sub.c -o obj/sub.o -Wall -g -I ./inc/
gcc obj/add.o obj/mian.o obj/mul.o obj/sub.o -o bin/app -Wall -g
mycode@vmware:~/Desktop/code/project$ tree
.
├── bin
│   └── app
├── inc
│   └── head.h
├── makefile
├── obj
│   ├── add.o
│   ├── mian.o
│   ├── mul.o
│   └── sub.o
└── src
    ├── add.c
    ├── mian.c
    ├── mul.c
    └── sub.c

4 directories, 11 files

评论