blob: 103806811ea3e0a24c2b2105fe4e60a5bc96c605 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
r"""This script downloads / packages & uploads Android SDK packages.
It could be run when we need to update sdk packages to latest version.
It has 2 usages:
1) download: downloading a new version of the SDK via sdkmanager
2) package: wrapping SDK directory into CIPD-compatible packages and
uploading the new packages via CIPD to server.
Providing '--dry-run' option to show what packages to be
created and uploaded without actually doing either.
Both downloading and uploading allows to either specify a package, or
deal with default packages (build-tools, platform-tools, platforms and
tools).
Example usage:
1) updating default packages:
$ update_sdk.py download
(optional) $ update_sdk.py package --dry-run
$ update_sdk.py package
2) updating a specified package:
$ update_sdk.py download -p "build-tools;27.0.3"
(optional) $ update_sdk.py package --dry-run -p build-tools \
--version 27.0.3
$ update_sdk.py package -p build-tools --version 27.0.3
Note that `package` could update the package argument to the checkout
version in .gn file //build/config/android/config.gni. If having git
changes, please prepare to upload a CL that updates the SDK version.
"""
from __future__ import print_function
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
_SRC_ROOT = os.path.realpath(
os.path.join(os.path.dirname(__file__), '..', '..', '..'))
_SRC_DEPS_PATH = os.path.join(_SRC_ROOT, 'DEPS')
_SDK_PUBLIC_ROOT = os.path.join(_SRC_ROOT, 'third_party', 'android_sdk',
'public')
_SDKMANAGER_PATH = os.path.join(_SRC_ROOT, 'third_party', 'android_sdk',
'public', 'tools', 'bin', 'sdkmanager')
_ANDROID_CONFIG_GNI_PATH = os.path.join(_SRC_ROOT, 'build', 'config', 'android',
'config.gni')
_TOOLS_LIB_PATH = os.path.join(_SDK_PUBLIC_ROOT, 'tools', 'lib')
_DEFAULT_DOWNLOAD_PACKAGES = [
'build-tools', 'platform-tools', 'platforms', 'tools'
]
# TODO(shenghuazhang): Search package versions from available packages through
# the sdkmanager, instead of hardcoding the package names w/ version.
# TODO(yliuyliu): we might not need the latest version if unstable,
# will double-check this later.
_DEFAULT_PACKAGES_DICT = {
'build-tools': 'build-tools;27.0.3',
'platforms': 'platforms;android-28',
'sources': 'sources;android-28',
}
_GN_ARGUMENTS_TO_UPDATE = {
'build-tools': 'default_android_sdk_build_tools_version',
'tools': 'default_android_sdk_tools_version_suffix',
'platforms': 'default_android_sdk_version',
}
_COMMON_JAR_SUFFIX_PATTERN = re.compile(
r'^common' # file name begins with 'common'
r'(-[\d\.]+(-dev)?)' # group of suffix e.g.'-26.0.0-dev', '-25.3.2'
r'\.jar$' # ends with .jar
)
def _DownloadSdk(arguments):
"""Download sdk package from sdkmanager.
If package isn't provided, update build-tools, platform-tools, platforms,
and tools.
Args:
arguments: The arguments parsed from argparser.
"""
for pkg in arguments.package:
# If package is not a sdk-style path, try to match a default path to it.
if pkg in _DEFAULT_PACKAGES_DICT:
print('Coercing %s to %s' % (pkg, _DEFAULT_PACKAGES_DICT[pkg]))
pkg = _DEFAULT_PACKAGES_DICT[pkg]
download_sdk_cmd = [
_SDKMANAGER_PATH, '--install',
'--sdk_root=%s' % arguments.sdk_root, pkg
]
if arguments.verbose:
download_sdk_cmd.append('--verbose')
subprocess.check_call(download_sdk_cmd)
def _FindPackageVersion(package, sdk_root):
"""Find sdk package version.
Two options for package version:
1) Use the version in name if package name contains ';version'
2) For simple name package, search its version from 'Installed packages'
via `sdkmanager --list`
Args:
package: The Android SDK package.
sdk_root: The Android SDK root path.
Returns:
The version of package.
Raises:
Exception: cannot find the version of package.
"""
sdkmanager_list_cmd = [
_SDKMANAGER_PATH,
'--list',
'--sdk_root=%s' % sdk_root,
]
if package in _DEFAULT_PACKAGES_DICT:
# Get the version after ';' from package name
package = _DEFAULT_PACKAGES_DICT[package]
return package.split(';')[1]
else:
# Get the package version via `sdkmanager --list`. The logic is:
# Check through 'Installed packages' which is at the first section of
# `sdkmanager --list` output, example:
# Installed packages:=====================] 100% Computing updates...
# Path | Version | Description
# ------- | ------- | -------
# build-tools;27.0.3 | 27.0.3 | Android SDK Build-Tools 27.0.3
# emulator | 26.0.3 | Android Emulator
# platforms;android-27 | 1 | Android SDK Platform 27
# tools | 26.1.1 | Android SDK Tools
#
# Available Packages:
# ....
# When found a line containing the package path, grap its version between
# the first and second '|'. Since the 'Installed packages' list section ends
# by the first new line, the check loop should be ended when reaches a '\n'.
output = subprocess.check_output(sdkmanager_list_cmd)
for line in output.splitlines():
if ' ' + package + ' ' in line:
# if found package path, catch its version which in the first '|...|'
return line.split('|')[1].strip()
if line == '\n': # Reaches the end of 'Installed packages' list
break
raise Exception('Cannot find the version of package %s' % package)
def _ReplaceVersionInFile(file_path, pattern, version, dry_run=False):
"""Replace the version of sdk package argument in file.
Check whether the version in file is the same as the new version first.
Replace the version if not dry run.
Args:
file_path: Path to the file to update the version of sdk package argument.
pattern: Pattern for the sdk package argument. Must capture at least one
group that the first group is the argument line excluding version.
version: The new version of the package.
dry_run: Bool. To show what packages would be created and packages, without
actually doing either.
"""
with tempfile.NamedTemporaryFile() as temp_file:
with open(file_path) as f:
for line in f:
new_line = re.sub(pattern, r'\g<1>\g<2>%s\g<3>\n' % version, line)
if new_line != line:
print(' Note: file "%s" argument ' % file_path +
'"%s" would be updated to "%s".' % (line.strip(), version))
temp_file.write(new_line)
if not dry_run:
temp_file.flush()
shutil.move(temp_file.name, file_path)
temp_file.delete = False
def GetCipdPackagePath(pkg_yaml_file):
"""Find CIPD package path in .yaml file.
There should one line in .yaml file, e.g.:
"package: chrome_internal/third_party/android_sdk/internal/q/add-ons" or
"package: chromium/third_party/android_sdk/public/platforms"
Args:
pkg_yaml_file: The yaml file to find CIPD package path.
Returns:
The CIPD package path in yaml file.
"""
cipd_package_path = ''
with open(pkg_yaml_file) as f:
pattern = re.compile(
# Match the argument with "package: "
r'(^\s*package:\s*)'
# The CIPD package path we want
r'([\w\/-]+)'
# End of string
r'(\s*?$)')
for line in f:
found = re.match(pattern, line)
if found:
cipd_package_path = found.group(2)
break
return cipd_package_path
def UploadSdkPackage(sdk_root, dry_run, service_url, package, yaml_file,
verbose):
"""Build and upload a package instance file to CIPD.
This would also update gn and ensure files to the package version as
uploading to CIPD.
Args:
sdk_root: Root of the sdk packages.
dry_run: Bool. To show what packages would be created and packages, without
actually doing either.
service_url: The url of the CIPD service.
package: The package to be uploaded to CIPD.
yaml_file: Path to the yaml file that defines what to put into the package.
Default as //third_party/android_sdk/public/cipd_*.yaml
verbose: Enable more logging.
Returns:
New instance ID when CIPD package created.
Raises:
IOError: cannot find .yaml file, CIPD package path or instance ID for
package.
CalledProcessError: cipd command failed to create package.
"""
pkg_yaml_file = yaml_file or os.path.join(sdk_root, 'cipd_%s.yaml' % package)
if not os.path.exists(pkg_yaml_file):
raise IOError('Cannot find .yaml file for package %s' % package)
cipd_package_path = GetCipdPackagePath(pkg_yaml_file)
if not cipd_package_path:
raise IOError('Cannot find CIPD package path in %s' % pkg_yaml_file)
if dry_run:
print('This `package` command (without -n/--dry-run) would create and ' +
'upload the package %s to CIPD.' % package)
else:
upload_sdk_cmd = [
'cipd', 'create', '-pkg-def', pkg_yaml_file, '-service-url', service_url
]
if verbose:
upload_sdk_cmd.extend(['-log-level', 'debug'])
output = subprocess.check_output(upload_sdk_cmd)
# Need to match pattern to find new instance ID.
# e.g.: chromium/third_party/android_sdk/public/platforms:\
# Kg2t9p0YnQk8bldUv4VA3o156uPXLUfIFAmVZ-Gm5ewC
pattern = re.compile(
# Match the argument with "Instance: %s:" for cipd_package_path
(r'(^\s*Instance: %s:)' % cipd_package_path) +
# instance ID e.g. DLK621q5_Bga5EsOr7cp6bHWWxFKx6UHLu_Ix_m3AckC.
r'([-\w.]+)'
# End of string
r'(\s*?$)')
for line in output.splitlines():
found = re.match(pattern, line)
if found:
# Return new instance ID.
return found.group(2)
# Raises error if instance ID not found.
raise IOError('Cannot find instance ID by creating package %s' % package)
def UpdateInstanceId(package,
deps_path,
dry_run,
new_instance_id,
release_version=None):
"""Find the sdk pkg version in DEPS and modify it as cipd uploading version.
TODO(shenghuazhang): use DEPS edition operations after issue crbug.com/760633
fixed.
DEPS file hooks sdk package with version with suffix -crX, e.g. '26.0.2-cr1'.
If pkg_version is the base number of the existing version in DEPS, e.g.
'26.0.2', return '26.0.2-cr2' as the version uploading to CIPD. If not the
base number, return ${pkg_version}-cr0.
Args:
package: The name of the package.
deps_path: Path to deps file which gclient hooks sdk pkg w/ versions.
dry_run: Bool. To show what packages would be created and packages, without
actually doing either.
new_instance_id: New instance ID after CIPD package created.
release_version: Android sdk release version e.g. 'o_mr1', 'p'.
"""
var_package = package
if release_version:
var_package = release_version + '_' + var_package
package_var_pattern = re.compile(
# Match the argument with "'android_sdk_*_version': '" with whitespaces.
r'(^\s*\'android_sdk_%s_version\'\s*:\s*\')' % var_package +
# instance ID e.g. DLK621q5_Bga5EsOr7cp6bHWWxFKx6UHLu_Ix_m3AckC.
r'([-\w.]+)'
# End of string
r'(\',?$)')
with tempfile.NamedTemporaryFile() as temp_file:
with open(deps_path) as f:
for line in f:
new_line = line
found = re.match(package_var_pattern, line)
if found:
instance_id = found.group(2)
new_line = re.sub(package_var_pattern,
r'\g<1>%s\g<3>' % new_instance_id, line)
print(
' Note: deps file "%s" argument ' % deps_path +
'"%s" would be updated to "%s".' % (instance_id, new_instance_id))
temp_file.write(new_line)
if not dry_run:
temp_file.flush()
shutil.move(temp_file.name, deps_path)
temp_file.delete = False
def ChangeVersionInGNI(package, arg_version, gn_args_dict, gni_file_path,
dry_run):
"""Change the sdk package version in config.gni file."""
if package in gn_args_dict:
version_config_name = gn_args_dict.get(package)
# Regex to parse the line of sdk package version gn argument, e.g.
# ' default_android_sdk_version = "27"'. Capture a group for the line
# excluding the version.
gn_arg_pattern = re.compile(
# Match the argument with '=' and whitespaces. Capture a group for it.
r'(^\s*%s\s*=\s*)' % version_config_name +
# Optional quote.
r'("?)' +
# Version number. E.g. 27, 27.0.3, -26.0.0-dev
r'(?:[-\w\s.]+)' +
# Optional quote.
r'("?)' +
# End of string
r'$')
_ReplaceVersionInFile(gni_file_path, gn_arg_pattern, arg_version, dry_run)
def GetToolsSuffix(tools_lib_path):
"""Get the gn config of package 'tools' suffix.
Check jar file name of 'common*.jar' in tools/lib, which could be
'common.jar', common-<version>-dev.jar' or 'common-<version>.jar'.
If suffix exists, return the suffix.
Args:
tools_lib_path: The path of tools/lib.
Returns:
The suffix of tools package.
"""
tools_lib_jars_list = os.listdir(tools_lib_path)
for file_name in tools_lib_jars_list:
found = re.match(_COMMON_JAR_SUFFIX_PATTERN, file_name)
if found:
return found.group(1)
def _GetArgVersion(pkg_version, package):
"""Get the argument version.
Args:
pkg_version: The package version.
package: The package name.
Returns:
The argument version.
"""
# Remove all chars except for digits and dots in version
arg_version = re.sub(r'[^\d\.]', '', pkg_version)
if package == 'tools':
suffix = GetToolsSuffix(_TOOLS_LIB_PATH)
if suffix:
arg_version = suffix
else:
arg_version = '-%s' % arg_version
return arg_version
def _UploadSdkPackage(arguments):
"""Upload SDK packages to CIPD.
Args:
arguments: The arguments parsed by argparser.
Raises:
IOError: Don't use --version/--yaml-file for default packages.
"""
packages = arguments.package
if not packages:
packages = _DEFAULT_DOWNLOAD_PACKAGES
if arguments.version or arguments.yaml_file:
raise IOError("Don't use --version/--yaml-file for default packages.")
for package in packages:
pkg_version = arguments.version
if not pkg_version:
pkg_version = _FindPackageVersion(package, arguments.sdk_root)
# Upload SDK package to CIPD, and update the package instance ID hooking
# in DEPS file.
new_instance_id = UploadSdkPackage(
os.path.join(arguments.sdk_root, '..'), arguments.dry_run,
arguments.service_url, package, arguments.yaml_file, arguments.verbose)
UpdateInstanceId(package, _SRC_DEPS_PATH, arguments.dry_run,
new_instance_id)
if package in _GN_ARGUMENTS_TO_UPDATE:
# Update the package version config in gn file
arg_version = _GetArgVersion(pkg_version, package)
ChangeVersionInGNI(package, arg_version, _GN_ARGUMENTS_TO_UPDATE,
_ANDROID_CONFIG_GNI_PATH, arguments.dry_run)
def main():
parser = argparse.ArgumentParser(
description='A script to download Android SDK packages '
'via sdkmanager and upload to CIPD.')
subparsers = parser.add_subparsers(title='commands')
download_parser = subparsers.add_parser(
'download',
help='Download sdk package to the latest version from sdkmanager.')
download_parser.set_defaults(func=_DownloadSdk)
download_parser.add_argument(
'-p',
'--package',
nargs=1,
default=_DEFAULT_DOWNLOAD_PACKAGES,
help='The package of the SDK needs to be installed/updated. '
'Note that package name should be a sdk-style path e.g. '
'"platforms;android-27" or "platform-tools". If package '
'is not specified, update "build-tools;27.0.3", "tools" '
'"platform-tools" and "platforms;android-27" by default.')
download_parser.add_argument(
'--sdk-root', help='base path to the Android SDK root')
download_parser.add_argument(
'-v', '--verbose', action='store_true', help='print debug information')
package_parser = subparsers.add_parser(
'package', help='Create and upload package instance file to CIPD.')
package_parser.set_defaults(func=_UploadSdkPackage)
package_parser.add_argument(
'-n',
'--dry-run',
action='store_true',
help='Dry run won\'t trigger creating instances or uploading packages. '
'It shows what packages would be created and uploaded to CIPD. '
'It also shows the possible updates of sdk version on files.')
package_parser.add_argument(
'-p',
'--package',
nargs=1,
help='The package to be uploaded to CIPD. Note that package '
'name is a simple path e.g. "platforms" or "build-tools" '
'which matches package name on CIPD service. Default by '
'build-tools, platform-tools, platforms and tools')
package_parser.add_argument(
'--version',
help='Version of the uploading package instance through CIPD.')
package_parser.add_argument(
'--yaml-file',
help='Path to *.yaml file that defines what to put into the package.'
'Default as //third_party/android_sdk/public/cipd_<package>.yaml')
package_parser.add_argument(
'--service-url',
help='The url of the CIPD service.',
default='https://chrome-infra-packages.appspot.com')
package_parser.add_argument(
'--sdk-root', help='base path to the Android SDK root')
package_parser.add_argument(
'-v', '--verbose', action='store_true', help='print debug information')
args = parser.parse_args()
if not args.sdk_root:
args.sdk_root = _SDK_PUBLIC_ROOT
args.func(args)
if __name__ == '__main__':
sys.exit(main())