blob: bad761cda69a5c0663ca88c828c605c0fb407d25 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu708310b2018-03-28 17:24:34 +08002# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Android utility.
6
7Terminology used in this module:
8 "product-variant" is sometimes called "target" and sometimes "flavor".
9 I prefer to use "flavor" in the code because
10 - "target" is too general
11 - sometimes, it is not in the form of "product-variant", for example,
12 "cts_arm_64"
13"""
14
15from __future__ import print_function
16import json
17import logging
18import os
Kuang-che Wud1d45b42018-07-05 00:46:45 +080019import tempfile
Kuang-che Wu999893c2020-04-13 22:06:22 +080020import urllib.request
Kuang-che Wu708310b2018-03-28 17:24:34 +080021
Zheng-Jie Changba212682020-09-18 16:30:08 +080022import google.oauth2.service_account
Zheng-Jie Change8a25fe2020-10-27 11:02:24 +080023import googleapiclient
24import googleapiclient.discovery
Zheng-Jie Changba212682020-09-18 16:30:08 +080025
Kuang-che Wu708310b2018-03-28 17:24:34 +080026from bisect_kit import cli
Kuang-che Wud1d45b42018-07-05 00:46:45 +080027from bisect_kit import codechange
Kuang-che Wue121fae2018-11-09 16:18:39 +080028from bisect_kit import errors
Kuang-che Wud1d45b42018-07-05 00:46:45 +080029from bisect_kit import git_util
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080030from bisect_kit import repo_util
Kuang-che Wu708310b2018-03-28 17:24:34 +080031from bisect_kit import util
32
33logger = logging.getLogger(__name__)
34
35ANDROID_URL_BASE = ('https://android-build.googleplex.com/'
36 'builds/branch/{branch}')
37BUILD_IDS_BETWEEN_URL_TEMPLATE = (
38 ANDROID_URL_BASE + '/build-ids/between/{end}/{start}')
39BUILD_INFO_URL_TEMPLATE = ANDROID_URL_BASE + '/builds?id={build_id}'
40
41
42def is_android_build_id(s):
43 """Is an Android build id."""
44 # Build ID is always a number
45 return s.isdigit()
46
47
48def argtype_android_build_id(s):
49 if not is_android_build_id(s):
50 msg = 'invalid android build id (%s)' % s
51 raise cli.ArgTypeError(msg, '9876543')
52 return s
53
54
55def fetch_android_build_data(url):
56 """Fetches file from android build server.
57
58 Args:
59 url: URL to fetch
60
61 Returns:
62 file content (str). None if failed.
63 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080064 # Fetching android build data directly will fail without authentication.
Kuang-che Wu708310b2018-03-28 17:24:34 +080065 # Following code is just serving as demo purpose. You should modify or hook
66 # it with your own implementation.
Kuang-che Wu084eef22020-03-11 18:29:48 +080067 logger.warning('fetch_android_build_data need to be hooked')
Kuang-che Wu708310b2018-03-28 17:24:34 +080068 try:
Kuang-che Wu68f022d2019-11-29 14:38:48 +080069 return urllib.request.urlopen(url).read().decode('utf8')
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080070 except urllib.request.URLError as e:
Kuang-che Wu708310b2018-03-28 17:24:34 +080071 logger.exception('failed to fetch "%s"', url)
Kuang-che Wue121fae2018-11-09 16:18:39 +080072 raise errors.ExternalError(str(e))
Kuang-che Wu708310b2018-03-28 17:24:34 +080073
74
75def is_good_build(branch, flavor, build_id):
76 """Determine a build_id was succeeded.
77
78 Args:
79 branch: The Android branch from which to retrieve the builds.
80 flavor: Target name of the Android image in question.
81 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
82 build_id: Android build id
83
84 Returns:
85 True if the given build was successful.
86 """
87
88 url = BUILD_INFO_URL_TEMPLATE.format(branch=branch, build_id=build_id)
Kuang-che Wu68f022d2019-11-29 14:38:48 +080089 build = json.loads(fetch_android_build_data(url))
Kuang-che Wu708310b2018-03-28 17:24:34 +080090 for target in build[0]['targets']:
91 if target['target']['name'] == flavor and target.get('successful'):
92 return True
93 return False
94
95
Kuang-che Wu0a902f42019-01-21 18:58:32 +080096def get_build_ids_between(branch, flavor, start, end):
Kuang-che Wu708310b2018-03-28 17:24:34 +080097 """Returns a list of build IDs.
98
99 Args:
100 branch: The Android branch from which to retrieve the builds.
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800101 flavor: Target name of the Android image in question.
102 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
Kuang-che Wu708310b2018-03-28 17:24:34 +0800103 start: The starting point build ID. (inclusive)
104 end: The ending point build ID. (inclusive)
105
106 Returns:
107 A list of build IDs. (str)
108 """
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800109 # Do not remove this argument because our internal implementation need it.
110 del flavor # not used
111
Kuang-che Wu708310b2018-03-28 17:24:34 +0800112 # TODO(kcwu): remove pagination hack after b/68239878 fixed
113 build_id_set = set()
114 tmp_end = end
115 while True:
116 url = BUILD_IDS_BETWEEN_URL_TEMPLATE.format(
117 branch=branch, start=start, end=tmp_end)
Kuang-che Wu68f022d2019-11-29 14:38:48 +0800118 query_result = json.loads(fetch_android_build_data(url))
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800119 found_new = set(int(x) for x in query_result['ids']) - build_id_set
Kuang-che Wu708310b2018-03-28 17:24:34 +0800120 if not found_new:
121 break
122 build_id_set.update(found_new)
123 tmp_end = min(build_id_set)
124
125 logger.debug('Found %d builds in the range.', len(build_id_set))
126
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800127 return [str(bid) for bid in sorted(build_id_set)]
Kuang-che Wu708310b2018-03-28 17:24:34 +0800128
129
130def lunch(android_root, flavor, *args, **kwargs):
131 """Helper to run commands with Android lunch env.
132
133 Args:
134 android_root: root path of Android tree
135 flavor: lunch flavor
136 args: command to run
137 kwargs: extra arguments passed to util.Popen
138 """
139 util.check_call('./android_lunch_helper.sh', android_root, flavor, *args,
140 **kwargs)
141
142
Zheng-Jie Changba212682020-09-18 16:30:08 +0800143def fetch_artifact_from_g3(flavor, build_id, filename, dest_folder):
Kuang-che Wu708310b2018-03-28 17:24:34 +0800144 """Fetches Android build artifact.
145
146 Args:
147 flavor: Android build flavor
148 build_id: Android build id
149 filename: artifact name
Zheng-Jie Changba212682020-09-18 16:30:08 +0800150 dest_folder: local path to store the fetched artifact
Kuang-che Wu708310b2018-03-28 17:24:34 +0800151 """
152 util.check_call('/google/data/ro/projects/android/fetch_artifact', '--target',
Zheng-Jie Changba212682020-09-18 16:30:08 +0800153 flavor, '--bid', build_id, filename, dest_folder)
154
155
156def fetch_artifact(flavor, build_id, filename, dest_folder):
157 """Fetches Android build artifact.
158
159 Args:
160 flavor: Android build flavor
161 build_id: Android build id
162 filename: artifact name
163 dest_folder: local path to store the fetched artifact
164 """
165 if os.environ.get('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON'):
166 api = AndroidInternalApi()
Zheng-Jie Change62d4fe2020-10-14 05:50:37 +0800167 api.download_artifact(build_id, flavor, filename,
Zheng-Jie Changba212682020-09-18 16:30:08 +0800168 os.path.join(dest_folder, filename))
169 else:
170 fetch_artifact_from_g3(flavor, build_id, filename, dest_folder)
Kuang-che Wu708310b2018-03-28 17:24:34 +0800171
172
173def fetch_manifest(android_root, flavor, build_id):
174 """Fetches Android repo manifest of given build.
175
176 Args:
177 android_root: root path of Android tree. Fetched manifest file will be
178 stored inside.
179 flavor: Android build flavor
180 build_id: Android build id
181
182 Returns:
183 the local filename of manifest (relative to android_root/.repo/manifests/)
184 """
185 # Assume manifest is flavor independent, thus not encoded into the file name.
186 manifest_name = 'manifest_%s.xml' % build_id
187 manifest_path = os.path.join(android_root, '.repo', 'manifests',
188 manifest_name)
189 if not os.path.exists(manifest_path):
190 fetch_artifact(flavor, build_id, manifest_name, manifest_path)
191 return manifest_name
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800192
193
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800194def lookup_build_timestamp(flavor, build_id):
195 """Lookup timestamp of Android prebuilt.
196
197 Args:
198 flavor: Android build flavor
199 build_id: Android build id
200
201 Returns:
202 timestamp
203 """
204 tmp_fn = tempfile.mktemp()
205 try:
206 fetch_artifact(flavor, build_id, 'BUILD_INFO', tmp_fn)
Kuang-che Wu74bcb642020-02-20 18:45:53 +0800207 with open(tmp_fn) as f:
208 data = json.load(f)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800209 return int(data['sync_start_time'])
210 finally:
211 if os.path.exists(tmp_fn):
212 os.unlink(tmp_fn)
213
214
215class AndroidSpecManager(codechange.SpecManager):
216 """Repo manifest related operations.
217
218 This class fetches and enumerates android manifest files, parses them,
219 and sync to disk state according to them.
220 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800221
222 def __init__(self, config):
223 self.config = config
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800224 self.manifest_dir = os.path.join(self.config['android_root'], '.repo',
225 'manifests')
226
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800227 def collect_float_spec(self, old, new, fixed_specs=None):
228 del fixed_specs # unused
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800229 result = []
230 path = 'default.xml'
231
232 commits = []
233 old_timestamp = lookup_build_timestamp(self.config['flavor'], old)
234 new_timestamp = lookup_build_timestamp(self.config['flavor'], new)
235 for timestamp, git_rev in git_util.get_history(self.manifest_dir, path):
236 if timestamp < old_timestamp:
237 commits = []
238 commits.append((timestamp, git_rev))
239 if timestamp > new_timestamp:
240 break
241
242 for timestamp, git_rev in commits:
243 result.append(
244 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
245 return result
246
247 def collect_fixed_spec(self, old, new):
248 result = []
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800249 revlist = get_build_ids_between(self.config['branch'],
250 self.config['flavor'], int(old), int(new))
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800251 for rev in revlist:
252 manifest_name = fetch_manifest(self.config['android_root'],
253 self.config['flavor'], rev)
254 path = os.path.join(self.manifest_dir, manifest_name)
255 timestamp = lookup_build_timestamp(self.config['flavor'], rev)
256 result.append(
257 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
258 return result
259
260 def _load_manifest_content(self, spec):
261 if spec.spec_type == codechange.SPEC_FIXED:
262 manifest_name = fetch_manifest(self.config['branch'],
263 self.config['flavor'], spec.name)
Kuang-che Wua5723492019-11-25 20:59:34 +0800264 with open(os.path.join(self.manifest_dir, manifest_name)) as f:
265 content = f.read()
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800266 else:
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800267 content = git_util.get_file_from_revision(self.manifest_dir, spec.name,
268 spec.path)
269 return content
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800270
271 def parse_spec(self, spec):
272 logging.debug('parse_spec %s', spec.name)
273 manifest_content = self._load_manifest_content(spec)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800274 parser = repo_util.ManifestParser(self.manifest_dir)
275 root = parser.parse_single_xml(manifest_content, allow_include=False)
276 spec.entries = parser.process_parsed_result(root)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800277 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800278 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +0800279 raise ValueError('fixed spec %r has unexpected floating entries' %
280 spec.name)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800281
282 def sync_disk_state(self, rev):
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800283 manifest_name = fetch_manifest(self.config['android_root'],
284 self.config['flavor'], rev)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800285 repo_util.sync(
286 self.config['android_root'],
287 manifest_name=manifest_name,
288 current_branch=True)
Zheng-Jie Changba212682020-09-18 16:30:08 +0800289
290
291class AndroidInternalApi:
292 """Wrapper of android internal api. Singleton class"""
293
294 _instance = None
295 _default_chunk_size = 20 * 1024 * 1024 # 20M
296 _retries = 5
297
298 def __new__(cls):
299 if cls._instance is None:
300 cls._instance = object.__new__(cls)
301 return cls._instance
302
303 def __init__(self):
304 if not os.environ.get('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON'):
Kuang-che Wuf0bfd182020-10-26 15:52:29 +0800305 raise errors.InternalError('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON '
306 'must defined when using Android Api')
Zheng-Jie Changba212682020-09-18 16:30:08 +0800307 self.credentials = self.get_credentials()
Zheng-Jie Change8a25fe2020-10-27 11:02:24 +0800308 self.service = googleapiclient.discovery.build(
Zheng-Jie Chang5f414572020-09-28 12:47:53 +0800309 'androidbuildinternal',
310 'v3',
311 credentials=self.credentials,
312 cache_discovery=False)
Zheng-Jie Changba212682020-09-18 16:30:08 +0800313
314 def get_credentials(self):
315 # TODO(zjchang): support compute engine type credential
Kuang-che Wuf0bfd182020-10-26 15:52:29 +0800316 # pylint: disable=line-too-long
Zheng-Jie Changba212682020-09-18 16:30:08 +0800317 credentials = google.oauth2.service_account.Credentials.from_service_account_file(
318 os.environ.get('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON'),
319 scopes=['https://www.googleapis.com/auth/androidbuild.internal'],
320 )
321 return credentials
322
323 def get_artifact_info(self, build_id, target, name, attempt_id=0):
324 """Get Android build artifact info.
325
326 Args:
327 build_id: Android build ID.
328 target: Android build target.
329 name: Artifact name.
330 attempt_id: A build attempt. Defaults to 0.
331
332 Returns:
333 Artifact info.
334
335 Raises:
336 errors.ExternalError
337 """
338 try:
339 artifact = self.service.buildartifact().get(
340 buildId=build_id,
341 target=target,
342 attemptId=attempt_id,
343 resourceId=name).execute(num_retries=self._retries)
344 except (googleapiclient.errors.HttpError, httplib2.HttpLib2Error) as e:
345 raise errors.ExternalError('Android API error: ' + str(e))
346
347 if artifact is None:
348 raise errors.ExternalError('Invalid artifact %s/%s/%s/%s' %
349 (build_id, target, attempt_id, name))
350 return artifact
351
352 def download_artifact(self, build_id, target, name, output_file,
353 attempt_id=0):
354 """Download one artifact.
355
356 Args:
357 build_id: Android build ID.
358 target: Android build target.
359 name: Artifact name.
360 output_file: Target file path.
361 attempt_id: A build attempt. Defaults to 0.
362
363 Raises:
364 errors.ExternalError
365 """
366 artifact = self.get_artifact_info(
367 build_id, target, name, attempt_id=attempt_id)
368 chunksize = min(int(artifact['size']), self._default_chunk_size)
369
370 download_request = self.service.buildartifact().get_media(
371 buildId=build_id, target=target, attemptId=attempt_id, resourceId=name)
372
373 try:
374 with open(output_file, mode='wb') as f:
Zheng-Jie Change8a25fe2020-10-27 11:02:24 +0800375 downloader = googleapiclient.http.MediaIoBaseDownload(
Zheng-Jie Changba212682020-09-18 16:30:08 +0800376 f, download_request, chunksize=chunksize)
377 done = False
378 while not done:
Kuang-che Wuf0bfd182020-10-26 15:52:29 +0800379 _status, done = downloader.next_chunk(num_retries=self._retries)
Zheng-Jie Changba212682020-09-18 16:30:08 +0800380 except (googleapiclient.errors.HttpError, httplib2.HttpLib2Error) as e:
381 raise errors.ExternalError('Android API error: ' + str(e))