Skip to content

Commit 2975a3c

Browse files
committed
Auto merge of #59926 - pietroalbini:android-sdk-manager, r=alexcrichton
ci: use a custom android sdk manager with pinning and mirroring Google's own sdkmanager has two issues that make it unsuitable for us: * Mirroring has to be done manually, which is annoying because we need to figure out on our own all the URLs to copy (I couldn't find any documentation when building this PR, had to use mitmproxy). * There is no support for pinning, which means an update on Google's side can break our CI, as it happened multiple times. This PR replaces all our usage of sdkmanager with a custom Python script which mimics its behavior, but with the two issues fixes. sdkmanager's logic for installing packages is thankfully very simple: the package name (like `system-images;android-18;default;armeabi-v7a`) is the directory where the package should live (with `;` replaced with `/`), so to install a package we only need to extract its contents in the right directory. r? @alexcrichton cc @kennytm fixes #59778
2 parents 07133ac + 4e920f2 commit 2975a3c

File tree

4 files changed

+224
-68
lines changed

4 files changed

+224
-68
lines changed

src/ci/docker/arm-android/Dockerfile

+7-9
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@ COPY scripts/android-ndk.sh /scripts/
77
RUN . /scripts/android-ndk.sh && \
88
download_and_make_toolchain android-ndk-r15c-linux-x86_64.zip arm 14
99

10-
# Note:
11-
# Do not upgrade to `openjdk-9-jre-headless`, as it will cause certificate error
12-
# when installing the Android SDK (see PR #45193). This is unfortunate, but
13-
# every search result suggested either disabling HTTPS or replacing JDK 9 by
14-
# JDK 8 as the solution (e.g. https://stackoverflow.com/q/41421340). :|
1510
RUN dpkg --add-architecture i386 && \
1611
apt-get update && \
1712
apt-get install -y --no-install-recommends \
1813
libgl1-mesa-glx \
1914
libpulse0 \
2015
libstdc++6:i386 \
21-
openjdk-8-jre-headless \
22-
tzdata
16+
openjdk-9-jre-headless \
17+
tzdata \
18+
wget \
19+
python3
2320

2421
COPY scripts/android-sdk.sh /scripts/
25-
RUN . /scripts/android-sdk.sh && \
26-
download_and_create_avd 4333796 armeabi-v7a 18 5264690
22+
COPY scripts/android-sdk-manager.py /scripts/
23+
COPY arm-android/android-sdk.lock /android/sdk/android-sdk.lock
24+
RUN /scripts/android-sdk.sh
2725

