blob: 281d93305f273c8aa8f78a6024dc933bb956c0d6 [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
23from bisect_kit import git_util
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080024from bisect_kit import repo_util
Kuang-che Wu708310b2018-03-28 17:24:34 +080025from bisect_kit import util
26
27logger = logging.getLogger(__name__)
28
29ANDROID_URL_BASE = ('https://android-build.googleplex.com/'
30 'builds/branch/{branch}')
31BUILD_IDS_BETWEEN_URL_TEMPLATE = (
32 ANDROID_URL_BASE + '/build-ids/between/{end}/{start}')
33BUILD_INFO_URL_TEMPLATE = ANDROID_URL_BASE + '/builds?id={build_id}'
34
35
36def is_android_build_id(s):
37 """Is an Android build id."""
38 # Build ID is always a number
39 return s.isdigit()
40
41
42def argtype_android_build_id(s):
43 if not is_android_build_id(s):
44 msg = 'invalid android build id (%s)' % s
45 raise cli.ArgTypeError(msg, '9876543')
46 return s
47
48
49def fetch_android_build_data(url):
50 """Fetches file from android build server.
51
52 Args:
53 url: URL to fetch
54
55 Returns:
56 file content (str). None if failed.
57 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080058 # Fetching android build data directly will fail without authentication.
Kuang-che Wu708310b2018-03-28 17:24:34 +080059 # Following code is just serving as demo purpose. You should modify or hook
60 # it with your own implementation.
61 logger.warn('fetch_android_build_data need to be hooked')
62 import urllib2
63 try:
64 return urllib2.urlopen(url).read()
65 except urllib2.URLError:
66 logger.exception('failed to fetch "%s"', url)
67 raise
68
69
70def is_good_build(branch, flavor, build_id):
71 """Determine a build_id was succeeded.
72
73 Args:
74 branch: The Android branch from which to retrieve the builds.
75 flavor: Target name of the Android image in question.
76 E.g. "cheets_x86-userdebug" or "cheets_arm-user".
77 build_id: Android build id
78
79 Returns:
80 True if the given build was successful.
81 """
82
83 url = BUILD_INFO_URL_TEMPLATE.format(branch=branch, build_id=build_id)
84 build = json.loads(fetch_android_build_data(url).decode('utf-8'))
85 for target in build[0]['targets']:
86 if target['target']['name'] == flavor and target.get('successful'):
87 return True
88 return False
89
90
91def get_build_ids_between(branch, start, end):
92 """Returns a list of build IDs.
93
94 Args:
95 branch: The Android branch from which to retrieve the builds.
96 start: The starting point build ID. (inclusive)
97 end: The ending point build ID. (inclusive)
98
99 Returns:
100 A list of build IDs. (str)
101 """
102 # TODO(kcwu): remove pagination hack after b/68239878 fixed
103 build_id_set = set()
104 tmp_end = end
105 while True:
106 url = BUILD_IDS_BETWEEN_URL_TEMPLATE.format(
107 branch=branch, start=start, end=tmp_end)
108 query_result = json.loads(fetch_android_build_data(url).decode('utf-8'))
109 found_new = set(map(int, query_result['ids'])) - build_id_set
110 if not found_new:
111 break
112 build_id_set.update(found_new)
113 tmp_end = min(build_id_set)
114
115 logger.debug('Found %d builds in the range.', len(build_id_set))
116
117 return map(str, sorted(build_id_set))
118
119
120def lunch(android_root, flavor, *args, **kwargs):
121 """Helper to run commands with Android lunch env.
122
123 Args:
124 android_root: root path of Android tree
125 flavor: lunch flavor
126 args: command to run
127 kwargs: extra arguments passed to util.Popen
128 """
129 util.check_call('./android_lunch_helper.sh', android_root, flavor, *args,
130 **kwargs)
131
132
133def fetch_artifact(flavor, build_id, filename, dest):
134 """Fetches Android build artifact.
135
136 Args:
137 flavor: Android build flavor
138 build_id: Android build id
139 filename: artifact name
140 dest: local path to store the fetched artifact
141 """
142 util.check_call('/google/data/ro/projects/android/fetch_artifact', '--target',
143 flavor, '--bid', build_id, filename, dest)
144
145
146def fetch_manifest(android_root, flavor, build_id):
147 """Fetches Android repo manifest of given build.
148
149 Args:
150 android_root: root path of Android tree. Fetched manifest file will be
151 stored inside.
152 flavor: Android build flavor
153 build_id: Android build id
154
155 Returns:
156 the local filename of manifest (relative to android_root/.repo/manifests/)
157 """
158 # Assume manifest is flavor independent, thus not encoded into the file name.
159 manifest_name = 'manifest_%s.xml' % build_id
160 manifest_path = os.path.join(android_root, '.repo', 'manifests',
161 manifest_name)
162 if not os.path.exists(manifest_path):
163 fetch_artifact(flavor, build_id, manifest_name, manifest_path)
164 return manifest_name
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800165
166
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800167def lookup_build_timestamp(flavor, build_id):
168 """Lookup timestamp of Android prebuilt.
169
170 Args:
171 flavor: Android build flavor
172 build_id: Android build id
173
174 Returns:
175 timestamp
176 """
177 tmp_fn = tempfile.mktemp()
178 try:
179 fetch_artifact(flavor, build_id, 'BUILD_INFO', tmp_fn)
180 data = json.load(open(tmp_fn))
181 return int(data['sync_start_time'])
182 finally:
183 if os.path.exists(tmp_fn):
184 os.unlink(tmp_fn)
185
186
187class AndroidSpecManager(codechange.SpecManager):
188 """Repo manifest related operations.
189
190 This class fetches and enumerates android manifest files, parses them,
191 and sync to disk state according to them.
192 """
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800193
194 def __init__(self, config):
195 self.config = config
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800196 self.manifest_dir = os.path.join(self.config['android_root'], '.repo',
197 'manifests')
198
199 def collect_float_spec(self, old, new):
200 result = []
201 path = 'default.xml'
202
203 commits = []
204 old_timestamp = lookup_build_timestamp(self.config['flavor'], old)
205 new_timestamp = lookup_build_timestamp(self.config['flavor'], new)
206 for timestamp, git_rev in git_util.get_history(self.manifest_dir, path):
207 if timestamp < old_timestamp:
208 commits = []
209 commits.append((timestamp, git_rev))
210 if timestamp > new_timestamp:
211 break
212
213 for timestamp, git_rev in commits:
214 result.append(
215 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
216 return result
217
218 def collect_fixed_spec(self, old, new):
219 result = []
220 revlist = get_build_ids_between(self.config['branch'], int(old), int(new))
221 for rev in revlist:
222 manifest_name = fetch_manifest(self.config['android_root'],
223 self.config['flavor'], rev)
224 path = os.path.join(self.manifest_dir, manifest_name)
225 timestamp = lookup_build_timestamp(self.config['flavor'], rev)
226 result.append(
227 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
228 return result
229
230 def _load_manifest_content(self, spec):
231 if spec.spec_type == codechange.SPEC_FIXED:
232 manifest_name = fetch_manifest(self.config['branch'],
233 self.config['flavor'], spec.name)
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800234 content = open(os.path.join(self.manifest_dir, manifest_name)).read()
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800235 else:
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800236 content = git_util.get_file_from_revision(self.manifest_dir, spec.name,
237 spec.path)
238 return content
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800239
240 def parse_spec(self, spec):
241 logging.debug('parse_spec %s', spec.name)
242 manifest_content = self._load_manifest_content(spec)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800243 parser = repo_util.ManifestParser(self.manifest_dir)
244 root = parser.parse_single_xml(manifest_content, allow_include=False)
245 spec.entries = parser.process_parsed_result(root)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800246 if spec.spec_type == codechange.SPEC_FIXED:
247 assert spec.is_static()
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800248
249 def sync_disk_state(self, rev):
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800250 manifest_name = fetch_manifest(self.config['android_root'],
251 self.config['flavor'], rev)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800252 repo_util.sync(
253 self.config['android_root'],
254 manifest_name=manifest_name,
255 current_branch=True)