makefile逻辑分析—FFmpeg makefile源码分析
作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音
在开始分析之前,讲一个 makefile 的调试技巧,推荐阅读《如何调试MAKEFILE变量》
make -f Makefile -f vars.mk HOSTPROGS
这里我对 vars.mk 做了点修改,因为源 vars.mk 没处理特殊字符,直接 echo 会报错。ffmpeg 的 makefile 的变量有很多特殊字符。我用了 warning 来输出,就不会报错。vars.mk 下载地址:百度网盘,提取码:i59d
先来看 FFmpeg 根目录的 makefile 的代码,如下:

上图中的 vpath 是定义搜索目录,因为你可以在其他目录执行 configure 文件,那样 SRC_PATH 变量就不是 ./ 当前目录了。
FFLIBS-$(CONFIG_AVRESAMPLE) += avresample
这些 CONFIG_AVRESAMPLE 之类额变量都是 yes 或者 no,他就是用 yes/no 来控制要不要某些库或者组件。而这些 yes/no 的变量 是 configure 脚本输出的。
下面的代码 是 检测 configure 有没执行,这个是这样的,configure会 生成 ffbuild/.config 文件,如果你改了 FFmpeg 里面的代码,代码文件就会比 .config 文件更新,如果你不执行 configure , 直接 make ,就会报错。
config.h: ffbuild/.config
ffbuild/.config: $(CONFIGURABLE_COMPONENTS)
@-tput bold 2>/dev/null
@-printf '\nWARNING: $(?) newer than config.h, rerun configure\n\n'
@-tput sgr0 2>/dev/null
到这里,我决定不再一行一行代码解析 FFmpeg 项目的 makefile,因为 FFmpeg 的 makefile 代码比较多,而且初学者,通常你不需要看完全部的 makefile 就能进行二次开发。
之前说过,8 千行 configure 最后也只是往编译器,链接器传递了一些参数,这些参数就被定义在 config.mak 文件里面,如下:

FFmpeg 的 makefile 比较分散,根目录有一个 makefile,各个库目录也有一个 makefile。库目录的 makefile 是通过 DOSUBDIR 函数 包含进来的,如下:

上图代码中,定义了 DOSUBDIR 函数,然后遍历 FFLIBS 数组变量 来调用,FFLIBS 就是 FFmpeg 的那 8个库。
因此,FFmpeg 项目根目录有一个主的 makefile (MAIN_MAKEFILE),然后各个子目录都有一个各自的 makefile。代码量有点多。但是我们比较常用的只有 5 个知识点。
提示:makefile 也只是一个构建工具,他最后也是调 gcc 来执行编译。
makefile 里面,我们比较常用的有 5 个知识点。
1,第一个 target 是什么,在哪里定义的?
makefile 的规则是,执行 make 命令的时候要指定一个 target,就是要编译哪个目标,如果不指定,就是编译 makefile 文件里面的第一个目标,第一个 target 也叫默认target。那 FFmpeg 的默认目标是什么?如下:

从上图可以看出,默认 target 是 all,因此,我们在编译 FFmpeg 的时候,无论执行 make 还是 make all 命令,效果是一样的。
2,把 .c 文件 编译成 .o 文件的规则在哪里?
我们知道,在编译阶段,各个 .c 文件都是独立编译的。而 makefile 也是调 gcc 来编译,那是在哪里调的 gcc 呢?
这些规则在 common.mak 文件里面,如下:

COMPILE_C 是一个函数调用,如下:

这里提醒一下,所有的子目录,所有的 .c 文件编译都是这个规则。这是通用的。也就是说,ffmpeg.c,ffplay.c 文件也是这一个规则。
我们现在来测试一下 是不是这一个规则,看下 ffplay.c 是不是在这里编译的。我们加个东西,如下:

我加了一个无意义的选项 -num666 在后面。然后用 make -n > t33.txt 来查看命令,-n 代表不执行编译只打印信息。如下:

果然,因此所有 的 .c 文件都是在 common.mak 文件里面定义编译规则的。
3,生成 FFmpeg 8个 动态库的规则在哪里?
FFmpeg 里面提供了 8 个库给开发者使用,这些库可以通过打包形成静态库,或者通过链接器来生成动态库。那这些生成静态库,动态库的规则在哪里呢?
以 libavcodec 库为例,libavcodec 目录的 makefile 里面的代码主要有3个重要的变量,HEADERS,OBJS,OBJS-YES,如下:


上面的 OBJ-YES 变量有 x264 的 .o 文件,x264 应该是以动态库或者静态库的方式引用的,这里为什么直接填 .o ,我也不太清楚,后面补充,暂时不管。
上面 allcodecs.o 等等 这些 .o 后缀的文件是目标文件,这些目标文件肯定要传递给 ar 命令打包成静态库,或者传递给 gcc链接器 生成动态库,那在哪里使用这个 OBJS 变量的呢?
在 library.mak 里面,如下:

这里我必须说一下,library.mak 里面的变量 OBJS, SUBDIR 跟 NAME 等等变量是从其他文件引入的,而且每次循环引入的变量值都不一样,循环发生在 DOSUBDIR 函数里面,如下:

FFmpeg 在各个库目录下的 makefile 文件定义了 OBJS 变量,例如 libavcodec\Makefile 里就定义了一个 OBJS 变量,然后 被 library.mak 使用。实际上我个人觉得 makefile 的语法变量有点乱,不太容易确定是局部变量还是全局,反正逻辑就是我上面说的那样。
现在我们已经找到了 在哪里生成动态库的了,就是 library.mak 文件第 51 行的地方,我讲一下我怎么找到这个地方,我是直接搜 SHFLAGS 变量的,因为这是动态库的 flags。
现在加一些调试代码,来确认是不是在这个 地方生成 libavcodec.so 文件的,添加的代码,加了个 -num77 如下:

