Linux> MakeFile基础

参考《Linux C编程一站式学习》

1. 基本规则

如要编译、链接main.c, stack.c, maze.c, 最终生成可执行文件main。
用手动输入gcc命令方式:

$gcc main.c stack.c maze.c -o main

如果有单个文件修改,还执行上面的命令,会全部.c文件都重新编译,耗费大量时间。
修改编译方式,仅编译修改的文件,以及最终的链接:

$gcc -c maze.c
$gcc main.o stack.o maze.o -o main

容易存在的问题:忘记修改了某一文件,文件量较大,查找困难。使用makefile文件自动处理。
要求makefile文件和源代码放在同一目录。

新建Makefile文件:

main: main.o stack.o maze.o
  gcc main.o stack.o maze.o -o main

main.o: main.c main.h stack.h maze.h
  gcc -c main.c

stack.o: stack.c stack.h main.h
  gcc -c stack.c

maze.o: maze.c maze.h main.h
  gcc -c maze.c

执行make命令,自动读取当前目录Makefile文件,进行相应编译步骤。
makefile 文件是由一系列规则组成,每条规则格式:

目标 : 条件
  命令1
  命令2
  ...

main: main.o stack.o maze.o
  gcc main.o stack.o maze.o -o main

如上面的main就是第一条规则的目标,main.o, stack.o, maze.o是这条规则的条件。
想要更新目标,必须首先更新所有条件;有一个条件更新了,就必须更新目标。
目标、条件所在行的下面,就是命令列表。每个命令必须以一个Tab开头,不能是空格。因为对于Tab开头的命令,make会创建一个Sheel进程执行它。

make执行步骤:

  1. 更新第一条规则的目标main(缺省目标),只要缺省目标更新完成就OK,其他工作都为这条服务。
    发现依赖的条件main.o stack.o maze.o,都还没更新

  2. 继续查找以这三个条件为目标的规则:main.o, stack.o, maze.o。同样未生成,也需要更新,但所依赖的条件都具备(前提是有这些文件)
    执行gcc 命令,以完成这三个目标;

  3. 最后执行gcc main.o stack.o maze.o -o main,以更新目标main。

什么时候目标需要更新?

  1. 目标没有生成,如目标main还没有生成;
  2. 某个条件需要更新,如目标添加了一个条件,;
  3. 某个条件的修改时间比目标晚,如已经执行make后,还修改了stack.h,会导致目标main, main.o, stack.o, 都需要更新;

clean规则
Makefile通常包含clean规则,用于清除编译过程产生的二进制文件(如.o,可执行文件),保留源文件
.PHONY: clean 用于解决文件名与规则同名问题,标识了.PHONY的clearn会声明为伪目标。

main: main.o stack.o maze.o
	gcc main.o stack.o maze.o -o main
main.o: main.c main.h stack.h maze.h
	gcc -c main.c
stack.o: stack.c stack.h main.h
	gcc -c statck.c
maze.o: maze.c maze.h main.h
	gcc -c maze.c

clean:
	@echo "cleaning project"
	-rm main *.o
	@echo "clean completed"

.PHONY: clean

执行clean规则(默认make命令不会执行clean):

$make clean

Makefile文件名
Makefile文件指的是符合Makefile文件规则的文件,而非文件名。make执行时,按照GNUmakefile、makefile、Makefile顺序,找到第一个存在的文件并执行。建议使用Makefile做文件名。有些UNIX系统的make命令不是GNU make,不会查找GNUmake。除非编写的文件包含GNU make特殊语法,否则不建议使用GNUmakefile作为文件名。

Makefile约定目标
包括clean在内,还有几个Makefile约定的名称,作为目标。

  • all 执行主要编译工作,通常用作缺省目标;
  • install 执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷贝到不同的安装目录;
  • clean 删除编译生成的二进制文件;
  • distclean 不仅删除编译生成的二进制文件,也删除其他生成的文件,如配置文件、格式转换文档;

2. 隐含规则和模式规则

一个目标依赖的所有条件,不一定非得写在一条规则中,可以拆开写。但同一个目标,至多只能有一个规则有命令列表,否则make警告且用最后一个规则的命令列表。
对于名为.o的目标,如果对应规则不含命令列表,会自动生成.c的条件。

main.o: main.c main.h stack.h maze.h 
  gcc -c main.c

等价于

