ffmpeg-configure编译分析 - C语言音视频技术

/ 0评 / 4

本文以 ffmpeg-n4.4.1 的版本为准,主要分析 ffmpeg 项目中 configure (shell脚本)的逻辑。

configure (shell 脚本)的代码里面有些不太容易理解的shell语法,在本文开头先进行一下讲解。

1,: ${ncols:=72} ,首先前面首字母是 : 冒号的shell用法是这样的,防止把 ncols 本身作为一个命令来执行,请看文章分析《shell 命令以 : 开头》

2,${ncols:=72} ,configure里面经常能见到 ${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 是从哪个数组提取出来的,其实就是命令行参数 $1 $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_SET 里面 就是可以直接设置的变量,$CMDLINE_APPEND里面就是只能 append 的变量。

从下图可以看到 prefix 这个变量的定义,就在 $CMDLINE_SET 的 $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 编译器把 .c 的文件编译成 .o 文件,$cc 变量一般是 gcc,所以编译器是 gcc。

3,test_ld() 尝试用编译器把 .c 的文件编译成 .o 文件,然后用链接器把 .o 文件搞成 .exe 可执行文件。这里需要注意一下,对于 gcc 来说,编译器跟链接器都是gcc.exe 。所以 $ld 跟 $cc 变量都是gcc。我这里只是讲解了其中一种情况,便于理解,实际上 test_ld() 里面调用的 是test_$type,$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

重要知识点:

  1. check_64bit() 里面用 sizeof(void *) 来判断是 64 位环境还是 32位环境。32位 void * 指针是 4个字节,64位是 8个字节。
  2. Makefile 文件本身就存在的,不是 configure 脚本生成的。网上有些文章讲解错误,configure并不会生成 Makefile,除非源码没有 Makefile。

相关阅读:

  1. shell 编程:冒号 后面跟 等号,加号,减号,问号的意义
  2. linux命令-strip-nm
  3. shell 命令以 : 开头
  4. 使用 Valgrind 检测 C++ 内存泄漏
  5. 如何将linux下的.a库转到windows下.lib库
  6. 雷神FFmpeg源代码简单分析:configure

资源下载:

1,有注释的 configure.sh 下载地址: 百度网盘,提取码:difz

TODO

  1. 试一下 --arch=x86_32 跟 --arch=x86_64 有什么区别?
  2. --enable-cross-compile 研究跨平台编译。

版权所属:知识星球:弦外之音,QQ:2338195090。 由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注