Kuang-che Wu | 6e4beca | 2018-06-27 17:45:02 +0800 | [diff] [blame^] | 1 | # -*- coding: utf-8 -*- |
Kuang-che Wu | 708310b | 2018-03-28 17:24:34 +0800 | [diff] [blame] | 2 | # 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 | |
| 7 | Terminology 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 | |
| 15 | from __future__ import print_function |
| 16 | import json |
| 17 | import logging |
| 18 | import os |
| 19 | |
| 20 | from bisect_kit import cli |
Kuang-che Wu | acb6efd | 2018-04-25 18:52:58 +0800 | [diff] [blame] | 21 | from bisect_kit import repo_util |
Kuang-che Wu | 708310b | 2018-03-28 17:24:34 +0800 | [diff] [blame] | 22 | from bisect_kit import util |
| 23 | |
| 24 | logger = logging.getLogger(__name__) |
| 25 | |
| 26 | ANDROID_URL_BASE = ('https://android-build.googleplex.com/' |
| 27 | 'builds/branch/{branch}') |
| 28 | BUILD_IDS_BETWEEN_URL_TEMPLATE = ( |
| 29 | ANDROID_URL_BASE + '/build-ids/between/{end}/{start}') |
| 30 | BUILD_INFO_URL_TEMPLATE = ANDROID_URL_BASE + '/builds?id={build_id}' |
| 31 | |
| 32 | |
| 33 | def is_android_build_id(s): |
| 34 | """Is an Android build id.""" |
| 35 | # Build ID is always a number |
| 36 | return s.isdigit() |
| 37 | |
| 38 | |
| 39 | def argtype_android_build_id(s): |
| 40 | if not is_android_build_id(s): |
| 41 | msg = 'invalid android build id (%s)' % s |
| 42 | raise cli.ArgTypeError(msg, '9876543') |
| 43 | return s |
| 44 | |
| 45 | |
| 46 | def fetch_android_build_data(url): |
| 47 | """Fetches file from android build server. |
| 48 | |
| 49 | Args: |
| 50 | url: URL to fetch |
| 51 | |
| 52 | Returns: |
| 53 | file content (str). None if failed. |
| 54 | """ |
Kuang-che Wu | acb6efd | 2018-04-25 18:52:58 +0800 | [diff] [blame] | 55 | # Fetching android build data directly will fail without authentication. |
Kuang-che Wu | 708310b | 2018-03-28 17:24:34 +0800 | [diff] [blame] | 56 | # Following code is just serving as demo purpose. You should modify or hook |
| 57 | # it with your own implementation. |
| 58 | logger.warn('fetch_android_build_data need to be hooked') |
| 59 | import urllib2 |
| 60 | try: |
| 61 | return urllib2.urlopen(url).read() |
| 62 | except urllib2.URLError: |
| 63 | logger.exception('failed to fetch "%s"', url) |
| 64 | raise |
| 65 | |
| 66 | |
| 67 | def is_good_build(branch, flavor, build_id): |
| 68 | """Determine a build_id was succeeded. |
| 69 | |
| 70 | Args: |
| 71 | branch: The Android branch from which to retrieve the builds. |
| 72 | flavor: Target name of the Android image in question. |
| 73 | E.g. "cheets_x86-userdebug" or "cheets_arm-user". |
| 74 | build_id: Android build id |
| 75 | |
| 76 | Returns: |
| 77 | True if the given build was successful. |
| 78 | """ |
| 79 | |
| 80 | url = BUILD_INFO_URL_TEMPLATE.format(branch=branch, build_id=build_id) |
| 81 | build = json.loads(fetch_android_build_data(url).decode('utf-8')) |
| 82 | for target in build[0]['targets']: |
| 83 | if target['target']['name'] == flavor and target.get('successful'): |
| 84 | return True |
| 85 | return False |
| 86 | |
| 87 | |
| 88 | def get_build_ids_between(branch, start, end): |
| 89 | """Returns a list of build IDs. |
| 90 | |
| 91 | Args: |
| 92 | branch: The Android branch from which to retrieve the builds. |
| 93 | start: The starting point build ID. (inclusive) |
| 94 | end: The ending point build ID. (inclusive) |
| 95 | |
| 96 | Returns: |
| 97 | A list of build IDs. (str) |
| 98 | """ |
| 99 | # TODO(kcwu): remove pagination hack after b/68239878 fixed |
| 100 | build_id_set = set() |
| 101 | tmp_end = end |
| 102 | while True: |
| 103 | url = BUILD_IDS_BETWEEN_URL_TEMPLATE.format( |
| 104 | branch=branch, start=start, end=tmp_end) |
| 105 | query_result = json.loads(fetch_android_build_data(url).decode('utf-8')) |
| 106 | found_new = set(map(int, query_result['ids'])) - build_id_set |
| 107 | if not found_new: |
| 108 | break |
| 109 | build_id_set.update(found_new) |
| 110 | tmp_end = min(build_id_set) |
| 111 | |
| 112 | logger.debug('Found %d builds in the range.', len(build_id_set)) |
| 113 | |
| 114 | return map(str, sorted(build_id_set)) |
| 115 | |
| 116 | |
| 117 | def lunch(android_root, flavor, *args, **kwargs): |
| 118 | """Helper to run commands with Android lunch env. |
| 119 | |
| 120 | Args: |
| 121 | android_root: root path of Android tree |
| 122 | flavor: lunch flavor |
| 123 | args: command to run |
| 124 | kwargs: extra arguments passed to util.Popen |
| 125 | """ |
| 126 | util.check_call('./android_lunch_helper.sh', android_root, flavor, *args, |
| 127 | **kwargs) |
| 128 | |
| 129 | |
| 130 | def fetch_artifact(flavor, build_id, filename, dest): |
| 131 | """Fetches Android build artifact. |
| 132 | |
| 133 | Args: |
| 134 | flavor: Android build flavor |
| 135 | build_id: Android build id |
| 136 | filename: artifact name |
| 137 | dest: local path to store the fetched artifact |
| 138 | """ |
| 139 | util.check_call('/google/data/ro/projects/android/fetch_artifact', '--target', |
| 140 | flavor, '--bid', build_id, filename, dest) |
| 141 | |
| 142 | |
| 143 | def fetch_manifest(android_root, flavor, build_id): |
| 144 | """Fetches Android repo manifest of given build. |
| 145 | |
| 146 | Args: |
| 147 | android_root: root path of Android tree. Fetched manifest file will be |
| 148 | stored inside. |
| 149 | flavor: Android build flavor |
| 150 | build_id: Android build id |
| 151 | |
| 152 | Returns: |
| 153 | the local filename of manifest (relative to android_root/.repo/manifests/) |
| 154 | """ |
| 155 | # Assume manifest is flavor independent, thus not encoded into the file name. |
| 156 | manifest_name = 'manifest_%s.xml' % build_id |
| 157 | manifest_path = os.path.join(android_root, '.repo', 'manifests', |
| 158 | manifest_name) |
| 159 | if not os.path.exists(manifest_path): |
| 160 | fetch_artifact(flavor, build_id, manifest_name, manifest_path) |
| 161 | return manifest_name |
Kuang-che Wu | acb6efd | 2018-04-25 18:52:58 +0800 | [diff] [blame] | 162 | |
| 163 | |
| 164 | class AndroidManifestManager(repo_util.ManifestManager): |
| 165 | """Manifest operations for Android repo""" |
| 166 | |
| 167 | def __init__(self, config): |
| 168 | self.config = config |
| 169 | self.flavor = config['flavor'] |
| 170 | |
| 171 | def sync_disk_state(self, rev): |
| 172 | manifest_name = self.fetch_manifest(rev) |
| 173 | repo_util.sync( |
| 174 | self.config['android_root'], |
| 175 | manifest_name=manifest_name, |
| 176 | current_branch=True) |
| 177 | |
| 178 | def fetch_git_repos(self, rev): |
| 179 | # TODO(kcwu): fetch git history but don't switch current disk state. |
| 180 | self.sync_disk_state(rev) |
| 181 | |
| 182 | def enumerate_manifest(self, old, new): |
| 183 | revlist = get_build_ids_between(self.config['branch'], int(old), int(new)) |
| 184 | |
| 185 | assert revlist[0] == old |
| 186 | assert revlist[-1] == new |
| 187 | return revlist |
| 188 | |
| 189 | def fetch_manifest(self, rev): |
| 190 | return fetch_manifest(self.config['android_root'], self.flavor, rev) |