Skip to content

build: build FFmpeg on every platform and make the media module part of the normal build - #292

Open
devopvoid wants to merge 11 commits into
feat/native-extension-apifrom
feat/media-platform-builds
Open

devopvoid wants to merge 11 commits into
feat/native-extension-apifrom
feat/media-platform-builds

Conversation

@devopvoid

Copy link
Copy Markdown
Owner

Stacked on #291, which adds the media module itself. This is the part that makes it build everywhere rather than only on the machine it was written on.

What this does

Cross-compilation for all seven targets. The media module had no platform profiles beyond Windows, so a Linux or macOS build had no compiler or sysroot for its target and CMake simply built for the host. It now takes a toolchain file the way webrtc-jni does, and its profiles mirror webrtc-jni's by name and by the file each one selects, so both native libraries of a given classifier are built for the same target the same way.

Reusing those toolchain files rather than writing a second set is deliberate: they are where this project says which compiler and which sysroot a target uses, and two answers would drift apart.

What configure is told about the target is worked out from what the toolchain file already set — the architecture from CMAKE_SYSTEM_PROCESSOR, the target OS from the platform, the compiler from CMAKE_C_COMPILER, the target flags from CMAKE_C_FLAGS, the sysroot from CMAKE_SYSROOT — so a target CMake can already build for needs no new case here. Apple is the exception it has to make: there the architecture is a compiler flag rather than a different compiler.

Cross-compiled arm and aarch64 start with --disable-asm. An assembler for a foreign architecture is a build prerequisite of its own and nothing here is fast enough yet to need one; correctness on every platform first, speed where it is measured.

CI. Windows gets MSYS2 with make and nasm, because FFmpeg's configure and makefiles are shell scripts and building it needs a POSIX shell even with MSVC; MSYS2_ROOT is exported so the module's CMake finds it wherever the runner image put it. Linux and macOS get nasm, which macOS needs even though the runner is Apple Silicon, because the Intel build is cross-compiled on it.

The FFmpeg install directory is cached the same way the WebRTC checkout already is, keyed by FFmpeg version, platform and the hash of the dependency script and the module pom, so only the first build of a platform pays for it. The three native build jobs in both workflows now check out submodules, since the FFmpeg source is one; test-natives does not, because it runs against downloaded natives and never builds the module.

The module joins the normal build. That also collapses the awkward part of the examples module. The media example needed a source directory, a compile execution and an output directory of its own, purely because a JPMS requires cannot be made conditional and the module it names has to exist. With the module always there, the example moves in beside the others and the whole profile becomes one dependency and one requires.

What is verified, and what is not

Verified on this machine:

  • Full reactor mvn verify green: 168 tests in webrtc, 19 in webrtc-java-media, all five modules succeeding
  • FFmpeg still builds from nothing for windows-x86_64, its configure line unchanged apart from an explicit --arch=x86_64
  • MediaFileExample runs from a plain mvn exec:java, with no profile and no extra class path
  • Every workflow and action file parses, and the VitePress build is green

Not verified: six of the seven platforms. Only windows-x86_64 can be built where this was written — no ARM64 cross tools, no Linux, no macOS. Everything for the other six is written from FFmpeg's documented cross-compile flags and this project's existing toolchain files, and CI is the only thing that can prove it. Expect the first run to want corrections.

Worth deciding before merge

The last commit makes the media module part of the normal build, which means anything building this project now needs the third-party/ffmpeg submodule checked out, plus make and nasm to build it. That is a new prerequisite for every contributor, not just for those who want the media module.

The original plan put that step after every platform builds in CI, not before. It is here so that CI builds the module at all and can prove the other six targets. If CI is not green, or if the prerequisite is not wanted yet, 4399bb2 is the tip of the branch and drops cleanly on its own; the first two commits stand without it.

The media module had no platform profiles beyond Windows, so a Linux or
macOS build had no compiler or sysroot for its target and CMake simply
built for the host.

It now takes a toolchain file the way webrtc-jni does, and its profiles
mirror webrtc-jni's by name and by the file each one selects, so both
native libraries of a given classifier are built for the same target the
same way. Keeping one set of toolchain files rather than a second copy
is deliberate: they are where this project says which compiler and which
sysroot a target uses, and two answers would drift.

