【嵌入式Linux基础】Makefile详解 - 教程

时隔多日,我真是AI偷懒,虽然学了很多了,但是记录也得加把劲了。

一、什么是 Makefile?

1.1 核心作用​

Makefile 是一个自动化构建脚本,用于定义项目的编译规则、依赖关系和执行流程。它的核心价值在于:​

  • 自动化编译:一键完成「源码 → 目标文件 → 可执行文件」的全流程​
  • 避免复杂命令行并减少编译时间
  • 增量构建:只重新编译修改过的文件,大幅提升开发效率​
  • 统一规范:为项目提供标准化的构建方式,便于团队协作​

1.2 适用场景​

C/C++ 等编译型语言项目(最经典场景)​

  • 多文件、多模块的复杂项目​
  • 需要自定义构建流程的场景(如文档生成、部署脚本)​
  • 跨平台项目的构建适配​

1.3 依赖工具​

Makefile 依赖 make 命令执行,主流操作系统默认预装:​
Linux/macOS:自带 GNU Make​
Windows:需安装 MinGWWSL

1.4 make 命令执行流程

  • make命令会在当前目录下按顺序寻找 GNUmakefile、makefile、 Makefile文件
  • Makefile中第一个目标文件作为最终目标
  • 按“堆栈”顺序,依序找到每个目标文件,判断新旧关系,必要时生成新的目标文件,直到最终生成最终目标。

1.5 make选项

选项作用说明示例
-h / --help显示 make 的帮助信息(包含所有选项说明)make -h 或 make --help
-v / --version显示 make 的版本信息make -v 或 make --version
-f / --file=指定要使用的 Makefile 文件(默认查找 Makefile 或 makefile)make -f MyMakefile (使用 MyMakefile)
-n / --just-print模拟执行(“干跑”),只显示要执行的命令,不实际运行make -n (查看构建步骤,不真正编译)
-B / --always-make强制重新构建所有目标(忽略文件时间戳,即使目标已 “最新”)make -B (完全重新编译所有内容)
-j / --jobs=指定并行执行的任务数(加速编译,n 为并行数量)make -j4 (4 个任务并行编译)
-k / --keep-going即使某个目标构建失败,继续执行其他可独立构建的目标make -k (一个模块出错,不中断其他)
-s / --silent静默模式,不显示执行的命令(只输出命令的结果)make -s (仅看构建结果,不看命令)
-t / --touch不执行命令,仅更新目标文件的时间戳(让 make 认为目标已 “最新”)make -t (标记目标为已更新)
-d显示调试信息(详细输出 make 的解析过程、依赖检查、执行逻辑等)make -d (调试复杂 Makefile 时用)
-C 切换到指定目录 后再执行 make(常用于嵌套 Makefile 调用) make -C src (进入 src 目录执行 make)

二、Makefile 基础语法

2.1 基本结构

  • 一个 Makefile 的核心由「规则(Rule)」组成,规则的基本格式:
目标(Target): 依赖(Prerequisites)​
    命令(Commands)

目标:要生成的文件(如可执行文件、目标文件)或执行的动作(如 clean)
依赖:生成目标所需的文件或其他目标
命令:实现目标的具体操作
例如:

main: main.c
	gcc main.c -o main

2.2 核心概念

1)默认目标

Make 会默认执行第一个规则的目标(无需指定目标名称)。如需指定默认目标,可在开头添加:

.DEFAULT_GOAL := main

如此一来,最终目标便会被强制指定为对应的目标,而不是第一个目标:

.DEFAULT_GOAL :=main
clean:
	rm -f main
mian: main.c
	gcc main.c -o main

2)伪目标(Phony Target)
用于执行动作(而非生成文件)的目标,需用 .PHONY 声明,避免与同名文件冲突:

.PHONY: clean
clean:
	rm -f main

如此一来,程序并不会直接执行clean ,而是当使用make clean的时候才会执行对应的动作。
clean般的伪目标,一般习惯放在末尾,不能放在开头

2.3 变量

用于简化规则、统一配置,分为自定义变量自动变量
1)自定义变量

CC := gcc  # 编译器​
CFLAGS := -Wall -g  # 编译选项(-Wall 显示警告,-g 生成调试信息)​
TARGET := app  # 目标文件名​
SRC := main.c utils.c  # 源码文件​
# 使用变量($(变量名))​
$(TARGET): $(SRC)​
    $(CC) $(CFLAGS) $(SRC) -o $(TARGET)
  • 变量赋值:常见的有如下三种
    • =延迟赋值,当使用= 赋值时,变量为使用时赋值
    • :=立即赋值,当使用:=赋值时,变量为直接赋值
    • ?=条件赋值,当使用?=赋值时,变量为空赋值
    • 举例说明
