Skip to content

Makefile基础

前言

从代码到可执行文件需要预处理,编译,汇编和链接这几个步骤。 而一个项目有多个源文件,如果只修改一个,就对所有源文件重新执行编译、链接步骤,就太浪费时间了,因此十分有必要引入 Makefile 工具:Makefile 工具可以根据文件依赖,自动找出那些需要重新编译和链接的源文件,并对它们执行相应的动作。

makefile三要素

目标,依赖,执行语句: image

基本语句

基本结构

# Makefile
main : main.c
        gcc main.c -o main

通配符和使用wildcard函数

Wildcard function使用方法

$(wildcard pattern)
使用举例:
# Makefile
main : $(wildcard *.c)
        gcc $(wildcard *.c) -o main

变量

变量使用通配符

# Makefile
SRCS := $(wildcard *.c)
main : $(SRCS)
        gcc $(SRCS) -o main

赋值和修改

递归赋值

foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:;echo $(foo)
# 打印的结果为 Huh?,$(foo)展开得到$(bar),$(bar)展开得到$(ugh),$(ugh)展开得到Huh?最终$(foo)展开得到Huh?

简单赋值

x := foo
y := $(x) bar
x := later
# 等效于:
# y := foo bar
# x := later

文本添加

objects = main.o foo.o bar.o utils.o
objects += another.o
# objects最终为main.o foo.o bar.o utils.o another.o

条件赋值

FOO ?= bar
# FOO最终为bar
foo := ugh
foo ?= Huh?
# foo最终为ugh

Makefile进阶

应对复杂的目录结构

当前的目录结构为:

.
├── Makefile
├── entry.c
├── func
   ├── bar.c
   └── bar.h
└── main
使用foreach函数遍历所有的头文件和源文件 使用方法:
$(foreach var,list,text)
使用举例:
# Makefile
#the list
SUBDIR := .
SUBDIR += ./func
#find .h file
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
#find .c file
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
#go
main : $(SRCS)
        gcc $(INCS) $(SRCS) -o main

分析编译过程

  • 预处理:预处理器将以字符 # 开头的命令展开、插入到原始的C程序中。比如我们在源文件中能经常看到的、用于头文件包含的 #include 命令,它的功能就是告诉预编译器,将指定头文件的内容插入的程序文本中,生成 .i文本文件。下图示解析: image
  • 编译阶段:编译器将文本文件 *.i 翻译成文本文件 *.s,它包含一个汇编语言程序。
  • 汇编阶段:汇编器将 *.s 翻译成机器语言指令,把这些指令打包成可重定位目标程序(relocatable object program)的格式,并保存在 *.o 文件中。
  • 链接阶段:在 bar.c 中我们定义了 Print_Progress_Bar 函数,该函数会保存在目标文件 bar.o 中。直到链接阶段,链接器才以某种方式将 Print_Progress_Bar 函数合并到 main 函数中去。在链接时如果没有指定 bar.o,链接器就无法找到 Print_Progress_Bar 函数,也就会提示找不到相关函数的定义。

模式规则和自动变量(两个问题)

目前要解决两个问题 1. 没有保存 .o 文件,这导致我们每次文件变动都要重新执行预处理、编译和汇编来得到目标文件,即使新得到的文件与旧文件完全没有差别(即编译用到的源文件没有任何变化,就跟 bar.c 一样)。 2. 有保存 .o 文件,则会遇到第二个问题,即依赖中没有指定头文件,这意味着只修改头文件的情况下,源文件不会重新编译得到新的可执行文件!

编译过程:

image

(.o文件的保存)解决第一个问题:

简单点可以:

SUBDIR := .
SUBDIR += ./func
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
main : ./entry.o ./func/bar.o
        gcc ./entry.o ./func/bar.o -o main
./entry.o : ./entry.c
        gcc -c $(INCS) ./entry.c -o ./entry.o
./func/bar.o : ./func/bar.c
        gcc -c $(INCS) ./func/bar.c -o ./func/bar.o
通过手动添加目标和依赖,我们实现了 *.o 文件的保存,同时还确保了源文件在更新后,只会在最小限度内重新编译 *.o 文件。现在我们可以利用符号 % 和自动变量,来让 Makefile 变得更加通用。首先聚焦于编译过程:
./entry.o : ./entry.c
        gcc -c $(INCS) ./entry.c -o ./entry.o
./func/bar.o : ./func/bar.c
        gcc -c $(INCS) ./func/bar.c -o ./func/bar.o
上下比较 ./entry.o./func/bar.o 的目标依赖及执行,可以发现新添加的、用于生成 *.o 文件的目标和依赖,有着相同的书写模式,这意味着存在通用的写法:
%.o : %.c
        gcc -c $(INCS) $< -o $@
image 这里我们用上了 % ,它的作用有些难以用语言概括,上述例子中, %.o 的作用是匹配所有以 .o 结尾的目标;而后面的 %.c% 的作用,则是将 %.o% 的内容原封不动的挪过来用。 更具体地例子是,%.o 可能匹配到目标 ./entry.o./func/bar.o,这样 % 的内容就会是 ./entry./func/bar,最后交给 %.c 时就变成了 ./entry.c./func/bar.c。 另外我们还使用到了自动变量 $< $@,其中 $< 指代依赖列表中的第一个依赖;而 $@ 指代目标。注意自动变量与普通变量不同,它不使用小括号。 结合起来使用,我们就得到了通用的生成 *.o 文件的写法:
# Makefile
SUBDIR := .
SUBDIR += ./func
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
main : ./entry.o ./func/bar.o
        gcc ./entry.o ./func/bar.o -o main