What configure is told about the target is worked out from what the
toolchain file already set -- the architecture from
CMAKE_SYSTEM_PROCESSOR, the target OS from the platform, the compiler
from CMAKE_C_COMPILER, the target flags from CMAKE_C_FLAGS, the sysroot
from CMAKE_SYSROOT -- so a target CMake can already build for needs no
new case. Apple is the exception it has to make: there the architecture
is a compiler flag rather than a different compiler.

Cross-compiled arm and aarch64 start with --disable-asm. An assembler
for a foreign architecture is a build prerequisite of its own, and
nothing here is fast enough yet to need one; correctness on every
platform first.

Only windows-x86_64 is verified, because it is the only target this
machine can build. It still builds from nothing and its configure line
is unchanged apart from an explicit --arch=x86_64, and the module's 19
tests pass. Everything else waits on CI.
The media module is about to become part of the normal build, so every
native build job has to be able to produce its FFmpeg, and none of them
could.

Windows gets MSYS2 with make and nasm, because FFmpeg's configure and
makefiles are shell scripts and building it needs a POSIX shell even
with MSVC. MSYS2_ROOT is exported so the media module's CMake finds it
wherever the runner image put it. Linux and macOS get nasm, which is
what FFmpeg assembles its x86 code with; macOS needs it even though the
runner is Apple Silicon, because the Intel build is cross-compiled on it.

The FFmpeg install directory is cached the same way the WebRTC checkout
already is, keyed by FFmpeg version, platform and the hash of the
dependency script and the module pom, so only the first build of a
platform pays for it.

The three native build jobs in each workflow now check out submodules,
since the FFmpeg source is one. test-natives does not, because it runs
against downloaded natives and never builds the module.
The module was opt-in behind a profile while it was being brought up.
It now builds with everything else, which is also what lets CI prove it
on the platforms this machine cannot build for.

That collapses the awkward part of the examples module. The media
example needed a source directory, a compile execution and an output
directory of its own, purely because a JPMS "requires" cannot be made
conditional and the module it names has to exist. With the module always
there, the example moves in beside the others and the whole profile
becomes one dependency and one "requires", exactly as its comment said
it would.

Anything building this project now needs the third-party/ffmpeg
submodule checked out, and make and nasm to build it. The README and the
guide say so where they used to describe the profile.
Every CI job failed at dependency resolution before a single mojo ran:

  Could not find artifact
  dev.onvoid.webrtc:webrtc-java-media:jar:windows-x86_64:0.19.0-SNAPSHOT

The module declared a dependency on its own classifier artifact so that
anything using it would get the natives along the way. That cannot work
on a clean machine: the artifact is built and attached by this same
module, so at the moment its dependencies are resolved nothing has
produced it yet. It only passed locally because an earlier install had
left the artifact in the local repository.

webrtc-java can do this because webrtc-jni, a module earlier in the
reactor, is what builds its natives, and because the artifact has been
published besides. Neither is true here.

So the dependency is gone, and anything using the module asks for the
natives itself with a second dependency carrying the platform
classifier. webrtc-examples does, and the guide says so. The module's
own tests never needed it: Surefire is given the directory the CMake
build collects the natives into.

Verified by emptying dev/onvoid/webrtc/webrtc-java-media out of the
local repository first, which reproduced the CI failure exactly, and
then building clean: 168 tests in webrtc and 19 in webrtc-java-media,
all five modules green.
Every job failed configuring the media module:

  CMake Error at dependencies/ffmpeg/CMakeLists.txt:162 (set):
    when parsing string
      C:\Users\runneradmin/ffmpeg/windows-x86_64
    Invalid character escape '\U'

Two things had to line up for this, which is why it never showed up
locally. Maven hands the install directory over as ${user.home} plus a
suffix, and on Windows that is a backslash path, which CMake reads as
the start of an escape sequence. And to_shell_path was a macro, so its
argument was substituted into the body as text rather than passed as a
value, leaving CMake to parse C:\Users\... as code.

It stayed hidden because that code only runs when FFmpeg has to be
built. This machine already had it installed, so every local build took
the short-circuit and never reached the macro. CI, starting from
nothing, reached it immediately.

