在Linux平台下我们经常使用make命令编译程序,make命令依赖于Makefile;
简介
Makefile 是一种用于自动化编译和构建程序的文件,它包含了一系列的规则(rules),这些规则指定了如何编译和链接程序中的源文件以生成可执行文件或其他非源代码文件。Makefile 使得编译过程更加高效和可重复,尤其是在处理包含多个源文件和复杂依赖关系的项目时。
Makefile 的基本结构由以下几个部分组成:
-
注释:以
#
开头的行为注释。 -
变量:Makefile 中可以定义变量来存储文件名、编译器选项等,以便在多处重复使用。
-
规则(Rules):Makefile 中的核心部分,每个规则定义了如何生成一个或多个目标文件(通常是可执行文件或对象文件)。规则的基本格式如下:
target: dependencies
command
#target:规则的目标,通常是想要生成的文件名。
#dependencies:规则的依赖项,生成目标所需要的文件列表。如果依赖项比目标文件更新,执行规则中的命令。
#command:要执行的命令,用于从依赖项生成目标。命令前必须以制表符(Tab)开始。
命令执行的两个条件:
- 依赖文件比目标文件新
- 目标文件还未生成
4. 伪目标(Phony Targets):不以文件名命名的目标,用于执行特定的操作,如清理构建目录等。伪目标通常以 .PHONY
声明。
5. 自动变量:Makefile中预定义的一些特殊变量,它们在规则执行时的上下文中自动获得值,不需要显式地定义。
用途:主要用于规则中的命令部分,代表文件名、选项等,简化了命令的编写。
语法:常见的自动变量包括$@
、$<
、$^
、$?
等。
$@
:表示规则中的目标文件。$<
:表示规则中的第一个依赖文件。$^
:表示规则中的所有依赖文件,构成一个列表,文件名之间以空格分隔。$?
:表示所有比目标文件新的依赖文件列表。
6. 即时变量、延时变量:
变量的定义语法形式如下:
A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一次定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果D在前面是延时变量,那么现在它还是延时变量;
下面是一个简单的 Makefile 示例,用于编译一个名为 hello
的程序,该程序由一个源文件 hello.c
构成:
# 定义编译器和编译选项
CC=gcc
CFLAGS=-Wall
# 定义目标文件
hello: hello.o
$(CC) $(CFLAGS) -o hello hello.o
# 定义如何编译 .c 文件到 .o 文件
hello.o: hello.c
$(CC) $(CFLAGS) -c hello.c
# 伪目标:清理编译生成的文件
.PHONY: clean
clean:
rm -f hello hello.o
在这个示例中,我们定义了两个规则:一个用于从 hello.c
生成 hello.o
,另一个用于从 hello.o
生成可执行文件 hello
。我们还定义了一个伪目标 clean
,用于删除编译过程中生成的文件。
Makefile 的强大之处在于它可以根据文件的修改时间来自动决定哪些文件需要重新编译,从而避免了不必要的编译操作,提高了编译效率;
通用Makefile
通用Makefile来自韦东山老师课程百问网嵌入式专家-韦东山嵌入式专注于嵌入式课程及硬件研发 (100ask.net)
通用的Makefile,它可以用来编译应用程序:
在顶层目录有两个文件:
各子级目录中的Makefile只需指定编译文件即可,由顶层Makefile.build编译打包;
通用Makefile中make命令使用
执行make命令时,它会去当前目录下查找名为“Makefile”的文件,并根据它的指示去执行操作,生成第一个目标。
我们可以使用“-f”选项指定文件不再使用名为“Makefile”的文件,比如:
make -f Makefile.build
我们可以使用“-C”选项指定目录,切换到其他目录里去,比如:
make -C a/ -f Makefile.build
我们可以指定目标,不再默认生成第一个目标:
make -C a/ -f Makefile.build other_target
通用Makefile详解
顶层目录的Makefile
- 主要是定义工具链前缀CROSS_COMPILE, 定义编译参数CFLAGS, 定义链接参数LDFLAGS, 这些参数就是文件中用export导出的各变量;
- 定义obj-y来指定根目录下要编进程序去的文件、子目录;
# 加“?”如果之前设置过交叉工具链,则这行代码不起作用
#定义了各种编译工具的使用方式
CROSS_COMPILE ?= #指定交叉编译工具链中各个工具的名称前缀
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM #将这些变量导出到环境中,使得这些变量在子 Makefile 中也可以使用。
export STRIP OBJCOPY OBJDUMP
#指定编译选项,初始化成三个选项:-Wall:开启所有警告信息,帮助开发者发现潜在的代码问题;
#-O2:设置优化级别为2,编译器会尝试进行中等程度的优化以提高程序的执行效率,同时尽量保持较快的编译
#速度和较小的调试开销。
#-g:生成调试信息,这对于使用调试器(如gdb)来调试程序非常有用。
#CFLAGS 又通过 += 操作符添加了另一个选项 ,指定头文件搜索路径,指定当前路径下的include子目录为搜索#路径
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
LDFLAGS := -lts -lpthread -lfreetype -lm #这个变量用于指定链接器(ld)的链接选项
export CFLAGS LDFLAGS #被设置为包含四个库选项
TOPDIR := $(shell pwd) #立即变量,TOPDIR等于shell命令pwd的结果
export TOPDIR #顶层目录的路径,通常用于指定其他文件或目录的相对路径。
TARGET := test #最终生成的可执行文件
#obj-y:这个变量包含了一组目录,这些目录包含要构建的子模块。Makefile将递归地进入这些目录并构建
obj-y += display/
obj-y += input/
obj-y += font/
obj-y += ui/
obj-y += page/
obj-y += config/
obj-y += business/
#调用 start_recursive_build 来递归编译,然后生成最终目标文件 test。
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
# 使用 make -C ./ -f $(TOPDIR)/Makefile.build 调用 Makefile.build 进行子目录递归构建。
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build #-C切换到当前目录中Makefile.build中
#$(TARGET) 目标使用 gcc 将所有的 built-in.o 链接成最终的可执行文件。
$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS) #将built-in.o连接成$(TARGET)可执行文件
clean: #删除所有.o文件和最终的目标文件,以清理构建环境。
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean: #除了执行clean命令外,还删除所有.d文件(通常用于存储依赖关系),以进行更彻底的清理。
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
顶层目录的Makefile.build
这是最复杂的部分,它的功能就是把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为built-in.o
处理递归编译的具体逻辑,定义了一个复杂的构建系统,它支持递归构建子目录中的目标,并处理依赖项文件。
PHONY := __build #定义伪目标,用于后续规则依赖
__build:
#初始化三个变量为默认值,确保在包含顶层 Makefile 之前,不会因为未定义或意外的重复定义而导致错误。
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
include Makefile #包含顶层目录的 Makefile,这使得 Makefile 中定义的变量和规则能够在这里使用。
#$(filter %/, $(obj-y)):从 obj-y 中筛选出所有以 / 结尾的项目(表示子目录),obj-y 数据来自顶#层Makefile,
#$(patsubst %/,%,$(...)):去掉这些项目中的斜杠 /,得到纯目录名称。
#subdir-y += $(__subdir-y):将这些子目录添加到 subdir-y 变量中,以便后续递归处理。
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y))) #从 obj-y 中提取的子目录列表。
subdir-y += $(__subdir-y) #所有需要递归构建的子目录。
#subdir_objs:存储了每个子目录的 built-in.o 文件路径(子目录内对象文件的汇总)。
#cur_objs:存储了当前目录中的所有对象文件。
#dep_files:生成了所有当前目录对象文件的依赖文件(.d 文件)的路径,用于增量编译。
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
#检查 dep_files 是否为空(即是否存在依赖文件),如果有,则包含这些依赖文件。
#这些 .d 文件通常用于记录源文件的依赖关系,帮助 make 决定哪些文件需要重新编译。
ifneq ($(dep_files),)
include $(dep_files)
endif
#将子目录目标添加到伪目标列表中,这样 make 就不会尝试将这些目录视为实际文件。
PHONY += $(subdir-y)
#__build 目标依赖于所有子目录目标($(subdir-y))和当前目录的 built-in.o 文件。
#只有当所有子目录的编译完成后,才会进行当前目录的链接操作。
__build : $(subdir-y) built-in.o
#针对每个子目录,使用 make -C $@ 进入子目录并执行该目录下的 Makefile.build 文件。
#这条规则确保子目录中的文件被正确编译。
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
#built-in.o 是一个汇总对象文件,它由当前目录的所有对象文件($(cur_objs))和所有子目录的 built-#in.o 文件($(subdir_objs))链接而成。
#-r 选项用于生成可重定位的目标文件。
built-in.o : $(subdir-y) $(cur_objs)
$(LD) -r -o $@ $(cur_objs) $(subdir_objs)
#这条规则用于将每个 .c 文件编译成 .o 文件。
#-Wp,-MD,$(dep_file):这部分选项告诉编译器生成 .d 文件,以便 make 能够知道哪些文件发生了变化。
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
#.PHONY 指示 make,这些目标(在 $(PHONY) 中列出的)不代表实际文件,即使它们与文件同名,也应该被
#视为规则,而不是文件。
.PHONY : $(PHONY)
各级子目录的Makefile
它最简单,形式如下:
EXTRA_CFLAGS := #它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项, 可以不设置
CFLAGS_file.o := # "CFLAGS_xxx.o", 它给当前目录下的xxx.c设置它自己的编译选项, 可以不设置
obj-y += file.o #表示把当前目录下的file.c编进程序里
obj-y += subdir/ #表示要进入subdir这个子目录下去寻找文件来编进程序里,是哪些文件由subdir目录下的Makefile决定。