系统构建_Makefile
前言
V1:Simplest C project
Makefile中的内置变量
Makefile 中的内置变量(builtin variables)是 Make 提供的一些默认变量,这些变量用于简化常见的构建任务,如编译、链接和其他操作。
它们可以被用户在 Makefile 中直接使用,也可以在需要时覆盖它们的默认值。
您可以在数据库中找到的许多其他变量:
make --print-data-base | less
与模式规则相关的变量
%:用于模式规则中的通配符,表示任意匹配的字符。
例如,%.o: %.c 规则表示任何 .o 文件都可以通过与之同名的 .c 文件生成。
%.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
模式规则是目标包含 % 字符的规则(此处为 %.o: %.c)。
这个字符的意思是“正是其中之一”。这里用它来表示每个 .o 需要一个具有相同名称的 .c,并且 $(CC)... 将执行与 .o: .c 对一样多的次数,从而为每个源创建其相应的对象,一次一个。
输入输出文件相关的变量
-
$@:目标文件的名称,即当前规则生成的文件或目标。
-
$<:第一个依赖文件的名称,通常是源文件。
-
$^:所有依赖文件的名称列表,空格分隔。
-
$?:所有比目标文件更新的依赖文件的列表。
-
$(@D) 目标文件名的目录部分
-
$(@F)目标文件名的文件部分
自动递归调用相关的变量
MAKE 和 MAKEFLAGS 是内置变量。
MAKE 值对应于正在运行的 make 可执行文件,MAKEFLAGS 对应于其标志。
当从另一个 Makefile 执行一个 Makefile 时,被调用者的 MAKE MAKEFLAGS 变量将从调用者的 MAKE MAKEFLAGS 值继承。
-
为什么有用?
-
递归调用:如果一个 Makefile 调用另一个 Makefile,你可以使用 $(MAKE) 来确保调用的也是同一个 make 程序,而不是某个其他版本的 make。
-
系统兼容性:不同系统上可能有不同版本或位置的 make 程序。使用 $(MAKE) 可以确保你正在运行的 Makefile 中使用的是启动这个构建过程的 make,而不是其他版本。
-
Makefile中的隐式规则
顾名思义,隐式规则是隐式的,不需要编写。除了内置变量之外,所有隐式规则都可以在数据库中找到:
make -p -f/dev/null | less
C 编译隐式规则如下所示:
%.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
这说明将.c文件编译成.o文件这个步骤我们是不需要重复写了。
隐式规则中的文件查找
我一直有个疑惑,隐式规则中文件都是只写了个%.c之类的,那么这些文件到底是在哪里找到的?Makefile会自动搜索当前目录找吗?还是可以自动的递归搜索找?
都不是!以下以一个案例进行讲解:
若我在~/ics2023/abstract-machine目录下执行了@$(MAKE) -s -C $(AM_HOME)/klib archive
$(AM_HOME)/klib/Makefile的内容为:
NAME = klib
SRCS = $(shell find src/ -name "*.c")
//SRCS内容为src/cpp.c src/int64.c src/stdio.c src/stdlib.c src/string.c
include $(AM_HOME)/Makefile
其中$(AM_HOME)/Makefile参与的主要内容如下:
ARCH = riscv32-nemu
WORK_DIR = $(shell pwd) //即~/ics2023/abstract-machin/klib
DST_DIR = $(WORK_DIR)/build/$(ARCH) //即~/ics2023/abstract-machin/klib/build/riscv32-nemu
$(DST_DIR)/%.o: %.c
@mkdir -p $(dir $@) && echo + CC $<
@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)
$(ARCHIVE): $(OBJS)
@echo + AR "->" $(shell realpath $@ --relative-to .)
@$(AR) rcs $(ARCHIVE) $(OBJS)
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
archive: $(ARCHIVE)
需要注意一下点:
-
$(MAKE) -C /path
表示到/path
目录下执行Makefile,所以WORK_DIR = $(shell pwd)
才是会为~/ics2023/abstract-machin/klib
,而不是~/ics2023/abstract-machin/
-
依赖步骤:
- 首先执行
make archive
,发现其依赖$(ARCHIVE)
- 然后发现
$(ARCHIVE)
要依赖$(OBJS)
,$(OBJS)
中的内容是:~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/cpp.o
、~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/int64.o
、~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/stdio.o
、... - 接下来是我自己的一点思考,不一定正确:
$(OBJS)
在显示规则中未发现自己的依赖,所以开始到隐式规则中寻找依赖。
$(OBJS)
正好能和隐式规则中的$(DST_DIR)/%.o
匹配上,如:
$(OBJS)
中的~/ics2023/abstract-machin/klib/build/riscv32-nemu/src/cpp.o ...
与
$(DST_DIR)/%.o
中的~/ics2023/abstract-machin/klib/build/riscv32-nemu/%.o
%.o
匹配上了src/cpp.o ...
那么对应的
%.c
匹配上了src/cpp.c ...
,那么这些c文件是在当前工作目录下的%.c查找,即在~/ics2023/abstract-machin/klib/src/cpp.c ...
找。 - 首先执行
.PHONY:
在 Makefile
中,.PHONY
是一个伪目标(phony target),用来指定某些目标不对应实际的文件,而是为了执行某些命令。它的作用是避免文件名与目标名冲突,确保 make
在执行这些目标时不会因为目标名称与文件名称相同而产生混淆。
-
避免文件与目标重名时的冲突:
- 如果
Makefile
中有一个目标名为clean
,但是当前目录中也存在一个叫clean
的文件,make
在执行时可能会认为这个目标已经完成,因为这个文件存在,导致不再执行目标命令。 - 通过将
clean
声明为.PHONY
目标,make
就不会去检查是否存在与目标同名的文件,而是每次都执行该目标的命令。
- 如果
-
提高构建效率:
- 对于伪目标,
make
不需要检查目标文件的时间戳与依赖项的时间戳是否一致(这是make
判定目标是否需要重建的方式)。make
直接执行伪目标中的命令。
- 对于伪目标,
假设有如下 Makefile
:
clean:
rm -f *.o myprogram
如果在项目目录中存在一个名为 clean
的文件,make clean
就不会执行 rm
命令,因为 make
会认为 clean
目标已经完成(由于存在同名文件)。为了解决这个问题,我们可以使用 .PHONY
让 make
将 clean
视为伪目标:
.PHONY: clean
clean:
rm -f *.o myprogram
make
不会将.PHONY
目标和实际文件名相关联,也不会因为目录中存在同名文件而跳过该目标。- 即使有与目标同名的文件,
make
也会执行.PHONY
目标中的命令。 - 它通常用于构建非文件相关的目标,比如
clean
、install
、all
、test
等。
:=
和 =
的区别
在 Makefile
中,:=
和 =
都用于定义变量,但它们的工作方式不同,具体区别如下:
1. =
(延迟展开 / 惰性赋值)
- 延迟展开:使用
=
定义的变量在每次被引用时才进行 展开(计算/替换),也就是在使用变量时才会计算它的值,而不是在定义时计算。 - 如果变量的值依赖于其他变量,那么
=
会等到变量实际被使用时,去读取其他变量的最新值。
示例:
A = $(B)
B = hello
- 在这个例子中,
A
的值会在 使用A
时才确定,而在那时,B
的值已经是hello
,因此A
的值也是hello
。
延迟展开的效果:
- 如果
B
在定义后被修改,A
的值也会随着B
的修改而改变。
A = $(B)
B = hello
B = world
在这种情况下,当 A
被引用时,A
的值会是 world
。
2. :=
(立即展开 / 立即赋值)
- 立即展开:使用
:=
定义的变量会在 定义时 立即计算并确定值,之后无论再引用该变量多少次,值都保持不变。 - 如果
:=
定义的变量依赖于其他变量,那么在定义时就会立即计算其他变量的值,而不会在引用时重新计算。
示例:
A := $(B)
B = hello
- 在这个例子中,
A
在定义时就已经被赋值为hello
,因为B
在定义A
时的值是hello
。即使之后B
的值改变,A
的值仍然保持为hello
。
立即展开的效果:
- 即使
B
的值在定义后被修改,A
的值也不会受到影响。
A := $(B)
B = hello
B = world
在这种情况下,A
的值仍然是 hello
,即使 B
被修改成了 world
,因为 A
的值在定义时已经固定为 hello
。
@ 与 MAKEFLAGS
@
写在构建规则之前作用是不打印规则本身
all: $(NAME)
@echo "In Makefile1: MAKE=$(MAKE), MAKEFLAGS=$(MAKEFLAGS)"
如上述若没有@
,那么echo "In Makefile1: MAKE=$(MAKE), MAKEFLAGS=$(MAKEFLAGS)"
将会被输出
MAKEFLAGS
是在执行make时自动加上的参数
MAKEFLAGS += --no-print-directory
若去掉--no-print-directory
这行,那么将会输出类似如下的信息:
默认目标(default goal)
默认情况下,当你执行 make 而不指定任何目标时,Makefile 会构建这个默认目标。
-
第一个出现的目标:
Makefile
中第一个定义的目标就是默认目标。如果没有显式地指定其他默认目标,它将作为默认目标。- 示例:
在这个例子中,all: target1 target2 target1: echo "Building target1" target2: echo "Building target2"
all
是默认目标,因为它是Makefile
中第一个出现的目标。如果你运行make
,它会默认执行make all
。
- 示例:
-
可以自定义默认目标的名字:尽管
all
常常被用作默认目标,但你可以将默认目标设置为任何名字,只要它是第一个出现的目标。例如,你可以用build
或app
代替all
:- 示例:
在这个例子中,build: target1 target2 target1: echo "Building target1" target2: echo "Building target2"
build
是默认目标,因为它是第一个出现的目标。
- 示例:
GNU make
提供了一个内置函数 MAKECMDGOALS
和特殊目标 .DEFAULT_GOAL
,可以用来显式地指定默认目标。
-
使用
.DEFAULT_GOAL
:
你可以通过设置.DEFAULT_GOAL
来显式定义默认目标,即使它不是第一个目标。- 示例:
现在,无论.DEFAULT_GOAL := my_default_goal my_default_goal: echo "This is the default goal" other_target: echo "This is another target"
my_default_goal
是否在Makefile
中是第一个目标,它都会是默认目标。当你运行make
时,它会执行my_default_goal
。
- 示例:
-
查看当前默认目标:
如果你想知道当前默认目标是什么,可以运行以下命令:make -p | grep '.DEFAULT_GOAL'
这将显示默认目标(如果它被显式设置)。如果没有显式设置,结果将为空,并且第一个目标会被当作默认目标。
V2:Project that include headers
静默
.PHONY: clean fclean re
.SILENT:
通常,make 在执行之前会打印规则配方的每一行。特殊目标 .SILENT:静默指定为先决条件的规则输出.
当在没有先决条件的情况下使用它时,它会静默所有规则(包括隐式规则)。
我们上述@
是单行静默
还可以再MAKEFLAGS
中+=--silent
,这样的效果和.SILENT:
后面不加任何先决条件,即静默所有规则的效果是一样的。
info
打印自定义消息
%.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
$(info CREATED $@)
C编译隐式规则被覆盖
这里使用 info 函数来打印有关刚刚构建的内容的自定义消息。
我们更喜欢 info 而不是 shell echo,因为它是一个 make 函数。与只能在配方中使用的 echo 不同,info 可以在 Makefile 中的任何位置使用,这使得它对于调试非常强大。
处理头文件
CPPFLAGS 专用于预处理器的标志,例如 -I <include_dir>,它允许您不再需要编写标头的完整路径,而只需在源中编写其文件名
如果你有一个头文件在路径 /path/to/include/icecream.h,通常在源代码中需要写成:
#include "../../path/to/include/icecream.h"
但是如果你在 Makefile 中的 CPPFLAGS 里加入了 -I/path/to/include,编译器就知道去 /path/to/include 路径下寻找头文件,你就可以在源代码中简单地写:
#include "icecream.h"
V3:Project with any kind of directory structure
反斜杠换行
SRCS := \
main.c \
arom/coco.c \
base/milk.c \
base/water.c
我们可以通过以反斜杠结尾来分割该行,以增加 SRCS 内容的可读性并方便其修改。
替换引用
SRC_DIR := src
OBJ_DIR := obj
SRCS := \
main.c \
arom/coco.c \
base/milk.c \
base/water.c
SRCS := $(SRCS:%=$(SRC_DIR)/%)
OBJS := $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
字符串替换引用将变量的每一项的值替换为指定的更改。
$(SRCS:%=$(SRC_DIR)/%)
表示 % 表示的 SRCS 的每一项变成其自身 (%)
加上 ``$(SRC_DIR)/ `更改,因此 main.c 变成 src/main.c。
非常需要注意的是:
OBJS := $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
我们并不能将其写为:
OBJS := $(SRCS:%.c=$(OBJ_DIR)/%.o)
否则这相当于替换成了$(OBJ_DIR)/$(SRC_DIR)/%.o
。
举例来说,本来SRCS中的main.c,通过SRCS := $(SRCS:%=$(SRC_DIR)/%)
,得到src/main.c
若通过OBJS := $(SRCS:%.c=$(OBJ_DIR)/%.o)
,将会得到obj/src/main.o, 与预计的obj/main.o不符
V4: Static library
依赖文件(.d)
在 Makefile
中,依赖文件(Dependency Files)用于管理源代码文件之间的依赖关系,尤其是当源文件包含头文件(.h
文件)时。
依赖文件通常用于跟踪哪些源文件依赖哪些头文件,并在这些头文件发生变化时自动重新编译相关的目标文件(.o
文件)。
具体来说,.d 文件包含某个源文件所依赖的所有头文件列表。
例如,如果你有一个 main.c 文件,它包含了 main.h,那么 .d 文件中会列出 main.c 依赖于 main.h。如果 main.h 修改了,make 就会知道需要重新编译 main.c。
DEPS := $(OBJS:.o=.d)
-include $(DEPS)
(1) DEPS := $(OBJS:.o=.d)
-
这一行的作用是通过 模式替换 将
OBJS
列表中的.o
文件名替换为.d
文件名,并将生成的.d
文件列表存储在DEPS
变量中。 -
OBJS
是目标文件(如.o
文件),通常由.c
源文件生成。假设
OBJS
的内容为:OBJS = main.o utils.o
然后通过模式替换
$(OBJS:.o=.d)
,它会将main.o
替换为main.d
,utils.o
替换为utils.d
,因此:DEPS = main.d utils.d
这意味着每个目标文件(
.o
文件)都会有一个对应的依赖文件(.d
文件)来记录它的依赖关系。
(2) -include $(DEPS)
- 这一行代码的作用是包含生成的
.d
依赖文件,使make
知道每个目标文件依赖哪些头文件。 -include
的-
表示 “如果文件不存在,不要报错”。因为在第一次运行make
时,.d
依赖文件可能还没有被生成,所以使用-include
可以避免make
在找不到.d
文件时报错。
当 .d
文件被包含时,它会告诉 make
哪些头文件是每个目标文件的依赖项。这样,make
就能知道当某个 .h
文件发生变化时,哪些 .o
文件需要重新编译。
(3)依赖文件的生成
与源不同,当头文件被修改时,make 无法知道这一点,并且不会认为可执行文件已过期,因此不会重建它。
为了改变这种行为,我们应该添加适当的头文件作为附加先决条件:
#before #after
main.o: main.c main.o: main.c icecream.h
clang -c $< -o $@ clang -c $< -o $@
对多个源和标头手动执行此操作既乏味又容易出错。通过将 -MMD 添加到 CPPFLAGS,我们的编译器将自动为编译期间遇到的每个目标文件生成依赖项列表。 -MP 选项可防止头文件被删除或重命名时触发的错误。
makefile
中的 .d
文件通常是通过 GCC 的 -MMD
选项自动生成的。你可以在编译每个源文件时,让编译器同时生成对应的 .d
文件,用来记录依赖关系。
CC := gcc
CFLAGS := -Wall -Wextra -Werror
CPPFLAGS := -MMD -MP -I include
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
$(DIR_DUP)
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
$(info CREATED $@)
生成静态库
关于静态库的前置知识
在C语言中,静态库是指一组预先编译好的目标文件(object files),这些文件可以被打包到一个单一的库文件中,然后链接到程序的可执行文件。
假设有多个源文件,你想将它们打包成一个静态库,常用工具是 ar
(archive),这个工具可以将多个目标文件(.o
)打包成一个库文件(.a
)。
-
编译源文件:首先你需要将源文件编译成目标文件(
.o
)。这可以通过gcc
完成:gcc -c file1.c file2.c file3.c
这会生成
file1.o
、file2.o
和file3.o
。 -
打包静态库:使用
ar
工具将这些目标文件打包成一个静态库文件(通常以.a
结尾)。ar rcs libmylibrary.a file1.o file2.o file3.o
r
:添加文件到库中(或替换已存在的文件)。c
:创建库文件。s
:生成库文件的索引,以提高链接速度。
这样就会生成一个名为
libmylibrary.a
的静态库文件。 -
使用静态库:在使用静态库时,你可以在编译你的程序时通过
-L
和-l
选项链接静态库:gcc -o myprogram main.c -L. -lmylibrary
这里的
-L.
表示在当前目录下查找库文件,-lmylibrary
表示链接mylibrary.a
静态库(注意,-l
后面的库名省略了后缀.a
)。
Make构建
NAME := libicecream.a
AR := ar
ARFLAGS := -r -c -s
$(NAME): $(OBJS)
$(AR) $(ARFLAGS) $(NAME) $(OBJS)
$(info CREATED $(NAME))
使用 ar 创建静态库。
- -r 用新对象替换旧对象,
- -c 创建库(如果不存在)
- -s 将索引写入存档或更新现有库。
V5:Project that uses libraries
打印调试Make规则
一般情况下我们是要给Makefile加上.SILENT:
或者MAKEFLAGS+=--silent
以达到静默规则的效果
但是有时候我们需要查看规则写的是否正确,或者查看规则过程:
info-%:
$(MAKE) --dry-run --always-make $* | grep -v "info"
info-%:
- 模式规则:
info-%
是Makefile
中的一个模式规则,其中%
是一个通配符,表示任意文本。例如,如果你运行make info-myprogram
,%
就会匹配myprogram
,并将其作为$*
传递给规则。 - 目标:这个规则的目标是以
info-
开头,后面可以跟任意的目标名称。
$(MAKE)
$(MAKE)
是make
的内置变量,它指向当前正在执行的make
程序。这个变量确保无论你使用什么版本的make
,在递归调用时都使用相同的版本。
--dry-run
--dry-run
(或-n
)是make
的一个选项,它会显示make
将会执行的命令,而不实际执行这些命令。这意味着make
会显示如何构建目标,但不会真正运行这些构建命令。
--always-make
--always-make
(或-B
)选项强制make
总是执行规则,即使目标文件看起来是最新的也不会跳过它。这样,make
会展示如何重新构建目标,即使目标文件没有过期。
$*
$*
是一个自动化变量,它代表模式规则中%
匹配的部分。例如,如果你运行make info-myprogram
,$*
就会是myprogram
。因此,$(MAKE) --dry-run --always-make $*
就会运行make
并尝试构建myprogram
,但不实际执行,只是展示将会执行的命令。
| grep -v "info"
grep -v "info"
通过grep
过滤掉包含字符串info
的行。因为规则的名字是info-
开头的,make
在执行时可能会显示一些包含info
的行(比如它要执行的命令),这一步骤就是用来忽略这些与目标不相关的输出。
调试方案
How to trace Makefile targets for troubleshooting?
或者还可以使用remake:博客
Makefile与git
.PHONY: update
update:
git stash
git pull
git submodule update --init
git stash pop
git stash
- 作用:将当前工作目录中未提交的更改(修改和未追踪的文件)暂存起来,以便能够在清洁的状态下执行后续的
git pull
等操作。暂存的更改会被保存到一个 Git 的 "stash" 栈中。 - 目的:确保在执行
git pull
和git submodule update
之前,当前工作目录的更改不会影响这些操作。
git pull
- 作用:从远程仓库中拉取最新的代码更改,并合并到当前的本地分支中。它会获取远程仓库的更新并将它们应用到你的本地代码上。
- 目的:获取远程仓库的最新版本代码。
git submodule update --init
-
作用:
git submodule update
:更新项目中的 Git 子模块,使它们与.gitmodules
文件中定义的版本同步。--init
:如果某些子模块还没有被初始化,这个选项会初始化它们,即下载并检出子模块的代码。
-
目的:确保项目中的所有子模块被正确更新到指定的版本。
-
.gitmodules 是一个 Git 项目中的特殊配置文件,用于管理 Git 子模块。子模块是一种将其他 Git 仓库嵌入到主项目中的机制,允许你在一个项目中使用其他独立版本控制的仓库。
git stash pop
- 作用:恢复之前通过
git stash
暂存的更改。stash pop
会将stash
栈顶的内容重新应用到当前工作目录,并从stash
栈中删除这些暂存的更改。 - 目的:在完成拉取代码和更新子模块后,将之前的未提交更改重新应用到工作目录。
addprefix
CPPFLAGS := $(addprefix -I,$(INCS)) -MMD -MP
LDFLAGS := $(addprefix -L,$(dir $(LIBS_TARGET)))
LDLIBS := $(addprefix -l,$(LIBS))
CPPFLAGS
:为头文件目录加上-I
前缀,告知编译器头文件的搜索路径,并设置生成.d
依赖文件的标志。LDFLAGS
:为库文件所在的目录加上-L
前缀,告知链接器库文件的搜索路径。LDLIBS
:为每个库名加上-l
前缀,指定需要链接的库名。
CPPFLAGS
CPPFLAGS := $(addprefix -I,$(INCS)) -MMD -MP
CPPFLAGS
是 预处理器标志,用于向编译器的预处理器传递参数。它通常用于指定 头文件的搜索路径 和一些 预处理选项。$(addprefix -I,$(INCS))
:INCS
是包含头文件目录的变量列表,例如INCS = include/ extra_includes/
。addprefix -I, $(INCS)
会为INCS
中的每个目录加上-I
前缀(预处理器搜索路径标志)。最终效果类似于-Iinclude/ -Iextra_includes/
,表示让编译器在这两个目录下查找头文件。
-MMD
:表示生成 依赖文件(.d
文件),用来记录源文件和头文件之间的依赖关系。这用于自动化构建系统,使得make
知道哪些文件需要重新编译。-MP
:用于生成 空的依赖目标。它与-MMD
一起使用,确保当某个头文件被删除时,不会导致make
出错。
LDFLAGS
LDFLAGS := $(addprefix -L,$(dir $(LIBS_TARGET)))
LDFLAGS
是 链接器标志,用于向链接器传递参数,通常用于指定 库文件的搜索路径。$(addprefix -L,$(dir $(LIBS_TARGET)))
:LIBS_TARGET
是库文件的目标路径变量列表,假设它可能包含某些库文件路径,如/usr/local/lib/libexample.a
。$(dir $(LIBS_TARGET))
提取LIBS_TARGET
中每个库文件的 目录部分,例如/usr/local/lib/
。addprefix -L, $(dir $(LIBS_TARGET))
会为每个提取出来的目录路径加上-L
前缀,告诉链接器在这些目录中查找库文件。最终效果类似于-L/usr/local/lib/
。
LDLIBS
LDLIBS := $(addprefix -l,$(LIBS))
LDLIBS
是 链接库标志,指定需要链接的 库名称。这些库通常在由LDFLAGS
指定的路径中找到。$(addprefix -l,$(LIBS))
:LIBS
是库名列表,例如LIBS = example math
.addprefix -l,$(LIBS)
会为LIBS
中的每个库名加上-l
前缀,生成-lexample -lmath
,表示链接时需要使用example.a
和math.a
或.so
共享库。
dir
clean:
for f in $(dir $(LIBS_TARGET)); do $(MAKE) -C $$f clean; done
$(RM) $(OBJS) $(DEPS)
fclean: clean
for f in $(dir $(LIBS_TARGET)); do $(MAKE) -C $$f fclean; done
$(RM) $(NAME)
dir 函数意味着我们只想保留给定项目的目录部分,存在一个 notdir 函数,它执行相反的操作,仅保留给定项目的文件名。
V6:
Makefile内置变量
MAKECMDGOALS
MAKECMDGOALS 是 GNU make 中的一个内置变量,它存储了用户在命令行上指定的目标(goals)。
当你运行 make 时,make 会尝试构建你指定的目标,而 MAKECMDGOALS 就记录了这些目标的名字。
all: hello clean
hello:
@echo "Hello World"
clean:
@echo "Cleaning up..."
show-goals:
@echo "Goals: $(MAKECMDGOALS)"
make show-goals
: Goals: show-goals
Makefile函数
findstring
在 GNU Make 中,findstring 函数用于检查一个字符串是否包含另一个子字符串。
如果找到匹配项,findstring 返回这个子字符串;
如果没有找到匹配项,则返回空字符串。
$(findstring find, in)
- find:你要查找的子字符串。
- in:你要查找的字符串或文本。
wildcard
在 GNU Make 中,wildcard 函数用于匹配符合指定模式的文件名。
它通常用于查找目录中的文件,并生成文件列表。通过 wildcard 函数,你可以动态地获取目录下符合某种模式的文件,并将它们用于后续的构建规则中。
$(wildcard pattern)
- pattern:通配符模式,通常是文件路径模式,比如 .c、src/.h。
- 返回值:匹配到的文件名列表。如果没有匹配项,返回空字符串。
abspath
在 Makefile 中,abspath 是一个用于将相对路径转换为绝对路径的内置函数。它返回一个文件路径的绝对路径形式,而不必要求文件或目录实际存在。
$(abspath relative-path)
假设当前目录是 /home/user/project:
SRC_DIR := src
FULL_PATH := $(abspath $(SRC_DIR))
那么PULL_PATH就是/home/user/project/FULL_PATH
sort
sort 是 Makefile 的一个内置函数,它会对输入的单词列表进行排序,并去除重复的项。
排序结果按照字母顺序排列。
实战案例
处理普通C编译链接的Makefile
# 1. Basic Setup and Checks
# 2. Create Compilation Targets
WORK_LOAD = $(shell pwd)
DST_DIR = $(WORK_LOAD)/build
INCL = $(WORK_LOAD)/include
SRCS = $(shell find src/ -name "*.c")
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
TARGET = $(WORK_LOAD)/rioTest
DEPS := $(OBJS:.o=.d)
-include $(DEPS)
## 3. General Compilation Flags
CC := gcc
CFLAGS := -Wall -Wextra -Werror
CPPFLAGS := -MMD -MP -I $(INCL)
LD := $(CC)
## 4. Compilation Rules
$(DST_DIR)/%.o: %.c
@mkdir -p $(dir $@) && echo + CC $<
@$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
$(TARGET): $(OBJS)
@echo + LD $^
@$(LD) -o $@ $^
## 5. Miscellaneous
.PHONY: test clean
test : $(TARGET)
clean:
rm -rf $(WORK_LOAD)/build && rm $(TARGET)
info-%:
@echo $(OBJS)
$(MAKE) --dry-run --always-make $* | grep -v "info"
需要注意一点若上述LD是ld而不是gcc,那么可能会出现一些奇怪的问题,比如:
ld: warning: cannot find entry symbol _start;
或找不到标准库中的一些函数,printf,open之类的,即使添加了头文件
。
这是因为ld 默认并不会自动链接 C 标准库。如果你需要使用标准库函数,你需要显式添加 -lc 来链接 C 标准库
在 Linux 系统中,默认情况下,程序的入口点是 _start,这是由操作系统加载程序后执行的第一个位置。通常,_start 是由运行时启动代码(如 C 的 crt0.o 或 crt1.o)提供的。当使用 ld 直接链接时,ld 不会自动包含启动代码。这个启动代码通常是 crt0.o 或类似的文件,负责设置运行时环境并调用 main 函数。
但是对于上述问题gcc
都会自动帮我们解决