2826
ENV PATH=$PATH:/android/sdk/emulator
2927
ENV PATH=$PATH:/android/sdk/tools
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
emulator emulator-linux-5264690.zip 48c1cda2bdf3095d9d9d5c010fbfb3d6d673e3ea
2+
patcher;v4 3534162-studio.sdk-patcher.zip 046699c5e2716ae11d77e0bad814f7f33fab261e
3+
platform-tools platform-tools_r28.0.2-linux.zip 46a4c02a9b8e4e2121eddf6025da3c979bf02e28
4+
platforms;android-18 android-18_r03.zip e6b09b3505754cbbeb4a5622008b907262ee91cb
5+
system-images;android-18;default;armeabi-v7a sys-img/android/armeabi-v7a-18_r05.zip 580b583720f7de671040d5917c8c9db0c7aa03fd
6+
tools sdk-tools-linux-4333796.zip 8c7c28554a32318461802c1291d76fccfafde054
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
# Simpler reimplementation of Android's sdkmanager
3+
# Extra features of this implementation are pinning and mirroring
4+
5+
# These URLs are the Google repositories containing the list of available
6+
# packages and their versions. The list has been generated by listing the URLs
7+
# fetched while executing `tools/bin/sdkmanager --list`
8+
BASE_REPOSITORY = "https://dl.google.com/android/repository/"
9+
REPOSITORIES = [
10+
"sys-img/android/sys-img2-1.xml",
11+
"sys-img/android-wear/sys-img2-1.xml",
12+
"sys-img/android-wear-cn/sys-img2-1.xml",
13+
"sys-img/android-tv/sys-img2-1.xml",
14+
"sys-img/google_apis/sys-img2-1.xml",
15+
"sys-img/google_apis_playstore/sys-img2-1.xml",
16+
"addon2-1.xml",
17+
"glass/addon2-1.xml",
18+
"extras/intel/addon2-1.xml",
19+
"repository2-1.xml",
20+
]
21+
22+
# Available hosts: linux, macosx and windows
23+
HOST_OS = "linux"
24+
25+
# Mirroring options
26+
MIRROR_BUCKET = "rust-lang-ci2"
27+
MIRROR_BASE_DIR = "rust-ci-mirror/android/"
28+
29+
import argparse
30+
import hashlib
31+
import os
32+
import subprocess
33+
import sys
34+
import tempfile
35+
import urllib.request
36+
import xml.etree.ElementTree as ET
37+
38+
class Package:
39+
def __init__(self, path, url, sha1, deps=None):
40+
if deps is None:
41+
deps = []
42+
self.path = path.strip()
43+
self.url = url.strip()
44+
self.sha1 = sha1.strip()
45+
self.deps = deps
46+
47+
def download(self, base_url):
48+
_, file = tempfile.mkstemp()
49+
url = base_url + self.url
50+
subprocess.run(["curl", "-o", file, url], check=True)
51+
# Ensure there are no hash mismatches
52+
with open(file, "rb") as f:
53+
sha1 = hashlib.sha1(f.read()).hexdigest()
54+
if sha1 != self.sha1:
55+
raise RuntimeError(
56+
"hash mismatch for package " + self.path + ": " +
57+
sha1 + " vs " + self.sha1 + " (known good)"
58+
)
59+
return file
60+
61+
def __repr__(self):
62+
return "<Package "+self.path+" at "+self.url+" (sha1="+self.sha1+")"
63+
64+
def fetch_url(url):
65+
page = urllib.request.urlopen(url)
66+
return page.read()
67+
68+
def fetch_repository(base, repo_url):
69+
packages = {}
70+
root = ET.fromstring(fetch_url(base + repo_url))
71+
for package in root:
72+
if package.tag != "remotePackage":
73+
continue
74+
path = package.attrib["path"]
75+
76+
for archive in package.find("archives"):
77+
host_os = archive.find("host-os")
78+
if host_os is not None and host_os.text != HOST_OS:
79+
continue
80+
complete = archive.find("complete")
81+
url = os.path.join(os.path.dirname(repo_url), complete.find("url").text)
82+
sha1 = complete.find("checksum").text
83+
84+
deps = []
85+
dependencies = package.find("dependencies")
86+
if dependencies is not None:
87+
for dep in dependencies:
88+
deps.append(dep.attrib["path"])
89+
90+
packages[path] = Package(path, url, sha1, deps)
91+
break
92+
93+
return packages
94+
95+
def fetch_repositories():
96+
packages = {}
97+
for repo in REPOSITORIES:
98+
packages.update(fetch_repository(BASE_REPOSITORY, repo))
99+
return packages
100+
101+
class Lockfile:
102+
def __init__(self, path):
103+
self.path = path
104+
self.packages = {}
105+
if os.path.exists(path):
106+
with open(path) as f:
107+
for line in f:
108+
path, url, sha1 = line.split(" ")
109+
self.packages[path] = Package(path, url, sha1)
110+
111+
def add(self, packages, name, *, update=True):
112+
if name not in packages:
113+
raise NameError("package not found: " + name)
114+
if not update and name in self.packages:
115+
return
116+
self.packages[name] = packages[name]
117+
for dep in packages[name].deps:
118+
self.add(packages, dep, update=False)
119+
120+
def save(self):
121+
packages = list(sorted(self.packages.values(), key=lambda p: p.path))
122+
with open(self.path, "w") as f:
123+
for package in packages:
124+
f.write(package.path + " " + package.url + " " + package.sha1 + "\n")
125+
126+
def cli_add_to_lockfile(args):
127+
lockfile = Lockfile(args.lockfile)
128+
packages = fetch_repositories()
129+
for package in args.packages:
130+
lockfile.add(packages, package)
131+
lockfile.save()
132+
133+
def cli_update_mirror(args):
134+
lockfile = Lockfile(args.lockfile)
135+
for package in lockfile.packages.values():
136+
path = package.download(BASE_REPOSITORY)
137+
subprocess.run([
138+
"aws", "s3", "mv", path,
139+
"s3://" + MIRROR_BUCKET + "/" + MIRROR_BASE_DIR + package.url,
140+
"--profile=" + args.awscli_profile,
141+
], check=True)
142+
143+
def cli_install(args):
144+
lockfile = Lockfile(args.lockfile)
145+
for package in lockfile.packages.values():
146+
# Download the file from the mirror into a temp file
147+
url = "https://" + MIRROR_BUCKET + ".s3.amazonaws.com/" + MIRROR_BASE_DIR
148+
downloaded = package.download(url)
149+
# Extract the file in a temporary directory
150+
extract_dir = tempfile.mkdtemp()
151+
subprocess.run([
152+
"unzip", "-q", downloaded, "-d", extract_dir,
153+
], check=True)
154+
# Figure out the prefix used in the zip
155+
subdirs = [d for d in os.listdir(extract_dir) if not d.startswith(".")]
156+
if len(subdirs) != 1:
157+
raise RuntimeError("extracted directory contains more than one dir")
158+
# Move the extracted files in the proper directory
159+
dest = os.path.join(args.dest, package.path.replace(";", "/"))
160+
os.makedirs("/".join(dest.split("/")[:-1]), exist_ok=True)
161+
os.rename(os.path.join(extract_dir, subdirs[0]), dest)
162+
os.unlink(downloaded)
163+
164+
def cli():
165+
parser = argparse.ArgumentParser()
166+
subparsers = parser.add_subparsers()
167+
168+
add_to_lockfile = subparsers.add_parser("add-to-lockfile")
169+
add_to_lockfile.add_argument("lockfile")
170+
add_to_lockfile.add_argument("packages", nargs="+")
171+
add_to_lockfile.set_defaults(func=cli_add_to_lockfile)
172+
173+
update_mirror = subparsers.add_parser("update-mirror")
174+
update_mirror.add_argument("lockfile")
175+
update_mirror.add_argument("--awscli-profile", default="default")
176+
update_mirror.set_defaults(func=cli_update_mirror)
177+
178+
install = subparsers.add_parser("install")
179+
install.add_argument("lockfile")
180+
install.add_argument("dest")
181+
install.set_defaults(func=cli_install)
182+
183+
args = parser.parse_args()
184+
if not hasattr(args, "func"):
185+
print("error: a subcommand is required (see --help)")
186+
exit(1)
187+
args.func(args)
188+
189+
if __name__ == "__main__":
190+
cli()

