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