508. FFmpeg’s source is published beside its binaries, from the same place
Date: 2026-10-01
Status
Accepted. Closes book/src/TODO.md’s “The LGPL corresponding-source offer for
FFmpeg is not decided”, and with it the open row in docs/media-plan.md.
Completes what
ADR-0495 started when
it put FFmpeg on Maven Central, and adds a second refusal beside its
missing-target one.
Context
goldberry-media carries FFmpeg’s five shared libraries, with dav1d linked
statically into libavcodec, as ffmpeg-<target> classifier jars on Maven
Central (ADR-0495). Snapshots carry them as well as releases: every push to
master puts them in Central’s snapshot repository, where anyone can download
them.
FFmpeg is LGPL-2.1-or-later. Those jars contain the Library itself in object form, so the section that applies is §4:
You may copy and distribute the Library […] in object code or executable form […] provided that you accompany it with the complete corresponding machine-readable source code […]. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code.
The TODO entry cited §6, which is about a work that uses the Library: an application linked against it. That is the application’s obligation, not this build’s. What this build does is §4’s case, and §4 offers two ways to comply. It does not offer a written offer.
§0 says what “complete source code” means for a library: “all the source code
for all modules it contains, plus any associated interface definition files,
plus the scripts used to control compilation and installation of the library”.
So the source alone is not enough. The scripts are the media superbuild,
because that is what turned the source into these files. dav1d is a module the
shipped libavcodec contains. Its own licence, BSD-2, asks only for the notice.
Its source is still part of FFmpeg-as-shipped’s complete source.
A snapshot is not an exception. The licence concerns distribution, not version
labels, and a -SNAPSHOT jar on Central is downloadable object code like a
release is. The binaries that have already gone out as snapshots had the tags
and configure line in ffmpeg-NOTICE.txt, but no source.
The source also has to be exactly what was built. The superbuild clones FFmpeg
and dav1d from git with ExternalProject at the tags pinned in
gradle/libs.versions.toml. A tag can be moved, and nothing checked that one
had not been.
Decision
A classifier jar, ffmpeg-sources, goes beside the binaries under the same
coordinates. That makes it §4’s “equivalent access … from the same place”:
io.github.digitalsmile:goldberry-media:<version>:ffmpeg-sources, in the same
repository and with the same version, published by the same Gradle invocation.
There is one jar per version, not one per target, because the source is the
same for every target. It is about 24 MB compressed: 10,664 entries and
100 MB unpacked.
README.txt what this is, which binaries it is the source of, how
to rebuild offline and how to relink
ffmpeg-n8.1.3/ FFmpeg at the tag, as `git archive` writes it, + VERSION
dav1d-1.5.4/ dav1d at the tag, as `git archive` writes it
recipe/
media/src/main/cmake/ the superbuild: CMakeLists.txt, package.cmake,
checkout.cmake, ffmpeg_layout.c
gradle/libs.versions.toml where it reads the tags and commits from
notices/<target>/ffmpeg-NOTICE.txt each published target's exact configure line
licenses/ffmpeg.txt, licenses/dav1d.txt
META-INF/ Goldberry's LICENSE, NOTICE and third-party notices
The trees come from git, checked against pinned commits. :media:ffmpegSourcesJar
uses UpstreamSource in build-logic for each upstream. It shallow-fetches the
tag into a bare repository under media/.deps/sources, which outlives build/
so that later archives need no network. It refuses the tag unless it names the
commit pinned beside it in the catalog (ffmpegCommit, dav1dCommit). Then it
runs git archive on that commit. The clone’s info/attributes turns off
export-ignore and export-subst, so a future upstream attribute cannot leave
a file out or rewrite one. The superbuild runs the same check: checkout.cmake,
as an ExternalProject step between download and configure, refuses a clone
that is not at the pinned commit. The binaries and the jar are each checked
against the same commit, so the published source is the built source.
Two inputs were possible, and both meet “reproducible”. git archive of a
commit is a pure function of the commit, and the jar’s timestamps, order and
modes are fixed: the archive uses tar.umask=0022 and the files that do not
come from git are 0644. Two builds of the jar, the second from a fresh clone,
gave the same SHA-256. Only the git trees met “exactly what was built”. FFmpeg’s
release tarball is made by FFmpeg’s release process, not from the checkout this
build compiles. It contains a VERSION file the tag does not, and nothing
guarantees the rest is identical. A checksum committed for it would prove that
the tarball is the tarball, not that it is our source. A pinned commit id is the
same kind of integrity check as a checksum: it is a hash of the content and its
history, and it is the id the build itself checks.
The jar adds one file to the trees, ffmpeg-n8.1.3/VERSION, which holds the
tag. Outside a checkout, FFmpeg’s version.sh falls back to that file, as its
release tarballs do. With it, a rebuild reports n8.1.3 as the shipped
libraries do, and not the 8.1.3 in RELEASE. The README says this is the one
file that does not come from git.
The superbuild builds from the jar with nothing fetched.
-DGOLDBERRY_FFMPEG_SOURCE_DIR and -DGOLDBERRY_DAV1D_SOURCE_DIR replace each
clone with a tree on disk. The commit check is skipped there, because that
check was made when the jar was. The README gives the whole command line, and
the recipe is the same code the published builds ran, so it can rebuild them.
On linux-x64, an unpacked jar rebuilt with nothing fetched. The five libraries
came to 6696 KB, against 6708 KB from the git checkout,
ffmpeg-layout.properties was identical byte for byte, and libavutil
reported n8.1.3.
Publication refuses the binaries without the source. goldberry.publish
calls CorrespondingSource.require for every AbstractPublishToMaven task in
the graph. It runs when the task graph is ready, before any task runs, and
covers mavenLocal and Central alike. Any ffmpeg-<target> classifier without
ffmpeg-sources fails the build, for snapshots and releases alike, with a
message that names this ADR. The check is placed there and not in :media’s
publish task because modules upload one after another. A refusal inside the
ninth module would leave eight on Central, which is what happened with the
javadoc in ADR-0405. media/build.gradle
attaches the jar wherever it attaches a target, and removing that line is what
the check catches.
The jar needs git and the network, not FFmpeg. It fetches and does not
compile, so publish.yml’s Java-only maven job builds it with no change to
the workflow. The fetch reaches git.ffmpeg.org and code.videolan.org, the
hosts the Media workflow already clones from.
The notices say where the source is. ffmpeg-NOTICE.txt, written by
package.cmake into every binary jar, now gives each upstream’s commit and
names the ffmpeg-sources classifier of the same version. NOTICE, which
every Goldberry jar carries, and THIRD-PARTY-NOTICES.md point to it too.
Consequences
- Every version on Central carries about 24 MB more. A snapshot replaces the previous one, so the snapshot repository does not accumulate copies.
- The
mavenjob now depends on two more hosts being up. A failed fetch fails the publication, which is the right result: publishing the binaries without their source is what this decision rules out. - Moving the
ffmpegordav1dtag also means moving its commit pin (git ls-remote <repository> 'refs/tags/<tag>^{}'). If it is forgotten, the superbuild’s first clone fails, before anything is compiled. - A rebuild from the jar is not byte-identical to the published libraries.
FFmpeg stores its configure command in
libavutil, including the install prefix and thepkg-configpath (avutil_configuration()). dav1d names its version1.5.4rather than1.5.4-0-g54706fcwhen there is no.git, and the build directories end up in the files. The README says so. What Goldberry reads the libraries by, the layout file, comes out identical. - Only the linux-x64 rebuild from the jar has been run. The Windows and macOS steps are the superbuild’s own steps, with the same caveats: the Windows superbuild has not been built yet (ADR-0495).
- An application that redistributes these binaries takes on §4 itself. Shipping
the
ffmpeg-sourcesjar beside them meets it, andTHIRD-PARTY-NOTICES.mdsays so. UpstreamSourceTestmakes a repository with git, archives a tag throughexport-ignoreandexport-subst, refuses a moved tag, and archives again offline.CorrespondingSourceTestrefuses binaries without the source and checks that the convention,:media, the catalog and the three notices are wired to it.
Alternatives considered
- Upstream release tarballs, verified against a committed checksum. This is
reproducible, but not of the source that was built. The superbuild compiles a
git checkout, and FFmpeg’s tarball is a separate product of FFmpeg’s release
process, with at least a
VERSIONfile the tag does not have. Changing the superbuild to build from tarballs would make the two match, at the cost of a second fetch path for the build and a checksum to maintain beside a tag. The commit pin already gives a hash, and it is the one the build checks. - B: link to upstream (
ffmpeg.org/releases,git.ffmpeg.org). §4 accepts access “from the same place” as the object code. Another organisation’s server is not that place, and it can drop or move a tag without notice. A link also gives neither the recipe §0 counts as part of the source nor a single pinned dav1d. FFmpeg’s own legal checklist asks distributors to host the source themselves. - C: a written offer. §4 does not offer this option to someone distributing the Library itself. The offer is a §6(c) mechanism for works that use it, and the GPL’s version has to be honoured for three years by whoever receives a request. That makes it a process someone has to keep running, which costs more than 24 MB on Central.
- D: don’t ship FFmpeg on Central. ADR-0495 put it there because a
goldberry-mediawith nothing to load fails on every machine that adds it. Leaving the binaries out to avoid publishing their source would bring that back, for snapshots too.