A four-round investigation: bitcode flag, a misleading error, a real ordering bug, and finally LTO
13. A four-round investigation: bitcode flag, a misleading error, a real ordering bug, and finally LTO
This entry covers four CI failures that looked like the same issue (same error message, same hex value) but turned out to have three different root causes discovered across four separate rounds of investigation — worth reading as one continuous story, since each round corrected something believed settled in the previous one.
Round 1 — what looked like the whole story: the XCFramework assembly step failed with:
error: unable to find any architecture information in the binary at
'.../libmpv-combined.a': Unknown header: 0xb17c0de
ffmpeg.sh was passing -fembed-bitcode in --extra-cflags. Bitcode is
Apple's abandoned intermediate representation for App Store binaries,
deprecated starting Xcode 14 and non-functional by Xcode 16 (this
project's CI toolchain). 0x0B17C0DE is genuinely LLVM's real bitcode
wrapper magic number (verified directly against LLVM's own
documentation) — so this diagnosis wasn't wrong, exactly, but it turned
out to be incomplete: removing -fembed-bitcode was a legitimate fix
for a real latent problem (that flag becoming actively harmful on modern
Xcode, worth removing regardless), but it did not fix this particular
CI failure, because it wasn't actually the cause of it.
Round 2 — the error persisted after the "fix," with a misleading
detour: a later CI run, on a commit that no longer passed
-fembed-bitcode anywhere, hit the exact same error. Re-investigating
led first to a wrong turn: ci.sh's error handler only dumped
meson-log.txt (the meson setup/configure-phase log) on failure, which
this run showed ending in a completely successful-looking feature
summary — creating a false impression that the configure step was
somehow silently failing in a way that log couldn't show. This was a
real, separate gap worth fixing (meson-log.txt alone can't show a
compile-phase failure, since ninja's own output is what actually needs
inspecting for that), and ci.sh's error handler was improved to say so
explicitly. But it turned out this diagnostic gap wasn't the actual
explanation either.
Round 3 — the real root cause: carefully re-reading a full, later CI log line by line (not just grepping for "error") surfaced this:
==> Building mpv for ios-arm64
Building mpv-ios for ios-arm64...
Combining 18 static libs for ios-arm64...
"Building mpv-ios for ios-arm64..." should never appear here — this is
mpv-ios.sh's (the all-platform XCFramework assembly script's) own log
line, printed from inside ci.sh's per-platform loop, on its very
first (ios-arm64) iteration, before ios-arm64-simulator or
ios-x86_64-simulator had been built at all.
The cause: ci.sh's build loop called
./buildall.sh --platform "$platform" -n mpv-ios — passing mpv-ios
as the target name, not mpv. buildall.sh's own build() function
treats a target literally named mpv-ios as a special case that directly
invokes scripts/mpv-ios.sh (see that function's
if [[ "$1" == "mpv-ios" ]] branch) instead of building the mpv
dependency for the one platform currently being iterated. This meant
every single loop iteration was prematurely re-running the entire
XCFramework assembly — libtool-merging whatever partial, inconsistent
set of per-platform .a files happened to exist in prefix/ at that
moment, including platforms whose mpv hadn't been built yet in this
run. The resulting libmpv-combined.a was never a coherent, complete
archive — hence "unable to find any architecture information." The
0xb17c0de bitcode magic number showing up was very likely a genuine
leftover artifact from an old, pre-fix cached .a (from before entry 13
round 1's -fembed-bitcode removal) being swept into one of these
premature, incomplete merges — a real bitcode-tainted file was involved,
just not as the direct cause of this error the way round 1 assumed.
Actual fix: changed -n mpv-ios to -n mpv in ci.sh's per-platform
loop, so each iteration builds only the mpv dependency for its own
platform, and mpv-ios.sh (the real XCFramework assembly) runs exactly
once, after the loop, as originally intended.
A cache-invalidation footnote: this project's BUILD_LOGIC_REV
marker (see entry 6's cache-busting mechanism, later broadened in round 1
of this entry) was already in place and correctly bumped for the
-fembed-bitcode removal — that part of the process worked as designed.
It just wasn't sufficient on its own, since the actual bug wasn't a
compiled-artifact staleness problem at all, but a live logic error in how
ci.sh invoked buildall.sh on every run, cache or no cache.
Lesson: an error message pointing at a plausible, well-known culprit (a deprecated bitcode flag, complete with a matching magic-number coincidence) can be a real, legitimate problem to fix and still not be the problem causing the specific failure in front of you. Two rounds of "fix the thing that looks right" didn't resolve this — what did was reading a complete, unfiltered CI log line-by-line for output that shouldn't be there at all (an XCFramework-assembly log line appearing inside what should have been a single-platform dependency-build loop), rather than continuing to pattern-match on the same error signature across successive rounds. Worth remembering that grepping a log for "error" finds where something failed, not necessarily why — the actual explanatory line here was a completely unremarkable-looking status message in the wrong place, not anything that says "error" at all.
Round 4 — the same error, a third time, after the ordering bug was
genuinely fixed: with ci.sh corrected (round 3) and confirmed via log
to now build each platform exactly once and run XCFramework assembly
exactly once, afterward — the exact same Unknown header: 0xb17c0de
error still occurred. This time, tracing the log confirmed every
libtool -static merge (all three platforms) completed successfully
with no errors, and the failure happened afterward, specifically inside
xcodebuild -create-xcframework reading back ios-arm64's freshly,
correctly-merged libmpv-combined.a. This ruled out the round-3 ordering
bug as a contributing factor to this specific failure (it was real and
worth fixing regardless, but wasn't a cause of this error either) and
pointed at a genuinely corrupt object file somewhere in the 18 static
libs being merged for that platform.
Checking every buildscripts/scripts/*.sh file again for any remaining
bitcode-related flag turned up nothing (the -fembed-bitcode removal
from round 1 was confirmed still in place, and no other script ever had
it). This led to research into what else can cause a compiler to embed
LLVM bitcode/IR into an object file: LTO (Link-Time Optimization). clang
implements LTO by embedding LLVM IR/bitcode into object files as an
inherent part of the mechanism — not only when the separate
-fembed-bitcode flag is explicitly passed. dav1d.sh had
-Db_lto=true in its meson setup, and meson has a long-documented,
known-broken interaction between b_lto and static libraries
(mesonbuild/meson#1646) — exactly the --default-library=static
configuration this project's crossfile forces for every dependency.
Actual final fix: removed -Db_lto=true from dav1d.sh. As with the
earlier -fembed-bitcode removal, libtool -static merged the
LTO-tainted dav1d objects into libmpv-combined.a without any complaint
— the corruption was only ever caught later, by xcodebuild -create-xcframework's stricter validation, which is part of why this
took multiple rounds to fully localize: the tool that actually creates
the merged archive doesn't validate architecture information the way the
tool that consumes it afterward does.
Lesson, extending the same theme from round 2: "no -fembed-bitcode
anywhere" turned out not to mean "no bitcode/IR anywhere in any object
file" — LTO is a second, independent path to the same class of problem,
enabled by a completely different-looking meson option with no obvious
naming connection to "bitcode" at all. When a symptom is known to be
caused by a category of thing (embedded LLVM bitcode/IR) rather than one
specific flag, it's worth searching for every mechanism that produces
that category, not just the first one found — grep-ing for the literal
string that fixed it last time (bitcode) would never have found
-Db_lto=true, since that option's name doesn't mention bitcode at all.