Makefile 入门使用与深入解析(超详细!!!)

Makefile 入门使用与深入解析(超详细!!!)


1. 前言

在软件开发(尤其是 C/C++、嵌入式、Linux 应用)中,Makefile 是最经典的构建工具之一。它能自动化管理依赖、提高编译效率、支持跨平台和交叉编译,是任何工程化项目的必备工具。本文将结合实例,对 Makefile 进行详细讲解。


2. 为什么要用 Make / Makefile

  • 当项目只有一个 hello.c,你可以 gcc hello.c -o hello
  • 当项目有几十个 .c.h 文件时,手动管理编译命令不现实。
  • 修改一个文件后,只编译受影响的部分,节省时间。
  • 支持跨平台、交叉编译、安装、清理等完整工作流。

📌 简而言之:Makefile 能让你的项目 高效、可维护、自动化

在项目的实际开发过程中就算你只是修改一个文件,也需要重新编译所有的文件,会浪费了很多开发时间。 要解决这个问题,最好的方式就是把工程的编译规则写下来,让编译器自动加载该规则进行编译。 解决方法就是使用make和Makefile,这两个工具是搭配使用的,下面给大家介绍一下:

  • make工具:它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,然后对这些文件按照规则进行单独的编译,这样一来,就能避免重新编译项目的所有的文件。

  • Makefile文件:上面提到的规则、依赖关系主要是定义在这个Makefile文件中的,我们在其中合理地定义好文件的依赖关系之后,make工具就能精准地进行编译工作。

    在这里插入图片描述

我们管理一个项目工程,实质上就是管理项目文件间的依赖关系。 所以我们在学习和使用Makefile的时候,一定要牢牢抓住它这种面向依赖的思想, 心里一定要谨记,Makefile中所有的复杂、晦涩的语法都是更好地为解决依赖问题而存在的。 理解了它的本质目的之后,我们以后在学习它的过程中就不用死记硬背各种语法了, 只要想想这个本质目的,你会觉得一切都是那么地顺理成章

在学习Makefile知识之前,要现在脑海中初步建立Makefile知识点的整体框架,以此来指导进一步的学习。 接下来我们先整体看一下要学习Makefile的知识点,如下图所示:

在这里插入图片描述

1、 基础语法– 描述目标和依赖的特定格式,Makefile的核心。

2、 变量– 记录特定的信息,避免重复输入原始信息。尤其是手动输入原始信息很长时,特别好用。

3、 分支判断– 灵活控制多个不同的编译过程,方便兼容不同属性。

4、 头文件依赖– 监控头文件的变化,头文件也是程序的关键内容。

5、 隐含规则– 利用Makefile的一些默认规则,可以减少编写Makefile的工作量。

6、 自动化变量– 利用Makefile的默认的自动化变量,可以减少编写Makefile的工作量。

7、 模式规则– 灵活使用正则表达式,可以减少编写Makefile的工作量。

8、 函数– 使用Makefile的各种函数,可以更方便地实现Makefile的功能。

了解完Makefile的知识点,从上面的分析可以知道,Makefile的核心在于基础语法,用来描述目标和依赖的关系。 其他语法的目的,是为了减少我们编写Makefile工作量,让我们能够以更加优雅、更加简洁、更好维护的方式来实现Makefile的功能。 这跟我们程序开发是很相似的,不止要实现功能,还要兼顾程序的可读性、拓展性、可维护性等等。

3. Makefile 的基本结构

target: prerequisites
<TAB>command

接下来,我们做一个小的实验让大家对Makefile有一个大致的认知:

我们新建一个Makefile文件,在文件里面写入以下内容:

#Makefile格式
#目标:依赖的文件或其它目标
#Tab 命令1
#Tab 命令2
#第一个目标,是最终目标及make的默认目标
#目标a,依赖于目标targetc和targetb
#目标要执行的shell命令 ls -lh,列出目录下的内容
targeta: targetc targetb
     ls -lh
#目标b,无依赖
#目标要执行的shell命令,使用touch创建test.txt文件
targetb:
     touch test.txt
#目标c,无依赖
#目标要执行的shell命令,pwd显示当前路径
targetc:
     pwd
#目标d,无依赖
#由于abc目标都不依赖于目标d,所以直接make时目标d不会被执行
#可以使用make targetd命令执行
targetd:
     rm -f test.txt

