Just4U 不会算圈图的程序猿不是个好厨子

ffmpeg从入门到放弃(一):编译流程介绍

2021-09-21

ffmpeg configure选项及Makefile介绍(ffmpeg 版本n4.5-dev)

configure 介绍

从目前可查询到的记录(Fabrice Bellard, Wed Dec 20 00:02:47)可以看到,最早期的ffmpeg是没有configure文件的。 后来大概是出于配置项可扩展性的考虑,于2001年的7月22首次添加了configure文件。首版内容如下,仅有75行。 对比当前版本有7000多行,二十年来多出一百多倍。早期configure如下:

#!/bin/sh

# default parameters
prefix="/usr/local"
cc="gcc"
ar="ar"
cpu=`uname -m`
case "$cpu" in
  i386|i486|i586|i686)
    cpu="x86"
    mmx="yes"
  ;;
  *)
    mmx="no"
  ;;
esac
gprof="no"

if [ "$1" = "-h" -o "$1" = "--help" ] ; then
cat << EOF

Usage: configure [options]
Options: [defaults in brackets after descriptions]

  --help                  print this message
EOF
echo "  --prefix=PREFIX         install in PREFIX [$prefix]"
echo "  --cc=CC                 use C compiler CC [$cc]"
echo "  --cpu=CPU               force cpu to CPU  [$cpu]"
echo "  --disable-mmx           disable mmx usage"
echo "  --enable-gprof          enable profiling with gprof [$gprof]"
exit 1
fi
for opt do
  case "$opt" in
  --prefix=*) prefix=`echo $opt | cut -d '=' -f 2`
  ;;
  --cc=*) cc=`echo $opt | cut -d '=' -f 2`
  ;;
  --cpu=*) cpu=`echo $opt | cut -d '=' -f 2`
  ;;
  --disable-mmx) mmx="no"
  ;;
  --enable-gprof) gprof="yes"
  ;;
  esac
done

echo "Install prefix   $prefix"
echo "C compiler       $cc"
echo "CPU              $cpu"
echo "MMX enabled      $mmx"
echo "gprof enabled    $gprof"

echo "Creating config.mk and config.h"

echo "# Automatically generated by configure - do not modify" > config.mk
echo "/* Automatically generated by configure - do not modify */" > config.h

echo "PREFIX=$prefix" >> config.mk
echo "CC=$cc" >> config.mk
echo "AR=$ar" >> config.mk
if [ "$cpu" = "x86" ] ; then
  echo "CONFIG_CPU_X86=y" >> config.mk
  echo "#define CONFIG_CPU_X86 1" >> config.h
fi
if [ "$mmx" = "yes" ] ; then
  echo "CONFIG_MMX=y" >> config.mk
  echo "#define CONFIG_MMX 1" >> config.h
fi
if [ "$gprof" = "yes" ] ; then
  echo "CONFIG_GPROF=y" >> config.mk
fi
if [ "$gprof" = "yes" ] ; then
  echo "CONFIG_GPROF=y" >> config.mk
  echo "#define CONFIG_GPROF 1" >> config.h
fi

我们先来分析初版configure的思路。

  1. 设置一些变量 稍微注意的一点是这里的cpu是直接通过获取当前主机的类型获取到的, 所以可以推断这个版本是不支持交叉编译的。
  2. 帮助信息 通过-h或者--help触发。大概是因为当时选项太少了所以直接使用了if判断的方式。 (我第一次看到这个代码的时候窃喜,原来天才程序员也会偷懒哈哈哈哈哈)。 注意帮助信息是从19行的if开始一直到33行的fi结束。
  3. for循环处理选项。 这里作者偷了个懒,for循环的用法是for opt do,但是又不知道opt的定义在哪里, 这里的opt并不是bash的内置变量,只是省略in的话默认就是对configure传入的参数做循环,此处等价于: for opt in "$@" ;do
  4. 生成config.mk及config.h文件 本质就是根据前面输入的选项生成一堆配置文件,这些配置文件会在代码或者Makefile中用到, 从而编译出跟用户输入选项相关的二进制文件。