# 方式1: 拆分
main.o: main.h stack.h maze.h

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

#方式2:省略命令列表
main.o: main.h stack.h maze.h
# 删去了main.o下面的命令列表

原来的Makefile可以改写成更简洁的方式,从而省去编写3条命令列表规则,以及.o文件对于的.c文件的书写:

main: main.o stack.o maze.o
	gcc main.o stack.o maze.o -o main

main.o:  main.h stack.h maze.h
stack.o: stack.h main.h
maze.o:  maze.h main.h

clean:
	-rm main *.o

.PHONY: clean

3.变量

Makefile 定义变量,详细参见Makefile变量的定义和使用
基本语法:

变量名 = 值列表

变量名称命名方式类同C语言变量名(数字、字母、下划线组成,不能以数字开头)。值列表可以是一个值,也可以是多个值。调用变量的时候,可以用$(变量名)来引用值列表。

foo = $(bar)
bar = Hub?

all:
  @echo $(foo)

make执行时,会打印处Hub?。
只有执行规则all的命令列表的时候,才会展开\((foo),得到\)(bar),再得到Hub?

这种性质好处:可以把变量值推迟到后面定义。
坏处:可能写出无穷递归的定义,如

A = $(A)

# 或者
A = $(B)
B = $(A)

=递归赋值
=递归赋值,变量的值推迟到后面的定义
下面变量CC、CFLAGS、CPPFLAGS推迟到变量定义

main.o: main.c
  $(CC) $(CFLAGS) $(CPPFLAGS) -c $<

cc = gcc
CFLAGS = -O -g
CPPFLAGS = -Iinclude

:=简单赋值
:=赋值,会把值列表中出现的引用的变量立即展开。

x := foo
y := $(x) bar

all:
  @echo "-$(y)-"

make读到y := \((x) bar定义时,立即展开\)(x),使得y值为"foo bar"。当x、y定义顺序颠倒时,y展开值为" bar",注意两者区别。

y := $(x) bar
x := foo

?=条件赋值
条件赋值?=,如果变量没有定义过,则定义该变量,但引用不立即展开;如果之前已经定义过变量,则忽略该条赋值。

x ?= foo
y ?= $(x) bar
y ?= foo

all:
  @echo "-$(y)-"

变量y值为foo bar,起作用的是第一次出现y的定义的语句。

+=追加赋值
追加赋值+=,能给变量追加值

objects = main.o
objects += &(foo)
foo = foo.o bar.o

objects值为main.o foo.o bar.o
+= 保持了 := 的特性,如果变量还没定义过就用 += 赋值,相当于 = 。

常用特殊变量
特殊变量不需要赋值,在不同上下文自动赋值。常用特殊变量有:

  • $@,表示规则中的目标
  • $<,表示规则中第一个条件
  • $?,表示规则中所有比目标新的条件,组成一个列表,用空格分隔
  • $^,表示规则中所有条件,组成一个列表,用空格分隔

针对\(@和\)^,如规则:

main: main.o stack.o maze.o
  gcc main.o stack.o maze.o -o main

可以改写成:

main: main.o stack.o maze.o
  gcc $^ -o $@

好处是即使规则添加新的条件,和目标文件,编译命令(gcc语句)也不用修改,减少出错可能性。

针对$?,如有一个文件libsome.a依赖于几个目标文件:

libsome.a: foo.o bar.o lose.o win.o
  ar r libsome.a $?
  ranlib libsome.a

使用$? 可以让只有更新过的目标文件才需要重新打包到libsoma.a中,没更新过的目标文件已经在libsome.a中,不必重新打包。

常见变量及缺省值
隐含规则常用到的变量,以及缺省值见下表。可以在Makefile文件中重新定义这些变量。

变量名 用途描述 缺省值
AR 静态库打包命令的名字 ar
ARFLAGS 静态库打包命令的选项 rv
AS 汇编器的名字 as
ASFLAGS 汇编器的选项 未定义
CC C编译器的名字 cc
CFLAGS C编译器的选项 未定义
CXX C++编译器的名字 g++
CXXFLAGS C++编译器的选项 未定义
LD 链接器的名字 ld
LDFLAGS 链接器的选项 未定义
TARGET_ARCH 和目标平台相关的命令行选项 未定义
OUTPUT_OPTION 输出的命令行选项 -o $@
LINK.o 把.o文件链接在一起的命令行 $(CC) $(LDFLAGS) $(TARGET_ARCH)
LINK.c 把.c文件链接在一起的命令行 $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
LINKCC.cc 把.cc文件(C++源文件)链接在一起的命令行 $(CXX) $(CXXFLAGS) $(CPPFLAGS)
COMPILE.c 编译.c文件的命令行 $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
COMPILE.cc 编译.cc文件的命令行 $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
RM 删除命令的名字 rm -f

