blob: a9d5a6f0b15dfa828decede296c90b1602ee0867 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module for build host support."""
import os
import pipes
import signal
import subprocess
import cr
# Controls what verbosity level turns on command trail logging
_TRAIL_VERBOSITY = 2
def PrintTrail(trail):
print 'Command expanded the following variables:'
for key, value in trail:
if value == None:
value = ''
print ' ', key, '=', value
class Host(cr.Plugin, cr.Plugin.Type):
"""Base class for implementing cr hosts.
The host is the main access point to services provided by the machine cr
is running on. It exposes information about the machine, and runs external
commands on behalf of the actions.
"""
def __init__(self):
super(Host, self).__init__()
def Matches(self):
"""Detects whether this is the correct host implementation.
This method is overridden by the concrete implementations.
Returns:
true if the plugin matches the machine it is running on.
"""
return False
@classmethod
def Select(cls):
for host in cls.Plugins():
if host.Matches():
return host
def _Execute(self, command,
shell=False, capture=False, silent=False,
ignore_dry_run=False, return_status=False,
ignore_interrupt_signal=False):
"""This is the only method that launches external programs.
It is a thin wrapper around subprocess.Popen that handles cr specific
issues. The command is expanded in the active context so that variables
are substituted.
Args:
command: the command to run.
shell: whether to run the command using the shell.
capture: controls wether the output of the command is captured.
ignore_dry_run: Normally, if the context is in dry run mode the command is
printed but not executed. This flag overrides that behaviour, causing
the command to be run anyway.
return_status: switches the function to returning the status code rather
the output.
ignore_interrupt_signal: Ignore the interrupt signal (i.e., Ctrl-C) while
the command is running. Useful for letting interactive programs manage
Ctrl-C by themselves.
Returns:
the status if return_status is true, or the output if capture is true,
otherwise nothing.
"""
with cr.context.Trace():
command = [cr.context.Substitute(arg) for arg in command if arg]
command = filter(bool, command)
trail = cr.context.trail
if not command:
print 'Empty command passed to execute'
exit(1)
if cr.context.verbose:
print ' '.join(command)
if cr.context.verbose >= _TRAIL_VERBOSITY:
PrintTrail(trail)
if ignore_dry_run or not cr.context.dry_run:
out = None
if capture:
out = subprocess.PIPE
elif silent:
out = open(os.devnull, "w")
try:
p = subprocess.Popen(
command, shell=shell,
env={k: str(v) for k, v in cr.context.exported.items()},
stdout=out)
except OSError:
print 'Failed to exec', command
# Don't log the trail if we already have
if cr.context.verbose < _TRAIL_VERBOSITY:
PrintTrail(trail)
exit(1)
try:
if ignore_interrupt_signal:
signal.signal(signal.SIGINT, signal.SIG_IGN)
output, _ = p.communicate()
finally:
if ignore_interrupt_signal:
signal.signal(signal.SIGINT, signal.SIG_DFL)
if silent:
out.close()
if return_status:
return p.returncode
if p.returncode != 0:
print 'Error {0} executing command {1}'.format(p.returncode, command)
exit(p.returncode)
return output or ''
return ''
@cr.Plugin.activemethod
def Shell(self, *command):
command = ' '.join([pipes.quote(arg) for arg in command])
return self._Execute([command], shell=True, ignore_interrupt_signal=True)
@cr.Plugin.activemethod
def Execute(self, *command):
return self._Execute(command, shell=False)
@cr.Plugin.activemethod
def ExecuteSilently(self, *command):
return self._Execute(command, shell=False, silent=True)
@cr.Plugin.activemethod
def CaptureShell(self, *command):
return self._Execute(command,
shell=True, capture=True, ignore_dry_run=True)
@cr.Plugin.activemethod
def Capture(self, *command):
return self._Execute(command, capture=True, ignore_dry_run=True)
@cr.Plugin.activemethod
def ExecuteStatus(self, *command):
return self._Execute(command,
ignore_dry_run=True, return_status=True)
@cr.Plugin.activemethod
def YesNo(self, question, default=True):
"""Ask the user a yes no question
This blocks until the user responds.
Args:
question: The question string to show the user
default: True if the default response is Yes
Returns:
True if the response was yes.
"""
options = 'Y/n' if default else 'y/N'
result = raw_input(question + ' [' + options + '] ').lower()
if result == '':
return default
return result in ['y', 'yes']
@classmethod
def SearchPath(cls, name, paths=[]):
"""Searches the PATH for an executable.
Args:
name: the name of the binary to search for.
Returns:
the set of executables found, or an empty list if none.
"""
result = []
extensions = ['']
extensions.extend(os.environ.get('PATHEXT', '').split(os.pathsep))
paths = [cr.context.Substitute(path) for path in paths if path]
paths = paths + os.environ.get('PATH', '').split(os.pathsep)
for path in paths:
partial = os.path.join(path, name)
for extension in extensions:
filename = partial + extension
if os.path.exists(filename) and filename not in result:
result.append(filename)
return result