src/ci/docker/scripts/android-sdk.sh

100644100755
+21-59
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,28 @@ set -ex
22

33
export ANDROID_HOME=/android/sdk
44
PATH=$PATH:"${ANDROID_HOME}/tools/bin"
5+
LOCKFILE="${ANDROID_HOME}/android-sdk.lock"
56

6-
download_sdk() {
7-
mkdir -p /android
8-
curl -fo sdk.zip "https://dl.google.com/android/repository/sdk-tools-linux-$1.zip"
9-
unzip -q sdk.zip -d "$ANDROID_HOME"
10-
rm -f sdk.zip
11-
}
12-
13-
download_sysimage() {
14-
abi=$1
15-
api=$2
16-
17-
# See https://developer.android.com/studio/command-line/sdkmanager.html for
18-
# usage of `sdkmanager`.
19-
#
20-
# The output from sdkmanager is so noisy that it will occupy all of the 4 MB
21-
# log extremely quickly. Thus we must silence all output.
22-
yes | sdkmanager --licenses > /dev/null
23-
yes | sdkmanager platform-tools \
24-
"platforms;android-$api" \
25-
"system-images;android-$api;default;$abi" > /dev/null
26-
}
27-
28-
download_emulator() {
29-
# Download a pinned version of the emulator since upgrades can cause issues
30-
curl -fo emulator.zip "https://dl.google.com/android/repository/emulator-linux-$1.zip"
31-
rm -rf "${ANDROID_HOME}/emulator"
32-
unzip -q emulator.zip -d "${ANDROID_HOME}"
33-
rm -f emulator.zip
34-
}
35-
36-
create_avd() {
37-
abi=$1
38-
api=$2
7+
# To add a new packages to the SDK or to update an existing one you need to
8+
# run the command:
9+
#
10+
# android-sdk-manager.py add-to-lockfile $LOCKFILE <package-name>
11+
#
12+
# Then, after every lockfile update the mirror has to be synchronized as well:
13+
#
14+
# android-sdk-manager.py update-mirror $LOCKFILE
15+
#
16+
/scripts/android-sdk-manager.py install "${LOCKFILE}" "${ANDROID_HOME}"
3917

40-
# See https://developer.android.com/studio/command-line/avdmanager.html for
41-
# usage of `avdmanager`.
42-
echo no | avdmanager create avd \
43-
-n "$abi-$api" \
44-
-k "system-images;android-$api;default;$abi"
45-
}
18+
details=$(cat "${LOCKFILE}" \
19+
| grep system-images \
20+
| sed 's/^system-images;android-\([0-9]\+\);default;\([a-z0-9-]\+\) /\1 \2 /g')
21+
api="$(echo "${details}" | awk '{print($1)}')"
22+
abi="$(echo "${details}" | awk '{print($2)}')"
4623

47-
download_and_create_avd() {
48-
download_sdk $1
49-
download_sysimage $2 $3
50-
create_avd $2 $3
51-
download_emulator $4
52-
}
24+
# See https://developer.android.com/studio/command-line/avdmanager.html for
25+
# usage of `avdmanager`.
26+
echo no | avdmanager create avd \
27+
-n "$abi-$api" \
28+
-k "system-images;android-$api;default;$abi"
5329

54-
# Usage:
55-
#
56-
# download_and_create_avd 4333796 armeabi-v7a 18 5264690
57-
#
58-
# 4333796 =>
59-
# SDK tool version.
60-
# Copy from https://developer.android.com/studio/index.html#command-tools
61-
# armeabi-v7a =>
62-
# System image ABI
63-
# 18 =>
64-
# Android API Level (18 = Android 4.3 = Jelly Bean MR2)
65-
# 5264690 =>
66-
# Android Emulator version.
67-
# Copy from the "build_id" in the `/android/sdk/emulator/emulator -version` output

0 commit comments

Comments
 (0)