4.自动处理头文件的依赖关系

前面Makefile写成这样:

main: main.o stack.o maze.o
	gcc main.o stack.o maze.o -o main

main.o:  main.h stack.h maze.h
stack.o: stack.h main.h
maze.o:  maze.h main.h

clean:
	-rm main *.o

.PHONY: clean

其中,写main.o, stack.o, maze.o这3个目标的规则时,还需要查看源文件,找到它们依赖的头文件。然而,手动查找很容易出错,而且修改以后可能又需要全部重新查找,另外也容易忘记修改Makefile规则。为解决该问题,避免手动查看每个源码文件以来的头文件,可以利用gcc提供的-M选项,自动生成目标文件和源文件依赖关系。

-M选项,会找到自动生成目标文件和源文件的依赖关系,也就是生成对于.o文件,所包含的头文件。会把所有头文件所包含的头文件,都找出来。

$ gcc -M main.c
main.o: main.c /usr/include/stdc-predef.h /usr/include/stdio.h \
 /usr/include/x86_64-linux-gnu/bits/libc-header-start.h \
 /usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \
 /usr/include/x86_64-linux-gnu/bits/wordsize.h \
 /usr/include/x86_64-linux-gnu/bits/long-double.h \
 /usr/include/x86_64-linux-gnu/gnu/stubs.h \
 /usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
 /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h \
 /usr/include/x86_64-linux-gnu/bits/types.h \
 /usr/include/x86_64-linux-gnu/bits/typesizes.h \
 /usr/include/x86_64-linux-gnu/bits/types/__FILE.h \
 /usr/include/x86_64-linux-gnu/bits/types/FILE.h \
 /usr/include/x86_64-linux-gnu/bits/libio.h \
 /usr/include/x86_64-linux-gnu/bits/_G_config.h \
 /usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \
 /usr/lib/gcc/x86_64-linux-gnu/7/include/stdarg.h \
 /usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
 /usr/include/x86_64-linux-gnu/bits/sys_errlist.h main.h stack.h maze.h

-MM选项,不会找包含的系统头文件,只会找用户自定义头文件。

gcc -MM *.c
main.o: main.c main.h stack.h maze.h
maze.o: maze.c maze.h main.h
stack.o: stack.c stack.h main.h

问题是:如何把这些依赖规则包含到Makefile中?
GNU make官方手册建议:

all: main

main: main.o stack.o maze.o
  gcc -MM $^ -o $@
clean: 
  -rm main *.o
.POHNY: clean

sources = main.c stack.c maze.c
include $(sources:.c=.d)
%.d: %.c
  set -e; rm -f $@;\
  $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
  sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
  rm -f $@.$$$$

sources变量包含要编译的所有.c文件,$(sources:.c=.d)是一个变量替换语法,把sources变量中每一项.c替换成.d,所以includes语句相当于:

include main.d stack.d maze.d

include类似于C中的#include 预编译指令,表示包含三个文件main.d,stack.d,maze.d。如果工作目录是干净的,只有.c, .h, Makefil文件,运行make结果:

$ make
makefile:12: main.d: 没有那个文件或目录
makefile:12: stack.d: 没有那个文件或目录
makefile:12: maze.d: 没有那个文件或目录
set -e; rm -f maze.d; \
cc -MM  maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$
set -e; rm -f stack.d; \
cc -MM  stack.c > stack.d.$$; \
sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \
rm -f stack.d.$$
set -e; rm -f main.d; \
cc -MM  main.c > main.d.$$; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \
rm -f main.d.$$
cc    -c -o main.o main.c
cc    -c -o stack.o stack.c
cc    -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main

一开始找不到.d文件,所以make会警告。但是make也会把include的文件名也当做目标来尝试更新,而这些目标适用模式规则%.d:%c,所以执行它的命令列表,如生成maze.d的命令(一条命令):

