makefile最基本是由三个要素组成,分别为:目标文件,依赖文件,规则(make默认只执行第一条规则,并不是传统语言的按序执行每一条命令,make执行的时候会自动判断目标文件的依赖,若不存在依赖或者依赖更新了,才会去执行对应的依赖文件的规则,所有一般将最终文件所需的生成文件作为第一条规则)。
若不存在依赖或者依赖更新了,才会去执行对应的依赖文件的规则。这一特性确保了make在执行时候跳过对未更新依赖的执行,在编译大型项目的时候这一点尤为重要。
Makefile
由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(prerequisites),以及生成目标文件的命令。
例如,要生成m.txt
,依赖a.txt
与b.txt
,规则如下:
# 目标文件: 依赖文件1 依赖文件2
m.txt: a.txt b.txt
cat a.txt b.txt > m.txt
一条规则的格式为目标文件: 依赖文件1 依赖文件2 ...
,紧接着,以Tab开头的是命令,用来生成目标文件。上述规则使用cat
命令合并了a.txt
与b.txt
,并写入到m.txt
。用什么方式生成目标文件make
并不关心,因为命令完全是我们自己写的,可以是编译命令,也可以是cp
、mv
等任何shell中的命令。
在 Makefile 中,伪目标(Phony Targets)是指那些不是实际文件的目标,它们只是用来执行一些特定的命令,而不是生成实际文件。伪目标通常用于执行某些操作,如清理、安装、测试等,而不关心这些操作是否生成了文件。
make
会认为目标是一个文件,并会检查该文件是否存在。如果目标文件已经存在且没有被修改,make
就不会执行对应的规则。PHONY
变量来声明,告诉 make
这个目标不需要与实际文件关联。避免与实际文件同名冲突: 如果你有一个目标和文件同名(例如目标 clean
和文件 clean
),make
会将 clean
当作文件来看待,检查文件的更新时间是否需要重新生成。这时,声明 clean
为伪目标可以避免 make
去寻找或更新名为 clean
的文件。
执行非文件操作: 伪目标用于执行诸如清理、构建、安装、测试等操作,这些操作并不生成实际的文件,而是执行一组命令。
1.clean
伪目标: 这是一个常见的伪目标,通常用于删除中间文件、临时文件或构建产物(例如 .o
文件、可执行文件等)。
.PHONY: clean
clean:
rm -f *.o my_program
clean
目标删除所有 .o
文件和名为 my_program
的可执行文件。2.install
伪目标: 用于安装程序或库文件。
.PHONY: install
install:
cp my_program /usr/local/bin/
install
目标将 my_program
复制到 /usr/local/bin/
目录。3. test
伪目标: 用于执行测试。
.PHONY: test
test:
./run_tests.sh
test
目标执行 run_tests.sh
脚本来运行测试。 隐式规则是 make
内置的一些规则,用于简化常见的构建过程,无需手动指定每个目标的构建命令。当 make
遇到一个目标时,它会根据目标的文件名和文件类型(例如扩展名)自动查找是否存在适用于该目标的显式规则,如果没有显式规则,make
会尝试使用隐式规则来推断如何构建该目标。
.c/.cpp
到 .o
一个常见的隐式规则是将 .c
文件编译为 .o
文件,形如:
.o:
$(CC) -c $(CFLAGS) $<
make
需要构建一个 .o
文件时,它会使用默认的隐式规则。$<
表示第一个依赖文件,即 .c
文件。$(CC)
是编译器,$(CFLAGS)
是编译选项。.c/.cpp
到 .exe
假设你有一个源文件 main.c
,目标是生成一个可执行文件 main.exe
,那么 make
会使用隐式规则来自动推断如何链接 .o
文件,形如:
.exe:
$(CC) $(LDFLAGS) -o $@ $^
$(CC)
是编译器,$(LDFLAGS)
是链接器选项。$@
代表目标文件,$^
代表所有的依赖文件(通常是 .o
文件)。2.1.1 以换行符分隔
cd:
cmd1
cmd2
cmd3
执行每次命令的时候都会创建一个独立的Shell环境,并不会影响当前目录。
2.1.2 以' ; '符号分隔
cd:
cmd1;cmd2;cmd3;
在同一个shell环境内执行所有命令
2.1.3 以' && '符号分隔
cd:
cmd1&&cmd2&&cmd3
当某条命令失败时,后续命令不会继续执行
默认情况下,make
会打印出它执行的每一条命令。如果我们不想打印某一条命令,可以在命令前加上@
,表示不打印命令(但是仍然会执行):
no_output:
@echo 'not display'
echo 'will display'
make
在执行命令时,会检查每一条命令的返回值,如果返回错误(非0值),就会中断执行。有些时候,我们想忽略错误,继续执行后续命令,可以在需要忽略错误的命令前加上-
:
ignore_error:
-rm zzz.txt
echo 'ok'
可以自定义变量$(CC)
替换命令cc,
$(CFLAGS)命令用来指定编译器的标志或选项:
$(TARGET): $(OBJS)
$(CC) -o $(TARGET) $(OBJS)
例如使用交叉编译时,指定编译器:
CC = gcc
CFLAGS = -Wall -g
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
自动变量是 make
在规则执行时自动设置的变量。它们根据规则的上下文变化,提供了构建目标和依赖文件时的信息。自动变量主要是用于在规则中灵活地引用目标和依赖文件。
$@
:目标文件的名称。
foo.o: foo.c
中,$@
被替换为 foo.o
。$<
:规则中第一个依赖文件的名称。
foo.o: foo.c bar.c
中,$<
会被替换为 foo.c
。$^
:所有依赖文件的名称(不重复)。
foo.o: foo.c bar.c
中,$^
被替换为 foo.c bar.c
。$?
:所有比目标文件更新的依赖文件。
foo.o: foo.c bar.c
中,如果 foo.c
更新了,$?
将会是 foo.c
。$*
:不带扩展名的目标文件名。
foo.o: foo.c
中,$*
被替换为 foo
。Makefile 中有几种不同的变量赋值方式,它们之间的区别在于变量的展开时机:
=
(递归展开/延迟展开): 变量的值在使用时才会被展开。这意味着变量的值会受到之后对同一个变量的重新赋值的影响。这种展开方式称为递归展开或延迟展开。
x = $(y)
y = hello
all:
@echo $(x) # 输出 hello
在这个例子中,x
的值在 all
规则执行时才会被展开,此时 y
的值是 hello
,所以 $(x)
的值也是 hello
。
使用 =
进行递归定义时需要小心,避免出现循环依赖,例如 x = $(y)
和 y = $(x)
会导致无限循环。
:=
(直接展开/立即展开): 变量的值在定义时就会被立即展开。这意味着变量的值在定义后不会再改变,即使之后对同一个变量进行了重新赋值。这种展开方式称为直接展开或立即展开。
x := $(y)
y := hello
all:
@echo $(x) # 输出空字符串
在这个例子中,x
的值在定义时就被立即展开,此时 y
还没有定义,所以 $(x)
的值为空字符串。即使之后 y
被定义为 hello
,x
的值也不会改变。
+=
(追加赋值): 用于向变量追加值。
objects = main.o
objects += foo.o
objects += bar.o
all:
@echo $(objects) # 输出 main.o foo.o bar.o
?=
(条件赋值): 只有在变量未定义时才进行赋值。
foo ?= hello
foo = world
all:
@echo $(foo) # 输出 world
bar ?= hello
all2:
@echo $(bar) # 输出 hello
在这个例子中,foo
已经被赋值为 world
,所以 foo ?= hello
不会生效。而 bar
最初没有定义,所以 bar ?= hello
会将 bar
的值设置为 hello
。
1.patsubst
- 模式替换patsubst (pattern, replacement, text)
功能:用于将 text
中符合 pattern
的部分替换为 replacement
。
示例:
SRCS = foo.c bar.c baz.c
OBJS = $(patsubst %.c, %.o, $(SRCS))
# 输出:OBJS = foo.o bar.o baz.o
2.wildcard
- 通配符匹配wildcard (pattern)
功能:返回当前文件夹下匹配指定模式的所有文件。
示例:
SRCS = $(wildcard *.c)
# 如果当前目录有 foo.c 和 bar.c,SRCS 会是 "foo.c bar.c"
foreach
- 遍历列表foreach (var, list, code)
功能:对列表 list
中的每个元素执行 code
,每个元素通过 var
访问。
示例:
$(foreach file, $(SRCS), echo $(file);)
# 这个会依次输出 SRCS 中每个文件的名字
4.addsuffix
- 在每个元素后添加后缀addsuffix (suffix, names)
功能:对列表 list
中的每个元素执行 code
,每个元素通过 var
访问。
示例:
OBJS = $(addsuffix .o, $(SRCS))
# 如果 SRCS = foo bar,那么 OBJS 会是 foo.o bar.o
addprefix添加前缀。
$(dir )
功能:$(dir
会从
中提取出文件的路径部分,去除文件名部分,返回路径的目录部分,如果给定的是一个目录路径,会返回这个路径本身。如果给定的是一个文件路径,会返回文件所在的目录路径。
示例:
$(dir /home/user/project/main.c)
# 结果为:/home/user/project/
abspath
-用于取绝对路径BUILD_DIR := $(abspath ./build)
这里的 $(abspath ./build)
表示取 ./build
的绝对路径。 这个函数的主要用途是避免在不同位置使用同一个 Makefile 时,找不到目录或文件的问题。
7. bashname-
去掉文件名中的后缀名file = example.txt
output = $(basename $(file))
这里的$(basename $(file)) 表示去除file中每个字符串的后缀部分,即 .txt
。
shell-
执行 Shell 命令CWD = $(shell pwd)
这里的 $(shell pwd)
表示执行 pwd
命令,其结果为 pwd
命令输出的当前工作目录。
notdir
-
获取文件名中的非目录部分OBJS = ./build/main.o ./build/func.o
NOTDIR_OBJS = $(notdir $(OBJS))
这里的 $(notdir \$(OBJS))
表示获取 OBJS
中每个字符串的非目录部分,即 main.o func.o
。
在 C/C++ 项目的编译过程中,头文件(.h
文件)的引用依赖问题是一个常见的挑战。具体来说,头文件的修改可能会影响到多个源文件(.c
或 .cpp
文件),因此当头文件发生变化时,相关的源文件需要重新编译。
一种常见的方法是显式地为每个源文件添加依赖关系。在这种方法中,你会在 Makefile 中指定哪些源文件依赖于哪些头文件,这样 make
就能知道哪些源文件需要在头文件发生变化时重新编译。例如(一个main.c文件包含了hello.h头文件):
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out
$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)
# 模式匹配规则:当make需要目标 xyz.o 时,自动生成一条 xyz.o: xyz.c 规则:
%.o: %.c
@echo 'compiling $<...'
cc -c -o $@ $<
clean:
rm -f *.o $(TARGET))
必须每次修改HDRS变量,非常麻烦。
gcc
生成自动依赖 gcc
可以通过 -M
系列选项自动生成依赖文件,这些文件会列出每个源文件所依赖的头文件。通过这种方法,make
可以根据头文件的修改自动触发重新编译。
例如:
$ cc -MM main.c
main.o: main.c hello.h
上述输出告诉我们,编译main.o
依赖main.c
和hello.h
两个文件。
因此,我们可以利用GCC的这个功能,对每个.c
文件都生成一个依赖项,通常我们把它保存到.d
文件中,再用include
引入到Makefile
,就相当于自动化完成了每个.c
文件的精准依赖。
我们改写上一节的Makefile
如下:
# 列出所有 .c 文件:
SRCS = $(wildcard *.c)
# 根据SRCS生成 .o 文件列表:
OBJS = $(SRCS:.c=.o)
# 根据SRCS生成 .d 文件列表:
DEPS = $(SRCS:.c=.d)
TARGET = world.out
# 默认目标:
$(TARGET): $(OBJS)
$(CC) -o $@ $^
# 模式规则,表示对于每个 .c 文件,生成一个对应的 .d 依赖文件。
%.d: %.c
# 删除已有的 .d 文件($@ 是目标文件,即 .d 文件)
rm -f $@; \
# 使用 gcc 的 -MM 选项生成依赖信息,$< 是第一个依赖文件(即 .c 文件)。这会生成包含头文件依赖的 .d 文件,并将结果存储在临时文件 [email protected] 中
$(CC) -MM $< >[email protected]; \
# 使用 sed 命令处理 .d 文件内容,将依赖的 .o 文件规则格式化为符合 make 的依赖格式。最终输出到 .d 文件中。
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < [email protected] > $@; \
rm -f [email protected]
# 模式规则:生成 .o 文件
%.o: %.c
$(CC) -c -o $@ $<
clean:
rm -rf *.o *.d $(TARGET)
# 引入所有 .d 文件(相当于在此处复制了.d文件的内容):
include $(DEPS)
这个 Makefile
用于管理项目中的源文件编译、依赖关系和最终目标的生成。首先,它列出了所有的 .c
文件并生成了对应的 .o
和 .d
文件列表。每当 .c
文件有变化时,.d
文件记录的头文件依赖信息会确保只重新编译必要的源文件。Makefile
通过 gcc -MM
选项生成依赖文件,然后,它使用 $(CC)
编译器将 .c
文件编译为 .o
文件,最后通过链接这些 .o
文件生成目标可执行文件 world.out
。通过 include $(DEPS)
引入所有 .d
文件内容(引入.c文件的依赖内容),make
能够自动追踪源文件与头文件的依赖,确保文件变动时能正确地触发重新编译,避免不必要的重复编译。clean
目标则用于删除中间生成的文件,保持项目目录整洁。
首先要做的事情是创建一个C的目录框架,并且放置一些多续项目都拥有的,基本的文件和目录。一个规范的目录框架可以更好的兼容别人的工作习惯和减少自身对文件执行目录的修改,这也是非常重要的一环,可以极大的便利自身。
以下是一个推荐的C工作目录框架:
project/
│
├── LICENSE # 项目的许可协议
├── README.md # 项目的简要说明
├── Makefile # 项目的构建文件
│
├── bin/ # 存放可执行文件
│
├── build/ # 存放构建过程中生成的文件,如库文件
│
├── src/ # 存放源代码文件,通常包含 .c 和 .h 文件
│ ├── main.c
│ ├── main.h
│ └── hello.c
│
├── tests/ # 存放自动化测试代码
│ ├── test.c
│ └── tess.h
└── minunit.h
make
工具来自动化构建过程。Makefile
自动生成。.c
文件(源文件)和 .h
文件(头文件)。接下来可以整理出一个通用的编译.c文件的Makefile,对于不同的文件存放情况,只需要简单修改部分路径信息即可。
SRC_DIR = ./src
TEST_DIR = ./tests
BUILD_DIR = ./build
BIN_DIR = ./bin
TARGET = $(BIN_DIR)/main.out
TEST_TARGET = $(BIN_DIR)/test.out
CC = cc
CFLAGS = -Wall -g -std=c99
# ./src/*.c
SRC_SRCS := $(wildcard $(SRC_DIR)/*.c)
# ./tests/*.c
TEST_SRCS := $(wildcard $(TEST_DIR)/*.c)
# ./src/*.c => ./build/*.o
SRC_OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRC_SRCS))
# ./tests/*.c => ./build/*.o
TEST_OBJS := $(patsubst $(TEST_DIR)/%.c,$(BUILD_DIR)/%.o,$(TEST_SRCS))
# ./src/*.c => ./build/*.d
SRC_DEPS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.d,$(SRC_SRCS))
# ./tests/*.c => ./build/*.d
TEST_DEPS := $(patsubst $(TEST_DIR)/%.c,$(BUILD_DIR)/%.d,$(TEST_SRCS))
# 默认目标:构建src目录下的可执行文件
all: $(TARGET)
# 运行src下编译的可执行程序
run: $(TARGET)
./$(TARGET)
# 构建测试文件目标
test: $(TEST_TARGET)
@echo "building test objects..."
# 运行测试可执行文件
test_run: $(TEST_TARGET)
./$(TEST_TARGET)
# build/*.d 的规则由 src/和tests/*.c文件生成:
$(BUILD_DIR)/%.d: $(SRC_DIR)/%.c $(TEST_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) -MM $< > $@
# build/*.o 的规则由 src/*.c 生成:
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<
# build/*.o 的规则由 test/*.c 生成:
$(BUILD_DIR)/%.o: $(TEST_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<
# 链接src中可执行文件:
$(TARGET): $(SRC_OBJS)
@echo "buiding $@..."
@mkdir -p $(dir $@)
$(CC) -o $(TARGET) $(SRC_OBJS)
# 链接测试可执行文件:
$(TEST_TARGET): $(TEST_OBJS) $(SRC_OBJS)
@echo "buiding $@..."
@mkdir -p $(dir $@)
$(CC) -o $(TEST_TARGET) $(TEST_OBJS) $(SRC_OBJS)
.PHONY: clean build all run test test_run
# 清理 build 目录:
clean:
@echo "Cleaning build directory..."
rm -rf $(BUILD_DIR)/*
# 生成标准的C目录框架
build:
@echo "Creating directory structure..."
mkdir -p $(BUILD_DIR) $(SRC_DIR) $(TEST_DIR) $(BIN_DIR)
touch LICENSE README.md
# 引入所有 .d 文件内的依赖规则:
-include $(SRC_DEPS)
-include $(TEST_DEPS)
该 Makefile 用于编译 C 项目,支持分别编译 src 目录下的源代码生成可执行文件 bin/main.out,以及编译 tests 目录下的测试代码并与 src 代码链接生成测试可执行文件 bin/test.out。使用 make 或 make all 构建主程序,make run 运行主程序,make test 构建测试程序,make test_run 运行测试程序,make clean 清理构建产物,make build 创建项目目录结构。它使用 wildcard 获取源文件列表,patsubst 进行文件名转换,并自动生成依赖关系文件 .d 以实现增量编译,同时通过独立的规则处理 src 和 tests 目录下的 .c 文件,避免命名冲突。
Makefile
中的 gcc
选项用于控制编译过程,设置不同的编译行为。下面是常见的 GCC 编译选项的总结,特别文章的内容中出现过的:
-g
:启用调试信息。在编译过程中生成调试符号,方便使用调试工具(如 gdb
)进行调试。这是开发和调试阶段常用的选项。-O2
:启用常规优化。在编译时启用常规优化,目的是使程序在运行时更加高效。-O2
是一个常用的优化级别,既提供了优化性能,又不会带来过多的编译时间开销。-O3
:启用更强的优化。这个优化级别更激进,但可能会增加编译时间,且有时会导致代码行为的微小变化。-Wall
:启用所有警告。这是一个常用的选项,它会启用大部分警告信息,有助于开发者及时发现潜在的问题。-Wextra
:启用额外的警告。比 -Wall
更加严格,除了 -Wall
启用的警告外,还会启用更多的警告信息,帮助发现更多潜在的代码问题。-I
:指定头文件搜索路径。例如,-Isrc
表示将 src
目录添加到头文件的搜索路径中,这样编译器在查找头文件时会首先检查 src
目录。-rdynamic
:保留符号信息。这个选项使得在生成的可执行文件中保留符号表,使得动态库能够获取符号信息,通常用于需要动态加载符号的程序。-fPIC
:生成位置无关代码(Position Independent Code)。该选项用于编译动态库时,使得生成的目标文件可以在内存中任意位置加载,适用于动态库的构建。-DNDEBUG
:定义宏 NDEBUG
,通常用于禁用调试代码。在发布版本中,NDEBUG
宏常常被定义,以关闭调试相关的代码或断言(如 assert
)。-ldl
:链接动态加载库(libdl
)。此库提供了动态加载共享库的功能,适用于需要动态加载的程序。$(OPTLIBS)
:一般是用户自定义的库链接选项,用于将项目依赖的其他库链接进来。-c
:仅编译源文件,而不进行链接。通常用于生成目标文件(.o
文件)。-MM
:生成依赖文件,主要用于自动化构建系统。会输出 .d
文件,这些文件描述了源文件与头文件之间的依赖关系,通常用于构建新的依赖规则。-shared
:生成共享库(动态库),而不是静态库或可执行文件。通常与 -fPIC
一起使用,用于构建动态链接库(.so
文件)。-o
:指定输出文件名。例如,-o $(TARGET)
将编译结果输出到目标文件中。-l
:链接指定的库。例如,-lm
表示链接数学库 libm
。-rcs
(在 ar
命令中使用):ar
是用来创建静态库的工具,-rcs
选项分别表示创建库文件、替换已有文件以及生成符号表(加速链接)。