# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A commandline tool to check and update packages in external/
Example usage:
updater.sh checkall
updater.sh update kotlinc
"""
import argparse
import json
import os
import subprocess
import time
from google.protobuf import text_format # pylint: disable=import-error
from git_updater import GitUpdater
from github_archive_updater import GithubArchiveUpdater
import fileutils
import git_utils
import updater_utils
UPDATERS = [GithubArchiveUpdater, GitUpdater]
def color_string(string, color):
"""Changes the color of a string when print to terminal."""
colors = {
'FRESH': '\x1b[32m',
'STALE': '\x1b[31;1m',
'ERROR': '\x1b[31m',
}
end_color = '\033[0m'
return colors[color] + string + end_color
def build_updater(proj_path):
"""Build updater for a project specified by proj_path.
Reads and parses METADATA file. And builds updater based on the information.
Args:
proj_path: Absolute or relative path to the project.
Returns:
The updater object built. None if there's any error.
"""
proj_path = fileutils.get_absolute_project_path(proj_path)
try:
metadata = fileutils.read_metadata(proj_path)
except text_format.ParseError as err:
print('{} {}.'.format(color_string('Invalid metadata file:', 'ERROR'),
err))
return None
try:
updater = updater_utils.create_updater(metadata, proj_path, UPDATERS)
except ValueError:
print(color_string('No supported URL.', 'ERROR'))
return None
return updater
def has_new_version(updater):
"""Whether an updater found a new version."""
return updater.get_current_version() != updater.get_latest_version()
def _message_for_calledprocesserror(error):
return '\n'.join([error.stdout.decode('utf-8'),
error.stderr.decode('utf-8')])
def check_update(proj_path):
"""Checks updates for a project. Prints result on console.
Args:
proj_path: Absolute or relative path to the project.
"""
print(
'Checking {}. '.format(fileutils.get_relative_project_path(proj_path)),
end='')
updater = build_updater(proj_path)
if updater is None:
return (None, 'Failed to create updater')
try:
updater.check()
if has_new_version(updater):
print(color_string(' Out of date!', 'STALE'))
else:
print(color_string(' Up to date.', 'FRESH'))
return (updater, None)
except (IOError, ValueError) as err:
print('{} {}.'.format(color_string('Failed.', 'ERROR'),
err))
return (updater, str(err))
except subprocess.CalledProcessError as err:
msg = _message_for_calledprocesserror(err)
print('{}\n{}'.format(msg, color_string('Failed.', 'ERROR')))
return (updater, msg)
def _process_update_result(path):
res = {}
updater, err = check_update(path)
if err is not None:
res['error'] = str(err)
else:
res['current'] = updater.get_current_version()
res['latest'] = updater.get_latest_version()
return res
def _check_some(paths, delay):
results = {}
for path in paths:
relative_path = fileutils.get_relative_project_path(path)
results[relative_path] = _process_update_result(path)
time.sleep(delay)
return results
def _check_all(delay):
results = {}
for path, dirs, files in os.walk(fileutils.EXTERNAL_PATH):
dirs.sort(key=lambda d: d.lower())
if fileutils.METADATA_FILENAME in files:
# Skip sub directories.
dirs[:] = []
relative_path = fileutils.get_relative_project_path(path)
results[relative_path] = _process_update_result(path)
time.sleep(delay)
return results
def check(args):
"""Handler for check command."""
if args.all:
results = _check_all(args.delay)
else:
results = _check_some(args.paths, args.delay)
if args.json_output is not None:
with open(args.json_output, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def update(args):
"""Handler for update command."""
try:
_do_update(args)
except subprocess.CalledProcessError as err:
msg = _message_for_calledprocesserror(err)
print(
'{}\n{}'.format(
msg,
color_string(
'Failed to upgrade.',
'ERROR')))
TMP_BRANCH_NAME = 'tmp_auto_upgrade'
def _do_update(args):
updater, err = check_update(args.path)
if updater is None:
return
if not has_new_version(updater) and not args.force:
return
full_path = fileutils.get_absolute_project_path(args.path)
if args.branch_and_commit:
git_utils.checkout(full_path, args.remote_name + '/master')
try:
git_utils.delete_branch(full_path, TMP_BRANCH_NAME)
except subprocess.CalledProcessError as err:
# Still continue if the branch doesn't exist.
pass
git_utils.start_branch(full_path, TMP_BRANCH_NAME)
updater.update()
if args.branch_and_commit:
msg = 'Upgrade {} to {}\n\nTest: None'.format(
args.path, updater.get_latest_version())
git_utils.add_file(full_path, '*')
git_utils.commit(full_path, msg)
if args.push_change:
git_utils.push(full_path, args.remote_name)
if args.branch_and_commit:
git_utils.checkout(full_path, args.remote_name + '/master')
def parse_args():
"""Parses commandline arguments."""
parser = argparse.ArgumentParser(
description='Check updates for third party projects in external/.')
subparsers = parser.add_subparsers(dest='cmd')
subparsers.required = True
# Creates parser for check command.
check_parser = subparsers.add_parser(
'check', help='Check update for one project.')
check_parser.add_argument(
'paths', nargs='*',
help='Paths of the project. '
'Relative paths will be resolved from external/.')
check_parser.add_argument(
'--json_output',
help='Path of a json file to write result to.')
check_parser.add_argument(
'--all', action='store_true',
help='If set, check updates for all supported projects.')
check_parser.add_argument(
'--delay', default=0, type=int,
help='Time in seconds to wait between checking two projects.')
check_parser.set_defaults(func=check)
# Creates parser for update command.
update_parser = subparsers.add_parser('update', help='Update one project.')
update_parser.add_argument(
'path',
help='Path of the project. '
'Relative paths will be resolved from external/.')
update_parser.add_argument(
'--force',
help='Run update even if there\'s no new version.',
action='store_true')
update_parser.add_argument(
'--branch_and_commit', action='store_true',
help='Starts a new branch and commit changes.')
update_parser.add_argument(
'--push_change', action='store_true',
help='Pushes change to Gerrit.')
update_parser.add_argument(
'--remote_name', default='aosp', required=False,
help='Upstream remote name.')
update_parser.set_defaults(func=update)
return parser.parse_args()
def main():
"""The main entry."""
args = parse_args()
args.func(args)
if __name__ == '__main__':
main()