The path is now normalised with file(TO_CMAKE_PATH) as it arrives, and
to_shell_path is a function, which passes its arguments as values.
Either change alone would have been enough; both are worth having,
since the first fixes this input and the second fixes the class of it.

Verified the way it should have been the first time: by deleting the
FFmpeg install directory so that the build path is the one that runs.
The failure reproduces exactly beforehand, and afterwards
"mvn clean package -DskipTests" builds FFmpeg from nothing and every
module succeeds, with the module's 19 tests green.
… with

The first CI run that reached FFmpeg turned up three separate faults,
all of them from reusing webrtc-jni's toolchain files. Those files are
not neutral descriptions of a target: they encode how webrtc-jni links
WebRTC's C++ ABI, and this module links no C++ ABI at all.

On Linux they compile with -nostdinc++, because webrtc-jni supplies
WebRTC's own libc++ through include paths of its own. This module
inherited the flag and none of the paths, so its first #include <string>
failed. It now uses toolchain files of its own that name the
distribution's cross compilers and nothing else. linux-x86_64 needs no
file at all, being built natively on the runner.

Cross builds also tripped over stripping. make install strips what it
installs with whichever strip is on the PATH, and the host's cannot read
a binary for another architecture: "unable to recognise the format of
the input file". FFmpeg is configured with --disable-stripping when
cross-compiling, and the toolchain files name the cross binutils.

On macOS the toolchain file set CMAKE_SYSTEM_NAME, which makes CMake
report a cross build even when host and target agree, so configure was
handed --enable-cross-compile for a native build along with
--arch=aarch64. Apple's clang knows that architecture as arm64 and
rejects the other outright, which configure reports only as being unable
to create an executable. Neither macOS target is a cross build -- the
Intel one runs under "arch -x86_64" -- so neither takes a file now, and
the architecture is spelled the way Apple spells it.

windows-aarch64 passed and keeps webrtc-jni's file, which suits it:
there the C++ standard library comes from MSVC either way.

Verified as far as this machine can: windows-x86_64 unaffected, with the
full reactor green. The other targets wait on CI again.
windows-x86_64 built everything, including the media module, and then
failed in webrtc-java-examples:

  Could not find artifact
  dev.onvoid.webrtc:webrtc-java-media:jar:windows-x86_64:0.19.0-SNAPSHOT

The examples asked for the media module's natives as a dependency. CI
runs "mvn package" and then "mvn -B jar:jar surefire:test", and the
second of those invokes goals directly rather than running a lifecycle,
so nothing reaches the package phase where that jar is attached and
there is nothing to resolve against. The first command had succeeded
moments earlier, which is why only the second failed.

webrtc-java survives the same pattern only because its classifier
artifact has been published and is already in the local repository.

So the natives are off the dependency graph entirely. The example needs
them at run time, and exec-maven-plugin is pointed at the directory the
CMake build collects them into, which is how the media module's own
tests have always found them.

Verified by running both CI commands in order against a local repository
with the media artifacts deleted: 168 tests in webrtc and 19 in
webrtc-java-media, every module green, and the example still plays.
Both macOS jobs still stopped at the same place:

  cc is unable to create an executable file.
  C compiler test failed.

The configure line was right by then -- --arch=arm64, no spurious
cross-compile -- and the only unusual thing left on it was --cc, naming
the compiler inside the Xcode toolchain:

  --cc=/Applications/Xcode_16.1.app/.../XcodeDefault.xctoolchain/usr/bin/cc

Calling that binary directly skips the /usr/bin/cc shim, and the shim is
what runs xcrun to point SDKROOT at an SDK. Without one there is no
libSystem to link against, so the link test fails and configure reports
it as being unable to create an executable. So --cc is no longer passed
on Apple: configure's own default is plain "cc", which goes through the
shim. Linux still gets it, since there the compiler is chosen
deliberately and there is no shim to lose.

That diagnosis came from reading a configure line and reasoning about
it, which is a poor way to fix a build. configure writes the real reason
into ffbuild/config.log and says almost nothing on the console, and on a
build machine nobody can reach that is the same as writing it nowhere.
The tail of that file is now printed when the build fails, so the next
one of these is read rather than guessed at.

