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