Skip to content

Commit 4e920f2

Browse files
committed
ci: use a custom android sdk manager with pinning and mirroring
1 parent ee1474a commit 4e920f2

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)