在这里需要注意的几个地方:出现的第一个目标,是最终目标及make的默认目标,在上面的例子中,targeta就是默认目标。

目标文件说明
targeta这是Makefile中的第一个目标代号,在符号“:”后 面的内容表示它依赖于targetc和targetb目标,它自身的命令为“ls -lh”,列出当前目录下的内容。
targetb这个目标没有依赖其它内容,它要执行的命令为“touch test.txt”,即创建一个test.txt文件。
targetc这个目标同样也没有依赖其它内容,它要执行的命令为“pwd”,就是简单地显示当前的路径。
targetd这个目标无依赖其它内容,它要执行的命令为“rm -f test.txt”,删除目录下的test.txt文件。与targetb、c不同的是,targeta不依赖targetd,所以不会执行。

在这里插入图片描述

如果我们想要单独执行Makefile中的某个目标,可以使用”make 目标名“的语法,例如上图中分别执行了”make targetd“ 和”make targetc“指令,在执行”make targetd”目标时,可看到它的命令rm -f test.txt被执行,test.txt文件被删除。

从这个过程,可了解到make程序会根据Makefile中描述的目标与依赖关系,执行达成目标需要的shell命令。简单来说,Makefile就是用来指导make程序如何干某些事情的清单。

3.1. 目标与依赖

  • 目标 (target):最终要生成的文件(如可执行程序、库、.o 文件)。指make要做的事情,可以是一个简单的代号,也可以是目标文件,需要顶格书写,前面不能有空格或Tab。一个Makefile可以有多个目标,写在最前面的第一 个目标,会被Make程序确立为 “默认目标”,例如前面的targeta。
  • 依赖 (prerequisites):生成目标所依赖的文件。

3.2.命令(Recipe)

  • 每条命令前必须用 Tab 开头!!! 一定不能使用空格进行代替,make不会识别。
  • 命令执行时会显示在终端,加 @ 可以隐藏命令本身。

3.3 伪目标

.PHONY 声明伪目标,避免与真实文件冲突。例如我们的项目已经存在一个clean文件,可是在Makefile文件里又需要定义clean目标文件,此时make才知道我们需要执行的make clean是什么。只要我们不期待生成目标文件,就应该把它定义成伪目标,前面的演示代码修改如下:

示例:

#使用.PHONY表示targeta是个伪目标
.PHONY:targeta
#目标a,依赖于目标targetc和targetb
#目标要执行的shell命令 ls -lh,列出目录下的内容
targeta: targetc targetb
	ls -lh

#使用.PHONY表示targetb是个伪目标
.PHONY:targetb
#目标b,无依赖
#目标要执行的shell命令,使用touch创建test.txt文件
targetb:
	touch test.txt

#使用.PHONY表示targetc是个伪目标
.PHONY:targetc
#目标c,无依赖
#目标要执行的shell命令,pwd显示当前路径
targetc:
	pwd

#使用.PHONY表示targetd是个伪目标  
.PHONY:targetd   #当然使用clean更加明了 也可以是.PHONY:clean  后面也需要进行相应的修改  
#目标d,无依赖
#由于abc目标都不依赖于目标d,所以直接make时目标d不会被执行
#可以使用make targetd命令执行
targetd:
	rm -f test.txt

GNU组织发布的软件工程代码的Makefile,常常会有类似以上代码中定义的clean伪目标,用于清 除编译的输出文件。常见 的还有**“all”、“install”、“print”、“tar”等分别用于编译所有内容、安装已编译好的程序、列出被修改的文件及打包成tar文件**。虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的Makefile。


4. Makefile 的编译过程

make 执行的过程大致分为以下几步:

  1. 读取 Makefile:加载规则、变量、函数。
  2. 构建依赖关系图:建立目标与依赖的有向图。
  3. 判断目标是否需要更新:比较时间戳,若目标比依赖旧或不存在,则需要重建。
  4. 执行命令:从依赖树底层开始,逐级执行命令。
  5. 生成最终目标:完成用户指定的目标。

📌 示例:

#Makefile格式
#目标文件:依赖的文件
#Tab 命令1
#Tab 命令2
hello_main: hello_main.o hello_func.o
	gcc -o hello_main hello_main.o hello_func.o
