blob: e1e972b81a835554bad259d184260f13de2335a8 [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 apiclient
23import google.oauth2.service_account
24
Kuang-che Wu708310b2018-03-28 17:24:34 +080025from bisect_kit import cli
Kuang-che Wud1d45b42018-07-05 00:46:45 +080026from bisect_kit import codechange
Kuang-che Wue121fae2018-11-09 16:18:39 +080027from bisect_kit import errors
Kuang-che Wud1d45b42018-07-05 00:46:45 +080028from bisect_kit import git_util
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080029from bisect_kit import repo_util
Kuang-che Wu708310b2018-03-28 17:24:34 +080030from bisect_kit import util
31
32logger = logging.getLogger(__name__)
33
34ANDROID_URL_BASE = ('https://android-build.googleplex.com/'
35 'builds/branch/{branch}')
36BUILD_IDS_BETWEEN_URL_TEMPLATE = (
37 ANDROID_URL_BASE + '/build-ids/between/{end}/{start}')
38BUILD_INFO_URL_TEMPLATE = ANDROID_URL_BASE + '/builds?id={build_id}'
39
40
41def is_android_build_id(s):
42 """Is an Android build id."""
43 # Build ID is always a number
44 return s.isdigit()
45
46
47def argtype_android_build_id(s):
48 if not is_android_build_id(s):
49 msg = 'invalid android build id (%s)' % s
50 raise cli.ArgTypeError(msg, '9876543')
51 return s
52
53
54def fetch_android_build_data(url):
55 """Fetches file from android build server.
56
57 Args:
58 url: URL to fetch
59
60 Returns:
61 file content (str). None if failed.
62 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080063 # Fetching android build data directly will fail without authentication.
Kuang-che Wu708310b2018-03-28 17:24:34 +080064 # Following code is just serving as demo purpose. You should modify or hook
65 # it with your own implementation.
Kuang-che Wu084eef22020-03-11 18:29:48 +080066 logger.warning('fetch_android_build_data need to be hooked')
Kuang-che Wu708310b2018-03-28 17:24:34 +080067 try:
Kuang-che Wu68f022d2019-11-29 14:38:48 +080068 return urllib.request.urlopen(url).read().decode('utf8')
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080069 except urllib.request.URLError as e:
Kuang-che Wu708310b2018-03-28 17:24:34 +080070 logger.exception('failed to fetch "%s"', url)
Kuang-che Wue121fae2018-11-09 16:18:39 +080071 raise errors.ExternalError(str(e))
Kuang-che Wu708310b2018-03-28 17:24:34 +080072
73
74def is_good_build(branch, flavor, build_id):
75 """Determine a build_id was succeeded.
76
77 Args:
78 branch: The Android branch from which to retrieve the builds.
79 flavor: Target name of the Android image in question.
80 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
81 build_id: Android build id
82
83 Returns:
84 True if the given build was successful.
85 """
86
87 url = BUILD_INFO_URL_TEMPLATE.format(branch=branch, build_id=build_id)
Kuang-che Wu68f022d2019-11-29 14:38:48 +080088 build = json.loads(fetch_android_build_data(url))
Kuang-che Wu708310b2018-03-28 17:24:34 +080089 for target in build[0]['targets']:
90 if target['target']['name'] == flavor and target.get('successful'):
91 return True
92 return False
93
94
Kuang-che Wu0a902f42019-01-21 18:58:32 +080095def get_build_ids_between(branch, flavor, start, end):
Kuang-che Wu708310b2018-03-28 17:24:34 +080096 """Returns a list of build IDs.
97
98 Args:
99 branch: The Android branch from which to retrieve the builds.
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800100 flavor: Target name of the Android image in question.
101 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
Kuang-che Wu708310b2018-03-28 17:24:34 +0800102 start: The starting point build ID. (inclusive)
103 end: The ending point build ID. (inclusive)
104
105 Returns:
106 A list of build IDs. (str)
107 """
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800108 # Do not remove this argument because our internal implementation need it.
109 del flavor # not used
110
Kuang-che Wu708310b2018-03-28 17:24:34 +0800111 # TODO(kcwu): remove pagination hack after b/68239878 fixed
112 build_id_set = set()
113 tmp_end = end
114 while True:
115 url = BUILD_IDS_BETWEEN_URL_TEMPLATE.format(
116 branch=branch, start=start, end=tmp_end)
Kuang-che Wu68f022d2019-11-29 14:38:48 +0800117 query_result = json.loads(fetch_android_build_data(url))
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800118 found_new = set(int(x) for x in query_result['ids']) - build_id_set
Kuang-che Wu708310b2018-03-28 17:24:34 +0800119 if not found_new:
120 break
121 build_id_set.update(found_new)
122 tmp_end = min(build_id_set)
123
124 logger.debug('Found %d builds in the range.', len(build_id_set))
125
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800126 return [str(bid) for bid in sorted(build_id_set)]
Kuang-che Wu708310b2018-03-28 17:24:34 +0800127
128
129def lunch(android_root, flavor, *args, **kwargs):
130 """Helper to run commands with Android lunch env.
131
132 Args:
133 android_root: root path of Android tree
134 flavor: lunch flavor
135 args: command to run
136 kwargs: extra arguments passed to util.Popen
137 """
138 util.check_call('./android_lunch_helper.sh', android_root, flavor, *args,
139 **kwargs)
140
141
Zheng-Jie Changba212682020-09-18 16:30:08 +0800142def fetch_artifact_from_g3(flavor, build_id, filename, dest_folder):
Kuang-che Wu708310b2018-03-28 17:24:34 +0800143 """Fetches Android build artifact.
144
145 Args:
146 flavor: Android build flavor
147 build_id: Android build id
148 filename: artifact name
Zheng-Jie Changba212682020-09-18 16:30:08 +0800149 dest_folder: local path to store the fetched artifact
Kuang-che Wu708310b2018-03-28 17:24:34 +0800150 """
151 util.check_call('/google/data/ro/projects/android/fetch_artifact', '--target',
Zheng-Jie Changba212682020-09-18 16:30:08 +0800152 flavor, '--bid', build_id, filename, dest_folder)
153
154
155def fetch_artifact(flavor, build_id, filename, dest_folder):
156 """Fetches Android build artifact.
157
158 Args:
159 flavor: Android build flavor
160 build_id: Android build id
161 filename: artifact name
162 dest_folder: local path to store the fetched artifact
163 """
164 if os.environ.get('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON'):
165 api = AndroidInternalApi()
Zheng-Jie Change62d4fe2020-10-14 05:50:37 +0800166 api.download_artifact(build_id, flavor, filename,
Zheng-Jie Changba212682020-09-18 16:30:08 +0800167 os.path.join(dest_folder, filename))
168 else:
169 fetch_artifact_from_g3(flavor, build_id, filename, dest_folder)
Kuang-che Wu708310b2018-03-28 17:24:34 +0800170
171
172def fetch_manifest(android_root, flavor, build_id):
173 """Fetches Android repo manifest of given build.
174
175 Args:
176 android_root: root path of Android tree. Fetched manifest file will be
177 stored inside.
178 flavor: Android build flavor
179 build_id: Android build id
180
181 Returns:
182 the local filename of manifest (relative to android_root/.repo/manifests/)
183 """
184 # Assume manifest is flavor independent, thus not encoded into the file name.
185 manifest_name = 'manifest_%s.xml' % build_id
186 manifest_path = os.path.join(android_root, '.repo', 'manifests',
187 manifest_name)
188 if not os.path.exists(manifest_path):
189 fetch_artifact(flavor, build_id, manifest_name, manifest_path)
190 return manifest_name
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800191
192
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800193def lookup_build_timestamp(flavor, build_id):
194 """Lookup timestamp of Android prebuilt.
195
196 Args:
197 flavor: Android build flavor
198 build_id: Android build id
199
200 Returns:
201 timestamp
202 """
203 tmp_fn = tempfile.mktemp()
204 try:
205 fetch_artifact(flavor, build_id, 'BUILD_INFO', tmp_fn)
Kuang-che Wu74bcb642020-02-20 18:45:53 +0800206 with open(tmp_fn) as f:
207 data = json.load(f)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800208 return int(data['sync_start_time'])
209 finally:
210 if os.path.exists(tmp_fn):
211 os.unlink(tmp_fn)
212
213
214class AndroidSpecManager(codechange.SpecManager):
215 """Repo manifest related operations.
216
217 This class fetches and enumerates android manifest files, parses them,
218 and sync to disk state according to them.
219 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800220
221 def __init__(self, config):
222 self.config = config
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800223 self.manifest_dir = os.path.join(self.config['android_root'], '.repo',
224 'manifests')
225
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800226 def collect_float_spec(self, old, new, fixed_specs=None):
227 del fixed_specs # unused
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800228 result = []
229 path = 'default.xml'
230
231 commits = []
232 old_timestamp = lookup_build_timestamp(self.config['flavor'], old)
233 new_timestamp = lookup_build_timestamp(self.config['flavor'], new)
234 for timestamp, git_rev in git_util.get_history(self.manifest_dir, path):
235 if timestamp < old_timestamp:
236 commits = []
237 commits.append((timestamp, git_rev))
238 if timestamp > new_timestamp:
239 break
240
241 for timestamp, git_rev in commits:
242 result.append(
243 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
244 return result
245
246 def collect_fixed_spec(self, old, new):
247 result = []
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800248 revlist = get_build_ids_between(self.config['branch'],
249 self.config['flavor'], int(old), int(new))
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800250 for rev in revlist:
251 manifest_name = fetch_manifest(self.config['android_root'],
252 self.config['flavor'], rev)
253 path = os.path.join(self.manifest_dir, manifest_name)
254 timestamp = lookup_build_timestamp(self.config['flavor'], rev)
255 result.append(
256 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
257 return result
258
259 def _load_manifest_content(self, spec):
260 if spec.spec_type == codechange.SPEC_FIXED:
261 manifest_name = fetch_manifest(self.config['branch'],
262 self.config['flavor'], spec.name)
Kuang-che Wua5723492019-11-25 20:59:34 +0800263 with open(os.path.join(self.manifest_dir, manifest_name)) as f:
264 content = f.read()
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800265 else:
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800266 content = git_util.get_file_from_revision(self.manifest_dir, spec.name,
267 spec.path)
268 return content
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800269
270 def parse_spec(self, spec):
271 logging.debug('parse_spec %s', spec.name)
272 manifest_content = self._load_manifest_content(spec)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800273 parser = repo_util.ManifestParser(self.manifest_dir)
274 root = parser.parse_single_xml(manifest_content, allow_include=False)
275 spec.entries = parser.process_parsed_result(root)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800276 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800277 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +0800278 raise ValueError('fixed spec %r has unexpected floating entries' %
279 spec.name)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800280
281 def sync_disk_state(self, rev):
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800282 manifest_name = fetch_manifest(self.config['android_root'],
283 self.config['flavor'], rev)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800284 repo_util.sync(
285 self.config['android_root'],
286 manifest_name=manifest_name,
287 current_branch=True)
Zheng-Jie Changba212682020-09-18 16:30:08 +0800288
289
290class AndroidInternalApi:
291 """Wrapper of android internal api. Singleton class"""
292
293 _instance = None
294 _default_chunk_size = 20 * 1024 * 1024 # 20M
295 _retries = 5
296
297 def __new__(cls):
298 if cls._instance is None:
299 cls._instance = object.__new__(cls)
300 return cls._instance
301
302 def __init__(self):
303 if not os.environ.get('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON'):
304 raise errors.InternalError(
305 'SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON must defined when using Android Api'
306 )
307 self.credentials = self.get_credentials()
308 self.service = apiclient.discovery.build(
309 'androidbuildinternal', 'v3', credentials=self.credentials)
310
311 def get_credentials(self):
312 # TODO(zjchang): support compute engine type credential
313 credentials = google.oauth2.service_account.Credentials.from_service_account_file(
314 os.environ.get('SKYLAB_CLOUD_SERVICE_ACCOUNT_JSON'),
315 scopes=['https://www.googleapis.com/auth/androidbuild.internal'],
316 )
317 return credentials
318
319 def get_artifact_info(self, build_id, target, name, attempt_id=0):
320 """Get Android build artifact info.
321
322 Args:
323 build_id: Android build ID.
324 target: Android build target.
325 name: Artifact name.
326 attempt_id: A build attempt. Defaults to 0.
327
328 Returns:
329 Artifact info.
330
331 Raises:
332 errors.ExternalError
333 """
334 try:
335 artifact = self.service.buildartifact().get(
336 buildId=build_id,
337 target=target,
338 attemptId=attempt_id,
339 resourceId=name).execute(num_retries=self._retries)
340 except (googleapiclient.errors.HttpError, httplib2.HttpLib2Error) as e:
341 raise errors.ExternalError('Android API error: ' + str(e))
342
343 if artifact is None:
344 raise errors.ExternalError('Invalid artifact %s/%s/%s/%s' %
345 (build_id, target, attempt_id, name))
346 return artifact
347
348 def download_artifact(self, build_id, target, name, output_file,
349 attempt_id=0):
350 """Download one artifact.
351
352 Args:
353 build_id: Android build ID.
354 target: Android build target.
355 name: Artifact name.
356 output_file: Target file path.
357 attempt_id: A build attempt. Defaults to 0.
358
359 Raises:
360 errors.ExternalError
361 """
362 artifact = self.get_artifact_info(
363 build_id, target, name, attempt_id=attempt_id)
364 chunksize = min(int(artifact['size']), self._default_chunk_size)
365
366 download_request = self.service.buildartifact().get_media(
367 buildId=build_id, target=target, attemptId=attempt_id, resourceId=name)
368
369 try:
370 with open(output_file, mode='wb') as f:
371 downloader = apiclient.http.MediaIoBaseDownload(
372 f, download_request, chunksize=chunksize)
373 done = False
374 while not done:
375 status, done = downloader.next_chunk(num_retries=self._retries)
376 except (googleapiclient.errors.HttpError, httplib2.HttpLib2Error) as e:
377 raise errors.ExternalError('Android API error: ' + str(e))