Building android-cuttlefish on Ubuntu 26.04: four layers of "too new"
I’m working through an AOSP course pinned to android-15.0.0_r36, and the course
device is Cuttlefish — Android’s
virtual reference device. Booting it needs host packages (cuttlefish-base,
cuttlefish-user) that you build yourself from the android-cuttlefish repo:
git clone https://github.com/google/android-cuttlefish
cd android-cuttlefish
tools/buildutils/build_packages.sh
On a supported distro that’s the whole story. My build machine runs
Ubuntu 26.04 LTS, which turned out to be newer than anything the pinned
toolchain had ever met. The script died four times, each failure one layer
deeper in the stack than the last: apt metadata, a prebuilt linker, a C
library header, another C library header. Here’s the whole onion, peeled.
Everything below is against android-cuttlefish 1.57.0 at commit
762bf3a53, bazel 8.5.1, glibc 2.42.
Layer 0: the script dies silently
The first run produced no error I noticed — just no debs. The only artifact
was base/cuttlefish-common-build-deps_1.57.0_amd64.deb, which is a strong
diagnostic on its own: that file is the equivs metapackage that
mk-build-deps generates from debian/control. Its presence means dependency
resolution started; the absence of everything else means installing it
failed, and the script’s set -e bailed before debuild ever ran — so there
was no .build log to read either.
The way to see why apt refused it:
apt-get -s install ./base/cuttlefish-common-build-deps_*.deb
That printed two problems.
Layer 1: libxml2 is not a package anymore
Depends: libxml2 but it is not installable
libxml2 bumped its soname (libxml2.so.2 → libxml2.so.16), and following
Debian convention the binary package was renamed accordingly: on Ubuntu 25.10+
there is libxml2-16, and a bare libxml2 package no longer exists.
android-cuttlefish’s base/debian/control hardcodes libxml2 in both
Build-Depends and the runtime Depends of cuttlefish-base.
Fix: a two-line patch to base/debian/control, replacing libxml2, with
libxml2-16, in both places. (The other unsatisfiable entry was bazel —
tools/buildutils/installbazel.sh hadn’t actually run; re-running it with
sudo set up Google’s apt repo and installed bazel 9.2.0.)
Layer 2: the hermetic linker wants the old libxml2 back
With dependencies sorted, debuild ran, bazel fetched its world (after
riding out some 429 RESOURCE_EXHAUSTED rate limits from
chromium.googlesource.com — those are transient, just retry; bazel caches
every repo it managed to fetch) — and then every single link step failed:
external/toolchains_llvm++llvm+llvm_toolchain_llvm/bin/ld.lld:
error while loading shared libraries: libxml2.so.2:
cannot open shared object file: No such file or directory
The cuttlefish build doesn’t use your system compiler. It downloads a
hermetic LLVM toolchain (LLD 19.1.7) via toolchains_llvm — prebuilt
binaries, built on an older distro, dynamically linked against
libxml2.so.2. The same soname bump from layer 1, but now baked into a
binary I can’t rebuild. Irony noted: the distro renamed a package, and the
“hermetic” toolchain turned out to be hermetic in every direction except its
own runtime dependencies.
Fix (no root needed): readelf -d showed ld.lld has
RUNPATH = $ORIGIN/../lib, i.e. it searches the toolchain’s own lib/
directory. So: grab the old libraries from Ubuntu 24.04’s archive and drop
them in there.
curl -sO http://archive.ubuntu.com/ubuntu/pool/main/libx/libxml2/libxml2_2.9.14+dfsg-1.3ubuntu3.8_amd64.deb
curl -sO http://archive.ubuntu.com/ubuntu/pool/main/i/icu/libicu74_74.2-1ubuntu3_amd64.deb
dpkg -x libxml2_*.deb x && dpkg -x libicu74_*.deb x # extract, no install
TLIB=~/.cache/bazel/_bazel_$USER/<hash>/external/toolchains_llvm++llvm+llvm_toolchain_llvm/lib
cp -a x/usr/lib/x86_64-linux-gnu/libxml2.so.2* x/usr/lib/x86_64-linux-gnu/libicu*.74* "$TLIB"/
One subtlety that cost me an iteration: RUNPATH is not transitive.
ld.lld found libxml2.so.2 via its own RUNPATH, but libxml2.so.2’s
dependency on libicuuc.so.74 is resolved using libxml2’s search path,
not the executable’s — and the copied libraries have none. So each copied
.so needs its own:
patchelf --set-rpath '$ORIGIN' "$TLIB"/libxml2.so.2 "$TLIB"/libicu*.74.2
After that, ldd $TLIB/../bin/ld.lld came back clean. Caveat: this lives in
bazel’s cache, so bazel clean --expunge wipes it. The durable variant is
sudo dpkg -i on those two debs — the old libxml2 and libicu74 packages
coexist happily with libxml2-16 since they ship different sonames.
Layer 3: glibc 2.42 made memchr const-correct, boringssl objects
Next run, the linker worked and the compiler complained — in boringssl:
internal.h:1052:10: error: returning 'const void *' from a function with
result type 'void *' discards qualifiers
[-Werror,-Wincompatible-pointer-types-discards-qualifiers]
return memchr(s, c, n);
glibc 2.42 wrapped memchr, strchr and friends in a _Generic macro
(__glibc_const_generic) so that passing a const pointer returns a const
pointer — the const-correct overloads C++ always had, now in C. boringssl’s
OPENSSL_memchr wrapper passes a const void * in and returns plain
void *, which used to be fine and is now a qualifier-discarding conversion.
boringssl builds with -Werror, so a new warning is a broken build.
Fix: disable exactly that diagnostic, workspace-wide, by appending to
base/cvd/.bazelrc (which the debuild-invoked bazel reads):
build --copt=-Wno-incompatible-pointer-types-discards-qualifiers
-Werror stays on for everything else; this only stops the one new warning
from being promoted to an error.
Layer 4: glibc 2.42 ships C23 once_flag, mesa already had one
One more run, one more layer — mesa this time:
c11/threads.h:121:25: error: typedef redefinition with different types
('pthread_once_t' (aka 'int') vs '__once_flag')
typedef pthread_once_t once_flag;
mesa bundles a C11-threads emulation layer (src/c11/threads.h) that
typedefs once_flag on top of pthreads. C23 moved call_once/once_flag
into <stdlib.h>, and glibc 2.42 implements that — so the moment mesa’s
header includes <stdlib.h> (which it does, before its own typedef), the
name is already taken, with a different underlying type.
Fix: patch mesa to yield when glibc got there first. The neat part is
how to apply it: the cuttlefish build fetches mesa as a bazel
git_repository, and that rule accepts a patches attribute. So the patch
lives in the android-cuttlefish tree and bazel re-applies it on every fetch —
it survives cache wipes, unlike the layer-2 hack.
The patch itself guards the typedef behind the macro glibc defines:
# ifdef ONCE_FLAG_INIT
/* glibc >= 2.42 <stdlib.h> already provides C23 once_flag / ONCE_FLAG_INIT */
# else
typedef pthread_once_t once_flag;
# define ONCE_FLAG_INIT PTHREAD_ONCE_INIT
# endif
plus a one-line cast in threads_posix.c (glibc’s once_flag is a
one-member struct wrapping the same int, so pthread_once needs a
(pthread_once_t *) on the pointer). Registered in
base/cvd/build_external/mesa/mesa.MODULE.bazel:
git_repository(
name = "mesa",
build_file = "@//build_external/mesa:BUILD.mesa.bazel",
commit = "224e91e39836d3ca31f80df2e0379e55c70574f8",
patch_args = ["-p1"],
patches = ["@//build_external/mesa:glibc-2.42-c23-once_flag.patch"],
remote = "https://android.googlesource.com/platform/external/mesa3d",
)
And then it just built
cuttlefish-base_1.57.0_amd64.deb
cuttlefish-common_1.57.0_amd64.deb
cuttlefish-defaults_1.57.0_amd64.deb
cuttlefish-integration_1.57.0_amd64.deb
cuttlefish-metrics_1.57.0_amd64.deb
cuttlefish-orchestration_1.57.0_amd64.deb
cuttlefish-user_1.57.0_amd64.deb
Seven debs in the repo root (debuild writes to the parent of the package
directory), ready for dpkg -i.
What I’m taking away
- A missing log is itself a symptom. No
.buildfile plus a lone*-build-deps*.debpins the failure to themk-build-deps -istep before the real build starts.apt-get -s installon that deb names the culprit. - Each retry was cheaper than the last. bazel’s action cache carried ~10,000 completed actions across failures, so iterating on fixes cost minutes, not hours. Fail-fix-rerun is a perfectly good strategy here.
- “Hermetic” toolchains still touch the host — at minimum through the
dynamic loader.
lddon the toolchain’s binaries is the first thing to check when a fresh distro breaks a downloaded toolchain. - glibc 2.42 is a mini soname-bump event for C codebases. The
const-generic string functions and the C23
<stdlib.h>additions each broke a vendored dependency that had been warning-clean for a decade. Expect more of these anywhere-Werrormeets a bleeding-edge distro. - Prefer fixes that live in the source tree (a
debian/controlpatch, a.bazelrcline, apatches=attribute) over fixes that live in caches. Two of my four fixes survivebazel clean --expunge; the ones that don’t are documented right next to the ones that do.