blob: fd695a1e30809bce104fe1ed5e2987ce4f698e68 [file] [log] [blame]
# Lint as: python2
# Copyright 2016 The Cobalt Authors. All Rights Reserved.
#
# 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.
"""Utilities to use the toolchain from the Android NDK."""
import errno
import fcntl
import logging
import os
import re
import shutil
import subprocess
import sys
import time
import zipfile
import requests
from starboard.tools import build
# Which version of the Android NDK and CMake to install and build with.
# Note that build.gradle parses these out of this file too.
_NDK_VERSION = '21.1.6352462'
_CMAKE_VERSION = '3.10.2.4988404'
# Packages to install in the Android SDK.
# Get available packages from "sdkmanager --list --verbose"
_ANDROID_SDK_PACKAGES = [
'build-tools;30.0.0',
'cmake;' + _CMAKE_VERSION,
'cmdline-tools;1.0',
'emulator',
'extras;android;m2repository',
'extras;google;m2repository',
'ndk;' + _NDK_VERSION,
'patcher;v4',
'platforms;android-30',
'platform-tools',
]
# Seconds to sleep before writing "y" for android sdk update license prompt.
_SDK_LICENSE_PROMPT_SLEEP_SECONDS = 5
# Location from which to download the SDK command-line tools
# see https://developer.android.com/studio/index.html#command-tools
_SDK_URL = 'https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip'
# Location from which to download the Android NDK.
# see https://developer.android.com/ndk/downloads (perhaps in "NDK archives")
_NDK_ZIP_REVISION = 'android-ndk-r20b'
_NDK_ZIP_FILE = _NDK_ZIP_REVISION + '-linux-x86_64.zip'
_NDK_URL = 'https://dl.google.com/android/repository/' + _NDK_ZIP_FILE
_STARBOARD_TOOLCHAINS_DIR = build.GetToolchainsDir()
# The path to the Android SDK, if placed inside of starboard-toolchains.
_STARBOARD_TOOLCHAINS_SDK_DIR = os.path.join(_STARBOARD_TOOLCHAINS_DIR,
'AndroidSdk')
_ANDROID_HOME = os.environ.get('ANDROID_HOME')
if _ANDROID_HOME:
_SDK_PATH = _ANDROID_HOME
else:
_SDK_PATH = _STARBOARD_TOOLCHAINS_SDK_DIR
_NDK_PATH = os.path.join(_SDK_PATH, 'ndk', _NDK_VERSION)
_SDKMANAGER_TOOL = os.path.join(_SDK_PATH, 'cmdline-tools', '1.0', 'bin',
'sdkmanager')
def GetNdkPath():
return _NDK_PATH
def GetSdkPath():
return _SDK_PATH
def _DownloadAndUnzipFile(url, destination_path):
"""Download a zip file from a url and uncompress it."""
shutil.rmtree(destination_path, ignore_errors=True)
try:
os.makedirs(destination_path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
dl_file = os.path.join(destination_path, 'tmp.zip')
request = requests.get(url, allow_redirects=True)
open(dl_file, 'wb').write(request.content)
_UnzipFile(dl_file, destination_path)
def _UnzipFile(zip_path, dest_path):
"""Extract all files and restore permissions from a zip file."""
zip_file = zipfile.ZipFile(zip_path)
for info in zip_file.infolist():
zip_file.extract(info.filename, dest_path)
os.chmod(os.path.join(dest_path, info.filename), info.external_attr >> 16L)
def InstallSdkIfNeeded():
"""Download the SDK and NDK if not already available."""
# Hold an exclusive advisory lock on the _STARBOARD_TOOLCHAINS_DIR, to
# prevent issues with modification for multiple variants.
try:
toolchains_dir_fd = os.open(_STARBOARD_TOOLCHAINS_DIR, os.O_RDONLY)
fcntl.flock(toolchains_dir_fd, fcntl.LOCK_EX)
if os.environ.get('ANDROID_NDK_HOME'):
logging.warning('Warning: ANDROID_NDK_HOME is deprecated and ignored.')
if _ANDROID_HOME:
if not os.access(_SDKMANAGER_TOOL, os.X_OK):
logging.error('Error: ANDROID_HOME is set but SDK is not present.')
sys.exit(1)
logging.warning('Warning: Using Android SDK in ANDROID_HOME,'
' which is not automatically updated.\n'
' The following package versions are installed:')
installed_packages = _GetInstalledSdkPackages()
for package in _ANDROID_SDK_PACKAGES:
version = installed_packages.get(package, '< NOT INSTALLED >')
msg = ' {:30} : {}'.format(package, version)
logging.warning(msg)
if not os.path.exists(GetNdkPath()):
logging.error('Error: ANDROID_HOME is is missing NDK %s.', _NDK_VERSION)
sys.exit(1)
reply = raw_input(
'Do you want to continue using your custom Android tools? [y/N]')
if reply.upper() != 'Y':
sys.exit(1)
else:
logging.warning('Checking Android SDK.')
_DownloadInstallOrUpdateSdk()
finally:
fcntl.flock(toolchains_dir_fd, fcntl.LOCK_UN)
os.close(toolchains_dir_fd)
def _GetInstalledSdkPackages():
"""Returns a map of installed package name to package version."""
# We detect and parse either new-style or old-style output from sdkmanager:
#
# [new-style]
# Installed packages:
# --------------------------------------
# build-tools;25.0.2
# Description: Android SDK Build-Tools 25.0.2
# Version: 25.0.2
# Installed Location: <abs path>
# ...
#
# [old-style]
# Installed packages:
# Path | Version | Description | Location
# ------- | ------- | ------- | -------
# build-tools;25.0.2 | 25.0.2 | Android SDK Build-Tools 25.0.2 | <rel path>
# ...
section_re = re.compile(r'^[A-Z][^:]*:$')
version_re = re.compile(r'^\s+Version:\s+(\S+)')
p = subprocess.Popen([_SDKMANAGER_TOOL, '--list', '--verbose'],
stdout=subprocess.PIPE)
installed_package_versions = {}
new_style = False
old_style = False
for line in iter(p.stdout.readline, ''):
# Throw away loading progress indicators up to the last CR.
line = line.split('\r')[-1]
if section_re.match(line):
if new_style or old_style:
# We left the new/old style installed packages section
break
if line.startswith('Installed'):
# Read the header of this section to determine new/old style
line = p.stdout.readline().strip()
new_style = line.startswith('----')
old_style = line.startswith('Path')
if old_style:
line = p.stdout.readline().strip()
if not line.startswith('----'):
logging.error('Error: Unexpected SDK package listing format')
continue
# We're in the installed packages section, and it's new-style
if new_style:
if not line.startswith(' '):
pkg_name = line.strip()
else:
m = version_re.match(line)
if m:
installed_package_versions[pkg_name] = m.group(1)
# We're in the installed packages section, and it's old-style
elif old_style:
fields = [f.strip() for f in line.split('|', 2)]
if len(fields) >= 2:
installed_package_versions[fields[0]] = fields[1]
# Discard the rest of the output
p.communicate()
return installed_package_versions
def _IsOnBuildbot():
return 'BUILDBOT_BUILDERNAME' in os.environ
def _DownloadInstallOrUpdateSdk():
"""Downloads (if necessary) and installs/updates the Android SDK."""
# If we can't access the "sdkmanager" tool, we need to download the SDK
if not os.access(_SDKMANAGER_TOOL, os.X_OK):
logging.warning('Downloading Android SDK to %s',
_STARBOARD_TOOLCHAINS_SDK_DIR)
if os.path.exists(_STARBOARD_TOOLCHAINS_SDK_DIR):
shutil.rmtree(_STARBOARD_TOOLCHAINS_SDK_DIR)
_DownloadAndUnzipFile(_SDK_URL, _STARBOARD_TOOLCHAINS_SDK_DIR)
# TODO: Remove this workaround for sdkmanager incorrectly picking up the
# "tools" directory from the ZIP as the name of its component.
# https://issuetracker.google.com/issues/67495440#comment32
# https://issuetracker.google.com/issues/150943631
if not os.access(_SDKMANAGER_TOOL, os.X_OK):
old_tools_dir = os.path.join(_STARBOARD_TOOLCHAINS_SDK_DIR, 'tools')
new_tools_dir = os.path.join(_STARBOARD_TOOLCHAINS_SDK_DIR,
'cmdline-tools', '1.0')
os.mkdir(os.path.dirname(new_tools_dir))
os.rename(old_tools_dir, new_tools_dir)
if not os.access(_SDKMANAGER_TOOL, os.X_OK):
logging.error('SDK download failed.')
sys.exit(1)
# Run the "sdkmanager" command with the appropriate packages
if _IsOnBuildbot():
stdin = subprocess.PIPE
else:
stdin = sys.stdin
p = subprocess.Popen(
[_SDKMANAGER_TOOL, '--verbose'] + _ANDROID_SDK_PACKAGES, stdin=stdin)
if _IsOnBuildbot():
try:
# Accept "Terms and Conditions" (android-sdk-license)
time.sleep(_SDK_LICENSE_PROMPT_SLEEP_SECONDS)
p.stdin.write('y\n')
except IOError:
logging.warning('There were no SDK licenses to accept.')
p.wait()
if p.returncode:
raise RuntimeError(
'\nFailed to install packages with sdkmanager. Exit status = %d' %
p.returncode)