blob: 64a67ce9cc788d03d3167cd602c976601d098a14 [file] [log] [blame]
# 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 ConfigParser
import fcntl
import hashlib
import logging
import os
import re
import shutil
import StringIO
import subprocess
import sys
import time
import urllib
import zipfile
from starboard.tools import build
# The API level of NDK standalone toolchain to install. This should be the
# minimum API level on which the app is expected to run. If some feature from a
# newer NDK level is needed, this may be increased with caution.
# https://developer.android.com/ndk/guides/stable_apis.html
#
# Using 24 will lead to missing symbols on API 23 devices.
# https://github.com/android-ndk/ndk/issues/126
_ANDROID_NDK_API_LEVEL = '21'
# Packages to install in the Android SDK.
# Get available packages from "sdkmanager --list --verbose"
_ANDROID_SDK_PACKAGES = [
'build-tools;28.0.3',
'cmake;3.6.4111459',
'emulator',
'extras;android;m2repository',
'extras;google;m2repository',
'lldb;3.1',
'ndk-bundle',
'patcher;v4',
'platforms;android-28',
'platform-tools',
'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/sdk-tools-linux-3859397.zip'
_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
_ANDROID_NDK_HOME = os.environ.get('ANDROID_NDK_HOME')
if _ANDROID_NDK_HOME:
_NDK_PATH = _ANDROID_NDK_HOME
else:
_NDK_PATH = os.path.join(_SDK_PATH, 'ndk-bundle')
_SDKMANAGER_TOOL = os.path.join(_SDK_PATH, 'tools', 'bin', 'sdkmanager')
# Maps the Android ABI to the architecture name of the toolchain.
_TOOLS_ABI_ARCH_MAP = {
'x86': 'x86',
'armeabi': 'arm',
'armeabi-v7a': 'arm',
'arm64-v8a': 'arm64',
}
_SCRIPT_HASH_PROPERTY = 'SdkUtils.Hash'
with open(__file__, 'rb') as script:
_SCRIPT_HASH = hashlib.md5(script.read()).hexdigest()
def GetToolsPath(abi):
"""Returns the path where the NDK standalone toolchain should be."""
tools_arch = _TOOLS_ABI_ARCH_MAP[abi]
tools_dir = 'android_toolchain_api{}_{}'.format(_ANDROID_NDK_API_LEVEL,
tools_arch)
return os.path.realpath(os.path.join(_STARBOARD_TOOLCHAINS_DIR, tools_dir))
def GetEnvironmentVariables(abi):
"""Returns a dictionary of environment variables to provide to GYP."""
tools_bin = os.path.join(GetToolsPath(abi), 'bin')
return {
'CC': os.path.join(tools_bin, 'clang'),
'CXX': os.path.join(tools_bin, 'clang++'),
}
def _CheckStamp(dir_path):
"""Checks that the specified directory is up-to-date with the NDK."""
stamp_path = os.path.join(dir_path, 'ndk.stamp')
return (
os.path.exists(stamp_path) and
_ReadNdkRevision(stamp_path) == _GetInstalledNdkRevision() and
_ReadProperty(stamp_path, _SCRIPT_HASH_PROPERTY) == _SCRIPT_HASH)
def _UpdateStamp(dir_path):
"""Updates the stamp file in the specified directory to the NDK revision."""
path = GetNdkPath()
properties_path = os.path.join(path, 'source.properties')
stamp_path = os.path.join(dir_path, 'ndk.stamp')
shutil.copyfile(properties_path, stamp_path)
with open(stamp_path, 'a') as stamp:
stamp.write('{} = {}\n'.format(_SCRIPT_HASH_PROPERTY, _SCRIPT_HASH))
def GetNdkPath():
return _NDK_PATH
def GetSdkPath():
return _SDK_PATH
def _ReadNdkRevision(properties_path):
return _ReadProperty(properties_path, 'pkg.revision')
def _ReadProperty(properties_path, property_key):
with open(properties_path, 'r') as f:
ini_str = '[properties]\n' + f.read()
config = ConfigParser.RawConfigParser()
config.readfp(StringIO.StringIO(ini_str))
try:
return config.get('properties', property_key)
except ConfigParser.NoOptionError:
return None
def _GetInstalledNdkRevision():
"""Returns the installed NDK's revision."""
path = GetNdkPath()
properties_path = os.path.join(path, 'source.properties')
try:
return _ReadNdkRevision(properties_path)
except IOError:
logging.error("Error: Can't read NDK properties in %s", properties_path)
sys.exit(1)
def InstallSdkIfNeeded(abi):
"""Installs appropriate SDK/NDK and NDK standalone tools if needed."""
_MaybeDownloadAndInstallSdkAndNdk()
_MaybeMakeToolchain(abi)
def _DownloadAndUnzipFile(url, destination_path):
dl_file, dummy_headers = urllib.urlretrieve(url)
_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 _MaybeDownloadAndInstallSdkAndNdk():
"""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 _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, '< MISSING! >')
msg = ' {:30} : {}'.format(package, version)
logging.warning(msg)
else:
logging.warning('Checking Android SDK.')
_DownloadInstallOrUpdateSdk()
if _ANDROID_NDK_HOME:
logging.warning('Warning: Using Android NDK in ANDROID_NDK_HOME,'
' which is not automatically updated')
ndk_revision = _GetInstalledNdkRevision()
logging.warning('Using Android NDK version %s', ndk_revision)
if _ANDROID_HOME or _ANDROID_NDK_HOME:
reply = raw_input(
'Do you want to continue using your custom Android tools? [yN]')
if reply.upper() != 'Y':
sys.exit(1)
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, ''):
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)
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():
time.sleep(_SDK_LICENSE_PROMPT_SLEEP_SECONDS)
try:
p.stdin.write('y\n')
except IOError:
logging.warning('There were no SDK licenses to accept.')
p.wait()
def _MaybeMakeToolchain(abi):
"""Run the NDK's make_standalone_toolchain.py if necessary."""
tools_arch = _TOOLS_ABI_ARCH_MAP[abi]
tools_path = GetToolsPath(abi)
if _CheckStamp(tools_path):
logging.info('NDK %s toolchain already at %s', tools_arch,
_GetInstalledNdkRevision())
return
logging.warning('Installing NDK %s toolchain %s in %s', tools_arch,
_GetInstalledNdkRevision(), tools_path)
if os.path.exists(tools_path):
shutil.rmtree(tools_path)
# Run the NDK script to make the standalone toolchain
script_path = os.path.join(GetNdkPath(), 'build', 'tools',
'make_standalone_toolchain.py')
args = [
script_path, '--arch', tools_arch, '--api', _ANDROID_NDK_API_LEVEL,
'--stl', 'libc++', '--install-dir', tools_path
]
script_proc = subprocess.Popen(args)
rc = script_proc.wait()
if rc != 0:
raise RuntimeError('%s failed.' % script_path)
_UpdateStamp(tools_path)