Building android-cuttlefish on Ubuntu 26.04: four layers of "too new"

· aospcuttlefishdebugginglinux


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.2libxml2.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 bazeltools/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