Building Universal FFmpeg Custom Binaries

ByJason Bagley

Published Mon Apr 01 2024

Motivation

I am using a very pared down set of FFMpeg features for a macOS project that I build into a custom library. I had a script set up to configure the build which worked fine on my Intel based MacBook Pro. Then I upgraded to an Apple Silicon MacBookPro and wanted to run natively, or at least see what happened when I did. To build, FFMpeg uses autoconf which produces a makefile that then handles the build.

The simplest thing which could work but didn't

Autoconf supports passing flags for the compiler in the call to configure. I was already passing -arch x86_64 as part of --extra-cflags. Its documentation says that I may provide more than one architecture on macOS. That page notes that this doesn't always work.

Based on that, I tried -arch x86_64 -arch arm64', and as foreshadowed it indeed did not work. Autoconf's compiler tests would fail when trying the x86_64 build with clang. I went through some more permutations of arguments, but in the end I had to do separate builds for each architecture.

Building

I use a build directory rather than calling configure in the top of my FFmpeg working copy and dirtying it up. I am running this from that build dir.

Here is bash psuedocode of my build script:

for arch in x86_64 arm64; do

   rm -rf Makefile config.h ffbuild lib* tests 

    ../configure --cc=clang \
                 --extra-cflags="-arch $arch" \
                 --extra-ldflags="-arch $arch"  \
                 --build_suffix="$install_dir"  \
                 --prefix="path/to/where/I/want/the/libs/to/be/copied/by/make/install" \
                 # other flags for ffmpeg configuration, see '../configure -h' output
    make -j
    make install
done

Looping through the desired architectures, first it cleans the build directory to get rid of the previous architecture output, then configures, building and installing the library.

In the end, you have two sets of library files installed into the path passed with --prefix with the suffixes _x86-64 and _arm64 provided by the --build_suffix argument.

I needed to pass the architecture again with --extra-ldflags to avoid the autoconf compiler test failing. The most basic test for compiler support would fail when an x86_64 .o file couldn't be linked to an arm64 exectuable.

Linking The Universal Libraries

The call to lipo turned out to be simple. The trickier part for me was building the array of names to pass to lipo as input files. I don't remember the last time I used these.

lib_output_dir=<the same path I used as the --prefix argument to configure above>
cd "$lib_output_dir"
for lib_name in <lib names I produced in my build without the file extension, e.g. libavcodec>; do
    input_names=()
    for arch in x86_64 arm64; do
        input_names+=(${lib_name}_{$arch}.a)
    done
    /usr/bin/lipo -create -output ${lib_name}.a ${input_names[*]}
    rm ${input_names}[*]}
done
cd -

It changes to the directory where the first section installed the libraries. Of course, lipo could be run on the libs within the build folder, but my script was already installing them, and I found this more convenient and simple.

For each architecture, it builds the array of library filenames, including the architecture suffix of each. That array gives the typical DRY advantages and keeps the invocation of lipo simple.

You can confirm it worked using the file utility. You should see a line for each architecture it its output.

That's it. If you have a better way, or see something I can do better here, let me know.

Previous

Thinking Like a Programmer: Heuristics
We want to help you turbo-charge your decision making.