set -e; rm -f maze.d; \
  cc -MM maze.c > maze.d.$$; \
  sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
  rm -f maze.d.$$

这一条命令分为5个子命令,用";"隔开,用续行符""写成4行。执行步骤:

  1. set -e命令设置当前Shell进程为这样的状态:如果它执行的任何一条命令的退出状态非零,则立刻终止,不再执行后续命令。
  2. 把原来的maze.d删掉。
  3. 重新生成maze.c的依赖关系,保存成文件maze.d.1234(假设当前Shell进程的id是1234)。因为Makefile中\(有特殊含义,要表示字面意思需要写2个\),所以Makefile中的四个\(传给Shell变成2个\),2个$$在Shell表示当前进程的id,一般用它给临时文件起名,以保证文件名唯一。
  4. 该sed命令较复杂,主要作用是查找替换。maze.d.1234的内容应该是maze.o: maze.c maze.h main.h,经过sed处理后存为maze.d,内容是maze.o maze.d: maze.c maze.h main.h。
  5. 最后把临时文件maze.d.1234删掉。

只要Makefile自身或包含的文件,有一个文件在make过程中被更新,make就会重新读取整个Makefile及其包含的所有文件。经过上面的命令后,main.d, stack.d, maze.d都生成了,就可以正常包含进来(如果此时还未生成,make就要报错而不是警告),相当于Makefile中添加了3条规则:

main.o main.d: main.c main.h stack.h maze.h
maze.o maze.d: maze.c maze.h main.h
stack.o stack.d: stack.c stack.h main.h

假如在main.c中添加一行#include "foo.h",那么:

  1. main.c的修改日期变了,根据规则main.o main.d: main.c main.h stack.h maze.h 要重新生成main.o, main.d。生成main.o的规则有2条:
main.o: main.c main.h stack.h maze.h
%o: %.c
# commands to execute (built-in:
  $(COMPILE.c) $(OUTPUT_OPTION) $<

第一条规则是把规则main.o main.d: main.c main.h stack.h maze.h拆开写得到,第二条规则是隐含规则,%和*都是通配符,会执行cc命令重新编译main.c得到main.o.
参考Makefile中的匹配符


生成main.d的规则也有2条,main.d的内容更新为main.o main.d: main.c main.h stack.h maze.h foo.h:

main.d: main.c main.h stack.h maze.h
%.d: %.c
  set -e; rm -f $@; \
  $(CC) -MM $(CPPGLAGS) $< > $@.$$$$; \
  sed 's,\($*\)\.o[:]*,\1.o $@ : ,g' <$@.$$$$ > $@; \
  rm -f $@.$$$$
  1. 由于main.d被Makefile包含,main.d被更新又导致make重新读取整个Makefile,把新的main.d包含进来,于是新的依赖关系生效了。

常用make命令行选项

-n选项
只打印要执行的命令,不会针对执行命令,主要用于检查Makefile写得是否正确,也可以查看命令的执行顺序。

-C选项
可以切换到另一个目录执行那个目录下的Makefile,比如先退到上一级目录再执行Makefile(假设源代码都放在testmake目录下):

$ cd ..
$ make -C testmake
make: Entering direcotry `/home/djkings/testmake`
cc    -c -o main.o main.c
cc    -c -o stack.o stack.c
cc    -c -o maze.o maze.c
gcc  main.o stack.o maze.o -o main
make: Leaving directory `/home/djkings/testmake`

一般规模较大的项目会把不同的模块或子系统的源代码放在不同子目录中,然后在每个子目录下都写一个该目录的Makefile,然后在一个总的Makefile中用make -C命令执行每个子目录下的Makefile。

-g选项
如果这次编译想加调试选项-g,但不想每次编译都加-g选项,可以在命令行定义CFLAGS变量,而不必修改Makefile编译完了再改回来(不用修改Makefile文件本身):

$ make CFLAGS=-g 
cc    -c -o main.o main.c
cc    -c -o stack.o stack.c
cc    -c -o maze.o maze.c
gcc  main.o stack.o maze.o -o main

make命令支持用=或:=定义变量,这里是定义CFLAGS变量,缺省值是未定义。如果Makefile中定义了CFLAGS变量,则命令行的值覆盖Makefile的值。

posted @ 2021-03-26 23:05  明明1109  阅读(233)  评论(0编辑  收藏  举报