blob: 7771e47b801c8080b6e3aa4f4ba6fc8a749540b7 [file] [log] [blame]
Peter Mayo193f68f2011-04-19 19:08:21 -04001 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
Scott Zawalski6bc41ac2010-09-08 12:47:28 -07002# 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
David Jameseeefb852011-05-19 21:32:38 -070013import time
Simon Glass53ed2302011-02-08 18:42:16 -080014from terminal import Color
15
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070016
17_STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
18
David Jameseeefb852011-05-19 21:32:38 -070019class GitPushFailed(Exception):
20 """Raised when a git push failed after retry."""
21 pass
Tan Gao2f310882010-09-10 14:50:47 -070022
23class CommandResult(object):
24 """An object to store various attributes of a child process."""
25
26 def __init__(self):
27 self.cmd = None
28 self.error = None
29 self.output = None
30 self.returncode = None
31
32
33class RunCommandError(Exception):
34 """Error caught in RunCommand() method."""
Don Garrettb85946a2011-03-10 18:11:08 -080035 def __init__(self, msg, cmd):
36 self.cmd = cmd
37 Exception.__init__(self, msg)
Tan Gao2f310882010-09-10 14:50:47 -070038
Don Garrettb85946a2011-03-10 18:11:08 -080039 def __eq__(self, other):
40 return (type(self) == type(other) and
41 str(self) == str(other) and
42 self.cmd == other.cmd)
43
44 def __ne__(self, other):
45 return not self.__eq__(other)
Tan Gao2f310882010-09-10 14:50:47 -070046
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070047def RunCommand(cmd, print_cmd=True, error_ok=False, error_message=None,
48 exit_code=False, redirect_stdout=False, redirect_stderr=False,
Doug Anderson6781f942011-01-14 16:21:39 -080049 cwd=None, input=None, enter_chroot=False, shell=False,
Peter Mayo193f68f2011-04-19 19:08:21 -040050 env=None, extra_env=None, ignore_sigint=False,
51 combine_stdout_stderr=False):
David James6db8f522010-09-09 10:49:11 -070052 """Runs a command.
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070053
Tan Gao2990a4d2010-09-22 09:34:27 -070054 Args:
Peter Mayo193f68f2011-04-19 19:08:21 -040055 cmd: cmd to run. Should be input to subprocess.Popen. If a string, shell
56 must be true. Otherwise the command must be an array of arguments, and
57 shell must be false.
Tan Gao2990a4d2010-09-22 09:34:27 -070058 print_cmd: prints the command before running it.
59 error_ok: does not raise an exception on error.
60 error_message: prints out this message when an error occurrs.
61 exit_code: returns the return code of the shell command.
62 redirect_stdout: returns the stdout.
63 redirect_stderr: holds stderr output until input is communicated.
64 cwd: the working directory to run this cmd.
65 input: input to pipe into this command through stdin.
66 enter_chroot: this command should be run from within the chroot. If set,
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070067 cwd must point to the scripts directory.
Peter Mayo193f68f2011-04-19 19:08:21 -040068 shell: Controls whether we add a shell as a command interpreter. See cmd
69 since it has to agree as to the type.
70 env: If non-None, this is the environment for the new process. If
71 enter_chroot is true then this is the environment of the enter_chroot,
72 most of which gets removed from the cmd run.
73 extra_env: If set, this is added to the environment for the new process.
74 In enter_chroot=True case, these are specified on the post-entry
75 side, and so are often more useful. This dictionary is not used to
76 clear any entries though.
Doug Anderson6781f942011-01-14 16:21:39 -080077 ignore_sigint: If True, we'll ignore signal.SIGINT before calling the
78 child. This is the desired behavior if we know our child will handle
79 Ctrl-C. If we don't do this, I think we and the child will both get
80 Ctrl-C at the same time, which means we'll forcefully kill the child.
Chris Sosa66c8c252011-02-17 11:44:09 -080081 combine_stdout_stderr: Combines stdout and stdin streams into stdout.
Tan Gao2990a4d2010-09-22 09:34:27 -070082
83 Returns:
84 A CommandResult object.
85
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070086 Raises:
87 Exception: Raises generic exception on error with optional error_message.
88 """
89 # Set default for variables.
90 stdout = None
91 stderr = None
92 stdin = None
Tan Gao2f310882010-09-10 14:50:47 -070093 cmd_result = CommandResult()
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070094
95 # Modify defaults based on parameters.
Tan Gao2990a4d2010-09-22 09:34:27 -070096 if redirect_stdout: stdout = subprocess.PIPE
97 if redirect_stderr: stderr = subprocess.PIPE
Chris Sosa66c8c252011-02-17 11:44:09 -080098 if combine_stdout_stderr: stderr = subprocess.STDOUT
Tan Gao2990a4d2010-09-22 09:34:27 -070099 # TODO(sosa): gpylint complains about redefining built-in 'input'.
100 # Can we rename this variable?
101 if input: stdin = subprocess.PIPE
Peter Mayo193f68f2011-04-19 19:08:21 -0400102
David James6db8f522010-09-09 10:49:11 -0700103 if isinstance(cmd, basestring):
Peter Mayo193f68f2011-04-19 19:08:21 -0400104 if not shell:
105 raise Exception('Cannot run a string command without a shell')
106 cmd = ['/bin/sh', '-c', cmd]
107 shell = False
108 elif shell:
109 raise Exception('Cannot run an array command with a shell')
110
111 # If we are using enter_chroot we need to use enterchroot pass env through
112 # to the final command.
113 if enter_chroot:
114 cmd = ['./enter_chroot.sh', '--'] + cmd
115 if extra_env:
116 for (key, value) in extra_env.items():
117 cmd.insert(1, '%s=%s' % (key, value))
118 elif extra_env:
119 if env is not None:
120 env = env.copy()
121 else:
122 env = os.environ.copy()
123
124 env.update(extra_env)
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700125
126 # Print out the command before running.
127 if print_cmd:
Don Garrettf3eac242011-04-13 17:50:20 -0700128 if cwd:
Peter Mayo193f68f2011-04-19 19:08:21 -0400129 Info('RunCommand: %r in %s' % (cmd, cwd))
Don Garrettf3eac242011-04-13 17:50:20 -0700130 else:
Peter Mayo193f68f2011-04-19 19:08:21 -0400131 Info('RunCommand: %r' % cmd)
Doug Andersona8d22de2011-01-13 16:22:58 -0800132 cmd_result.cmd = cmd
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700133
134 try:
David James9102a892010-12-02 10:21:49 -0800135 proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin, stdout=stdout,
Peter Mayo193f68f2011-04-19 19:08:21 -0400136 stderr=stderr, shell=False, env=env)
Doug Anderson6781f942011-01-14 16:21:39 -0800137 if ignore_sigint:
138 old_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN)
139 try:
140 (cmd_result.output, cmd_result.error) = proc.communicate(input)
141 finally:
142 if ignore_sigint:
143 signal.signal(signal.SIGINT, old_sigint)
144
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700145 if exit_code:
Tan Gao2f310882010-09-10 14:50:47 -0700146 cmd_result.returncode = proc.returncode
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700147
148 if not error_ok and proc.returncode:
Peter Mayo193f68f2011-04-19 19:08:21 -0400149 msg = ('Command "%r" failed.\n' % cmd +
Tan Gao2f310882010-09-10 14:50:47 -0700150 (error_message or cmd_result.error or cmd_result.output or ''))
Don Garrettb85946a2011-03-10 18:11:08 -0800151 raise RunCommandError(msg, cmd)
Tan Gao2990a4d2010-09-22 09:34:27 -0700152 # TODO(sosa): is it possible not to use the catch-all Exception here?
Peter Mayo193f68f2011-04-19 19:08:21 -0400153 except OSError, e:
154 if not error_ok:
155 raise RunCommandError(str(e), cmd)
156 else:
157 Warning(str(e))
Tan Gao2990a4d2010-09-22 09:34:27 -0700158 except Exception, e:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700159 if not error_ok:
160 raise
161 else:
162 Warning(str(e))
163
Tan Gao2f310882010-09-10 14:50:47 -0700164 return cmd_result
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700165
166
Simon Glass5329b932011-03-14 16:49:04 -0700167#TODO(sjg): Remove this in favor of operation.Die
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700168def Die(message):
169 """Emits a red error message and halts execution.
170
Tan Gao2990a4d2010-09-22 09:34:27 -0700171 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700172 message: The message to be emitted before exiting.
173 """
174 print >> sys.stderr, (
175 Color(_STDOUT_IS_TTY).Color(Color.RED, '\nERROR: ' + message))
176 sys.exit(1)
177
178
Simon Glass5329b932011-03-14 16:49:04 -0700179#TODO(sjg): Remove this in favor of operation.Warning
Tan Gao2990a4d2010-09-22 09:34:27 -0700180# pylint: disable-msg=W0622
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700181def Warning(message):
182 """Emits a yellow warning message and continues execution.
183
Tan Gao2990a4d2010-09-22 09:34:27 -0700184 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700185 message: The message to be emitted.
186 """
187 print >> sys.stderr, (
188 Color(_STDOUT_IS_TTY).Color(Color.YELLOW, '\nWARNING: ' + message))
189
190
Simon Glass5329b932011-03-14 16:49:04 -0700191# This command is deprecated in favor of operation.Info()
192# It is left here for the moment so people are aware what happened.
193# The reason is that this is not aware of the terminal output restrictions such
194# as verbose, quiet and subprocess output. You should not be calling this.
Simon Glass6b069ef2011-03-14 17:24:10 -0700195def Info(message):
196 """Emits a blue informational message and continues execution.
197
198 Args:
199 message: The message to be emitted.
200 """
201 print >> sys.stderr, (
202 Color(_STDOUT_IS_TTY).Color(Color.BLUE, '\nINFO: ' + message))
Scott Zawalski98ac6b22010-09-08 15:59:23 -0700203
204
205def ListFiles(base_dir):
206 """Recurively list files in a directory.
207
Tan Gao2990a4d2010-09-22 09:34:27 -0700208 Args:
Scott Zawalski98ac6b22010-09-08 15:59:23 -0700209 base_dir: directory to start recursively listing in.
210
211 Returns:
212 A list of files relative to the base_dir path or
213 An empty list of there are no files in the directories.
214 """
215 directories = [base_dir]
216 files_list = []
217 while directories:
218 directory = directories.pop()
219 for name in os.listdir(directory):
220 fullpath = os.path.join(directory, name)
221 if os.path.isfile(fullpath):
222 files_list.append(fullpath)
223 elif os.path.isdir(fullpath):
224 directories.append(fullpath)
225
226 return files_list
Tan Gao2990a4d2010-09-22 09:34:27 -0700227
228
229def IsInsideChroot():
230 """Returns True if we are inside chroot."""
231 return os.path.exists('/etc/debian_chroot')
232
233
234def GetSrcRoot():
235 """Get absolute path to src/scripts/ directory.
236
237 Assuming test script will always be run from descendent of src/scripts.
238
239 Returns:
240 A string, absolute path to src/scripts directory. None if not found.
241 """
242 src_root = None
243 match_str = '/src/scripts/'
244 test_script_path = os.path.abspath('.')
245
246 path_list = re.split(match_str, test_script_path)
247 if path_list:
248 src_root = os.path.join(path_list[0], match_str.strip('/'))
249 Info ('src_root = %r' % src_root)
250 else:
251 Info ('No %r found in %r' % (match_str, test_script_path))
252
253 return src_root
254
255
256def GetChromeosVersion(str_obj):
257 """Helper method to parse output for CHROMEOS_VERSION_STRING.
258
259 Args:
260 str_obj: a string, which may contain Chrome OS version info.
261
262 Returns:
263 A string, value of CHROMEOS_VERSION_STRING environment variable set by
264 chromeos_version.sh. Or None if not found.
265 """
266 if str_obj is not None:
267 match = re.search('CHROMEOS_VERSION_STRING=([0-9_.]+)', str_obj)
268 if match and match.group(1):
269 Info ('CHROMEOS_VERSION_STRING = %s' % match.group(1))
270 return match.group(1)
271
272 Info ('CHROMEOS_VERSION_STRING NOT found')
273 return None
274
275
276def GetOutputImageDir(board, cros_version):
277 """Construct absolute path to output image directory.
278
279 Args:
280 board: a string.
281 cros_version: a string, Chrome OS version.
282
283 Returns:
284 a string: absolute path to output directory.
285 """
286 src_root = GetSrcRoot()
287 rel_path = 'build/images/%s' % board
288 # ASSUME: --build_attempt always sets to 1
289 version_str = '-'.join([cros_version, 'a1'])
290 output_dir = os.path.join(os.path.dirname(src_root), rel_path, version_str)
291 Info ('output_dir = %s' % output_dir)
292 return output_dir
Chris Sosa471532a2011-02-01 15:10:06 -0800293
294
295def FindRepoDir(path=None):
296 """Returns the nearest higher-level repo dir from the specified path.
297
298 Args:
299 path: The path to use. Defaults to cwd.
300 """
301 if path is None:
302 path = os.getcwd()
303 path = os.path.abspath(path)
304 while path != '/':
305 repo_dir = os.path.join(path, '.repo')
306 if os.path.isdir(repo_dir):
307 return repo_dir
308 path = os.path.dirname(path)
309 return None
310
311
312def ReinterpretPathForChroot(path):
313 """Returns reinterpreted path from outside the chroot for use inside.
314
315 Args:
316 path: The path to reinterpret. Must be in src tree.
317 """
318 root_path = os.path.join(FindRepoDir(path), '..')
319
320 path_abs_path = os.path.abspath(path)
321 root_abs_path = os.path.abspath(root_path)
322
323 # Strip the repository root from the path and strip first /.
324 relative_path = path_abs_path.replace(root_abs_path, '')[1:]
325
326 if relative_path == path_abs_path:
327 raise Exception('Error: path is outside your src tree, cannot reinterpret.')
328
329 new_path = os.path.join('/home', os.getenv('USER'), 'trunk', relative_path)
330 return new_path
331
332
David Jameseeefb852011-05-19 21:32:38 -0700333def GetPushBranch(branch, cwd):
334 """Gets the appropriate push branch for the specified branch / directory.
335
336 If branch has a valid tracking branch, we should push to that branch. If
337 the tracking branch is a revision, we can't push to that, so we should look
338 at the default branch from the manifest.
339
340 Args:
341 branch: Branch to examine for tracking branch.
342 cwd: Directory to look in.
343 """
344 info = {}
345 for key in ('remote', 'merge'):
346 cmd = ['git', 'config', 'branch.%s.%s' % (branch, key)]
347 info[key] = RunCommand(cmd, redirect_stdout=True, cwd=cwd).output.strip()
348 if not info['merge'].startswith('refs/heads/'):
349 output = RunCommand(['repo', 'manifest', '-o', '-'], redirect_stdout=True,
350 cwd=cwd).output
351 m = re.search(r'<default[^>]*revision="(refs/heads/[^"]*)"', output)
352 assert m
353 info['merge'] = m.group(1)
354 assert info['merge'].startswith('refs/heads/')
355 return info['remote'], info['merge'].replace('refs/heads/', '')
356
357
358def GitPushWithRetry(branch, cwd, dryrun=False, retries=5):
359 """General method to push local git changes.
360
361 Args:
362 branch: Local branch to push. Branch should have already been created
363 with a local change committed ready to push to the remote branch. Must
364 also already be checked out to that branch.
365 cwd: Directory to push in.
366 dryrun: Git push --dry-run if set to True.
367 retries: The number of times to retry before giving up, default: 5
368
369 Raises:
370 GitPushFailed if push was unsuccessful after retries
371 """
372 remote, push_branch = GetPushBranch(branch, cwd)
373 for retry in range(1, retries + 1):
374 try:
375 RunCommand(['git', 'remote', 'update'], cwd=cwd)
David Jamese77bd562011-06-08 11:57:38 -0700376 try:
377 RunCommand(['git', 'rebase', '%s/%s' % (remote, push_branch)], cwd=cwd)
378 except RunCommandError:
379 # Looks like our change conflicts with upstream. Cleanup our failed
380 # rebase.
381 RunCommand(['git', 'rebase', '--abort'], error_ok=True, cwd=cwd)
382 raise
David Jameseeefb852011-05-19 21:32:38 -0700383 push_command = ['git', 'push', remote, '%s:%s' % (branch, push_branch)]
384 if dryrun:
385 push_command.append('--dry-run')
386
387 RunCommand(push_command, cwd=cwd)
388 break
389 except RunCommandError:
390 if retry < retries:
391 print 'Error pushing changes trying again (%s/%s)' % (retry, retries)
392 time.sleep(5 * retry)
393 else:
394 raise GitPushFailed('Failed to push change after %s retries' % retries)
395
396
Chris Sosa471532a2011-02-01 15:10:06 -0800397def GetCallerName():
398 """Returns the name of the calling module with __main__."""
399 top_frame = inspect.stack()[-1][0]
400 return os.path.basename(top_frame.f_code.co_filename)
401
402
403class RunCommandException(Exception):
404 """Raised when there is an error in OldRunCommand."""
Don Garrettb85946a2011-03-10 18:11:08 -0800405 def __init__(self, msg, cmd):
406 self.cmd = cmd
407 Exception.__init__(self, msg)
408
409 def __eq__(self, other):
410 return (type(self) == type(other) and
411 str(self) == str(other) and
412 self.cmd == other.cmd)
413
414 def __ne__(self, other):
415 return not self.__eq__(other)
Chris Sosa471532a2011-02-01 15:10:06 -0800416
417
418def OldRunCommand(cmd, print_cmd=True, error_ok=False, error_message=None,
419 exit_code=False, redirect_stdout=False, redirect_stderr=False,
420 cwd=None, input=None, enter_chroot=False, num_retries=0):
421 """Legacy run shell command.
422
423 Arguments:
424 cmd: cmd to run. Should be input to subprocess.POpen. If a string,
425 converted to an array using split().
426 print_cmd: prints the command before running it.
427 error_ok: does not raise an exception on error.
428 error_message: prints out this message when an error occurrs.
429 exit_code: returns the return code of the shell command.
430 redirect_stdout: returns the stdout.
431 redirect_stderr: holds stderr output until input is communicated.
432 cwd: the working directory to run this cmd.
433 input: input to pipe into this command through stdin.
434 enter_chroot: this command should be run from within the chroot. If set,
435 cwd must point to the scripts directory.
436 num_retries: the number of retries to perform before dying
437
438 Returns:
439 If exit_code is True, returns the return code of the shell command.
440 Else returns the output of the shell command.
441
442 Raises:
443 Exception: Raises RunCommandException on error with optional error_message.
444 """
445 # Set default for variables.
446 stdout = None
447 stderr = None
448 stdin = None
449 output = ''
450
451 # Modify defaults based on parameters.
452 if redirect_stdout: stdout = subprocess.PIPE
453 if redirect_stderr: stderr = subprocess.PIPE
454 if input: stdin = subprocess.PIPE
455 if enter_chroot: cmd = ['./enter_chroot.sh', '--'] + cmd
456
457 # Print out the command before running.
458 if print_cmd:
459 Info('PROGRAM(%s) -> RunCommand: %r in dir %s' %
460 (GetCallerName(), cmd, cwd))
461
462 for retry_count in range(num_retries + 1):
463 try:
464 proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin,
465 stdout=stdout, stderr=stderr)
466 (output, error) = proc.communicate(input)
467 if exit_code and retry_count == num_retries:
468 return proc.returncode
469
470 if proc.returncode == 0:
471 break
472
473 raise RunCommandException('Command "%r" failed.\n' % (cmd) +
Don Garrettb85946a2011-03-10 18:11:08 -0800474 (error_message or error or output or ''),
475 cmd)
Chris Sosa471532a2011-02-01 15:10:06 -0800476 except RunCommandException as e:
477 if not error_ok and retry_count == num_retries:
478 raise e
479 else:
480 Warning(str(e))
481 if print_cmd:
482 Info('PROGRAM(%s) -> RunCommand: retrying %r in dir %s' %
483 (GetCallerName(), cmd, cwd))
484
485 return output