blob: f1ca031e6a56a79c3f69f8ef439ede6493665298 [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 Wu708310b2018-03-28 17:24:34 +080020
21from bisect_kit import cli
Kuang-che Wud1d45b42018-07-05 00:46:45 +080022from bisect_kit import codechange
Kuang-che Wue121fae2018-11-09 16:18:39 +080023from bisect_kit import errors
Kuang-che Wud1d45b42018-07-05 00:46:45 +080024from bisect_kit import git_util
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080025from bisect_kit import repo_util
Kuang-che Wu708310b2018-03-28 17:24:34 +080026from bisect_kit import util
27
28logger = logging.getLogger(__name__)
29
30ANDROID_URL_BASE = ('https://android-build.googleplex.com/'
31 'builds/branch/{branch}')
32BUILD_IDS_BETWEEN_URL_TEMPLATE = (
33 ANDROID_URL_BASE + '/build-ids/between/{end}/{start}')
34BUILD_INFO_URL_TEMPLATE = ANDROID_URL_BASE + '/builds?id={build_id}'
35
36
37def is_android_build_id(s):
38 """Is an Android build id."""
39 # Build ID is always a number
40 return s.isdigit()
41
42
43def argtype_android_build_id(s):
44 if not is_android_build_id(s):
45 msg = 'invalid android build id (%s)' % s
46 raise cli.ArgTypeError(msg, '9876543')
47 return s
48
49
50def fetch_android_build_data(url):
51 """Fetches file from android build server.
52
53 Args:
54 url: URL to fetch
55
56 Returns:
57 file content (str). None if failed.
58 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080059 # Fetching android build data directly will fail without authentication.
Kuang-che Wu708310b2018-03-28 17:24:34 +080060 # Following code is just serving as demo purpose. You should modify or hook
61 # it with your own implementation.
62 logger.warn('fetch_android_build_data need to be hooked')
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080063 from six.moves import urllib
Kuang-che Wu708310b2018-03-28 17:24:34 +080064 try:
Kuang-che Wu68f022d2019-11-29 14:38:48 +080065 return urllib.request.urlopen(url).read().decode('utf8')
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080066 except urllib.request.URLError as e:
Kuang-che Wu708310b2018-03-28 17:24:34 +080067 logger.exception('failed to fetch "%s"', url)
Kuang-che Wue121fae2018-11-09 16:18:39 +080068 raise errors.ExternalError(str(e))
Kuang-che Wu708310b2018-03-28 17:24:34 +080069
70
71def is_good_build(branch, flavor, build_id):
72 """Determine a build_id was succeeded.
73
74 Args:
75 branch: The Android branch from which to retrieve the builds.
76 flavor: Target name of the Android image in question.
77 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
78 build_id: Android build id
79
80 Returns:
81 True if the given build was successful.
82 """
83
84 url = BUILD_INFO_URL_TEMPLATE.format(branch=branch, build_id=build_id)
Kuang-che Wu68f022d2019-11-29 14:38:48 +080085 build = json.loads(fetch_android_build_data(url))
Kuang-che Wu708310b2018-03-28 17:24:34 +080086 for target in build[0]['targets']:
87 if target['target']['name'] == flavor and target.get('successful'):
88 return True
89 return False
90
91
Kuang-che Wu0a902f42019-01-21 18:58:32 +080092def get_build_ids_between(branch, flavor, start, end):
Kuang-che Wu708310b2018-03-28 17:24:34 +080093 """Returns a list of build IDs.
94
95 Args:
96 branch: The Android branch from which to retrieve the builds.
Kuang-che Wu0a902f42019-01-21 18:58:32 +080097 flavor: Target name of the Android image in question.
98 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
Kuang-che Wu708310b2018-03-28 17:24:34 +080099 start: The starting point build ID. (inclusive)
100 end: The ending point build ID. (inclusive)
101
102 Returns:
103 A list of build IDs. (str)
104 """
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800105 # Do not remove this argument because our internal implementation need it.
106 del flavor # not used
107
Kuang-che Wu708310b2018-03-28 17:24:34 +0800108 # TODO(kcwu): remove pagination hack after b/68239878 fixed
109 build_id_set = set()
110 tmp_end = end
111 while True:
112 url = BUILD_IDS_BETWEEN_URL_TEMPLATE.format(
113 branch=branch, start=start, end=tmp_end)
Kuang-che Wu68f022d2019-11-29 14:38:48 +0800114 query_result = json.loads(fetch_android_build_data(url))
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800115 found_new = set(int(x) for x in query_result['ids']) - build_id_set
Kuang-che Wu708310b2018-03-28 17:24:34 +0800116 if not found_new:
117 break
118 build_id_set.update(found_new)
119 tmp_end = min(build_id_set)
120
121 logger.debug('Found %d builds in the range.', len(build_id_set))
122
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800123 return [str(bid) for bid in sorted(build_id_set)]
Kuang-che Wu708310b2018-03-28 17:24:34 +0800124
125
126def lunch(android_root, flavor, *args, **kwargs):
127 """Helper to run commands with Android lunch env.
128
129 Args:
130 android_root: root path of Android tree
131 flavor: lunch flavor
132 args: command to run
133 kwargs: extra arguments passed to util.Popen
134 """
135 util.check_call('./android_lunch_helper.sh', android_root, flavor, *args,
136 **kwargs)
137
138
139def fetch_artifact(flavor, build_id, filename, dest):
140 """Fetches Android build artifact.
141
142 Args:
143 flavor: Android build flavor
144 build_id: Android build id
145 filename: artifact name
146 dest: local path to store the fetched artifact
147 """
Kuang-che Wuc7ee6c82019-01-16 17:05:49 +0800148 service_account_args = []
149 # These arguments are for bisector service and not required for users of
150 # bisect-kit using personal account.
151 if os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_EMAIL'):
152 assert os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_KEY')
153 service_account_args += [
154 '--email',
155 os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_EMAIL'),
156 '--apiary_service_account_private_key_path',
157 os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_KEY'),
158 ]
159
Kuang-che Wu708310b2018-03-28 17:24:34 +0800160 util.check_call('/google/data/ro/projects/android/fetch_artifact', '--target',
Kuang-che Wuc7ee6c82019-01-16 17:05:49 +0800161 flavor, '--bid', build_id, filename, dest,
162 *service_account_args)
Kuang-che Wu708310b2018-03-28 17:24:34 +0800163
164
165def fetch_manifest(android_root, flavor, build_id):
166 """Fetches Android repo manifest of given build.
167
168 Args:
169 android_root: root path of Android tree. Fetched manifest file will be
170 stored inside.
171 flavor: Android build flavor
172 build_id: Android build id
173
174 Returns:
175 the local filename of manifest (relative to android_root/.repo/manifests/)
176 """
177 # Assume manifest is flavor independent, thus not encoded into the file name.
178 manifest_name = 'manifest_%s.xml' % build_id
179 manifest_path = os.path.join(android_root, '.repo', 'manifests',
180 manifest_name)
181 if not os.path.exists(manifest_path):
182 fetch_artifact(flavor, build_id, manifest_name, manifest_path)
183 return manifest_name
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800184
185
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800186def lookup_build_timestamp(flavor, build_id):
187 """Lookup timestamp of Android prebuilt.
188
189 Args:
190 flavor: Android build flavor
191 build_id: Android build id
192
193 Returns:
194 timestamp
195 """
196 tmp_fn = tempfile.mktemp()
197 try:
198 fetch_artifact(flavor, build_id, 'BUILD_INFO', tmp_fn)
Kuang-che Wu74bcb642020-02-20 18:45:53 +0800199 with open(tmp_fn) as f:
200 data = json.load(f)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800201 return int(data['sync_start_time'])
202 finally:
203 if os.path.exists(tmp_fn):
204 os.unlink(tmp_fn)
205
206
207class AndroidSpecManager(codechange.SpecManager):
208 """Repo manifest related operations.
209
210 This class fetches and enumerates android manifest files, parses them,
211 and sync to disk state according to them.
212 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800213
214 def __init__(self, config):
215 self.config = config
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800216 self.manifest_dir = os.path.join(self.config['android_root'], '.repo',
217 'manifests')
218
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800219 def collect_float_spec(self, old, new, fixed_specs=None):
220 del fixed_specs # unused
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800221 result = []
222 path = 'default.xml'
223
224 commits = []
225 old_timestamp = lookup_build_timestamp(self.config['flavor'], old)
226 new_timestamp = lookup_build_timestamp(self.config['flavor'], new)
227 for timestamp, git_rev in git_util.get_history(self.manifest_dir, path):
228 if timestamp < old_timestamp:
229 commits = []
230 commits.append((timestamp, git_rev))
231 if timestamp > new_timestamp:
232 break
233
234 for timestamp, git_rev in commits:
235 result.append(
236 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
237 return result
238
239 def collect_fixed_spec(self, old, new):
240 result = []
Kuang-che Wu0a902f42019-01-21 18:58:32 +0800241 revlist = get_build_ids_between(self.config['branch'],
242 self.config['flavor'], int(old), int(new))
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800243 for rev in revlist:
244 manifest_name = fetch_manifest(self.config['android_root'],
245 self.config['flavor'], rev)
246 path = os.path.join(self.manifest_dir, manifest_name)
247 timestamp = lookup_build_timestamp(self.config['flavor'], rev)
248 result.append(
249 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
250 return result
251
252 def _load_manifest_content(self, spec):
253 if spec.spec_type == codechange.SPEC_FIXED:
254 manifest_name = fetch_manifest(self.config['branch'],
255 self.config['flavor'], spec.name)
Kuang-che Wua5723492019-11-25 20:59:34 +0800256 with open(os.path.join(self.manifest_dir, manifest_name)) as f:
257 content = f.read()
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800258 else:
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800259 content = git_util.get_file_from_revision(self.manifest_dir, spec.name,
260 spec.path)
261 return content
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800262
263 def parse_spec(self, spec):
264 logging.debug('parse_spec %s', spec.name)
265 manifest_content = self._load_manifest_content(spec)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800266 parser = repo_util.ManifestParser(self.manifest_dir)
267 root = parser.parse_single_xml(manifest_content, allow_include=False)
268 spec.entries = parser.process_parsed_result(root)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800269 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800270 if not spec.is_static():
271 raise ValueError(
272 'fixed spec %r has unexpected floating entries' % spec.name)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800273
274 def sync_disk_state(self, rev):
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800275 manifest_name = fetch_manifest(self.config['android_root'],
276 self.config['flavor'], rev)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800277 repo_util.sync(
278 self.config['android_root'],
279 manifest_name=manifest_name,
280 current_branch=True)