aboutsummaryrefslogtreecommitdiff
path: root/cli/lib/core/tool.py
blob: 575236c016c363c2a9f911535e4f1ad474485007 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#
# 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, source_top=None, build_out=None,
                                bsp=None):
        """Sets Android environment variables.

        Android has a few common variables used by a variety of tools. This
        function sets any environment variables that can be derived from
        the given arguments.

        All arguments are optional, any variables that depend on missing
        arguments will not be modified. Requirements are:
            ANDROID_BUILD_TOP: source_top
            ANDROID_HOST_OUT: build_out
            ANDROID_PRODUCT_OUT: build_out, bsp

        Args:
            source_top: root of the source tree.
            build_out: root of the build output folder.
            bsp: the BSP name.
        """
        if source_top:
            self.environment['ANDROID_BUILD_TOP'] = source_top

        if build_out:
            self.environment['ANDROID_HOST_OUT'] = os.path.join(
                build_out, 'host', util.GetHostArch())

        if build_out and bsp:
            self.environment['ANDROID_PRODUCT_OUT'] = util.GetAndroidProductOut(
                build_out, bsp)

    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, build_out, bsp=None, env=None):
        """Initializes a HostToolWrapper.

        Args:
            path: tool path relative to <build_top>/out/host/<arch>/bin/.
            build_out: root of the build output folder where the tool lives.
            bsp: the BSP name. Optional, but should be set for any tool that
                uses ANDROID_PRODUCT_OUT (e.g. fastboot and adb).
            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(build_out=build_out, bsp=bsp)
        self._tool_path = os.path.join(self.environment['ANDROID_HOST_OUT'],
                                       'bin', path)


class HostToolRunner(object):
    """Serves as a HostToolWrapper factory."""

    def __init__(self, build_out):
        self._build_out = build_out

    def run(self, path, args):
        host_tool = HostToolWrapper(path, self._build_out)
        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, target, env=None):
        """Initializes a ProvisionDeviceTool.

        Args:
            target: The Target to provision.
            env: a dictionary of additional environmental variables to set.
        """
        # Initialize path to '' at first so we can use ANDROID_PRODUCT_OUT.
        super(ProvisionDeviceTool, self).__init__('', env=env)
        self.set_android_environment(
            source_top=util.GetOSPath(target.os_version),
            build_out=target.platform_build_cache(),
            bsp=target.board)
        self._tool_path = os.path.join(self.environment['ANDROID_PRODUCT_OUT'],
                                       'provision-device')
        self._target = target

        # 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._target.get_device().linked(self._target.os_version):
            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)