blob: 9cb9a673f71e099731af9c6301b542bd604bd4d4 [file] [log] [blame]
Scott Zawalski6bc41ac2010-09-08 12:47:28 -07001# 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
Chris Sosa471532a2011-02-01 15:10:06 -08007import inspect
Scott Zawalski98ac6b22010-09-08 15:59:23 -07008import os
Tan Gao2990a4d2010-09-22 09:34:27 -07009import re
Doug Anderson6781f942011-01-14 16:21:39 -080010import signal
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070011import subprocess
12import sys
13
14_STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
15
Tan Gao2f310882010-09-10 14:50:47 -070016
17class CommandResult(object):
18 """An object to store various attributes of a child process."""
19
20 def __init__(self):
21 self.cmd = None
22 self.error = None
23 self.output = None
24 self.returncode = None
25
26
27class RunCommandError(Exception):
28 """Error caught in RunCommand() method."""
29 pass
30
31
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070032def RunCommand(cmd, print_cmd=True, error_ok=False, error_message=None,
33 exit_code=False, redirect_stdout=False, redirect_stderr=False,
Doug Anderson6781f942011-01-14 16:21:39 -080034 cwd=None, input=None, enter_chroot=False, shell=False,
35 env=None, ignore_sigint=False):
David James6db8f522010-09-09 10:49:11 -070036 """Runs a command.
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070037
Tan Gao2990a4d2010-09-22 09:34:27 -070038 Args:
39 cmd: cmd to run. Should be input to subprocess.Popen.
40 print_cmd: prints the command before running it.
41 error_ok: does not raise an exception on error.
42 error_message: prints out this message when an error occurrs.
43 exit_code: returns the return code of the shell command.
44 redirect_stdout: returns the stdout.
45 redirect_stderr: holds stderr output until input is communicated.
46 cwd: the working directory to run this cmd.
47 input: input to pipe into this command through stdin.
48 enter_chroot: this command should be run from within the chroot. If set,
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070049 cwd must point to the scripts directory.
Tan Gao2990a4d2010-09-22 09:34:27 -070050 shell: If shell is True, the specified command will be executed through
51 the shell.
Doug Anderson6781f942011-01-14 16:21:39 -080052 env: If non-None, this is the environment for the new process.
53 ignore_sigint: If True, we'll ignore signal.SIGINT before calling the
54 child. This is the desired behavior if we know our child will handle
55 Ctrl-C. If we don't do this, I think we and the child will both get
56 Ctrl-C at the same time, which means we'll forcefully kill the child.
Tan Gao2990a4d2010-09-22 09:34:27 -070057
58 Returns:
59 A CommandResult object.
60
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070061 Raises:
62 Exception: Raises generic exception on error with optional error_message.
63 """
64 # Set default for variables.
65 stdout = None
66 stderr = None
67 stdin = None
Tan Gao2f310882010-09-10 14:50:47 -070068 cmd_result = CommandResult()
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070069
70 # Modify defaults based on parameters.
Tan Gao2990a4d2010-09-22 09:34:27 -070071 if redirect_stdout: stdout = subprocess.PIPE
72 if redirect_stderr: stderr = subprocess.PIPE
73 # TODO(sosa): gpylint complains about redefining built-in 'input'.
74 # Can we rename this variable?
75 if input: stdin = subprocess.PIPE
David James6db8f522010-09-09 10:49:11 -070076 if isinstance(cmd, basestring):
77 if enter_chroot: cmd = './enter_chroot.sh -- ' + cmd
78 cmd_str = cmd
79 else:
80 if enter_chroot: cmd = ['./enter_chroot.sh', '--'] + cmd
81 cmd_str = ' '.join(cmd)
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070082
83 # Print out the command before running.
84 if print_cmd:
David James6db8f522010-09-09 10:49:11 -070085 Info('RunCommand: %s' % cmd_str)
Doug Andersona8d22de2011-01-13 16:22:58 -080086 cmd_result.cmd = cmd
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070087
88 try:
David James9102a892010-12-02 10:21:49 -080089 proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin, stdout=stdout,
Doug Anderson6781f942011-01-14 16:21:39 -080090 stderr=stderr, shell=shell, env=env)
91 if ignore_sigint:
92 old_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN)
93 try:
94 (cmd_result.output, cmd_result.error) = proc.communicate(input)
95 finally:
96 if ignore_sigint:
97 signal.signal(signal.SIGINT, old_sigint)
98
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070099 if exit_code:
Tan Gao2f310882010-09-10 14:50:47 -0700100 cmd_result.returncode = proc.returncode
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700101
102 if not error_ok and proc.returncode:
Tan Gao2f310882010-09-10 14:50:47 -0700103 msg = ('Command "%s" failed.\n' % cmd_str +
104 (error_message or cmd_result.error or cmd_result.output or ''))
105 raise RunCommandError(msg)
Tan Gao2990a4d2010-09-22 09:34:27 -0700106 # TODO(sosa): is it possible not to use the catch-all Exception here?
107 except Exception, e:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700108 if not error_ok:
109 raise
110 else:
111 Warning(str(e))
112
Tan Gao2f310882010-09-10 14:50:47 -0700113 return cmd_result
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700114
115
116class Color(object):
117 """Conditionally wraps text in ANSI color escape sequences."""
118 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
119 BOLD = -1
120 COLOR_START = '\033[1;%dm'
121 BOLD_START = '\033[1m'
122 RESET = '\033[0m'
123
124 def __init__(self, enabled=True):
125 self._enabled = enabled
126
127 def Color(self, color, text):
128 """Returns text with conditionally added color escape sequences.
129
Tan Gao2990a4d2010-09-22 09:34:27 -0700130 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700131 color: Text color -- one of the color constants defined in this class.
132 text: The text to color.
133
134 Returns:
135 If self._enabled is False, returns the original text. If it's True,
136 returns text with color escape sequences based on the value of color.
137 """
138 if not self._enabled:
139 return text
140 if color == self.BOLD:
141 start = self.BOLD_START
142 else:
143 start = self.COLOR_START % (color + 30)
144 return start + text + self.RESET
145
146
147def Die(message):
148 """Emits a red error message and halts execution.
149
Tan Gao2990a4d2010-09-22 09:34:27 -0700150 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700151 message: The message to be emitted before exiting.
152 """
153 print >> sys.stderr, (
154 Color(_STDOUT_IS_TTY).Color(Color.RED, '\nERROR: ' + message))
155 sys.exit(1)
156
157
Tan Gao2990a4d2010-09-22 09:34:27 -0700158# pylint: disable-msg=W0622
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700159def Warning(message):
160 """Emits a yellow warning message and continues execution.
161
Tan Gao2990a4d2010-09-22 09:34:27 -0700162 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700163 message: The message to be emitted.
164 """
165 print >> sys.stderr, (
166 Color(_STDOUT_IS_TTY).Color(Color.YELLOW, '\nWARNING: ' + message))
167
168
169def Info(message):
170 """Emits a blue informational message and continues execution.
171
Tan Gao2990a4d2010-09-22 09:34:27 -0700172 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700173 message: The message to be emitted.
174 """
175 print >> sys.stderr, (
176 Color(_STDOUT_IS_TTY).Color(Color.BLUE, '\nINFO: ' + message))
Scott Zawalski98ac6b22010-09-08 15:59:23 -0700177
178
179def ListFiles(base_dir):
180 """Recurively list files in a directory.
181
Tan Gao2990a4d2010-09-22 09:34:27 -0700182 Args:
Scott Zawalski98ac6b22010-09-08 15:59:23 -0700183 base_dir: directory to start recursively listing in.
184
185 Returns:
186 A list of files relative to the base_dir path or
187 An empty list of there are no files in the directories.
188 """
189 directories = [base_dir]
190 files_list = []
191 while directories:
192 directory = directories.pop()
193 for name in os.listdir(directory):
194 fullpath = os.path.join(directory, name)
195 if os.path.isfile(fullpath):
196 files_list.append(fullpath)
197 elif os.path.isdir(fullpath):
198 directories.append(fullpath)
199
200 return files_list
Tan Gao2990a4d2010-09-22 09:34:27 -0700201
202
203def IsInsideChroot():
204 """Returns True if we are inside chroot."""
205 return os.path.exists('/etc/debian_chroot')
206
207
208def GetSrcRoot():
209 """Get absolute path to src/scripts/ directory.
210
211 Assuming test script will always be run from descendent of src/scripts.
212
213 Returns:
214 A string, absolute path to src/scripts directory. None if not found.
215 """
216 src_root = None
217 match_str = '/src/scripts/'
218 test_script_path = os.path.abspath('.')
219
220 path_list = re.split(match_str, test_script_path)
221 if path_list:
222 src_root = os.path.join(path_list[0], match_str.strip('/'))
223 Info ('src_root = %r' % src_root)
224 else:
225 Info ('No %r found in %r' % (match_str, test_script_path))
226
227 return src_root
228
229
230def GetChromeosVersion(str_obj):
231 """Helper method to parse output for CHROMEOS_VERSION_STRING.
232
233 Args:
234 str_obj: a string, which may contain Chrome OS version info.
235
236 Returns:
237 A string, value of CHROMEOS_VERSION_STRING environment variable set by
238 chromeos_version.sh. Or None if not found.
239 """
240 if str_obj is not None:
241 match = re.search('CHROMEOS_VERSION_STRING=([0-9_.]+)', str_obj)
242 if match and match.group(1):
243 Info ('CHROMEOS_VERSION_STRING = %s' % match.group(1))
244 return match.group(1)
245
246 Info ('CHROMEOS_VERSION_STRING NOT found')
247 return None
248
249
250def GetOutputImageDir(board, cros_version):
251 """Construct absolute path to output image directory.
252
253 Args:
254 board: a string.
255 cros_version: a string, Chrome OS version.
256
257 Returns:
258 a string: absolute path to output directory.
259 """
260 src_root = GetSrcRoot()
261 rel_path = 'build/images/%s' % board
262 # ASSUME: --build_attempt always sets to 1
263 version_str = '-'.join([cros_version, 'a1'])
264 output_dir = os.path.join(os.path.dirname(src_root), rel_path, version_str)
265 Info ('output_dir = %s' % output_dir)
266 return output_dir
Chris Sosa471532a2011-02-01 15:10:06 -0800267
268
269def FindRepoDir(path=None):
270 """Returns the nearest higher-level repo dir from the specified path.
271
272 Args:
273 path: The path to use. Defaults to cwd.
274 """
275 if path is None:
276 path = os.getcwd()
277 path = os.path.abspath(path)
278 while path != '/':
279 repo_dir = os.path.join(path, '.repo')
280 if os.path.isdir(repo_dir):
281 return repo_dir
282 path = os.path.dirname(path)
283 return None
284
285
286def ReinterpretPathForChroot(path):
287 """Returns reinterpreted path from outside the chroot for use inside.
288
289 Args:
290 path: The path to reinterpret. Must be in src tree.
291 """
292 root_path = os.path.join(FindRepoDir(path), '..')
293
294 path_abs_path = os.path.abspath(path)
295 root_abs_path = os.path.abspath(root_path)
296
297 # Strip the repository root from the path and strip first /.
298 relative_path = path_abs_path.replace(root_abs_path, '')[1:]
299
300 if relative_path == path_abs_path:
301 raise Exception('Error: path is outside your src tree, cannot reinterpret.')
302
303 new_path = os.path.join('/home', os.getenv('USER'), 'trunk', relative_path)
304 return new_path
305
306
307def GetCallerName():
308 """Returns the name of the calling module with __main__."""
309 top_frame = inspect.stack()[-1][0]
310 return os.path.basename(top_frame.f_code.co_filename)
311
312
313class RunCommandException(Exception):
314 """Raised when there is an error in OldRunCommand."""
315 pass
316
317
318def OldRunCommand(cmd, print_cmd=True, error_ok=False, error_message=None,
319 exit_code=False, redirect_stdout=False, redirect_stderr=False,
320 cwd=None, input=None, enter_chroot=False, num_retries=0):
321 """Legacy run shell command.
322
323 Arguments:
324 cmd: cmd to run. Should be input to subprocess.POpen. If a string,
325 converted to an array using split().
326 print_cmd: prints the command before running it.
327 error_ok: does not raise an exception on error.
328 error_message: prints out this message when an error occurrs.
329 exit_code: returns the return code of the shell command.
330 redirect_stdout: returns the stdout.
331 redirect_stderr: holds stderr output until input is communicated.
332 cwd: the working directory to run this cmd.
333 input: input to pipe into this command through stdin.
334 enter_chroot: this command should be run from within the chroot. If set,
335 cwd must point to the scripts directory.
336 num_retries: the number of retries to perform before dying
337
338 Returns:
339 If exit_code is True, returns the return code of the shell command.
340 Else returns the output of the shell command.
341
342 Raises:
343 Exception: Raises RunCommandException on error with optional error_message.
344 """
345 # Set default for variables.
346 stdout = None
347 stderr = None
348 stdin = None
349 output = ''
350
351 # Modify defaults based on parameters.
352 if redirect_stdout: stdout = subprocess.PIPE
353 if redirect_stderr: stderr = subprocess.PIPE
354 if input: stdin = subprocess.PIPE
355 if enter_chroot: cmd = ['./enter_chroot.sh', '--'] + cmd
356
357 # Print out the command before running.
358 if print_cmd:
359 Info('PROGRAM(%s) -> RunCommand: %r in dir %s' %
360 (GetCallerName(), cmd, cwd))
361
362 for retry_count in range(num_retries + 1):
363 try:
364 proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin,
365 stdout=stdout, stderr=stderr)
366 (output, error) = proc.communicate(input)
367 if exit_code and retry_count == num_retries:
368 return proc.returncode
369
370 if proc.returncode == 0:
371 break
372
373 raise RunCommandException('Command "%r" failed.\n' % (cmd) +
374 (error_message or error or output or ''))
375 except RunCommandException as e:
376 if not error_ok and retry_count == num_retries:
377 raise e
378 else:
379 Warning(str(e))
380 if print_cmd:
381 Info('PROGRAM(%s) -> RunCommand: retrying %r in dir %s' %
382 (GetCallerName(), cmd, cwd))
383
384 return output