blob: d00c2c642b90a87815976bf64e47fd9e84e63e35 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Android utility.
Terminology used in this module:
"product-variant" is sometimes called "target" and sometimes "flavor".
I prefer to use "flavor" in the code because
- "target" is too general
- sometimes, it is not in the form of "product-variant", for example,
"cts_arm_64"
"""
from __future__ import print_function
import json
import logging
import os
import tempfile
import urllib.request
from bisect_kit import cli
from bisect_kit import codechange
from bisect_kit import errors
from bisect_kit import git_util
from bisect_kit import repo_util
from bisect_kit import util
logger = logging.getLogger(__name__)
ANDROID_URL_BASE = ('https://android-build.googleplex.com/'
'builds/branch/{branch}')
BUILD_IDS_BETWEEN_URL_TEMPLATE = (
ANDROID_URL_BASE + '/build-ids/between/{end}/{start}')
BUILD_INFO_URL_TEMPLATE = ANDROID_URL_BASE + '/builds?id={build_id}'
def is_android_build_id(s):
"""Is an Android build id."""
# Build ID is always a number
return s.isdigit()
def argtype_android_build_id(s):
if not is_android_build_id(s):
msg = 'invalid android build id (%s)' % s
raise cli.ArgTypeError(msg, '9876543')
return s
def fetch_android_build_data(url):
"""Fetches file from android build server.
Args:
url: URL to fetch
Returns:
file content (str). None if failed.
"""
# Fetching android build data directly will fail without authentication.
# Following code is just serving as demo purpose. You should modify or hook
# it with your own implementation.
logger.warning('fetch_android_build_data need to be hooked')
try:
return urllib.request.urlopen(url).read().decode('utf8')
except urllib.request.URLError as e:
logger.exception('failed to fetch "%s"', url)
raise errors.ExternalError(str(e))
def is_good_build(branch, flavor, build_id):
"""Determine a build_id was succeeded.
Args:
branch: The Android branch from which to retrieve the builds.
flavor: Target name of the Android image in question.
E.g. "cheets_x86-userdebug" or "cheets_arm-user".
build_id: Android build id
Returns:
True if the given build was successful.
"""
url = BUILD_INFO_URL_TEMPLATE.format(branch=branch, build_id=build_id)
build = json.loads(fetch_android_build_data(url))
for target in build[0]['targets']:
if target['target']['name'] == flavor and target.get('successful'):
return True
return False
def get_build_ids_between(branch, flavor, start, end):
"""Returns a list of build IDs.
Args:
branch: The Android branch from which to retrieve the builds.
flavor: Target name of the Android image in question.
E.g. "cheets_x86-userdebug" or "cheets_arm-user".
start: The starting point build ID. (inclusive)
end: The ending point build ID. (inclusive)
Returns:
A list of build IDs. (str)
"""
# Do not remove this argument because our internal implementation need it.
del flavor # not used
# TODO(kcwu): remove pagination hack after b/68239878 fixed
build_id_set = set()
tmp_end = end
while True:
url = BUILD_IDS_BETWEEN_URL_TEMPLATE.format(
branch=branch, start=start, end=tmp_end)
query_result = json.loads(fetch_android_build_data(url))
found_new = set(int(x) for x in query_result['ids']) - build_id_set
if not found_new:
break
build_id_set.update(found_new)
tmp_end = min(build_id_set)
logger.debug('Found %d builds in the range.', len(build_id_set))
return [str(bid) for bid in sorted(build_id_set)]
def lunch(android_root, flavor, *args, **kwargs):
"""Helper to run commands with Android lunch env.
Args:
android_root: root path of Android tree
flavor: lunch flavor
args: command to run
kwargs: extra arguments passed to util.Popen
"""
util.check_call('./android_lunch_helper.sh', android_root, flavor, *args,
**kwargs)
def fetch_artifact(flavor, build_id, filename, dest):
"""Fetches Android build artifact.
Args:
flavor: Android build flavor
build_id: Android build id
filename: artifact name
dest: local path to store the fetched artifact
"""
service_account_args = []
# These arguments are for bisector service and not required for users of
# bisect-kit using personal account.
if os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_EMAIL'):
assert os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_KEY')
service_account_args += [
'--email',
os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_EMAIL'),
'--apiary_service_account_private_key_path',
os.environ.get('ANDROID_CLOUD_SERVICE_ACCOUNT_KEY'),
]
util.check_call('/google/data/ro/projects/android/fetch_artifact', '--target',
flavor, '--bid', build_id, filename, dest,
*service_account_args)
def fetch_manifest(android_root, flavor, build_id):
"""Fetches Android repo manifest of given build.
Args:
android_root: root path of Android tree. Fetched manifest file will be
stored inside.
flavor: Android build flavor
build_id: Android build id
Returns:
the local filename of manifest (relative to android_root/.repo/manifests/)
"""
# Assume manifest is flavor independent, thus not encoded into the file name.
manifest_name = 'manifest_%s.xml' % build_id
manifest_path = os.path.join(android_root, '.repo', 'manifests',
manifest_name)
if not os.path.exists(manifest_path):
fetch_artifact(flavor, build_id, manifest_name, manifest_path)
return manifest_name
def lookup_build_timestamp(flavor, build_id):
"""Lookup timestamp of Android prebuilt.
Args:
flavor: Android build flavor
build_id: Android build id
Returns:
timestamp
"""
tmp_fn = tempfile.mktemp()
try:
fetch_artifact(flavor, build_id, 'BUILD_INFO', tmp_fn)
with open(tmp_fn) as f:
data = json.load(f)
return int(data['sync_start_time'])
finally:
if os.path.exists(tmp_fn):
os.unlink(tmp_fn)
class AndroidSpecManager(codechange.SpecManager):
"""Repo manifest related operations.
This class fetches and enumerates android manifest files, parses them,
and sync to disk state according to them.
"""
def __init__(self, config):
self.config = config
self.manifest_dir = os.path.join(self.config['android_root'], '.repo',
'manifests')
def collect_float_spec(self, old, new, fixed_specs=None):
del fixed_specs # unused
result = []
path = 'default.xml'
commits = []
old_timestamp = lookup_build_timestamp(self.config['flavor'], old)
new_timestamp = lookup_build_timestamp(self.config['flavor'], new)
for timestamp, git_rev in git_util.get_history(self.manifest_dir, path):
if timestamp < old_timestamp:
commits = []
commits.append((timestamp, git_rev))
if timestamp > new_timestamp:
break
for timestamp, git_rev in commits:
result.append(
codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
return result
def collect_fixed_spec(self, old, new):
result = []
revlist = get_build_ids_between(self.config['branch'],
self.config['flavor'], int(old), int(new))
for rev in revlist:
manifest_name = fetch_manifest(self.config['android_root'],
self.config['flavor'], rev)
path = os.path.join(self.manifest_dir, manifest_name)
timestamp = lookup_build_timestamp(self.config['flavor'], rev)
result.append(
codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
return result
def _load_manifest_content(self, spec):
if spec.spec_type == codechange.SPEC_FIXED:
manifest_name = fetch_manifest(self.config['branch'],
self.config['flavor'], spec.name)
with open(os.path.join(self.manifest_dir, manifest_name)) as f:
content = f.read()
else:
content = git_util.get_file_from_revision(self.manifest_dir, spec.name,
spec.path)
return content
def parse_spec(self, spec):
logging.debug('parse_spec %s', spec.name)
manifest_content = self._load_manifest_content(spec)
parser = repo_util.ManifestParser(self.manifest_dir)
root = parser.parse_single_xml(manifest_content, allow_include=False)
spec.entries = parser.process_parsed_result(root)
if spec.spec_type == codechange.SPEC_FIXED:
if not spec.is_static():
raise ValueError('fixed spec %r has unexpected floating entries' %
spec.name)
def sync_disk_state(self, rev):
manifest_name = fetch_manifest(self.config['android_root'],
self.config['flavor'], rev)
repo_util.sync(
self.config['android_root'],
manifest_name=manifest_name,
current_branch=True)