Makefile学习1
Makefile简介
Makefile是在Linux环境下 C/C 程序开发必须要掌握的一个工程管理文件。当你使用make命令去编译一个工程项目时,make工具会首先到这个项目的根目录下去寻找Makefile文件,然后才能根据这个文件去编译程序。
linux下编写程序,因为早期没有成熟的IDE,一般都是使用不同的命令进行编译:将源文件分别使用编译器、汇编器、链接器编译成可执行文件,然后手动运行。
要将 .c源文件编译成可执行文件,一般需要预处理、编译、汇编、链接四个步骤,每个步骤会分别调用预处理器、编译器、汇编器、链接器来完成。
在Linux环境下,安装了GCC编译器,在程序的安装目录下面会有各种二进制可执行文件:
- cpp:预处理器
- ccl:编译器
- as:汇编器
- ld:链接器
- ar:静态库制作工具
程序在编译过程中会分别使用这些工具,完成程序编译的每个流程。
为了简化程序编译流程,GCC编译器一般会提供一个gcc命令:
代码语言:javascript复制gcc -o a.out helloworld.c
gcc会分别调用预处理器、编译器、汇编器和链接器来自动完成程序编译的整个过程,不需要用户一个命令一个命令分别输入了。
gcc还提供了一些列参数,用来控制编译流程:
代码语言:javascript复制-E #进行预处理,不作编译
-S #只做汇编处理
-c #进行编译,不链接
-o #指定生成可执行程序名
对于大型项目使用gcc编译的话,每编译一次,都要敲进去几万个源文件,太折腾了,此时自动化编译工具make就派上用场了:使用make编译程序,不需要每次都输入源文件,直接在命令行下敲击make命令,就可一键自动化完成编译。
使用make命令编译程序之前,需要编写一个Makefile文件:
代码语言:javascript复制a.out: helloworld.o
gcc -o a.out helloworld.o
helloworld.o: helloworld.c
gcc -c -o helloworld.o helloworld.c
clean:
rm -f a.out helloworld.o
makefile的文件名通常有三种格式:Makefile、makefile、GNUmakefile,make会在当前目录下自动寻找找三个文件名
如果同时存在执行makefile;如果没有找到的话,make就无法继续编译程序,产生一个错误并退出;如果名称自定的话,可以使用 -f 选项指定执行的文件
Makefile重要性
会不会写Makefile,是侧面可以看出一个人是否具有完成大型项目工程的能力。如果你是在Linux下进行C/C 开发,掌握Makefile可能让你更深地去理解项目,去掌控整个项目的构建和维护。
Makefile也是一个研究开源项目的利器。很多开源项目可能文档不完整,而Makefile就是开源项目的地图,从Makefile入手,可以让你快速窥探整个开源项目的框架和概貌,让你深入代码而不至于迷路。
掌握Makefile是一门必备技能,它和git、vim一样,掌握了这个“Linux三剑客”会让你的工作事半功倍、更加高效。
Makefile内容
简单的概括一下Makefile 中的内容,它主要包含有五个部分,分别是:
1) 显式规则
显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
2) 隐晦规则
由于我们的 make 命名有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 命令所支持的。
3) 变量的定义
在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
4) 文件指示
其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像C语言中的 include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像C语言中的预编译 #if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
5) 注释
Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符,这个就像 C/C 中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“#”。
Makefile规则
规则
Makefile通过规则进行构建可执行文件编译所依赖的关系树
规则是Makefile的基本组成单元。一个规则通常由目标、目标依赖和命令三部分构成:
代码语言:javascript复制目标:目标依赖
命令
a.out: hello.c
gcc -o a.out hello.c
说明:
a.out就是我们要生成的目标,目标一般是一个可执行文件。
目标依赖是指生成这个可执行文件所依赖的源文件,如 hello.c。
而命令则是如何将这些目标依赖生成对应的目标,一般是gcc命令、链接命令、objcopy命令,一些shell命令等。
命令必须使用tab键进行缩进,否则Makefile就会报错。
有的规则可能无目标依赖,仅仅是为了实现某种操作,如下面的clean命令:
代码语言:javascript复制clean:
rm -f a.out hello.o
使用make clean命令清理编译的文件时,会调用这个规则中的命令,不需要什么依赖,仅仅是执行删除操作,所以这个规则中并没有目标依赖。
规则中也可以没有命令,仅仅包含目标和目标依赖,仅仅用来描述一种依赖关系,但一个规则中一定要有一个目标。
默认目标
一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。
正常情况下,当你想编译生成a.out时,要使用make a.out命令。
Makefile文件中a.out是文件中的第一个目标,当我们在make编译时没有给make指定要生成的目标,make就会选择Makefile文件中的第一个目标作为默认目标。
多目标
一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。
如一个目标文件%.o都是由其对应的源文件%.c编译生成的,生成命令也是相同的:
代码语言:javascript复制%.o: %.c
gcc -o %.o %.c
多规则目标
多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。
伪目标
伪目标并不是一个真正的文件,可以看做是一个标签。
伪目标一般没有依赖关系,也不会生成对应的目标文件,可以无条件执行,纯粹是为了执行某一个命令。
伪目标可以在执行默认目标之前先执行。
代码语言:javascript复制.PHONY: clean
a.out: hello.o
gcc -o a.out hello.o
hello.o: hello.c
gcc -c -o hello.o hello.c
clean:
rm -f a.out hello.o
Makefile目标依赖
make第一次编译某个项目时,会依次编译所有的源文件。
但是当我们修改程序后,再次使用make编译,make只编译你新添加或修改了的源文件。
make是根据时间戳来判断一个规则中的目标依赖文件是否有更新。
make在编译程序时,会依次检查依赖关系树中的所有源文件的时间戳,如果发现某个文件的时间戳有更新,会认为这个文件有改动过,会重新编译这个源文件。
但是还有一种情况:在Makefile的规则中,一般不会把头文件添加到目标依赖中。当一个.c文件中包含多个头文件时,如果对应的头文件发生了变化,因为头文件没有包含在依赖关系树中,所以这个.c文件就不会重新编译:
代码语言:javascript复制//hello.c
#include <stdio.h>
#include "module.h"
int main(void)
{
printf("PI = %fn", PI);
func();
printf("hello zhaixue.cc!n");
return 0;
}
//module.c
#include <stdio.h>
void func(void)
{
printf("hello func!n");
}
//module.h
#ifndef __MODULE_H__
#define __MODULE_H__
#define PI 3.14
#endif
代码语言:javascript复制//Makefile
.PHONY: clean
a.out: hello.o module.o
gcc -o a.out hello.o module.o
hello.o: hello.c
gcc -c -o hello.o hello.c
module.o: module.c
gcc -c -o module.o module.c
clean:
rm -f a.out hello.o
修改module.h中的宏定义PI值为3.1415,再次使用make编译程序,make并没有重新编译,因为module.h并没有添加到Makefile的规则依赖目标中,所以无论你怎么修改module.h,都不会重新编译helloworld.c源文件。
头文件依赖
其中一个解决方法是将头文件module.h添加到规则的目标依赖列表中:
代码语言:javascript复制//Makefile
.PHONY: clean
a.out: hello.o module.o module.h
gcc -o a.out hello.o module.o
缺点:包含几十个头文件时,把包含的这些头文件都手工添加进去,工作量还是蛮大的。
自动生成头文件依赖关系
更高效的解决方法是:使用gcc -M 命令自动生成头文件依赖关系
通过gcc -M命令,我们就可以自动生成一个hello.o目标文件的依赖关系,就不需要我们手动将头文件添加到规则中了。
Makefile命令
命令一般由shell命令(echo、ls)和编译器的一些工具(gcc、ld、ar、objcopy等)组成,使用tab键缩进。
命令是make在编译程序时真正要执行的部分。对于规则中的每一个命令,make会开一个进程执行,每条命令执行完,make会监测每个命令的返回码。
若命令返回成功,make继续执行下一个命令;若命令执行出错,make会终止执行当前的规则,退出编译流程。
make每执行一条命令,会把当前的命令打印出来。
如果你不想在make编译的时候打印正在执行的执行,可以在每条命令的前面加一个@:
代码语言:javascript复制.PHONY: clean
a.out: hello.c
@echo "start compiling..."
@gcc -o a.out hello.c
@echo "compile done"
clean:
rm -f a.out hello.o
Makefile变量
变量定义和使用
可以在Makefile中定义一个变量val,使用使用 (val) 或 {val} 的形式去引用它。
可以定义一些变量,分别表示编译器名称、目标、目标依赖文件:
代码语言:javascript复制PHONY: clean
CC = gcc
BIN = a.out
OBJS = hello.o module.o
$(BIN): $(OBJS)
@echo "start compiling..."
@echo $(CC)
$(CC) -o $(BIN) $(OBJS)
@echo "compile done"
hello.o: hello.c
$(CC) -c -o hello.o hello.c
module.o: module.c
$(CC) -c -o module.o module.c
clean:
rm -f $(BIN) $(OBJS)
好处:便于维护makefile文件,例如当项目中需要添加更多的源文件时,你只需要更改OBJS的值就可以了。如果不使用变量的话,你得修改Makefile多处地方。
赋值
Makefile中的变量赋值有多种形式,比如:
- 条件赋值:?=
- 追加赋值: =
条件赋值是指一个变量如果没有被定义过,就直接给它赋值;如果之前被定义过,那么这条赋值语句就什么都不做。
代码语言:javascript复制CC = gcc
CC ?= arm-linux-gnueabi-gcc #不执行
$(BIN): $(OBJS)
@echo $(CC)
$(CC) -o $(BIN) $(OBJS)
追加赋值是指一个变量,以前已经被赋值,现在想给它增加新的值,此时可以使用 =追加赋值。
代码语言:javascript复制OBJS = hello.o
OBJS = module.o
立即变量/延迟变量
立即变量和延迟变量是按展开时间来划分的。
立即变量使用 := 操作符进行赋值,在解析阶段就直接展开了,顾名思义,立即展开变量。
延迟变量则是使用 = 操作符进行赋值,在make解析Makefile阶段不会立即展开,而是等到实际使用这个变量时才展开,获得其真正的值。
代码语言:javascript复制a = 1
b = 2
val_a := $(a)
val_b = $(b)
a = 10
b = 20
test:
echo $(val_a)
echo $(val_b)
解释:
val_a是立即变量,当make解析到:=赋值符号时,会把$(a)变量的值立即赋值给val_a,虽然后面a的值发生了变化,但val_a因为已经展开,所以值就不再发生变化。
而val_b则不同,因为是延迟展开变量,所以,当make解析到 = 符号时,并没有立即把(b)的值赋值给val_b,而是在运行echo命令时才对其展开,因为此时b的值已经是20,所以(val_b)的值是20。
应用:
立即展开变量一般用在规则中的目标、目标依赖中。make在解析Makefile阶段,需要这些变量有确切的值来构建依赖关系树。一个项目中的文件依赖关系在程序编译期间是固定不变的,因此需要立即变量在解析阶段就要有明确的值,立即展开。
延迟展开变量一般用在规则的命令行中,这些变量在make编译过程中被引用到才会展开,获得其实际的值。
自动变量
在Makefile中,大家经常会见到类似
^、$<这种类型的变量。
这种变量一般称为自动变量,自动变量是局部变量,作用域范围在当前的规则内,它们分别代表不同的含义:
- $@:目标
- $^:所有目标依赖
- $<:目标依赖列表中的第一个依赖
- $?:所有目标依赖中被修改过的文件
.PHONY: clean
CC = gcc
BIN = a.out
OBJS = hello.o module.o
$(BIN): $(OBJS)
@echo "start compiling..."
@echo $(OBJS)
$(CC) -o $@ $^
@echo "compile done"
hello.o: hello.c
$(CC) -c -o $@ $^
module.o: module.c
$(CC) -c -o $@ $^
clean:
rm -f $(BIN) $(OBJS)
还有一些自动变量不太常用,但是大家在以后阅读Makefile时可能会遇到,比如:
代码语言:javascript复制$%:当规则的目标是一个静态库文件时,$%代表静态库的一个成员名
$ :类似$^,但是保留了依赖文件中重复出现的文件
$*:在模式匹配和静态模式规则中,代表目标模式中%的部分。比如hello.c,当匹配模式为%.c时,$*表示hello
$(@D):表示目标文件的目录部分
$(@F):表示目标文件的文件名部分
$(*D):在模式匹配中,表示目标模式中%的目录部分
$(*F):在模式匹配中,表示目标模式中%的文件名部分
-: :告诉make在编译时忽略所有的错误
@: :告诉make在执行命令前不要显示命令
变量替换
字符串替换
代码语言:javascript复制.PHONY: all
SRC := main.c sub.c
OBJ := $(SRC:.c=.o)
all:
@echo "SRC = $(SRC)"
@echo "OBJ = $(OBJ)"
代码语言:javascript复制# make
SRC = main.c sub.c
OBJ = main.o sub.o
模式匹配替换
使用匹配符%匹配变量,使用 % 保留变量值中的指定字符串,然后其他部分使用指定字符串代替。
代码语言:javascript复制.PHONY: all
SRC := main.c sub.c
OBJ := $(SRC:%.c=%.o)
all:
@echo "SRC = $(SRC)"
@echo "OBJ = $(OBJ)"
环境变量
除了用户自定义的一些变量,make在解析Makefile中还会引入一些系统环境变量,如编译参数CFLAGS、SHELL、MAKE等。这
些变量在make开始运行时被载入到Makefile文件中,因为是全局性的系统环境变量,所以这些变量对所有的Makefile都有效。
若Makefile中有用户自定义的同名变量,系统环境变量将会被用户自定义的变量覆盖。若用户在命令行中传递跟系统环境变量同名的变量,系统环境变量也会被传递的同名变量覆盖。
代码语言:javascript复制PHONY:all
CFLAGS = -g
all:
@echo "CFLAGS = $(CFLAGS)"
@echo "SHELL = $(SHELL)"
@echo "MAKE = $(MAKE)"
@echo "HOSTNAME = $(HOSTNAME)"
代码语言:javascript复制wit@pc:/home/makefile/demo# make HOSTNAME=zz.cc #命令行传入
CFLAGS = -g
SHELL = /bin/sh
MAKE = make
HOSTNAME = zz.cc
除此之外,我们还可以通过export命令给Makefile传递变量,在shell环境下使用export命令,就相当于将对应变量声明为系统环境变量
Override指示符
override的作用及使用:
在一个Makefile中使用define、:=、= 定义的变量,我们可以在执行make命令时重新指定这个变量的值。
如果不希望在命令行指定的变量值替代在Makefile中的原来定义,那么我们可以在Makefile中使用指示符 override 对这个变量进行声明:
代码语言:javascript复制.PHONY: all
override web = www.baidu.com
all:
@echo "web = $(web)"
Makefile中的变量分为多种:追加变量、立即变量、展开变量、使用define定义的变量,它们都可以使用override修饰。
当一个追加变量在定义时使用了override,后续对它的值进行追加时,也需要使用带有override指示符的追加方式。否则对此变量值的追加不会有效。
代码语言:javascript复制.PHONY: all
override fruits = apple
override fruits = banana
all:
@echo "fruits = $(fruits)"
override的存在目的:
为了使用户可以改变或者追加哪些使用make命令行指定的变量的定义。从另一个角度上看,就是实现了在Makefile中增加或者修改命令行参数的一种机制。
比如在编译程序时,无论在命令行指定什么参数,编译器在编译时必需打开 -Wall选项,那么在Makefile中的CFLAGS应该这样定义:
代码语言:javascript复制.PHONY: all
override CFLAGS = -Wall
all:
@echo "CFLAGS = $(CFLAGS)"
代码语言:javascript复制# make
CFLAGS = -Wall
# make CFLAGS=-g
CFLAGS = -g -Wall
不使用override修饰:
代码语言:javascript复制# make
CFLAGS = -Wall
# make CFLAGS=-g
CFLAGS = -g
Makefile递归执行
在实际工程项目中,各个源文件通常存放在各个不同的目录中,make在编译工程项目时,会依次遍历各个不同的子目录,编译每个子目录下的源文件。
代码语言:javascript复制make -C subdir1 subdir2 subdir3 ...
上面的make 命令就等价于:
代码语言:javascript复制cd subdir1 && $(MAKE)
cd subdir2 && $(MAKE)
cd subdir3 && $(MAKE)
顶层Makefile:
代码语言:javascript复制.PHONY:all
all:
@echo "make start"
make -C subdir1
make -C subdir2
make -C subdir3
@echo "make done"
make通过 -C subdir参数,会分别到各个子目录下去执行,解析各个子目录下的Makefile并运行,遍历完所有的子目录
make依次遍历到各个子目录下解析新的Makefile时,项目顶层目录的主Makefile定义的一些变量,如何传递到子目录的Makefile文件中:将对应变量使用export声明为环境变量