%.o : %.c
        gcc -c $(INCS) $< -o $@

链接过程:

main : ./entry.o ./func/bar.o
        gcc ./entry.o ./func/bar.o -o main
我们不能通过wildcard函数来实现通用的写法,因为在最开始我们是无法匹配到 *.o 文件的,因为起初我们只有 *.c 文件, *.o 文件是后来生成的。

patsubst函数

转换一下思路,我们在获取所有源文件后,直接将 .c 后缀替换为 .o,而patsubst 函数可以用于模式文本替换。

$(patsubst pattern,replacement,text)
patsubst 函数的作用是匹配 text 文本中与 pattern 模式相同的部分,并将匹配内容替换为 replacement。于是链接步骤可以改写为:
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,%.o,$(SRCS))
main : $(OBJS)
        gcc $(OBJS) -o main
最终的Makefile内容为:
SUBDIR := .
SUBDIR += ./func
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,%.o,$(SRCS))
main : $(OBJS)
        gcc $(OBJS) -o main
%.o : %.c
        gcc -c $(INCS) $< -o $@

丰富完善Makefile的功能

指定*.o文件的输出路径

我们想要将 *.o 文件保存至指定目录,与源文件和头文件区分开:

SUBDIR := ./
SUBDIR += ./func
OUTPUT := ./output
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,$(OUTPUT)/%.o,$(SRCS))
main : $(OBJS)
        gcc $(OBJS) -o main
$(OUTPUT)/%.o : %.c
        mkdir -p $(dir $@)
        gcc -c $(INCS) $< -o $@
上述Makefile中使用了dir函数 mkdir -p $(dir $@)$@相当于目标 $(OUTPUT)/%.odir函数取得其路径,mkdir创建需要的目录。

伪目标

.PHONY : clean
OUTPUT := ./output
clean:
        rm -r $(OUTPUT)
使用 .PHONY声明一个伪目标 clean使用的时候输入 make clean就会执行 clean:之后的命令

简化终端输出

我们常通过 @ 符号,来禁止 Makefile 将执行的命令输出至终端上: 比如:

$(OUTPUT)/%.o : %.c
        mkdir -p $(dir $@)
        @gcc -c $(INCS) $< -o $@
执行 make之后 gcc -c $(INCS) $< -o $@命令就不会在终端输出 同时我们也可以使用echo命令来拟定自己的输出信息
clean:
        @echo try to clean...
        @rm -r $(OUTPUT)
        @echo Complete!

自动生成依赖(解决第二个问题)

我们要将头文件一同加入到 *.o 文件的依赖中,从而解决修改头文件后,包含该头文件的源文件不会重新编译的问题。 仅需在编译时指定 -MMD 选项,就能得到记录有依赖关系的 *.d 文件。 -MMD 选项包含两个动作,一是生成依赖关系,二是保存依赖关系到 *.d 文件。与其类似的选项还有 -MD,其作用与 -MMD 相同,差别在于 -MD 选项会将系统头文件一同添加到依赖关系中。 另外我们还可以指定 -MP 选项,这会为每个依赖添加一个没有任何依赖的伪目标。-MP 选项生成的伪目标,可以有效避免删除头文件时,Makefile 因找不到目标来更新依赖所报的错误。 最终的Makefile文件

SUBDIR := ./
SUBDIR += ./func
OUTPUT := ./output
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,$(OUTPUT)/%.o,$(SRCS))
DEPS := $(patsubst %.o,%.d,$(OBJS))
main : $(OBJS)
        @echo linking...
        @gcc $(OBJS) -o main
        @echo Complete!
$(OUTPUT)/%.o : %.c
        @echo compile $<...
        @mkdir -p $(dir $@)
        @gcc -MMD -MP -c $(INCS) $< -o $@
.PHONY : clean
clean:
        @echo try to clean...
        @rm -r $(OUTPUT)
        @echo Complete!
-include $(DEPS)
最后一行的 include 用于将指定文件的内容插入到当前文本中。初次编译,或者 make clean 后再次编译时,*.d 文件是不存在的,这通常会导致 include 操作报错。所以我们在 include 前加了 - 符号,其作用是指示 make 在 include 操作出错时忽略这个错误,不输出任何错误信息并继续执行接下来的操作。

通用模板

ROOT := $(shell pwd)
SUBDIR := $(ROOT)
SUBDIR += $(ROOT)/func
TARGET := main
OUTPUT := ./output
INCS := $(foreach dir,$(SUBDIR),-I$(dir))
SRCS := $(foreach dir,$(SUBDIR),$(wildcard $(dir)/*.c))
OBJS := $(patsubst $(ROOT)/%.c,$(OUTPUT)/%.o,$(SRCS))
DEPS := $(patsubst %.o,%.d,$(OBJS))
main : $(OBJS)
        @echo linking...
        @gcc $(OBJS) -o main
        @echo complete!
$(OUTPUT)/%.o : %.c
        @echo compile $<...
        @mkdir -p $(dir $@)
        @gcc -MMD -MP -c $(INCS) $< -o $@
.PHONY : clean
clean:
        @echo try to clean...
        @rm -r $(OUTPUT)
        @echo complete!
-include $(DEPS)

参考链接

写给初学者的makefile入门指南