Makefile进阶(上)
本文介绍了如何构建单目录多文件的C项目,重点讲解了Makefile的使用技巧。主要内容包括:1) 使用wildcard函数自动获取.c文件并转换为.o目标文件;2) 实现多文件编译为可执行程序;3) 将临时文件和可执行程序分离到不同目录。关键点包括:Makefile中的变量赋值方式(=与:=的区别)、addprefix函数的使用、目录结构管理等。通过示例展示了如何自动生成100个.c文件和对应的m
一、Makefile基础:从简单编译到自动化构建
Makefile是Unix/Linux系统中经典的构建工具,它通过描述文件之间的依赖关系和构建规则,实现了自动化编译过程。对于C/C++项目而言,一个合理的Makefile可以显著提高开发效率,确保构建的一致性和可重复性。
1.1 Makefile的核心概念
-
目标(Target):要生成的文件或要执行的操作,如可执行文件、目标文件或伪目标(.PHONY)。
-
依赖(Dependencies):生成目标所需要的文件或其他目标。
-
规则(Recipe):生成目标的具体命令,以Tab键开头。
1.2 一个最简单的Makefile示例
hello: hello.c
gcc hello.c -o hello
这个Makefile只有一个目标hello,它依赖于hello.c,生成规则是执行gcc编译命令。当hello.c更新后,再次执行make,hello会被重新编译。
二、多文件项目的构建挑战
当项目包含多个源文件时,手动编译每个文件变得繁琐且易错。假设项目有100个.c文件,如果手动编译:
gcc -c code1.c -o code1.o
gcc -c code2.c -o code2.o
...
gcc *.o -o project
这种方式不仅效率低下,而且容易遗漏文件更新。Makefile通过自动化收集源文件、定义模式规则,完美解决了这个问题。
三、自动化脚本与Makefile的协同
原文中的mkcode.sh脚本展示了如何批量生成源代码,这在教学、测试和原型开发中非常有用:
#!/bin/bash
num=100
suffix=c
template=template.txt
function mkcode() {
cnt=0
while [ $cnt -le $num ]; do
sed "s/fun/fun${cnt}/" ${template} > code${cnt}${suffix}
let cnt++
done
}
这个脚本通过模板替换生成100个函数,每个函数输出不同的消息。在实际项目中,类似的方法可用于:
-
生成测试数据
-
创建模拟接口
-
批量生成配置文件
四、Makefile中的关键函数
4.1 wildcard函数:自动收集源文件
SRCS = $(wildcard *.c)
这是Makefile中最常用的函数之一,它返回所有匹配模式的文件名。其优势包括:
-
自动化:无需手动维护源文件列表
-
灵活性:新增.c文件自动包含在构建中
-
可维护性:文件结构调整时Makefile无需修改
4.2 patsubst函数:路径替换
OBJS = $(patsubst %.c,%.o,$(SRCS))
这个函数将.c后缀替换为.o,实现源文件到目标文件的映射。在复杂项目中,可能还需要添加路径前缀:
OBJS = $(patsubst %.c,$(OBJS_DIR)/%.o,$(SRCS))
4.3 addprefix函数:添加路径前缀
BIN := $(addprefix $(BIN_DIR)/,$(BIN))
这个函数特别适用于组织构建输出,确保生成的文件位于指定目录中。
五、目录分离:优雅的构建输出管理
随着项目规模增大,将编译生成的中间文件和最终可执行文件与源代码分离变得尤为重要。原文中展示了这一过程的演进:
5.1 第一阶段:混乱的混合结构
code1.c
code1.o
code2.c
code2.o
...
project
所有文件混杂在一起,难以区分哪些是源码,哪些是构建产物。
5.2 第二阶段:分离的输出目录
code1.c
code2.c
...
objs/
code1.o
code2.o
...
bin/
project
这种分离带来的好处包括:
-
源码清晰:源码目录只包含原始文件,便于版本控制
-
清理方便:删除
objs和bin目录即可清理所有构建产物 -
多配置支持:可为不同构建配置(Debug/Release)创建不同的输出目录
-
团队协作:避免将构建产物误提交到版本库
5.3 实现目录分离的关键技术
OBJS_DIR = objs
BIN_DIR = bin
DIR_LIST = $(OBJS_DIR) $(BIN_DIR)
$(DIR_LIST):
@$(MKDIR) $@
这个规则确保在编译前创建必要的输出目录。$@表示当前目标名,即objs或bin。
六、Makefile变量赋值机制
Makefile中的变量赋值有两种主要方式,理解它们的区别至关重要:
6.1 递归展开赋值(=)
a = hello
b = $(a) world
a = goodbye
# b的值为"goodbye world"
这种赋值方式会延迟展开,直到变量被使用时才确定最终值。这种灵活性在某些场景下有用,但也可能导致意想不到的结果。
6.2 直接展开赋值(:=)
a = hello
b := $(a) world
a = goodbye
# b的值为"hello world"
这种方式立即展开变量,赋值时即确定值。在大多数情况下,特别是涉及路径拼接时,推荐使用:=以避免递归引用问题。
6.3 条件赋值(?=)
DEBUG ?= 0
仅在变量未定义时才赋值,常用于提供默认值。
6.4 追加赋值(+=)
CFLAGS += -Wall
在已有值后追加新内容,常用于累积编译选项。
七、模式规则:简化构建过程
模式规则(Pattern Rules)是Makefile的强大特性,允许为一批文件定义通用构建规则:
%.o: %.c
@echo "compiling $<"
@$(CC) $(CFLAGS) -c $< -o $@
-
%:匹配任意非空字符串的通配符 -
$<:第一个依赖文件(source) -
$@:目标文件(target)
当需要将输出定向到特定目录时:
$(OBJS_DIR)/%.o: %.c
@mkdir -p $(dir $@)
@echo "compiling $<"
@$(CC) $(CFLAGS) -c $< -o $@
这里使用了$(dir $@)自动创建目标文件所在的目录。
八、伪目标的使用
伪目标(.PHONY)用于声明那些不对应实际文件的目标:
.PHONY: all clean
常见的伪目标包括:
-
all:默认目标,通常构建所有内容
-
clean:清理构建产物
-
install:安装程序
-
test:运行测试
声明伪目标有两个重要作用:
-
防止与同名文件冲突
-
确保目标总是被执行(不考虑文件时间戳)
九、构建过程的可视化与调试
一个好的构建系统应该提供透明的构建过程。原文中通过echo命令输出构建信息:
%.o: %.c
@echo "compiling $<"
@$(CC) $(CFLAGS) -c $< -o $@
进一步可以增强调试支持:
ifeq ($(VERBOSE),1)
Q =
E = @echo
else
Q = @
E = @echo
endif
%.o: %.c
$(E) "CC $<"
$(Q)$(CC) $(CFLAGS) -c $< -o $@
这样可以通过make VERBOSE=1显示详细编译命令,便于调试。
十、增量编译:Makefile的核心优势
Makefile通过比较目标文件和依赖文件的时间戳,实现增量编译(仅重新编译发生变化的文件)。这是其相对于简单脚本的核心优势:
-
源文件变化:只重新编译对应的.o文件
-
头文件变化:需要重新编译所有包含该头文件的源文件
-
链接阶段:只有.o文件变化时才重新链接
要实现精确的依赖检测,特别是对头文件的依赖,可以使用GCC的-MMD选项:
DEPFLAGS = -MMD -MP
%.o: %.c
@$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
这会自动生成.d依赖文件,确保头文件变化时触发重新编译。
十一、从示例到实际项目
本文的示例虽然简单,但涵盖了实际项目中的核心需求:
-
多文件管理:通过wildcard自动收集
-
输出组织:分离源码和构建产物
-
变量控制:通过变量控制构建选项
-
清理机制:一键清理所有生成文件
在实际项目中,可能还需要:
-
支持多平台:检测操作系统并调整命令
-
外部依赖:添加库路径和链接选项
-
安装部署:添加install目标
-
配置管理:支持Debug/Release等不同配置
十二、Makefile的最佳实践
基于本文示例,总结以下最佳实践:
-
始终分离输出目录:保持源码目录干净
-
使用直接展开赋值:避免递归展开带来的问题
-
声明所有伪目标:防止与文件冲突
-
提供清晰的输出信息:便于理解构建过程
-
支持完整的清理:清理所有生成文件
-
利用模式规则:减少重复代码
-
包含自动依赖生成:确保头文件变化时正确重新编译
-
支持外部参数:如VERBOSE、DEBUG等
开始准备
template.c
#include <stdio.h>
void fun()
{
printf("hello fun!\n");
}
构建单⽬录多⽂件
mkcode.sh
#!/ bin / bash
num = 100 suffix =.c template = template.txt
function
mkcode()
{
cnt = 0 while[$cnt - le $num] do sed "s/fun/fun${cnt}/" ${template} > code${cnt} ${suffix};
let cnt++;
done
}
function mkmain()
{
cnt = 0 > main.c printf "#include <stdio.h>\n\nint main()\n{\n" >> main.c while[$cnt - le $num] do printf " fun${cnt}();\n" let cnt++ done >> main.c echo "}" >> main.c
}
mkcode mkmain
构建源⽂件
$ cat code99.c
#include <stdio.h>
void fun99()
{
printf("hello world!\n");
}
$ cat main.c
#include <stdio.h>
int main()
{
fun0();
fun1();
fun2();
fun3();
fun4();
...
}
场景1: 构建单模块,多⽂件项⽬
Step 1:显⽰当前⽬录下的源⽂件,并把他们的.c全部替换成为.o
Makefile
$ cat Makefile
#获取所有.c
SRCS = $(wildcard *.c)
#转换.c变成.o
OBJS = $(SRC :.c =.o)
#查看⼀下
test : @echo "Src:"
@echo $(SRCS)
@echo "OBj:"
@echo $(OBJS)
要点:
• SRCS 使⽤了 wildcard 函数来⾃动获取当前⽬录下所有的 .c ⽂件
• 当你在 Makefile 中使⽤ wildcard 函数时,它会返回所有匹配给定模式的⽂件名列表,这些⽂件名是当前⽬录(或指定⽬录,如果函数参数中包含了路径)下的实际存在的⽂件。这对于动态地确定需要编译的源⽂件列表特别有⽤,尤其是当源⽂件可能会随着项⽬的发展⽽增加或减少时。
• $(wildcard PATTERN...)
查看
$ make
Src:
code63.c code88.c code75.c code72.c code30.c code34.c code90.c code13.c
code4.c code53.c code76.c code6.c code99.c code80.c code11.c code100.c
code84.c code3.c code23.c code57.c code31.c code28.c code62.c code51.c
code26.c code95.c code49.c code16.c code55.c code18.c code65.c code67.c
code8.c code22.c code19.c code21.c code47.c code32.c code73.c code74.c
code69.c code15.c code87.c code36.c code92.c code44.c code56.c code71.c
code14.c code89.c code43.c code50.c code25.c code58.c code45.c code9.c
code33.c code1.c code64.c code93.c code98.c code20.c code66.c code83.c
code10.c code39.c code41.c code70.c code24.c code17.c code60.c code59.c
code61.c code27.c code86.c code37.c code85.c code54.c code5.c code46.c
code48.c code29.c code94.c code12.c code96.c code77.c code35.c code7.c
code40.c code42.c code97.c code68.c code78.c code52.c code91.c code79.c
code81.c code2.c main.c code82.c code38.c code0.c
OBjS:
code63.o code88.o code75.o code72.o code30.o code34.o code90.o code13.o
code4.o code53.o code76.o code6.o code99.o code80.o code11.o code100.o
code84.o code3.o code23.o code57.o code31.o code28.o code62.o code51.o
code26.o code95.o code49.o code16.o code55.o code18.o code65.o code67.o
code8.o code22.o code19.o code21.o code47.o code32.o code73.o code74.o
code69.o code15.o code87.o code36.o code92.o code44.o code56.o code71.o
code14.o code89.o code43.o code50.o code25.o code58.o code45.o code9.o
code33.o code1.o code64.o code93.o code98.o code20.o code66.o code83.o
code10.o code39.o code41.o code70.o code24.o code17.o code60.o code59.o
code61.o code27.o code86.o code37.o code85.o code54.o code5.o code46.o
code48.o code29.o code94.o code12.o code96.o code77.o code35.o code7.o
code40.o code42.o code97.o code68.o code78.o code52.o code91.o code79.o
code81.o code2.o main.o code82.o code38.o code0.o
Step 2:所有源⽂件,编译成为⽬标⽂件
更改 Makefile
#引⼊编译器
CC = gcc
#定义编译选项
FLAGS = -c
#获取所有.c
SRCS = $(wildcard *.c)
#转换.c变成.o
OBJS = $(SRCS :.c =.o)
#定义⽬标,让Makefile进⾏推导
.PHONY : all
all : $(OBJS) %
.o : %.c @echo "compling ... $<" @$(CC) $(FLAGS) $ <
-o $ @
#清理项⽬
.PHONY : clean
clean : @echo "clean ... Project"
@echo "$(OBJS)"
@rm -
f $(OBJS)
查看结果
$ make
compling ... code51.c
compling ... code33.c
compling ... code35.c
compling ... code18.c
compling ... code19.c
compling ... code38.c
compling ... code40.c
compling ... code77.c
compling ... code9.c
compling ... code65.c
compling ... code42.c
...
$ make clean
clean ... Project
code98.o code86.o code97.o code55.o code39.o code81.o code53.o code38.o
code76.o code5.o code6.o code90.o code80.o code8.o code3.o code48.o code47.o
code54.o code57.o code31.o code28.o code62.o code51.o code22.o code26.o
code67.o code95.o code16.o code100.o code18.o code65.o code99.o code42.o
code72.o code19.o code32.o code73.o code82.o code78.o code63.o code69.o
code71.o code15.o code87.o code36.o code92.o code46.o code56.o code21.o
code14.o code89.o code25.o code58.o code45.o code9.o code33.o code23.o
code1.o code64.o code93.o code20.o code66.o code27.o code83.o code10.o
code11.o code41.o code70.o code24.o code60.o code94.o code61.o code37.o
code59.o code29.o code44.o code7.o code12.o code96.o code4.o code35.o
code40.o code77.o code49.o code84.o code17.o code13.o code30.o code68.o
code52.o code91.o code79.o code74.o code50.o code2.o main.o code85.o code0.o
code43.o code88.o code75.o code34.o
Step 3:编译成为可执⾏程序
更改 Makefile
# 引⼊编译器
CC=gcc
# 定义编译选项
FLAGS=-c
# 获取所有.c
SRCS=$(wildcard *.c)
# 转换.c变成.o
OBJS=$(SRCS:.c=.o)
# 定义⽬标可执⾏
BIN=project
$(BIN):$(OBJS)
@echo "Link *.o to $(BIN)"
@$(CC) $^ -o $@
%.o:%.c
@echo "compling ... $<"
@$(CC) $(FLAGS) $< -o $@
# 清理项⽬
.PHONY:clean
clean:
@echo "clean ... Project"
@echo "$(OBJS)"
@rm -f $(OBJS)
查看结果
$ make
...
compling ... code82.c
compling ... code55.c
compling ... code29.c
compling ... code44.c
compling ... code33.c
compling ... code12.c
compling ... code96.c
compling ... code7.c
compling ... code49.c
Link *.o to project
$ ./project
hello fun0
hello fun1
hello fun2
...
场景2:临时⽂件、⽬标可执⾏与源代码分离
我们编译的时候,会形成很多的.o临时⽂件,还有可执⾏程序(⽬前是⼀个),这么多的临时⽂件,和源⽂件放在⼀起,就显得⾮常不优雅,我们想在make的时候,直接将⽬标.o和可执⾏程序分别在不同的⽬录下,这样后期在清理的之后,只要删除⽬录,就可以去掉所有的临时⽂件了
更改 Makefile
# 引⼊编译器
CC=gcc
# 添加其他命令
RM=rm
RM_FLAGS=-rf
MKDIR=mkdir
# 定义编译选项
FLAGS=-c
# 获取所有.c
SRCS=$(wildcard *.c)
# 转换.c变成.o
OBJS=$(SRCS:.c=.o)
# 定义⽬标可执⾏
BIN=project
# 添加新建的⽬录名称
OBJS_DIR=objs
BIN_DIR=bin
# 给最终形成的可执⾏程序添加路径
# 这⾥必须使⽤:=,要不然makefile会有递归引⽤的问题.
BIN:=$(addprefix $(BIN_DIR)/, $(BIN)) # bin/project
# 给形成的临时.o都添加⽬标路径
OBJS:=$(addprefix $(OBJS_DIR)/, $(OBJS)) #objs/XX.o
# 需要新建的⽂件夹
DIR_LIST=$(OBJS_DIR) $(BIN_DIR)
all:$(DIR_LIST) $(BIN)
$(DIR_LIST):
@$(MKDIR) $@
$(BIN):$(OBJS)
@echo "Link *.o to $(BIN)"
@$(CC) $^ -o $@
# 这⾥要添加路径,保证形成的.o放⼊指定路径下
$(OBJS_DIR)/%.o:%.c
@echo "compling ... $<"
@$(CC) $(FLAGS) $< -o $@
.PHONY:clean
clean:
@echo "clean ... Project"
@$(RM) $(RM_FLAGS) $(DIR_LIST)
运⾏结果
$ make
compling ... code51.c
compling ... code35.c
compling ... code18.c
compling ... code19.c
compling ... code38.c
...
$ tree bin
bin
└── project
$ tree objs
objs
├── code0.o
├── code100.o
├── code10.o
├── code11.o
├── code12.o
├── code13.o
├── code14.o
├── code15.o
├── code16.o
├── code17.o
├── code18.o
├── code19.o
├── code1.o
├── code20.o
├── code21.o
├── code22.o
├── code23.o
...
= 和 :=
=进⾏赋值,变量的值是整个makefile中最后被指定的值, 在make时,会把整个makefile展开,拉通决
定变量的值
a = hello
b = $(a) bit
a = world
#a = hello
#b := $(a) bit
#a = world
all:
@echo $(a)
@echo $(b)
$ make
world
world bit
”:=”就表⽰直接赋值,赋予当前位置的值, ”:=”才是真正意义上的直接赋值
a = hello
b := $(a) bit
a = world
all:
@echo $(a)
@echo $(b)
$ make
world
hello bit
📌
$(addprefix prefix,names…)是⼀个函数,⽤于将指定的前缀添加到⼀组空格分隔的⽂件名中。这个函数通常⽤于将相同的前缀添加到⼀组⽂件名或路径中,⾮常适合在
Makefile 中进⾏路径拼接操作。
• prefix:要添加的前缀。
• namesR:⼀组空格分隔的⽂件名或路径。
更多推荐


所有评论(0)