blob: 502550e99651677b8c9f3b60cfdf4da39c0950cd [file] [log] [blame]
#
# Copyright (C) 2015 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.
#
"""Support tools execution functionality."""
import os
import subprocess
from core import util
import error
class Error(error.Error):
"""Base class for all tool errors."""
class ExecuteError(Error):
"""Raised when the tool fails to execute."""
description = 'Failed to execute'
class ReturnError(Error):
"""Raised when the tool returns an error code."""
description = 'Error code returned from tool'
# Variables to pass through to all command calls. These are mostly needed
# for builds, but some tools use them as well (e.g. adb uses TERM).
DEFAULT_PASSTHROUGH_ENV = [
'http_proxy', 'https_proxy', 'ftp_proxy', 'rsync_proxy', 'no_proxy',
'HOME', 'USER', 'LANG', 'LOGNAME', 'SSH_AUTH_SOCK', 'PWD', 'TERM'
]
class ToolWrapper(object):
"""Wraps a host binary, target script, or build command.
The advantages of this over using subprocess directly are:
* Properly sets the execution working directory with set_cwd().
* Restricts passthrough environment to a safe set of defaults.
* Handles signal return codes properly.
* Helper function to set common environment variables.
Attributes:
environment: a dictionary of environment variables to pass.
"""
def __init__(self, path, env=None):
"""Initializes a ToolWrapper.
Args:
path: path to the tool executable.
env: a dictionary of additional environmental variables to set.
Can also be set after creation via the environment attribute.
"""
self._tool_path = path
self._cwd = None
self.environment = {var: os.environ[var]
for var in DEFAULT_PASSTHROUGH_ENV
if var in os.environ}
if env:
self.environment.update(env)
def set_cwd(self, cwd):
self._cwd = cwd
def set_android_environment(self, platform):
"""Sets Android environment variables.
Android has a few common variables used by a variety of tools. This
function sets ANDROID_BUILD_TOP, ANDROID_HOST_OUT, and
ANDROID_PRODUCT_OUT.
Args:
platform: The project.Platform the tool is being used for.
"""
self.environment['ANDROID_BUILD_TOP'] = platform.os.root
self.environment['ANDROID_HOST_OUT'] = os.path.join(
platform.build_cache, 'host', util.GetHostArch())
self.environment['ANDROID_PRODUCT_OUT'] = platform.product_out_cache
def add_caller_env_path(self):
"""Adds the caller's PATH environment variable.
Most tools do not want to inherit PATH to avoid any unexpected
interactions with the caller's environment. However, some tools
do need the PATH variable (e.g. shell scripts may need to be able
to find utilities like dirname).
If the tool already has a PATH, the caller's PATH is appended.
"""
caller_path = os.environ.get('PATH')
if caller_path is not None:
if 'PATH' in self.environment:
self.environment['PATH'] += os.pathsep + caller_path
else:
self.environment['PATH'] = caller_path
def path(self):
return self._tool_path
def exists(self):
return os.path.isfile(self._tool_path)
def run(self, arg_array=None, piped=False):
"""Executes the tool and blocks until completion.
Args:
arg_array: list of string arguments to pass to the tool.
piped: If true, send stdout and stderr to pipes.
Raises:
ExecuteError: if execution fails.
ReturnError: if execution returns a non-0 exit code.
Returns:
(out, err): The output to stdout and stderr of the called tool.
Will both be None if piped=False.
"""
# Make sure PWD is accurate on CWD change.
if self._cwd:
self.environment['PWD'] = os.path.abspath(self._cwd)
stdout = None
stderr = None
if piped:
stdout = subprocess.PIPE
stderr = subprocess.PIPE
try:
tool_process = subprocess.Popen(
[self._tool_path] + (arg_array or []),
env=self.environment, shell=False,
cwd=self._cwd, stdout=stdout,
stderr=stderr)
(out, err) = tool_process.communicate()
except OSError as e:
# Catch and re-raise so we can include the tool path in the message.
raise ExecuteError('"{}": {} [{}]'.format(
self._tool_path, e.errno, e.strerror))
# Exiting via signal gives negative return codes.
ret = tool_process.returncode
if ret < 0:
# Return the normal shell exit mask for being signaled.
ret = 128 - ret
if ret != 0:
raise ReturnError('"{}": {} ({})'.format(self._tool_path, ret, err),
errno=ret)
return (out, err)
class HostToolWrapper(ToolWrapper):
"""Wraps a tool from out/host/<arch>/bin/."""
def __init__(self, path, platform, env=None):
"""Initializes a HostToolWrapper.
Args:
path: tool path relative to <build_out>/host/<arch>/bin/.
platform: the platform the tool is associated with.
env: a dictionary of additional environmental variables to set.
"""
# Initialize path to '' at first so we can use ANDROID_HOST_OUT.
super(HostToolWrapper, self).__init__('', env=env)
self.set_android_environment(platform)
self._tool_path = os.path.join(self.environment['ANDROID_HOST_OUT'],
'bin', path)
class HostToolRunner(object):
"""Serves as a HostToolWrapper factory."""
def __init__(self, platform):
self._platform = platform
def run(self, path, args):
host_tool = HostToolWrapper(path, self._platform)
return host_tool.run(args)
class PathToolWrapper(ToolWrapper):
"""Wraps a tool expected to be in the user PATH."""
def __init__(self, program, env=None):
super(PathToolWrapper, self).__init__(program, env)
self.add_caller_env_path()
class PathToolRunner(object):
"""Serves as a PathToolWrapper factory."""
def run(self, path, args, piped=False):
path_tool = PathToolWrapper(path)
return path_tool.run(args, piped=piped)
class ProvisionDeviceTool(ToolWrapper):
"""Wraps the provision-device script.
provision-device is unique since it's built as part of the product
output rather than the host output, and also requires a pointer to
the source tree which other tools don't, so it's useful to create a
specific subclass for it.
Note: provision-device allows two special environment variables
ANDROID_PROVISION_VENDOR_PARTITIONS and ANDROID_PROVISION_OS_PARTITIONS
that replace ANDROID_BUILD_TOP and ANDROID_PRODUCT_OUT if present.
These are for advanced usage and could easily create confusion, so
are intentionally not passed through here; if a user requires these
they will have to call provision-device manually.
"""
def __init__(self, platform, provision_dir, env=None):
"""Initializes a ProvisionDeviceTool.
Args:
platform: The Platform to provision.
provision_dir: Directory containing img files to provision
and the provision-device script.
env: a dictionary of additional environmental variables to set.
"""
super(ProvisionDeviceTool, self).__init__(
os.path.join(provision_dir, 'provision-device'), env=env)
self.set_android_environment(platform)
# Adjust ANDROID_PRODUCT_OUT to use the provision dir instead of
# just the platform build.
self.environment['ANDROID_PRODUCT_OUT'] = provision_dir
self._platform = platform
# provision-device is a shell script, so it needs to know PATH in order
# to find utilities.
self.add_caller_env_path()
def run(self, arg_array=None, piped=False):
with self._platform.linked():
super(ProvisionDeviceTool, self).run(arg_array, piped=piped)
class BrunchToolWrapper(ToolWrapper):
"""Legacy tool wrapper used by Brunch.
This adds some additional functionality to the base ToolWrapper:
* Adds additional Brunch-specific environment variables.
* Sets PATH, ANDROID_PRODUCT_OUT, and ANDROID_BUILD_TOP using BDK config
settings instead of passed in variables.
"""
_UNTOUCHABLE_ENV = ['BDK_PATH', 'PATH', 'ANDROID_PRODUCT_OUT',
'ANDROID_BUILD_TOP']
def __init__(self, config, product_path, path):
super(BrunchToolWrapper, self).__init__(path)
self._product_path = product_path
self._config = config
self._env_path = ''
if os.environ.has_key('PATH'):
self._env_path = os.environ['PATH']
self._import_environ()
self.environment.update({
'BDK_PATH': util.DEPRECATED_GetDefaultOSPath(),
'PATH': os.pathsep.join([p for p
in [util.GetBDKPath('cli'), self._env_path]
if p]),
'ANDROID_PRODUCT_OUT': os.path.join(self._product_path, 'out',
'out-' + self._config.device,
'target', 'product',
self._config.device),
'ANDROID_BUILD_TOP': os.path.join(self._product_path, 'out', '.bdk')
})
def _import_environ(self):
"""Walk the global environment merging in allowed variables."""
extra_vars = self._config.bdk.allowed_environ
if extra_vars:
for var in extra_vars.split(' '):
if var in self._UNTOUCHABLE_ENV:
print ("Cannot passthrough environment variable '{0}' "
"as set in config/bdk/allowed_environ").format(var)
if os.environ.has_key(var):
self.environment[var] = os.environ[var]
class BrunchHostToolWrapper(BrunchToolWrapper):
"""Wraps a host tool for brunch workflows."""
TOOL_PATH_FMT = os.path.join('{0}', 'out', 'out-{1}',
'host', '{2}', 'bin', '{3}')
DEFAULT_ARCH = 'linux-x86'
def __init__(self, config, product_path, name, arch=DEFAULT_ARCH):
self._tool_name = name
self._host_arch = arch
super(BrunchHostToolWrapper, self).__init__(config, product_path, name)
self._tool_path = self._build_path()
def _build_path(self):
return self.TOOL_PATH_FMT.format(self._product_path,
self._config.device, self._host_arch,
self._tool_name)
class BrunchTargetToolWrapper(BrunchHostToolWrapper):
"""Wraps a target tool for brunch workflows."""
TOOL_PATH_FMT = os.path.join('{0}', 'out', 'out-{1}',
'target', 'product', '{2}', '{3}')
def _build_path(self):
return self.TOOL_PATH_FMT.format(self._product_path,
self._config.device,
self._config.device, self._tool_name)