#以下是make的默认规则,下面两行可以不写
#hello_main.o: hello_main.c
# gcc -c hello_main.c

#以下是make的默认规则,下面两行可以不写
#hello_func.o: hello_func.c
# gcc -c hello_func.c

在Makefile的实际应用中,通常会把编译和最终的链接过程分开。也就是说,我们的hello_main目标文件本质上并不是依赖hello_main.c和hello_func.c文件,而是依赖于hello_main.o和hello_func.o,把这两个文件链接起来就能得到我们最终想要的hello_main目标文件。另外,由于make有一条默认规则,当找不到xxx. o文件时,会查找目录下的同名xxx.c文件进行编译,也就是上面代码中被注释掉的地方。

我们在这里同样举例子说明:

hello_main.c:

#include "hello_func.h"

int main()
{
    hello_func();
    return 0;
}

hello_func.h

void hello_func(void);

hello_func.c

#include <stdio.h>
#include "hello_func.h"

void hello_func(void)
{
    printf("hello, world! This is a C program.\n");
    for (int i=0; i<10; i++ ) {
        printf("hello world!!!\n",i);
    }
}

同样我们执行Makefile文件:

在这里插入图片描述


5. 变量与赋值方式

使用C自动编译成.o的默认规则有个缺陷,由于没有显式地表示.o依赖于.h头文件,假如我们修改了头文件的内容,那么.o并不会更新,这是不可接受的。并且默认规则使用固定的“cc”进行编译,假如我们想使用ARM-GCC或者其他的进行交叉编译,那么系统默认的“cc”会导致编译错误。

要解决这些问题并且让Makefile变得更加通用,需要引入变量和分支进行处理。

基础语法:

变量定义说明
=延时赋值,该变量只有在调用的时候,才会被赋值
:=直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。
?=若变量的值为空,则进行赋值,通常用于设置默认值。
+=追加赋值,可以往变量后面增加新的内容。

当我们想使用变量时,其语法如下:

$(变量名)

示例:

A = foo
B = $(A)
C := $(A)
A += bar
D ?= foo bar bar
all:
	@echo A=$(A)
	@echo B=$(B)
	@echo C=$(C)
	@echo D=$(D)

输出:

在这里插入图片描述

A = foo - 使用递归展开式定义变量 A
B = $(A) - B 延时赋值,会在使用时才赋值
C := $(A) - C 直接赋值,在定义时就立即赋值
A += bar - 追加操作,此时 A 变为 "foo bar"
D ?= foo bar bar - 条件赋值,如果 D 未定义才定义

6. 自动化变量

Makefile中还有其它自动化变量,此处仅列出方便以后使用到的时候进行查阅,见下表。

变量含义
$@当前目标
$<依赖中的第一个目标文件
$^所有依赖,如果依赖中有重复的,只保留一份
$% @ 类似,但 @类似,但 @类似,但%仅匹配“库”类型的目标文件
$+所有的依赖目标,即使依赖中有重复的也原样保留
$?所有比目标新的依赖

在后面我会给出相应的例子供大家学习。

7. 使用分支

就像前面所说,当我们想要切换编译器的时候,就可以使用条件分支语法。在Makefile中的条件分支语法如下:

ifeq(arg1, arg2)
分支1
else
分支2
endif

分支会比较括号内的参数“arg1”和“arg2”的值是否相同,如果相同,则为真,执行分支1的内容,否则的话,执行分支2 的内容,参数arg1和arg2可以是变量或者是常量。

# Makefile格式
# 目标文件:依赖的文件
# Tab 命令1
# Tab 命令2

# 定义变量
# ARCH默认为x86,使用gcc编译器,
# 否则使用arm编译器
ARCH ?= x86
TARGET = hello_main
CFLAGS = -I.
LDFLAGS =
DEPS = hello_func.h
OBJS = hello_main.o hello_func.o

# 根据输入的ARCH变量来选择编译器
# ARCH=x86,使用gcc
# ARCH=arm,使用arm-gcc
ifeq ($(ARCH),x86)
    CC = gcc
else
    CC = aarch64-linux-gnu-gcc
endif

# 默认目标
all: $(TARGET)

# 目标文件
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(LDFLAGS)

# *.o文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

# 伪目标
.PHONY: all clean run print
clean:
	rm -f *.o $(TARGET)

run: $(TARGET)
	./$(TARGET)