提示:之前 configure 的时候要使用 --enable-shared,然后才会执行到这条 动态库的 makefile 规则。
执行命令 make -n > t5.txt ,可以看到选项 -num777 加上去了,如下:

可以看到生成 libavcodec.so 依赖的 .o 文件太多了,非常多。
现在来找一下,把 .o 文件打包成 静态库的规则在哪里,猜测静态库跟动态库的规则是同一个文件的。 所以直接 在 library.mak 里面搜索 AR ,因为 config.mak 就是用 AR 这个变量来打包的。
AR 变量 在 Windows 系统是 lib.exe ,在 Linux 系统是 ar 命令,
library.mak 里面生成静态库的代码如下:

上图中的 OBJS 变量是在 libavcodec/makefile 里面定义的。各个库的 makefile 都会定义自己的 OBJS 变量。然后在 DOSUBDIR 函数循环使用
还是惯例,加个 -num888 在后面,如下:

这时候,我们一定要 重新 configure ,不要加 --enable-shared,这样这条规则才会执行到。他是有其他地方控制 生成动态库还是静态库的,make 只会生成静态库或者动态库,只能二选一。不过层层的 target 依赖找起来太麻烦了,初学者也不需要知道这个。只要知道 静态库,动态库的 makefile 规则在哪里就行。
执行命令 make -n > t6.txt ,可以看到选项 -num88 加上去了,如下:

4,生成 ffmpeg.exe ,ffplay.exe 的规则在哪里?
ffmpeg.exe 实际上也是调 那 8 个库实现的,ffmpeg.exe 相关的代码在 fftools 目录里面,ffmpeg.exe 相关的代码只有几个文件,如下:
ffmpeg.c,ffmpeg_opt.c ,cmdutils.c,ffmpeg_filter.c,ffmpeg_hw.c,ffmpeg_qsv.c ,ffmpeg_videotoolbox.c
后面的 ffmpeg_qsv.c 跟ffmpeg_videotoolbox.c 文件不是必须的,是可选的。也就是 生成 ffmpeg.exe 只需要 5个 C 文件。这 5个文件加起来只有 1万多行代码。
看完 这 1万多行代码,就能学会使用 FFmpeg API 函数了,是不是很简单。
回到 原来的问题。生成 ffmpeg.exe 的 makefile 规则在哪里?这个问题实际上要分析 fftools 目录的 makefile 文件。如下:

上面的代码有两个重点, AVPROGS 跟 OBJS-ffmpeg,生成 ffmpeg.exe 应该会用到这两个变量,只要搜索这两个变量应该就能找到。
我试了一下,根据 AVPROGS 跟 OBJS-ffmpeg找,有点麻烦,还是直接 搜 _g 就行 因为 FFmpeg 项目是生成 ffmpeg_g.exe,再生成 ffmpeg.exe 的,
生成 ffmpeg_g.exe 的规则 在根目录的 makefile 里面,如下:

上图用的是 % 模糊匹配所有的有 _g 的文件。 还是老套路,我们在 后面加个 -num999 ,执行 make -n 之后如下:

从上图可以看出,有 3个 num999 。证明我们找的地方是对的。
还有一个问题,上面的第 123 行代码, %$(PROGSSUF)_g$(EXESUF): $(FF_DEP_LIBS) ,_g 的规则是一个 target,target 要执行,肯定要被依赖,我们再找一下 这个 _g 在哪里被依赖了。实际上依赖代码就在附近,如下:

上面的规则实际上就是 ffmpeg.exe 依赖 ffmpeg_g.exe ,所以依赖 _g 的地方找到了。我们看到这个地方干了什么,可以看到 用了 strip,再看回去上面的打印日志,有一个关键的地方,如下:
strip -o ffmpeg ffmpeg_g
原来 ffmpeg_g.exe 就是带调试信息的可执行文件,而 ffmpeg.exe 是经过 strip 去掉调试信息的可执行文件。
提示:我为什么加 exe,其实 Linux 环境是没有 exe 后缀,但是为了区分我是在说 ffmpeg 可执行文件,还是指 FFmpeg 整个项目,所以我加了 exe。
5, make install 的逻辑在哪里
编译完 FFmpeg 之后,会生成 8 个 dll 跟 相关的 exe 文件,然后你只要执行 make install 命令,就会把这些 dll,头文件,exe 安装到 prefix 的目录。
那这个安装的逻辑是在 makefile 的那个地方呢?如下:
答:在 ffbuild/library.mak 里,如下:

上图里面,$(HEADERS) 与 $(LIBNAME) 这些变量都是在 libxxx 里面定义的,例如 libavutil/makefile 里面定义了自己的 HEADERS,要安装那些头文件。其他的 libavformat 同理,
ffbuild/library.mak 文件是在一个 foreach 循环里面被引入的,所以每一次循环,他的$(HEADERS) 与 $(LIBNAME) 这些变量都是不同的,如下:

如果你想导出一些新的头文件,例如 libavutil/thread.h,只需要在 libavutil/makefile 里面给 HEADERS 变量加上对应的头文件就行,如下:

因为默认情况,FFmpeg 是没有导出 thread.h 给外部使用的,这是 FFmpeg 封装的跨平台线程API。
至此,FFmpeg 项目里面比较常用的 makefile 知识,你已经掌握。后面就让我们二次开发 FFmpeg,加一个自己程序进去。FFmpeg 项目默认有 3 个程序 ffmpeg.exe ,ffplay.exe ,ffprobe.exe 。我们新加的程序就叫 loken.exe。
欢迎看下一篇文章。
