一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
本文以 ffmpeg-n4.4.1 的版本为准,主要分析 ffmpeg 项目中 configure (shell脚本)的逻辑。
configure (shell 脚本)的代码里面有些不太容易理解的shell语法,在本文开头先进行一下讲解。
1,: ${ncols:=72} ,首先前面首字母是 : 冒号的shell用法是这样的,防止把 ncols 本身作为一个命令来执行,请看文章分析《shell 命令以 : 开头》
2,{xx:=yy} 这种用法,这其实是一种变量的用法,大括号括起来是一个变量,请看文章分析 《shell 编程:冒号 后面跟 等号,加号,减号,问号的意义》
configure 的代码有7500行左右。不过逻辑并不复杂,主要是有大量的变量定义。
第 1~4013 行,基本全是变量,以及一些基本函数的定义,没有什么好分析,自行看代码即可。
第 1~4013 行 里面的重点有以下5点:
1,第 1~ 55行的 LC_ALL
等代码是 shell的本地化处理,然后用 try_exec() 判断 shell是不是 POSIX-compatible shell ,估计是早期比较多的人的shell环境不是 POSIX 兼容的环境,configure通不过,提了issue,所以 ffmpeg 作者在configure加了这个检测,告诉别人不是bug,不要提issue。这段代码其实不重要,不用纠结里面的细节,想了解细节可以看这篇文章《shell脚本中 LC_ALL=C 的含义》
2,enable() 跟 disable() 函数,把变量设置成 yes 或者 no。例如 执行 configure的时候指定 --enable-sdl2 就会 把 $sdl2 设置为yes。
3,#3786行 ,我们configure的时候可以指定 --arch=xxx, 指定cpu的架构,但大多数情况不用指定,configure里面会用 uname 命令,查询出当前环境是什么样的cpu架构,如下:
#3786行
# machine
if test "$target_os_default" = aix; then
arch_default=$(uname -p)
strip_default="strip -X32_64"
nm_default="nm -g -X32_64"
else
arch_default=$(uname -m)
fi
4,#3893行 主要是提取命令行参数,然后用sh_quote转义,后面再 把FFMPEG_CONFIGURATION写回去 日志里面,FFMPEG_CONFIGURATION 只是一个日志临时变量,这边还没开始处理命令行参数。
# 3893行
#开始解析命令行参数
for v in "$@"; do
r=${v#*=}
l=${v%"$r"}
r=$(sh_quote "$r")
FFMPEG_CONFIGURATION="${FFMPEG_CONFIGURATION# } ${l}${r}"
done
5,#3913 行 从 c 代码提取了 很多 LIST,这里我们可以看到 ffmpeg 把所有的设备,封装格式(formats),编码器(codec),都写在 allxxx.c 文件里面。
#3913行
INDEV_LIST=$(find_things_extern demuxer AVInputFormat libavdevice/alldevices.c indev)
MUXER_LIST=$(find_things_extern muxer AVOutputFormat libavformat/allformats.c)
DEMUXER_LIST=$(find_things_extern demuxer AVInputFormat libavformat/allformats.c)
ENCODER_LIST=$(find_things_extern encoder AVCodec libavcodec/allcodecs.c)
DECODER_LIST=$(find_things_extern decoder AVCodec libavcodec/allcodecs.c)
从 第4020 行,正式开始解析命令行参数,这个是重中之重,这里决定了, configure 后面的参数,是如何解析成shell里面的变量的,代码如下:
#第4020 行
for opt do
optval="${opt#*=}"
case "$opt" in
--extra-ldflags=*)
add_ldflags $optval
;;
# 省略代码 ...
--enable-*=*|--disable-*=*)
eval $(echo "${opt%%=*}" | sed 's/--/action=/;s/-/ thing=/')
is_in "${thing}s" $COMPONENT_LIST || die_unknown "$opt"
eval list=$$(toupper $thing)_LIST
name=$(echo "${optval}" | sed "s/,/_${thing}|/g")_${thing}
list=$(filter "$name" $list)
[ "$list" = "" ] && warn "Option $opt did not match anything"
test $action = enable && warn_if_gets_disabled $list
$action $list
;;
# 省略代码 ...
--enable-?*|--disable-?*)
eval $(echo "$opt" | sed 's/--/action=/;s/-/ option=/;s/-/_/g')
if is_in $option $COMPONENT_LIST; then
test $action = disable && action=unset
eval $action $$(toupper ${option%s})_LIST
elif is_in $option $CMDLINE_SELECT; then
$action $option
else
die_unknown $opt
fi
;;
# 省略代码 ...
*)
optname="${opt%%=*}"
optname="${optname#--}"
optname=$(echo "$optname" | sed 's/-/_/g')
if is_in $optname $CMDLINE_SET; then
eval $optname='$optval'
elif is_in $optname $CMDLINE_APPEND; then
append $optname "$optval"
else
die_unknown $opt
fi
;;
esac
done
上面的代码展示了 configure 命令行参数 --extra-ldflags="-L/home/loken/ffmpeg/build32/libfdk-aac/lib" ,--enable-libx264 之类的参数是如何解析成 shell 变量的。非重点代码我已经省略了。
上面是用一个 for 循环来处理命令行参数了,命令行参数一个一个都会 赋值 给 opt 变量,shell 这种语法比较简洁,新手可能一下子没看出来 opt 是从哪个数组提取出来的,其实就是命令行参数 2 $3 等等,都会一个一个赋值给 opt 变量。opt 只是一个名字,可以用其他的名字,例如 v,通过 for v do {...} 的方式遍历命令行参数。
for 循环的第一句代码就是 optval="${opt#*=}",又是用这种大括号的方式 提取出来 ----extra-cflags="xxx" 等于号后面的value 赋值给 optval。
主要逻辑就是一个 switch case,做正则匹配字符串,咱们用的比较多的就是 --prefix=/xxx(定义目录),--extra-cflags(定义gcc.exe 的参数), --extra-ldflags(定义 link.exe 的参数),--enable-xxx (开启某个选项)。
咱们就以下面这条编译命令做讲解,分析 命令行的参数是如何解析到 shell 变量的。
./configure.sh \
--prefix=/home/loken/ffmpeg/build32/ffmepg-4.4 \
--enable-gpl \
--enable-sdl2 \
--enable-zlib \
--enable-shared \
--enable-nonfree \
--enable-libx264 \
--enable-libfdk-aac \
--enable-libmp3lame \
--enable-libvpx \
--extra-cflags="-I/home/loken/ffmpeg/build32/libfdk-aac/include" \
--extra-ldflags="-L/home/loken/ffmpeg/build32/libfdk-aac/lib" \
--extra-cflags="-I/home/loken/ffmpeg/build32/libvpx/include" \
--extra-ldflags="-L/home/loken/ffmpeg/build32/libvpx/lib" \
--extra-cflags="-I/home/loken/ffmpeg/build32/libx264/include" \
--extra-ldflags="-L/home/loken/ffmpeg/build32/libx264/lib" \
--extra-cflags="-I/home/loken/ffmpeg/build32/libmp3lame/include" \
--extra-ldflags="-L/home/loken/ffmpeg/build32/libmp3lame/lib"
1,--prefix 跟 --extra-cflags 都是在 *)
逻辑中处理,把 optval 附加给 $extra_cflags 变量。
这里注意,configure 里面的shell 变量主要有两种,一种是直接 set,例如 $prefix=/usr ,把 prefix 变量设置成 /usr ,第二种是 需要执行 append 的变量,就是变量本身是已经有值的,后面的值,只能附加 append到后面,不能清除之前的内容,代码如下:
*)
optname="${opt%%=*}"
optname="${optname#--}"
optname=$(echo "$optname" | sed 's/-/_/g')
if is_in $optname $CMDLINE_SET; then
eval $optname='$optval'
elif is_in $optname $CMDLINE_APPEND; then
append $optname "$optval"
else
die_unknown $opt
fi
;;
CMDLINE_APPEND里面就是只能 append 的变量。
从下图可以看到 prefix 这个变量的定义,就在 PATHS_LIST 里面。
2, --extra-ldflags 在 --extra-ldflags=*
) 逻辑中处理,用 add_ldflags 把 optval 都 append(附加) 到变量 $LDFLAGS 里。
--extra-ldflags=*)
add_ldflags $optval
;;
3,--enable-libx264 在 --enable-?*|--disable-?*)
逻辑中处理,这里面的逻辑也比较简单,就是把 $libx264 变量设置为yes。
enable 还有一个逻辑处理是 --enable-*=*|--disable-*=*)
处理后面有等于号的命令行参数。
上面命令行参数解析完毕之后,后续的逻辑主要就是 Check 。
"以下部分内容引用雷神文章:"
Check部分是Configure中最重要的部分。该部分用于检查编译环境(例如数学函数,第三方类库等)。这一部分涉及到很多的函数。包括 check_cflags()、require()、check_lib()、check_func_headers()、check_mathfunc() 等等。这些函数之间的调用关系如下图所示:
从上图可以看到,非常多的函数是存在互相调用的情况的。例如 test_ld() 里面调用了 test_cc() 跟 test_cmd(),下面阐述一下这些函数的作用。
1,test_cmd() 这个是最基础的函数,基本所有的函数都会调这个 test_cmd ,这个函数就是尝试执行一个 命令,第一个参数($1) 就是要执行的命令。
2,test_cc() 用 cc 变量一般是 gcc,所以编译器是 gcc。
3,test_ld() 尝试用编译器把 .c 的文件编译成 .o 文件,然后用链接器把 .o 文件搞成 .exe 可执行文件。这里需要注意一下,对于 gcc 来说,编译器跟链接器都是gcc.exe 。所以 cc 变量都是gcc。我这里只是讲解了其中一种情况,便于理解,实际上 test_ld() 里面调用的 是test_type 变量是 cc 只是其中一种情况。
以上3个函数,test_cmd,test_cc,test_ld 是基础函数。后面的函数都是基于他们封装,决定要不要 add_cflags (添加某个编译器选项)或者 enable 开启某个选项。
这里埋个坑,他那些 check_matchfunc() 成功 之后,enable $funcs ,开启的这些函数,设置成yes,我也不知道具体用在哪些地方,下面还有几个函数需要讲解以下:
1,check_cflags() :检查某个编译器选项是否可以使用,能用就调 add_cflags 加进去 CFLAGS 变量。
2,check_lib() :这些 true 之后执行 enable 的函数 需要注意一下,他一开始就会 disable 某个选项,然后检测通过才会 enable。
3,其他的函数都是类似的原理,true 之后执行 enable 或者 add 加入某些东西。
重点知识:
1,enable_sanitized,sanitized 的作用是可以转移特殊字符,例如把空格转成_ 下划线。
2,test_cflags() 函数里面有个比较隐晦的写法,set -- $($cflags_filter "$@")
set -- 这个其实没有什么特别的意思,就是把参数转义一下,推荐阅读《What does "set --" do in this Dockerfile entrypoint?》
3,在 check_mathfunc() 里面有以下代码,也比较难懂,<<EOF 后面直接 && 了,直觉不是应该先把内容导入进去再执行 &&。
test_ld "cc" "$@" <<EOF && enable $func
#include <math.h>
float foo(float f, float g) { return $func($args); }
int main(void){ return (int) foo; }
EOF
解答:这就是shell的一种不太容易理解的语法,&& 不能放在最后一个EOF后,例如 <<EOF xxx EOF && enable $func
,如果这样写,后面的 && 肯定不会执行了。
Check部分检查完毕之后,环境没有问题,就会继续跑。生成 config.h,代码如下:
cat > $TMPH <<EOF
/* Automatically generated by configure - do not modify! */
#ifndef FFMPEG_CONFIG_H
#define FFMPEG_CONFIG_H
#define FFMPEG_CONFIGURATION "$(c_escape $FFMPEG_CONFIGURATION)"
# 省略代码....
#define HAVE_MMX2 HAVE_MMXEXT
#define SWS_MAX_FILTER_SIZE $sws_max_filter_size
EOF
# 省略代码....
print_config ARCH_ "$config_files" $ARCH_LIST
print_config HAVE_ "$config_files" $HAVE_LIST
print_config CONFIG_ "$config_files" $CONFIG_LIST \
$CONFIG_EXTRA \
$ALL_COMPONENTS \
echo "#endif /* FFMPEG_CONFIG_H */" >> $TMPH
echo "endif # FFMPEG_CONFIG_MAK" >> ffbuild/config.mak
# Do not overwrite an unchanged config.h to avoid superfluous rebuilds.、
# 注意这里
cp_if_changed $TMPH config.h
重点:
cp_if_changed $TMPH config.h
问题:在 configure的时候如果我们没指定 --toolchain,那 toolchain 的默认值是什么?
解答:$toolchain 的默认值是空。在以下switch case代码,不会跑进去任何一个条件,包括 *)
# 重点代码
case "$toolchain" in
*-asan)
#省略..
;;
*-msan)
#省略..
;;
*-tsan)
#省略..
;;
*-usan)
#省略..
;;
valgrind-*)
#省略..
;;
msvc)
#省略..
;;
icl)
#省略..
;;
#省略..
?*)
die "Unknown toolchain $toolchain"
;;
esac
重要知识点:
- check_64bit() 里面用 sizeof(void *) 来判断是 64 位环境还是 32位环境。32位 void * 指针是 4个字节,64位是 8个字节。
- Makefile 文件本身就存在的,不是 configure 脚本生成的。网上有些文章讲解错误,configure并不会生成 Makefile,除非源码没有 Makefile。
相关阅读:
- shell 编程:冒号 后面跟 等号,加号,减号,问号的意义
- linux命令-strip-nm
- shell 命令以 : 开头
- 使用 Valgrind 检测 C++ 内存泄漏
- 如何将linux下的.a库转到windows下.lib库
- 雷神FFmpeg源代码简单分析:configure
资源下载:
1,有注释的 configure.sh 下载地址: 百度网盘,提取码:difz
TODO
- 试一下 --arch=x86_32 跟 --arch=x86_64 有什么区别?
- --enable-cross-compile 研究跨平台编译。
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1,QQ:2338195090。