尽管当前的configure非常复杂,不过核心的内容与二十年前并没有本质区别。总体流程依然是解析用户的输入, 根据用户输入生成不同的配置文件,这些配置文件再被Makefile引用并编译,最终把用户不同的输入变成了不同的二进制文件。 与初代版本相比区别主要是:

  1. 配置选项非常多
  2. 需要支持多平台
  3. 需要支持交叉编译 这里面每一项都很复杂(比如show_help函数就有五百多行),选项多了以后首先就没法采用初代版本那种直接判断输入参数或者逐个switch case的情形, 因为这样几乎毫无扩展性,每次新增都要很多改动,需要支持多平台就需要了解不同平台的区别,实现功能或许简单, 但是想要做的优雅健壮复用性高就会很麻烦。交叉编译同理。

对于选项非常多的问题,ffmpeg首先定义了大量的变量(超过两千行的变量定义),并把选项分门别类的进行了整理,比如架构相关的配置,avcodec相关的, avdevice相关的,依赖库相关的等等,并使用了类似结构体的定义方式,通过结构体内在使用结构体最终把所有选项归到几个类别之中。 对于支持多平台及交叉编译,ffmpeg的方式是定义了大量的函数(上百个函数,几乎整个configure都是在定义函数),尽量把不同平台之间的区别限制在小函数内部。

当前版本configure依然保留了for opt do的结构,此处代码片段如下:

for opt do
    optval="${opt#*=}"
    case "$opt" in
        --extra-ldflags=*)
            add_ldflags $optval
        ;;
        --extra-ldexeflags=*)
            add_ldexeflags $optval
        ;;
        --extra-ldsoflags=*)
            add_ldsoflags $optval
        ;;
        --extra-ldlibflags=*)
            warn "The --extra-ldlibflags option is only provided for compatibility and will be\n"\
                 "removed in the future. Use --extra-ldsoflags instead."
            add_ldsoflags $optval
        ;;
        --extra-libs=*)
            add_extralibs $optval
        ;;
        (省略)
    esac
done

其中optval="${opt#*=}"是bash的扩展语法,功能是取opt中=符号左边的内容,然后分别匹配,并使用函数逐个处理。这里结构上与初始版本一致, 但实际上这里复杂的多,因为这里的每个case已经不单单是一个具体的选项,而是一类选项,具体的工作在case中的函数中实现。

之后通过一系列操作,得到配置参数后,把配置参数写入到ffbuild目录下的config.mak及config.h中,config.mak会被Makefile文件包含, config.h则被一系列的c文件包含,最终对编译及代码都产生影响。

Makefile 介绍

最初的Makefile非常简短,只有短短几十行,用来编译可执行文件ffmpeg、ffserver及库文件。此时还没有用到configure 脚本。

CFLAGS= -O2 -Wall -g -I./libav
LDFLAGS= -g -L./libav

PROG= ffmpeg ffserver

all: lib $(PROG)

lib:
    make -C libav all

ffmpeg: rmenc.o mpegmux.o asfenc.o jpegenc.o swfenc.o udp.o formats.o grab.o ffmpeg.o libav/libav.a
    gcc $(LDFLAGS) -o $@ $^ -lav -lm

ffserver: rmenc.o mpegmux.o asfenc.o jpegenc.o swfenc.o formats.o grab.o ffserver.o libav/libav.a
    gcc $(LDFLAGS) -o $@ $^ -lav -lpthread -lm

%.o: %.c
    gcc $(CFLAGS) -c -o $@ $<

clean:
    make -C libav clean
    rm -f *.o *~ gmon.out TAGS $(PROG)

