I could probably break this commit into more pieces.
---
This patch adds libc++ support for Android L (Android 5.0+) and up,
tested using the Android team's current compiler, a recent version of
the AOSP sysroot, and the x86[-64] Android Emulator.
CMake and Lit Configuration:
Add runtimes/cmake/android/Arch-${ARCH}.cmake files that configure CMake
to cross-compile to Android without using CMake's built-in NDK support
(which only works with an actual packaged NDK).
Add libcxx/cmake/caches/AndroidNDK.cmake that builds and tests libc++
(and libc++abi) for Android. This file configures libc++ to match what
the NDK distributes, e.g.:
- libc++_shared.so (includes libc++abi objects, there is no
libc++abi.so). libunwind is linked statically but not exported.
- libc++_static.a (does not include libc++abi) and libc++abi.a
- `std::__ndk1` namespace
- All the libraries are built with `__ANDROID_API__=21`, even when they
are linked to something targeting a higher API level.
(However, when the Android LLVM team builds these components, they do
not use these CMake cache files. Instead they use Python scripts to
configure the builds. See
https://android.googlesource.com/toolchain/llvm_android/.)
Add llvm-libc++[abi].android-ndk.cfg.in files that test the Android
NDK's libc++_shared.so. These files can target old or new Android
devices. The Android LLVM team uses these test files to test libc++ for
both arm/arm64 and x86/x86_64 architectures.
The Android testing mode works by setting %{executor} to adb_run.py,
which uses `adb push` and `adb shell` to run tests remotely. adb_run.py
always runs tests as the "shell" user even on an old emulator where "adb
unroot" doesn't work. The script has workarounds for old Android
devices. The script uses a Unix domain socket on the host
(--job-limit-socket) to restrict concurrent adb invocations. Compiling
the tests is a major part of libc++ testing run-time, so it's desirable
to exploit all the host cores without overburdening the test devices,
which can have far fewer cores.
BuildKite CI:
Add a builder to run-buildbot, `android-ndk-*`, that uses Android Clang
and an Android sysroot to build libc++, then starts an Android emulator
container to run tests.
Run the emulator and an adb server in a separate Docker container
(libcxx-ci-android-emulator), and create a separate Docker image for
each emulator OS system image. Set ADB_SERVER_SOCKET to connect to the
container's adb server. Running the only adb server inside the container
makes cleanup more reliable between test runs, e.g. the adb client
doesn't create a `~/.android` directory and the adb server can be
restarted along with the emulator using docker stop/run. (N.B. The
emulator insists on connecting to an adb server and will start one
itself if it can't connect to one.)
The suffix to the android-ndk-* job is a label that concisely specifies
an Android SDK emulator image. e.g.:
- "system-images;android-21;default;x86" ==> 21-def-x86
- "system-images;android-33;google_apis;x86_64" ==> 33-goog-x86_64
Fixes: https://github.com/llvm/llvm-project/issues/69270
Differential Revision: https://reviews.llvm.org/D139147
98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
#===----------------------------------------------------------------------===##
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
#===----------------------------------------------------------------------===##
|
|
|
|
import atexit
|
|
import os
|
|
import re
|
|
import select
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
from typing import List
|
|
|
|
|
|
def _get_cpu_count() -> int:
|
|
# Determine the number of cores by listing a /sys directory. Older devices
|
|
# lack `nproc`. Even if a static toybox binary is pushed to the device, it may
|
|
# return an incorrect value. (e.g. On a Nexus 7 running Android 5.0, toybox
|
|
# nproc returns 1 even though the device has 4 CPUs.)
|
|
job = subprocess.run(["adb", "shell", "ls /sys/devices/system/cpu"],
|
|
encoding="utf8", check=False,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
if job.returncode == 1:
|
|
# Maybe adb is missing, maybe ANDROID_SERIAL needs to be defined, maybe the
|
|
# /sys subdir isn't there. Most errors will be handled later, just use one
|
|
# job. (N.B. The adb command still succeeds even if ls fails on older
|
|
# devices that lack the shell_v2 adb feature.)
|
|
return 1
|
|
# Make sure there are no CR characters in the output. Pre-shell_v2, the adb
|
|
# stdout comes from a master pty so newlines are CRLF-delimited. On Windows,
|
|
# LF might also get expanded to CRLF.
|
|
cpu_listing = job.stdout.replace('\r', '\n')
|
|
|
|
# Count lines that match "cpu${DIGITS}".
|
|
result = len([line for line in cpu_listing.splitlines()
|
|
if re.match(r'cpu(\d)+$', line)])
|
|
|
|
# Restrict the result to something reasonable.
|
|
if result < 1:
|
|
result = 1
|
|
if result > 1024:
|
|
result = 1024
|
|
|
|
return result
|
|
|
|
|
|
def _job_limit_socket_thread(temp_dir: tempfile.TemporaryDirectory,
|
|
server: socket.socket, job_count: int) -> None:
|
|
"""Service the job limit server socket, accepting only as many connections
|
|
as there should be concurrent jobs.
|
|
"""
|
|
clients: List[socket.socket] = []
|
|
while True:
|
|
rlist = list(clients)
|
|
if len(clients) < job_count:
|
|
rlist.append(server)
|
|
rlist, _, _ = select.select(rlist, [], [])
|
|
for sock in rlist:
|
|
if sock == server:
|
|
new_client, _ = server.accept()
|
|
new_client.send(b"x")
|
|
clients.append(new_client)
|
|
else:
|
|
sock.close()
|
|
clients.remove(sock)
|
|
|
|
|
|
def adb_job_limit_socket() -> str:
|
|
"""An Android device can frequently have many fewer cores than the host
|
|
(e.g. 4 versus 128). We want to exploit all the device cores without
|
|
overburdening it.
|
|
|
|
Create a Unix domain socket that only allows as many connections as CPUs on
|
|
the Android device.
|
|
"""
|
|
|
|
# Create the job limit server socket.
|
|
temp_dir = tempfile.TemporaryDirectory(prefix="libcxx_")
|
|
sock_addr = temp_dir.name + "/adb_job.sock"
|
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
server.bind(sock_addr)
|
|
server.listen(1)
|
|
|
|
# Spawn a thread to service the socket. As a daemon thread, its existence
|
|
# won't prevent interpreter shutdown. The temp dir will still be removed on
|
|
# shutdown.
|
|
cpu_count = _get_cpu_count()
|
|
threading.Thread(target=_job_limit_socket_thread,
|
|
args=(temp_dir, server, cpu_count),
|
|
daemon=True).start()
|
|
|
|
return sock_addr
|