Makefile学习1

2023-10-17 18:02:05 浏览数 (1)

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中,大家经常会见到类似

@、

^、$<这种类型的变量。

这种变量一般称为自动变量,自动变量是局部变量,作用域范围在当前的规则内,它们分别代表不同的含义:

  • $@:目标
  • $^:所有目标依赖
  • $<:目标依赖列表中的第一个依赖
  • $?:所有目标依赖中被修改过的文件
代码语言:javascript复制
.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声明为环境变量

0 人点赞