etags:
    etags *.[ch] libav/*.[ch]

tar:
    (cd .. ; tar zcvf ffmpeg-0.3.tgz ffmpeg --exclude CVS --exclude TAGS )

第一个使用了configure脚本的Makefile如下,位于libavcodec下(Fabrice Bellard,Sun Jul 22 14:18:56):

include ../config.mk
CFLAGS= -O2 -Wall -g
LDFLAGS= -g

OBJS= common.o utils.o mpegvideo.o h263.o jrevdct.o jfdctfst.o \
      mpegaudio.o ac3enc.o mjpegenc.o resample.o dsputil.o \
      motion_est.o imgconvert.o imgresample.o msmpeg4.o \
      mpeg12.o h263dec.o rv10.o

# currently using libac3 for ac3 decoding
OBJS+= ac3dec.o \
       libac3/bit_allocate.o libac3/bitstream.o libac3/downmix.o \
       libac3/imdct.o  libac3/parse.o

# currently using mpglib for mpeg audio decoding
OBJS+= mpegaudiodec.o \
       mpglib/layer1.o mpglib/layer2.o mpglib/layer3.o \
       mpglib/dct64_i386.o mpglib/decode_i386.o  mpglib/tabinit.o

# i386 mmx specific stuff
ifdef CONFIG_MMX
OBJS += i386/fdct_mmx.o i386/fdctdata.o i386/sad_mmx.o i386/cputest.o \
    i386/dsputil_mmx.o
endif

LIB= libavcodec.a
TESTS= imgresample-test dct-test

all: $(LIB) apiexample

$(LIB): $(OBJS)
    rm -f $@
    $(AR) rcs $@ $(OBJS)

dsputil.o: dsputil.c dsputil.h

%.o: %.c
    $(CC) $(CFLAGS) -c -o $@ $<

%.o: %.s
nasm -f elf -o $@ $<

clean:
    rm -f *.o *~ *.a i386/*.o i386/*~ \
           libac3/*.o libac3/*~ \
           mpglib/*.o mpglib/*~ \
           apiexample $(TESTS)

# api example program
apiexample: apiexample.c $(LIB)
    $(CC) $(CFLAGS) -o $@ $< $(LIB) -lm

# testing progs

imgresample-test: imgresample.c
    $(CC) $(CFLAGS) -DTEST -o $@ $^

dct-test: dct-test.o jfdctfst.o i386/fdct_mmx.o i386/fdctdata.o fdctref.o
    $(CC) -o $@ $^

这里主要注意两点,一是开头的include ../config.mk,即前面通过configure生成的配置,二是中间部位的all: $(LIB) apiexample, 因为这是编译的开始。后来的Makefile虽然更复杂,但是总体也是这个思路。

现在版本的Makefile主文件也不过一百多行,是因为占数量最多的配置文件在ffbuild/config.mak中,现在的Makefile:

MAIN_MAKEFILE=1
include ffbuild/config.mak

vpath %.cpp  $(SRC_PATH)
vpath %.h    $(SRC_PATH)
vpath %.inc  $(SRC_PATH)
vpath %.m    $(SRC_PATH)
vpath %.S    $(SRC_PATH)
vpath %.asm  $(SRC_PATH)
vpath %.rc   $(SRC_PATH)
vpath %.v    $(SRC_PATH)
vpath %.texi $(SRC_PATH)
vpath %.cu   $(SRC_PATH)
vpath %.ptx  $(SRC_PATH)
vpath %/fate_config.sh.template $(SRC_PATH)

TESTTOOLS   = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch
HOSTPROGS  := $(TESTTOOLS:%=tests/%) doc/print_options

# $(FFLIBS-yes) needs to be in linking order
FFLIBS-$(CONFIG_AVDEVICE)   += avdevice
FFLIBS-$(CONFIG_AVFILTER)   += avfilter
FFLIBS-$(CONFIG_AVFORMAT)   += avformat
FFLIBS-$(CONFIG_AVCODEC)    += avcodec
FFLIBS-$(CONFIG_AVRESAMPLE) += avresample
FFLIBS-$(CONFIG_POSTPROC)   += postproc
FFLIBS-$(CONFIG_SWRESAMPLE) += swresample
FFLIBS-$(CONFIG_SWSCALE)    += swscale

FFLIBS := avutil

DATA_FILES := $(wildcard $(SRC_PATH)/presets/*.ffpreset) $(SRC_PATH)/doc/ffprobe.xsd

SKIPHEADERS = compat/w32pthreads.h

# first so "all" becomes default target
all: all-yes

include $(SRC_PATH)/tools/Makefile
include $(SRC_PATH)/ffbuild/common.mak

FF_EXTRALIBS := $(FFEXTRALIBS)
FF_DEP_LIBS  := $(DEP_LIBS)
FF_STATIC_DEP_LIBS := $(STATIC_DEP_LIBS)

$(TOOLS): %$(EXESUF): %.o
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $^ $(EXTRALIBS-$(*F)) $(EXTRALIBS) $(ELIBS)

target_dec_%_fuzzer$(EXESUF): target_dec_%_fuzzer.o $(FF_DEP_LIBS)
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $^ $(ELIBS) $(FF_EXTRALIBS) $(LIBFUZZER_PATH)

tools/target_bsf_%_fuzzer$(EXESUF): tools/target_bsf_%_fuzzer.o $(FF_DEP_LIBS)
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $^ $(ELIBS) $(FF_EXTRALIBS) $(LIBFUZZER_PATH)

target_dem_%_fuzzer$(EXESUF): target_dem_%_fuzzer.o $(FF_DEP_LIBS)
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $^ $(ELIBS) $(FF_EXTRALIBS) $(LIBFUZZER_PATH)

tools/target_dem_fuzzer$(EXESUF): tools/target_dem_fuzzer.o $(FF_DEP_LIBS)
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $^ $(ELIBS) $(FF_EXTRALIBS) $(LIBFUZZER_PATH)

tools/target_io_dem_fuzzer$(EXESUF): tools/target_io_dem_fuzzer.o $(FF_DEP_LIBS)
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $^ $(ELIBS) $(FF_EXTRALIBS) $(LIBFUZZER_PATH)


tools/enum_options$(EXESUF): ELIBS = $(FF_EXTRALIBS)
tools/enum_options$(EXESUF): $(FF_DEP_LIBS)
tools/sofa2wavs$(EXESUF): ELIBS = $(FF_EXTRALIBS)
tools/uncoded_frame$(EXESUF): $(FF_DEP_LIBS)
tools/uncoded_frame$(EXESUF): ELIBS = $(FF_EXTRALIBS)
tools/target_dec_%_fuzzer$(EXESUF): $(FF_DEP_LIBS)
tools/target_dem_%_fuzzer$(EXESUF): $(FF_DEP_LIBS)


CONFIGURABLE_COMPONENTS =                                           \
    $(wildcard $(FFLIBS:%=$(SRC_PATH)/lib%/all*.c))                 \
    $(SRC_PATH)/libavcodec/bitstream_filters.c                      \
    $(SRC_PATH)/libavcodec/parsers.c                                \
    $(SRC_PATH)/libavformat/protocols.c                             \

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

SUBDIR_VARS := CLEANFILES FFLIBS HOSTPROGS TESTPROGS TOOLS               \
               HEADERS ARCH_HEADERS BUILT_HEADERS SKIPHEADERS            \
               ARMV5TE-OBJS ARMV6-OBJS ARMV8-OBJS VFP-OBJS NEON-OBJS     \
               ALTIVEC-OBJS VSX-OBJS MMX-OBJS X86ASM-OBJS                \
               MIPSFPU-OBJS MIPSDSPR2-OBJS MIPSDSP-OBJS MSA-OBJS         \
               MMI-OBJS OBJS SLIBOBJS HOSTOBJS TESTOBJS

define RESET
$(1) :=
$(1)-yes :=
endef

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))))

include $(SRC_PATH)/fftools/Makefile
include $(SRC_PATH)/doc/Makefile
include $(SRC_PATH)/doc/examples/Makefile

libavcodec/avcodec.o libavformat/utils.o libavdevice/avdevice.o libavfilter/avfilter.o libavutil/utils.o libpostproc/postprocess.o libswresample/swresample.o libswscale/utils.o : libavutil/ffversion.h

$(PROGS): %$(PROGSSUF)$(EXESUF): %$(PROGSSUF)_g$(EXESUF)
ifeq ($(STRIPTYPE),direct)
    $(STRIP) -o $@ $<
else
    $(CP) $< $@
    $(STRIP) $@
endif

%$(PROGSSUF)_g$(EXESUF): $(FF_DEP_LIBS)
    $(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $(OBJS-$*) $(FF_EXTRALIBS)

VERSION_SH  = $(SRC_PATH)/ffbuild/version.sh
GIT_LOG     = $(SRC_PATH)/.git/logs/HEAD

.version: $(wildcard $(GIT_LOG)) $(VERSION_SH) ffbuild/config.mak
.version: M=@

libavutil/ffversion.h .version:
    $(M)$(VERSION_SH) $(SRC_PATH) libavutil/ffversion.h $(EXTRA_VERSION)
    $(Q)touch .version

# force version.sh to run whenever version might have changed
-include .version

install: install-libs install-headers

install-libs: install-libs-yes

install-data: $(DATA_FILES)
    $(Q)mkdir -p "$(DATADIR)"
    $(INSTALL) -m 644 $(DATA_FILES) "$(DATADIR)"

    uninstall: uninstall-data uninstall-headers uninstall-libs uninstall-pkgconfig

uninstall-data:
    $(RM) -r "$(DATADIR)"

clean::
    $(RM) $(CLEANSUFFIXES)
    $(RM) $(addprefix compat/,$(CLEANSUFFIXES)) $(addprefix compat/*/,$(CLEANSUFFIXES)) $(addprefix compat/*/*/,$(CLEANSUFFIXES))
    $(RM) -r coverage-html
    $(RM) -rf coverage.info coverage.info.in lcov