# 1. = 延迟赋值(使用时才计算,受后续修改影响)
x = $(y)
y = 100
x2 = $(y)
y = 200  # 后续修改y
# 2. := 立即赋值(定义时计算,不受后续修改影响)
a := $(b)
b = 300
a2 := $(b)
b = 400  # 后续修改b
# 3. ?= 条件赋值(仅变量未定义时生效)
c ?= 500  # c未定义,赋值生效
c ?= 600  # c已定义,赋值无效
d = 700   # d已提前定义
d ?= 800  # d已定义,赋值无效
# 打印变量值
all:
	@echo "= 延迟赋值:"
	@echo "x = $(x) "
	@echo "x2 = $(x2)"
	@echo ""
	@echo ":= 立即赋值:"
	@echo "a = $(a) "
	@echo "a2 = $(a2)"
	@echo ""
	@echo "?= 条件赋值:"
	@echo "c = $(c) "
	@echo "d = $(d)"

结果:
在这里插入图片描述

  • 需要注意的是,当我们在使用 make 变量名=值时,从命令行传入的值会直接覆盖定义好的值,但是可以通过添加 override 指示符,就可以使用在Makefile中设置的参数了
  • 绝对不能出现,循环嵌套!!!

2)自动化变量

  • 自动变量用于简化命令,无需重复写目标 / 依赖名称
  • 常见的自动化变量有$@ 、$^ 、 $< 、$?等。
    • $@ :当前规则的目标
    • $< :当前规则的第一个依赖文件
    • $^ :当前规则的所有依赖
    • $? :当前规则中比目标文件更新的所有依赖文件
  • 举例:
CC := gcc​ #编译器
CFLAGS := -Wall -g​ #编译选项
TARGET := main #目标文件
SRC := main.c utils.c​
​# 通过变量和自动化变量,Makefile文档可重复利用
$(TARGET): $(SRC)​
    $(CC) $(CFLAGS) $^ -o $@-o app​
.PHONY: clean​
clean:​
    rm -rf $(TARGET) *.o

3)预定义变量

  • AR:归档维护程序,默认为ar
  • AS:汇编程序,默认为as
  • CC:C编译程序,默认为cc
  • CPP:C预处理程序, 默认为cpp
  • RM:文件删除程序,默认为:rm –f
  • ARFLAGS:传给归档维护程序的参数,默认rv
  • ASFLAGS:传给汇编程序的参数,无默认值
  • CFLAGS:传给C编译器的参数,无默认值
  • CPPFLAGS:传给C预处理器的参数,无默认值
  • LDFLAGS:传给链接器的参数,无默认值

三、Makefile 进阶用法

3.1 条件判断

  • 语法
# 基本结构(else 可选)
conditional-directive
  text-if-true  # 条件为真时执行的内容
else
  text-if-false  # 条件为假时执行的内容(可选)
endif
  • 主要有四种条件判断 ifeq、ifneq、ifdef、ifndef
# 定义测试变量
VAR1 = hello
VAR2 = world
VAR3 = hello  # 与 VAR1 相等
# VAR4 未定义(用于 ifdef/ifndef 测试)
# 1. ifeq:判断两个参数是否相等(支持字符串/变量)
ifeq ($(VAR1), hello)
  MSG1 = "VAR1 等于 'hello'"
else
  MSG1 = "VAR1 不等于 'hello'"
endif
# 2. ifneq:判断两个参数是否不相等
ifneq ($(VAR1), $(VAR2))
  MSG2 = "VAR1 不等于 VAR2"
else
  MSG2 = "VAR1 等于 VAR2"
endif
# 3. ifdef:判断变量是否已定义(只要被赋值过,包括空值)
ifdef VAR3
  MSG3 = "VAR3 已定义"
else
  MSG3 = "VAR3 未定义"
endif
# 4. ifndef:判断变量是否未定义
ifndef VAR4
  MSG4 = "VAR4 未定义"
else
  MSG4 = "VAR4 已定义"
endif
# 打印结果
all:
	@echo "ifeq 测试: $(MSG1)"
	@echo "ifneq 测试: $(MSG2)"
	@echo "ifdef 测试: $(MSG3)"
	@echo "ifndef 测试: $(MSG4)"