Windows and Linux are unaffected: the argument was already withheld on
Windows, and the rest only runs after a failure. Verified by rebuilding
windows-x86_64 from nothing.
The media natives were built with the distribution's compilers and no
sysroot, so they needed a newer glibc than webrtc-java's do. A machine
could have run one and not the other, which is not a sensible thing to
ship in the same release.

They now build through webrtc-jni's toolchain files, which is what the
plan said in the first place, and so against the same sysroot. That also
means -nostdinc++, since everything Linux in this project uses the libc++
WebRTC bundles rather than whatever the build machine happens to have.
Not having understood that is what sent this down the distribution
toolchain road: the flag arrived without the include paths that go with
it and the first #include <string> failed, which looked like a reason to
abandon those files rather than to finish using them.

So this module now takes libc++ from webrtc.install.dir, and webrtc-jni
has to be built before it on Linux. That is the only thing it takes from
that build, and it is a C++ standard library, not any part of WebRTC: no
WebRTC symbol is linked here, and the two native libraries still meet
only through the C function table. The build says so plainly if the
headers are not there.

While in here, cross-compilation is decided by comparing host and target
rather than by CMAKE_CROSSCOMPILING, which every toolchain file in this
project sets simply by naming a system. Believing it is what handed
configure --enable-cross-compile for a native macOS build earlier, and
it would have done the same for linux-x86_64.

Verified on windows-x86_64, which is unaffected and still builds FFmpeg
from nothing with 19 tests green. Linux waits on CI.
The test-natives lane existed to prove that a cross compiled native
library actually runs on the machine it was built for, and it only ever
did that for webrtc-java. The media natives were built, packaged,
uploaded nowhere and never loaded on a real arm or arm64 machine, so
nothing said whether FFmpeg and the media library work there or merely
compile.

Both jars are uploaded now. The lane installs webrtc-java's as before,
and unpacks the media one into the directory the media module's tests
already read natives from, which is where its own build collects them.
Nothing else had to be taught where to look.

The native build is skipped there with -DskipNativeBuild, which turns
the two CMake executions off by moving them to no phase. Building FFmpeg
again on a runner whose whole job is to run it would prove nothing and
cost a quarter of an hour.

webrtc is installed rather than only tested, so that the media module
resolves it from the repository. Both in one reactor does not work, and
the reason is worth writing down because it is the second time it has
come up: Maven substitutes a module's own output for a dependency on it,
and a classifier artifact of that same module, which is where the
natives live, never reaches the class path. That is the same shape as
the failure that broke the examples lane.

Verified by doing locally what the lane does: unpacking the classifier
jar into target/natives and running both commands. 168 tests in webrtc,
19 in webrtc-java-media, no CMake in the second, and the media tests
load the unpacked libraries rather than any this machine built. The
first attempt, both modules in one reactor, failed exactly as described
above, which is how the reason is known rather than guessed.
The two arm lanes failed compiling the module, every translation unit
dying inside libc++ before it reached a line of ours:

  _LIBCPP_HARDENING_MODE_DEFAULT is not defined. This definition should
  be set at configuration time

linux-x86_64 passed, and the difference is in the toolchain files:
x86_64-linux-clang.cmake sets -nostdinc++ and the hardening mode itself,
while aarch64-linux-clang.cmake and aarch32-linux-clang.cmake set
neither. webrtc-jni never notices, because it links the webrtc target
and takes both from its PUBLIC flags. Nothing carries them to a module
that links no WebRTC target, which is this one by design.

So the module sets them on itself. The bundled libc++ has wanted its
hardening mode chosen at configuration time since branch-heads/7977, and
-nostdinc++ is what keeps clang from putting the GCC libstdc++ headers
it finds in the sysroot on the implicit search path, where they collide
with the bundled ones.

This is the same lesson as the last two rounds, arriving once more:
webrtc-jni's toolchain files describe half of an arrangement, and the
other half lives on the webrtc target. Reading only the file leaves the
half that matters behind.

Windows is untouched and still builds with 19 tests green. The arm lanes
wait on CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant