Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 1 | # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Common python commands used by various build scripts.""" |
| 6 | |
Scott Zawalski | 98ac6b2 | 2010-09-08 15:59:23 -0700 | [diff] [blame] | 7 | import os |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 8 | import re |
Doug Anderson | 6781f94 | 2011-01-14 16:21:39 -0800 | [diff] [blame^] | 9 | import signal |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 10 | import subprocess |
| 11 | import sys |
| 12 | |
| 13 | _STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() |
| 14 | |
Tan Gao | 2f31088 | 2010-09-10 14:50:47 -0700 | [diff] [blame] | 15 | |
| 16 | class CommandResult(object): |
| 17 | """An object to store various attributes of a child process.""" |
| 18 | |
| 19 | def __init__(self): |
| 20 | self.cmd = None |
| 21 | self.error = None |
| 22 | self.output = None |
| 23 | self.returncode = None |
| 24 | |
| 25 | |
| 26 | class RunCommandError(Exception): |
| 27 | """Error caught in RunCommand() method.""" |
| 28 | pass |
| 29 | |
| 30 | |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 31 | def RunCommand(cmd, print_cmd=True, error_ok=False, error_message=None, |
| 32 | exit_code=False, redirect_stdout=False, redirect_stderr=False, |
Doug Anderson | 6781f94 | 2011-01-14 16:21:39 -0800 | [diff] [blame^] | 33 | cwd=None, input=None, enter_chroot=False, shell=False, |
| 34 | env=None, ignore_sigint=False): |
David James | 6db8f52 | 2010-09-09 10:49:11 -0700 | [diff] [blame] | 35 | """Runs a command. |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 36 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 37 | Args: |
| 38 | cmd: cmd to run. Should be input to subprocess.Popen. |
| 39 | print_cmd: prints the command before running it. |
| 40 | error_ok: does not raise an exception on error. |
| 41 | error_message: prints out this message when an error occurrs. |
| 42 | exit_code: returns the return code of the shell command. |
| 43 | redirect_stdout: returns the stdout. |
| 44 | redirect_stderr: holds stderr output until input is communicated. |
| 45 | cwd: the working directory to run this cmd. |
| 46 | input: input to pipe into this command through stdin. |
| 47 | enter_chroot: this command should be run from within the chroot. If set, |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 48 | cwd must point to the scripts directory. |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 49 | shell: If shell is True, the specified command will be executed through |
| 50 | the shell. |
Doug Anderson | 6781f94 | 2011-01-14 16:21:39 -0800 | [diff] [blame^] | 51 | env: If non-None, this is the environment for the new process. |
| 52 | ignore_sigint: If True, we'll ignore signal.SIGINT before calling the |
| 53 | child. This is the desired behavior if we know our child will handle |
| 54 | Ctrl-C. If we don't do this, I think we and the child will both get |
| 55 | Ctrl-C at the same time, which means we'll forcefully kill the child. |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 56 | |
| 57 | Returns: |
| 58 | A CommandResult object. |
| 59 | |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 60 | Raises: |
| 61 | Exception: Raises generic exception on error with optional error_message. |
| 62 | """ |
| 63 | # Set default for variables. |
| 64 | stdout = None |
| 65 | stderr = None |
| 66 | stdin = None |
Tan Gao | 2f31088 | 2010-09-10 14:50:47 -0700 | [diff] [blame] | 67 | cmd_result = CommandResult() |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 68 | |
| 69 | # Modify defaults based on parameters. |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 70 | if redirect_stdout: stdout = subprocess.PIPE |
| 71 | if redirect_stderr: stderr = subprocess.PIPE |
| 72 | # TODO(sosa): gpylint complains about redefining built-in 'input'. |
| 73 | # Can we rename this variable? |
| 74 | if input: stdin = subprocess.PIPE |
David James | 6db8f52 | 2010-09-09 10:49:11 -0700 | [diff] [blame] | 75 | if isinstance(cmd, basestring): |
| 76 | if enter_chroot: cmd = './enter_chroot.sh -- ' + cmd |
| 77 | cmd_str = cmd |
| 78 | else: |
| 79 | if enter_chroot: cmd = ['./enter_chroot.sh', '--'] + cmd |
| 80 | cmd_str = ' '.join(cmd) |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 81 | |
| 82 | # Print out the command before running. |
| 83 | if print_cmd: |
David James | 6db8f52 | 2010-09-09 10:49:11 -0700 | [diff] [blame] | 84 | Info('RunCommand: %s' % cmd_str) |
Doug Anderson | a8d22de | 2011-01-13 16:22:58 -0800 | [diff] [blame] | 85 | cmd_result.cmd = cmd |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 86 | |
| 87 | try: |
David James | 9102a89 | 2010-12-02 10:21:49 -0800 | [diff] [blame] | 88 | proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin, stdout=stdout, |
Doug Anderson | 6781f94 | 2011-01-14 16:21:39 -0800 | [diff] [blame^] | 89 | stderr=stderr, shell=shell, env=env) |
| 90 | if ignore_sigint: |
| 91 | old_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN) |
| 92 | try: |
| 93 | (cmd_result.output, cmd_result.error) = proc.communicate(input) |
| 94 | finally: |
| 95 | if ignore_sigint: |
| 96 | signal.signal(signal.SIGINT, old_sigint) |
| 97 | |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 98 | if exit_code: |
Tan Gao | 2f31088 | 2010-09-10 14:50:47 -0700 | [diff] [blame] | 99 | cmd_result.returncode = proc.returncode |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 100 | |
| 101 | if not error_ok and proc.returncode: |
Tan Gao | 2f31088 | 2010-09-10 14:50:47 -0700 | [diff] [blame] | 102 | msg = ('Command "%s" failed.\n' % cmd_str + |
| 103 | (error_message or cmd_result.error or cmd_result.output or '')) |
| 104 | raise RunCommandError(msg) |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 105 | # TODO(sosa): is it possible not to use the catch-all Exception here? |
| 106 | except Exception, e: |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 107 | if not error_ok: |
| 108 | raise |
| 109 | else: |
| 110 | Warning(str(e)) |
| 111 | |
Tan Gao | 2f31088 | 2010-09-10 14:50:47 -0700 | [diff] [blame] | 112 | return cmd_result |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 113 | |
| 114 | |
| 115 | class Color(object): |
| 116 | """Conditionally wraps text in ANSI color escape sequences.""" |
| 117 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) |
| 118 | BOLD = -1 |
| 119 | COLOR_START = '\033[1;%dm' |
| 120 | BOLD_START = '\033[1m' |
| 121 | RESET = '\033[0m' |
| 122 | |
| 123 | def __init__(self, enabled=True): |
| 124 | self._enabled = enabled |
| 125 | |
| 126 | def Color(self, color, text): |
| 127 | """Returns text with conditionally added color escape sequences. |
| 128 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 129 | Args: |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 130 | color: Text color -- one of the color constants defined in this class. |
| 131 | text: The text to color. |
| 132 | |
| 133 | Returns: |
| 134 | If self._enabled is False, returns the original text. If it's True, |
| 135 | returns text with color escape sequences based on the value of color. |
| 136 | """ |
| 137 | if not self._enabled: |
| 138 | return text |
| 139 | if color == self.BOLD: |
| 140 | start = self.BOLD_START |
| 141 | else: |
| 142 | start = self.COLOR_START % (color + 30) |
| 143 | return start + text + self.RESET |
| 144 | |
| 145 | |
| 146 | def Die(message): |
| 147 | """Emits a red error message and halts execution. |
| 148 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 149 | Args: |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 150 | message: The message to be emitted before exiting. |
| 151 | """ |
| 152 | print >> sys.stderr, ( |
| 153 | Color(_STDOUT_IS_TTY).Color(Color.RED, '\nERROR: ' + message)) |
| 154 | sys.exit(1) |
| 155 | |
| 156 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 157 | # pylint: disable-msg=W0622 |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 158 | def Warning(message): |
| 159 | """Emits a yellow warning message and continues execution. |
| 160 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 161 | Args: |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 162 | message: The message to be emitted. |
| 163 | """ |
| 164 | print >> sys.stderr, ( |
| 165 | Color(_STDOUT_IS_TTY).Color(Color.YELLOW, '\nWARNING: ' + message)) |
| 166 | |
| 167 | |
| 168 | def Info(message): |
| 169 | """Emits a blue informational message and continues execution. |
| 170 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 171 | Args: |
Scott Zawalski | 6bc41ac | 2010-09-08 12:47:28 -0700 | [diff] [blame] | 172 | message: The message to be emitted. |
| 173 | """ |
| 174 | print >> sys.stderr, ( |
| 175 | Color(_STDOUT_IS_TTY).Color(Color.BLUE, '\nINFO: ' + message)) |
Scott Zawalski | 98ac6b2 | 2010-09-08 15:59:23 -0700 | [diff] [blame] | 176 | |
| 177 | |
| 178 | def ListFiles(base_dir): |
| 179 | """Recurively list files in a directory. |
| 180 | |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 181 | Args: |
Scott Zawalski | 98ac6b2 | 2010-09-08 15:59:23 -0700 | [diff] [blame] | 182 | base_dir: directory to start recursively listing in. |
| 183 | |
| 184 | Returns: |
| 185 | A list of files relative to the base_dir path or |
| 186 | An empty list of there are no files in the directories. |
| 187 | """ |
| 188 | directories = [base_dir] |
| 189 | files_list = [] |
| 190 | while directories: |
| 191 | directory = directories.pop() |
| 192 | for name in os.listdir(directory): |
| 193 | fullpath = os.path.join(directory, name) |
| 194 | if os.path.isfile(fullpath): |
| 195 | files_list.append(fullpath) |
| 196 | elif os.path.isdir(fullpath): |
| 197 | directories.append(fullpath) |
| 198 | |
| 199 | return files_list |
Tan Gao | 2990a4d | 2010-09-22 09:34:27 -0700 | [diff] [blame] | 200 | |
| 201 | |
| 202 | def IsInsideChroot(): |
| 203 | """Returns True if we are inside chroot.""" |
| 204 | return os.path.exists('/etc/debian_chroot') |
| 205 | |
| 206 | |
| 207 | def GetSrcRoot(): |
| 208 | """Get absolute path to src/scripts/ directory. |
| 209 | |
| 210 | Assuming test script will always be run from descendent of src/scripts. |
| 211 | |
| 212 | Returns: |
| 213 | A string, absolute path to src/scripts directory. None if not found. |
| 214 | """ |
| 215 | src_root = None |
| 216 | match_str = '/src/scripts/' |
| 217 | test_script_path = os.path.abspath('.') |
| 218 | |
| 219 | path_list = re.split(match_str, test_script_path) |
| 220 | if path_list: |
| 221 | src_root = os.path.join(path_list[0], match_str.strip('/')) |
| 222 | Info ('src_root = %r' % src_root) |
| 223 | else: |
| 224 | Info ('No %r found in %r' % (match_str, test_script_path)) |
| 225 | |
| 226 | return src_root |
| 227 | |
| 228 | |
| 229 | def GetChromeosVersion(str_obj): |
| 230 | """Helper method to parse output for CHROMEOS_VERSION_STRING. |
| 231 | |
| 232 | Args: |
| 233 | str_obj: a string, which may contain Chrome OS version info. |
| 234 | |
| 235 | Returns: |
| 236 | A string, value of CHROMEOS_VERSION_STRING environment variable set by |
| 237 | chromeos_version.sh. Or None if not found. |
| 238 | """ |
| 239 | if str_obj is not None: |
| 240 | match = re.search('CHROMEOS_VERSION_STRING=([0-9_.]+)', str_obj) |
| 241 | if match and match.group(1): |
| 242 | Info ('CHROMEOS_VERSION_STRING = %s' % match.group(1)) |
| 243 | return match.group(1) |
| 244 | |
| 245 | Info ('CHROMEOS_VERSION_STRING NOT found') |
| 246 | return None |
| 247 | |
| 248 | |
| 249 | def GetOutputImageDir(board, cros_version): |
| 250 | """Construct absolute path to output image directory. |
| 251 | |
| 252 | Args: |
| 253 | board: a string. |
| 254 | cros_version: a string, Chrome OS version. |
| 255 | |
| 256 | Returns: |
| 257 | a string: absolute path to output directory. |
| 258 | """ |
| 259 | src_root = GetSrcRoot() |
| 260 | rel_path = 'build/images/%s' % board |
| 261 | # ASSUME: --build_attempt always sets to 1 |
| 262 | version_str = '-'.join([cros_version, 'a1']) |
| 263 | output_dir = os.path.join(os.path.dirname(src_root), rel_path, version_str) |
| 264 | Info ('output_dir = %s' % output_dir) |
| 265 | return output_dir |