结果:
在这里插入图片描述

3.2 函数

Makefile 内置常用函数,用于文件查找、字符串处理等,支持的函数语法如下:

$(function  arg1,arg2,…)

常用函数:

  • foreach函数:
$(foreach  ,,)

功能::把list中的单词逐一取出,送入var指定的变量中,然后再执行test表达式。每执行一次test返回一个字串,所有字串用空格连接起来就是函数的返回值。

names:=a b c
 $(foreach  n,  $(names),  $(n).c)
返回:a.c b.c c.c
  • if函数:
$(if ,,)

功能:如条件为真(condition非空),则返回then部分,否则返回else部分(或空)

tmp=
res=$(if tmp,tmp not exist,tmp exist)
all:
	@echo $(m)
# 结果为 tmp not exist
  • wildcard函数:
$(wildcard )

功能:匹配当前目录下所有符合模式的文件(支持*等通配符),返回文件名列表(空格分隔)。

# 匹配当前目录下所有.c文件
src_files := $(wildcard *.c)
# 若当前目录有a.c、b.c、main.c,则返回:a.c b.c main.c
  • patsubst函数:
$(patsubst ,,)

功能:对 中的字符串进行模式替换,中的%匹配任意字符,替换为中%对应的部分(保留原结构)。

# 将.c文件替换为.o文件
obj_files := $(patsubst %.c,%.o,a.c b.c main.c)
# 返回:a.o b.o main.o
  • notdir函数:
$(notdir )

功能:从文件路径列表中移除目录部分,仅保留文件名。

# 提取路径中的文件名
files := $(notdir src/a.c include/b.h lib/c.so)
# 返回:a.c b.h c.so
  • shell函数:
$(shell )

功能:执行指定的 shell 命令,返回命令的输出结果(自动去除末尾换行符)。

# 获取当前目录路径
cur_dir := $(shell pwd)
# 若当前目录为/home/user/project,则返回:/home/user/project
# 获取当前时间(格式:年-月-日)
today := $(shell date +%Y-%m-%d)
# 若当天是2025-11-10,则返回:2025-11-10

常用的函数如上,当然还有很多函数自行解锁吧。

3.3 Makefile嵌套执行

  • Makefile 嵌套执行指的是在一个主 Makefile 中调用其他子目录中的 Makefile(通常用于大型项目的模块化管理,将不同模块的编译规则拆分到各自目录的 Makefile中)。

在这里插入图片描述

  • 例如:
    1)目录结构:
project/
├── Makefile       # 主 Makefile(嵌套执行入口)
├── src/           # 源码目录
│   ├── main.c
│   └── Makefile   # src 目录的子 Makefile
└── test/          # 测试目录
    ├── test.c
    └── Makefile   # test 目录的子 Makefile

2)子目录 Makefile 实现:
src/Makefile

# 目标:生成可执行文件 app
app: main.c
	gcc main.c -o app
	@echo "src 目录编译完成:生成 app"
# 清理 src 目录生成的文件
clean:
	rm -f app
	@echo "src 目录清理完成"

test/Makefile

# 目标:生成测试可执行文件 test_app
test_app: test.c
	gcc test.c -o test_app
	@echo "test 目录编译完成:生成 test_app"
# 清理 test 目录生成的文件
clean:
	rm -f test_app
	@echo "test 目录清理完成"

3)主Makefile实现

# 定义子目录列表
SUBDIRS := src test
# 默认目标:编译所有子目录
all: $(SUBDIRS)
# 嵌套执行子目录的 make(% 匹配子目录名)
$(SUBDIRS):
	@echo "\n===== 开始编译 $@ 目录 ====="
	$(MAKE) -C $@  # -C $@ 切换到子目录 $@,执行该目录的 Makefile
	@echo "===== $@ 目录编译结束 =====\n"
# 清理所有子目录的生成文件
clean:
	@echo "\n===== 开始清理所有目录 ====="
	$(foreach dir, $(SUBDIRS), $(MAKE) -C $(dir) clean;)  # 循环调用子目录的 clean 目标
	@echo "===== 所有目录清理结束 =====\n"
.PHONY: all clean $(SUBDIRS)  # 声明伪目标,避免与同名目录冲突

执行 make 或者 make all

总结

makefile博大精深,还有很多内容如隐含规则等并没有在本文中介绍,日后精进了,再写其进阶。

posted on 2025-12-08 09:38  ljbguanli  阅读(73)  评论(0)    收藏  举报