distclean:: clean
    $(RM) .version avversion.h config.asm config.h mapfile  \
        ffbuild/.config ffbuild/config.* libavutil/avconfig.h \
        version.h libavutil/ffversion.h libavcodec/codec_names.h \
        libavcodec/bsf_list.c libavformat/protocol_list.c \
        libavcodec/codec_list.c libavcodec/parser_list.c \
        libavfilter/filter_list.c libavdevice/indev_list.c libavdevice/outdev_list.c \
        libavformat/muxer_list.c libavformat/demuxer_list.c
ifeq ($(SRC_LINK),src)
    $(RM) src
endif
    $(RM) -rf doc/examples/pc-uninstalled

config:
    $(SRC_PATH)/configure $(value FFMPEG_CONFIGURATION)

build: all alltools examples testprogs
check: all alltools examples testprogs fate

include $(SRC_PATH)/tests/Makefile

$(sort $(OUTDIRS)):
    $(Q)mkdir -p $@

# Dummy rule to stop make trying to rebuild removed or renamed headers
%.h:
    @:

# Disable suffix rules.  Most of the builtin rules are suffix rules,
# so this saves some time on slow systems.
.SUFFIXES:

.PHONY: all all-yes alltools build check config testprogs
.PHONY: *clean install* uninstall*

这里强调两个地方,第一个是所有跟include有关的地方,这部分内容最终包含进来的话实际Makefile也相当庞大。

第二个是Makefile的入口all: all-yes这个地方。查遍整个ffmpeg,也查不到这个叫做all-yes的依赖在哪里, 查看log记录可以发现,最初这行代码其实是在tools/Makefile内部的,不过tools/Makefile内部依然没有all-yes这个依赖. log中作者说把这样移动到这里是为了保证这个地方是整个编译到开始。

实际上这里的all-yes没有任何实际意义,纯粹是一个记号,把它去掉也不影响编译。 原因是tools/Makefile内部也有all这个target,根据Makefile的语法规则, 如果有不同的依赖但是生成相同的目标,则make只会执行最后出现的目标,所以这里all:all-yes只是表示make从all开始, 但是首先执行的却是tools/Makefile中的all,即all: $(AVPROGS)这行代码。

关于第二点有兴趣的可以参考我之前的回复What does “all-yes” mean in ffmpeg Makefile


Similar Posts

Comments