# 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()
|