本文以 ffmpeg-n4.4.1 的版本为准,主要分析 ffmpeg 项目中 Makefile 的逻辑。
我的环境是window10 + msys2 ,以这个环境为基础进行讲解。
《ffmpeg-configure编译分析》 之前已经讲解了 configure 的作用,就是根据不同的编译环境,生成了相应的 configure.h 跟 config.mak。例如 如果指定 --toolchain=msvc ,makefile里面的规则 就会使用vs2019的 cl.exe 跟 link.exe 来编译跟链接 C代码。如果没指定 toolchain, 就会默认使用 msys2 里面的 gcc.exe 来编译跟链接 C代码。
msys2 里面执行./configure --toolchain=msvc
跟 ./configure
生成的 config.mak 差异如下。
从上图可以看到,configure 的时候可以通过 --toolchain 影响编译的时候使用什么样的编译器。
常见错误:
1,Makefile:25: *** missing separator. Stop.
上面的错误基本上是因为 makefile 中要执行的命令,前面不是tab键,而是几个空格,编辑器问题。
在开始分析之前,讲一个 makefile 的调试技巧,推荐阅读《如何调试MAKEFILE变量》
make -f Makefile -f vars.mk HOSTPROGS
这里我对 vars.mk 做了点修改,因为源 vars.mk 没处理特殊字符,直接echo 会报错。ffmpeg makefile 的变量很多特殊字符。我用了 warning 来输出,就不会报错。
vars.mk 文件内容如下:
%:
$(warning '$*=$($*)')
d-%:
# @printf "%s =888 %s\n" "abc" $($*)
$(warning '$*=$($*)')
# @echo '$*=$($*)'
# @echo ' origin = $(origin $*)'
# @echo ' value = $(value $*)'
# @echo ' flavor = $(flavor $*)'
现在从第一行代码开始分析 ffmpeg 里 Makefile 文件的逻辑。不熟悉 makefile 的可以先看一遍《跟我一起写 makefile》
第1~2行代码如下:
MAIN_MAKEFILE=1
include ffbuild/config.mak
定义 MAIN_MAKEFILE ,然后把 config.mak include进来。config.mak 文件里面全是一些变量的定义。
PS:MAIN_MAKEFILE 可以影响改变 config.mak 的 SRC_PATH 变量,估计是处理不同目录的情况。
第4~16行代码如下:
vpath %.c $(SRC_PATH)
vpath %.cpp $(SRC_PATH)
vpath %.h $(SRC_PATH)
vpath %.inc $(SRC_PATH)
省略
这个其实就是指导make 的时候文件搜索路径,vpath 的用法请阅读 《跟我一起写 Makefile(四)》
第18~19行代码如下:
TESTTOOLS = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch
HOSTPROGS := $(TESTTOOLS:%=tests/%) doc/print_options
上面的代码只是声明变量,推荐阅读《Makefile 中:= ?= += =的区别》
第37~39行代码如下:
# first so "all" becomes default target
all: all-yes
上面定义了第一个 target (编译目标),makefile的语法是,make的时候如果不指定target ,第一个不以 . 开头的target 就是默认target。推荐阅读《makefile default target》
最后把 all all-yes 定义为 伪目标。伪目标可以防止生成文件。
.PHONY: all all-yes alltools build check config testprogs
这里剧透一下,因为此时 all 这个 target 依赖于 all-yes ,所以make会去找 all-yes 的target,all-yes 的 target 定义在 ffbuild/library.mak 里,如下
all-$(CONFIG_STATIC): $(SUBDIR)$(LIBNAME) $(SUBDIR)lib$(FULLNAME).pc
all-$(CONFIG_SHARED): $(SUBDIR)$(SLIBNAME) $(SUBDIR)lib$(FULLNAME).pc
第40~41行代码如下:
include $(SRC_PATH)/tools/Makefile
include $(SRC_PATH)/ffbuild/common.mak
引入了两个 Makefile 文件,首先 tools/Makefile 有以下重点:
1,tools\Makefile
里面的有个变量 COMPILE_C 比较复杂,用来编译 tool 目录下的C代码的。
我在shell 里执行了这个命令 ,make tools/target_dec_tiff_fuzzer.o
,上图中的 $* 是 % 匹配到的内容,所以$* = tiff
下面是 COMPILE_C 这个变量的具体的值
可以看到 COMPILE_C 实际上执行了3个命令。
- 用 cl.exe 编译器 生成内容,传递给 awk,再输出给 tools/target_dec_tiff_fuzzer.d,在后面会把这个 .d 后缀文件 include 进去。
- printf 打印一下提示信息。
- 正式用 cl.exe 编译 target_dec_fuzer.c 的代码,
2,-include $(wildcard tools/*.d)
前面带一个 - 是防止报错,推荐阅读 《Difference between "include" and "-include" in a makefile》
重要知识点:
- gcc.exe 或 cl.exe 后面 -D 代表定义一个宏,传递进去C代码里面。例如 -DFFMPEG_DECODER=tiff
- makefile 语法,$* 是 % 匹配到的内容,所以$* = tiff
还有一个比较重要的知识点,tools/Makefile 里面使用 COMPILE_C 变量,是 ffbuild/common.mak 里面定义的,但是 tools/Makefile 缺是在 ffbuild/common.mak 前面 include 进去的。
这个要注意一下,makefile的语法 先后顺序 比较模糊的,他是先把所有的 makefile 展开再求值,推荐阅读 《Makefile 中:= ?= += =的区别》,里面的 =
跟 :=
充分表明了,makefile 是展开后再求值。
接下来分析 引入 的 ffbuild/common.mak
文件,部分代码如下:
#
# common bits used by all libraries
#
DEFAULT_X86ASMD=.dbg
ifeq ($(DBG),1)
X86ASMD=$(DEFAULT_X86ASMD)
else
X86ASMD=
endif
ifndef SUBDIR
ifndef V
Q = @
ECHO = printf "$(1)\t%s\n" $(2)
BRIEF = CC CXX OBJCC HOSTCC HOSTLD AS X86ASM AR LD STRIP CP WINDRES NVCC
makefile 的调试非常麻烦,这里介绍个方法,例如我想知道有没跑进去 ifndef SUBDIR 这个条件,可以使用这个命令 make -f Makefile -f vars.mk d-ECHO
打印出 ECHO 变量。
common.mak 文件都是一些通用的变量定义,不太容易理解的地方有以下:
1,请看下面代码,他这些 规则,command 里面只有一个 $(COMPILE_MMI) ,后面没有接上文件名,看上去不知道要编译什么文件,实际上,要编译的文件名就在 $(COMPILE_MMI) 变量里面,通过上面的 % 匹配到的字符,传递进去给 $(COMPILE_MMI) ,可以把 $(COMPILE_MMI) 打印出来,就能知道具体逻辑。
%_mmi.o: %_mmi.c
$(COMPILE_MMI)
%_msa.o: %_msa.c
$(COMPILE_MSA)
%.o: %.c
$(COMPILE_C)
2,请看下面代码,target 后面带一个 |
,我也不知道是做什么用的。
$(OBJS): | $(sort $(dir $(OBJS)))
$(HOBJS): | $(sort $(dir $(HOBJS)))
$(HOSTOBJS): | $(sort $(dir $(HOSTOBJS)))
$(SLIBOBJS): | $(sort $(dir $(S
第98~107行代码如下:
define DOSUBDIR
$(foreach V,$(SUBDIR_VARS),$(eval $(call RESET,$(V))))
SUBDIR := $(1)/
include $(SRC_PATH)/$(1)/Makefile
-include $(SRC_PATH)/$(1)/$(ARCH)/Makefile
-include $(SRC_PATH)/$(1)/$(INTRINSICS)/Makefile
include $(SRC_PATH)/ffbuild/library.mak
endef
$(foreach D,$(FFLIBS),$(eval $(call DOSUBDIR,lib$(D))))
上面这段代码特别重要,有一个命令包DOSUBDIR。在该命令包通过包含libavXXX/Makefile和library.mak等文件,定义了FFmpeg类库(例如libavformat,libavcodec,libavutil等)的依赖关系。
注意看最后一个 ffbuild/library.mak
这个makefile 是最后包含进来的,专门处理之前 include进来的文件。DOSUBDIR 的调用是在一个 foreach 循环里面的,举个例子:
libavdevice\Makefile
文件里面 定义了 DESC = FFmpeg device handling library
,
然后 library.mak
里面就会使用这个 DESC 变量,把他展开,如下,library.mak 第 42 行代码。
$(SUBDIR)lib$(FULLNAME).pc: $(SUBDIR)version.h ffbuild/config.sh | $(SUBDIR)
$$(M) $$(SRC_PATH)/ffbuild/pkgconfig_generate.sh $(NAME) "$(DESC)"
不同的类库有不同 的 DESC 定义。libavformat\Makefile 的 DESC 定义如下:
DESC = FFmpeg container format library
ffbuild/library.mak 是最重要的一个文件,他就是 make 命令的入口,你可以理解为 main() 函数入口函数,第一个 编译命令就是从这里运行的,具体流程是怎样的?请继续看,之前说过,根目录的 Makefile 在入口定义了 all: all-yes ,所以 all 就是make的默认 target,默认目标。
all 依赖 all-yes,所以就会导致 ffbuild/library.mak 下面这段代码被执行。
all-$(CONFIG_STATIC): $(SUBDIR)$(LIBNAME) $(SUBDIR)lib$(FULLNAME).pc
all-$(CONFIG_SHARED): $(SUBDIR)$(SLIBNAME) $(SUBDIR)lib$(FULLNAME).pc
上面的 $(CONFIG_STATIC) 展开后是 yes 或者 no,就是 all-yes ,可以看到,他后面有一堆的 prerequisites ,这一堆 prerequisites 就会导致后面继续去找依赖,找 .o 文件,.o 文件没有找到,就会执行 cl.exe 或者gcc.exe 从 C代码编译出来 .o 文件。大概就是这样一个逻辑。可以用make -n 查看具体的编译命令。
所以说 ffbuild/library.mak 是最重要的一个文件
入口找到了,其他的一些编译规则自行查看即可, makefile 分析完毕。
资源下载:
1,带本人注释的 Makefile 下载地址,百度网盘,提取码:f5aj
相关阅读:
版权所属:知识星球:弦外之音,QQ:2338195090。 由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。