print:
	@echo "Architecture: $(ARCH)"
	@echo "Compiler: $(CC)"
	@echo "Target: $(TARGET)"

在这里插入图片描述


8. 模式规则

在上面的例子中也是用到了很多模式规则,例如下面这个:”%”是一个通配符,功能类似”*”,如”%.o”表示所有以”.o”结尾的文件。所以”%.o : %.c”在本例子中等价 于”hello_main.o : hello_main.c”、”hello_func.o : hello_func.c”,即等价于o文件依赖于c文件的默认规则。不过这行代码后面的” ( D E P S ) ”表示它除了依赖 c 文件,还依赖于变量” (DEPS)”表示它除了依赖c文件,还依赖于变量” (DEPS)表示它除了依赖c文件,还依赖于变量(DEPS)”表示的头文件,所以当头文件修改的话,o文件也会被重新编译。

# *.o文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

可自动匹配所有 .c → .o 的规则,避免重复写。


9. Makefile 内置函数详解

在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的*.o或 可执行文件也放到专门的编译输出目录方便整理,如下图所示。示例中*.h头文件 放在includes目录下,*.c文件放在sources目录下,编译输出存放在build中。

实现这些复杂的操作通常需要使用Makefile的函数。GNU Make 提供了大量函数,这里重点讲四个:notdirpatsubstwildcardforeach

函数格式及示例

在Makefile中调用函数的方法跟变量的使用 类似,以“ ( ) ”或“ ()”或“ (){}”符号包含函数名和参数,具体语法如下:

$(函数名 参数)
#或者使用花括号
${函数名 参数}

9.1. notdir—— 去除目录函数

notdir函数用于去除文件路径中的目录部分。它的格式如下:

语法:

$(notdir 文件名)

示例:

# 示例 Makefile
files = src/main.c lib/utils.c include/header.h
files_no_dir = $(notdir $(files))

all:
	@echo "原始文件列表: $(files)"
	@echo "去掉目录后的文件列表: $(files_no_dir)"

在这里插入图片描述


9.2. patsubst —— 模式替换函数

当输入的字符串符合匹配规则,那么使用替换规则来替换字符串,当匹配规则中有“%”号时,替换规 则也可以例程“%”号来提取“%”匹配的内容加入到最后替换的字符串中。

语法:

$(patsubst 匹配规则, 替换规则, 输入的字符串)

示例:

# 示例 Makefile
src_files = src/main.c src/utils.c src/network.c
obj_files = $(patsubst %.c,%.o,$(src_files))

all:
	@echo "src_files: $(src_files)"
	@echo "obj_files: $(obj_files)"

在这里插入图片描述


9.3. wildcard —— 文件匹配函数

wildcard函数用于获取文件列表,并使用空格分隔开。它的格式如下:

语法:

$(wildcard 匹配规则)

示例:

# 示例 Makefile
# 假设当前目录下有 main.c, utils.c, network.c, test.c 文件
c_files = $(wildcard *.c)
h_files = $(wildcard *.h)

all:
	@echo "c_files: $(c_files)"
	@echo "h_files: $(h_files)"

在这里插入图片描述

常用于自动收集源文件。

9.4. foreach —— 循环函数

语法:

$(foreach var,list,text)

示例:

# 示例 Makefile
files = main.c utils.c network.c
result = $(foreach file,$(files),Processing $(file))

all:
	@echo "$(result)"

在这里插入图片描述


10. 清理与安装

install: program
    install -m 755 program /usr/local/bin/

clean:
    rm -rf build *.o program

.PHONY: install clean

11. 常见技巧与调试方法

  • make -n 只显示命令不执行。
  • make -B 强制重建。
  • make -j4 并行构建。
  • make -d 调试依赖判断。
  • 命令前 @ 隐藏输出,- 忽略错误。

12. 常见陷阱

  • Tab 与空格混用:命令必须以 Tab 开头。
  • 伪目标未声明clean 必须加 .PHONY
  • 依赖不完整:未追踪头文件变化,导致编译不更新。

13. 小结

Makefile 是构建系统的基石,掌握它能让你在 Linux、嵌入式开发中事半功倍。从简单的规则开始,逐步使用模式规则、内置函数、条件判断和多目录管理,就能写出高效、清晰、可维护的 Makefile。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值