blob: bd2743a0616672d7e3d751a725e415a5722c0333 [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
David James645bf9e2011-05-26 23:50:49 -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 James645bf9e2011-05-26 23:50:49 -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,
Chris Sosa66c8c252011-02-17 11:44:09 -080050 env=None, ignore_sigint=False, combine_stdout_stderr=False):
David James6db8f522010-09-09 10:49:11 -070051 """Runs a command.
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070052
Tan Gao2990a4d2010-09-22 09:34:27 -070053 Args:
54 cmd: cmd to run. Should be input to subprocess.Popen.
55 print_cmd: prints the command before running it.
56 error_ok: does not raise an exception on error.
57 error_message: prints out this message when an error occurrs.
58 exit_code: returns the return code of the shell command.
59 redirect_stdout: returns the stdout.
60 redirect_stderr: holds stderr output until input is communicated.
61 cwd: the working directory to run this cmd.
62 input: input to pipe into this command through stdin.
63 enter_chroot: this command should be run from within the chroot. If set,
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070064 cwd must point to the scripts directory.
Tan Gao2990a4d2010-09-22 09:34:27 -070065 shell: If shell is True, the specified command will be executed through
66 the shell.
Doug Anderson6781f942011-01-14 16:21:39 -080067 env: If non-None, this is the environment for the new process.
68 ignore_sigint: If True, we'll ignore signal.SIGINT before calling the
69 child. This is the desired behavior if we know our child will handle
70 Ctrl-C. If we don't do this, I think we and the child will both get
71 Ctrl-C at the same time, which means we'll forcefully kill the child.
Chris Sosa66c8c252011-02-17 11:44:09 -080072 combine_stdout_stderr: Combines stdout and stdin streams into stdout.
Tan Gao2990a4d2010-09-22 09:34:27 -070073
74 Returns:
75 A CommandResult object.
76
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070077 Raises:
78 Exception: Raises generic exception on error with optional error_message.
79 """
80 # Set default for variables.
81 stdout = None
82 stderr = None
83 stdin = None
Tan Gao2f310882010-09-10 14:50:47 -070084 cmd_result = CommandResult()
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070085
86 # Modify defaults based on parameters.
Tan Gao2990a4d2010-09-22 09:34:27 -070087 if redirect_stdout: stdout = subprocess.PIPE
88 if redirect_stderr: stderr = subprocess.PIPE
Chris Sosa66c8c252011-02-17 11:44:09 -080089 if combine_stdout_stderr: stderr = subprocess.STDOUT
Tan Gao2990a4d2010-09-22 09:34:27 -070090 # TODO(sosa): gpylint complains about redefining built-in 'input'.
91 # Can we rename this variable?
92 if input: stdin = subprocess.PIPE
David James6db8f522010-09-09 10:49:11 -070093 if isinstance(cmd, basestring):
94 if enter_chroot: cmd = './enter_chroot.sh -- ' + cmd
95 cmd_str = cmd
96 else:
97 if enter_chroot: cmd = ['./enter_chroot.sh', '--'] + cmd
98 cmd_str = ' '.join(cmd)
Scott Zawalski6bc41ac2010-09-08 12:47:28 -070099
100 # Print out the command before running.
101 if print_cmd:
David James6db8f522010-09-09 10:49:11 -0700102 Info('RunCommand: %s' % cmd_str)
Doug Andersona8d22de2011-01-13 16:22:58 -0800103 cmd_result.cmd = cmd
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700104
105 try:
David James9102a892010-12-02 10:21:49 -0800106 proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin, stdout=stdout,
Doug Anderson6781f942011-01-14 16:21:39 -0800107 stderr=stderr, shell=shell, env=env)
108 if ignore_sigint:
109 old_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN)
110 try:
111 (cmd_result.output, cmd_result.error) = proc.communicate(input)
112 finally:
113 if ignore_sigint:
114 signal.signal(signal.SIGINT, old_sigint)
115
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700116 if exit_code:
Tan Gao2f310882010-09-10 14:50:47 -0700117 cmd_result.returncode = proc.returncode
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700118
119 if not error_ok and proc.returncode:
Tan Gao2f310882010-09-10 14:50:47 -0700120 msg = ('Command "%s" failed.\n' % cmd_str +
121 (error_message or cmd_result.error or cmd_result.output or ''))
Don Garrettb85946a2011-03-10 18:11:08 -0800122 raise RunCommandError(msg, cmd)
Tan Gao2990a4d2010-09-22 09:34:27 -0700123 # TODO(sosa): is it possible not to use the catch-all Exception here?
124 except Exception, e:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700125 if not error_ok:
126 raise
127 else:
128 Warning(str(e))
129
Tan Gao2f310882010-09-10 14:50:47 -0700130 return cmd_result
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700131
132
Simon Glass5329b932011-03-14 16:49:04 -0700133#TODO(sjg): Remove this in favor of operation.Die
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700134def Die(message):
135 """Emits a red error message and halts execution.
136
Tan Gao2990a4d2010-09-22 09:34:27 -0700137 Args:
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700138 message: The message to be emitted before exiting.
139 """
140 print >> sys.stderr, (
141 Color(_STDOUT_IS_TTY).Color(Color.RED, '\nERROR: ' + message))
142 sys.exit(1)
143
144
Simon Glass5329b932011-03-14 16:49:04 -0700145#TODO(sjg): Remove this in favor of operation.Warning
Tan Gao2990a4d2010-09-22 09:34:27 -0700146# pylint: disable-msg=W0622
Scott Zawalski6bc41ac2010-09-08 12:47:28 -0700147def Warning(message):
148 """Emits a yellow warning message and continues 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.
152 """
153 print >> sys.stderr, (
154 Color(_STDOUT_IS_TTY).Color(Color.YELLOW, '\nWARNING: ' + message))
155
156
Simon Glass5329b932011-03-14 16:49:04 -0700157# This command is deprecated in favor of operation.Info()
158# It is left here for the moment so people are aware what happened.
159# The reason is that this is not aware of the terminal output restrictions such
160# as verbose, quiet and subprocess output. You should not be calling this.
Simon Glass6b069ef2011-03-14 17:24:10 -0700161def Info(message):
162 """Emits a blue informational message and continues execution.
163
164 Args:
165 message: The message to be emitted.
166 """
167 print >> sys.stderr, (
168 Color(_STDOUT_IS_TTY).Color(Color.BLUE, '\nINFO: ' + message))
Scott Zawalski98ac6b22010-09-08 15:59:23 -0700169
170
171def ListFiles(base_dir):
172 """Recurively list files in a directory.
173
Tan Gao2990a4d2010-09-22 09:34:27 -0700174 Args:
Scott Zawalski98ac6b22010-09-08 15:59:23 -0700175 base_dir: directory to start recursively listing in.
176
177 Returns:
178 A list of files relative to the base_dir path or
179 An empty list of there are no files in the directories.
180 """
181 directories = [base_dir]
182 files_list = []
183 while directories:
184 directory = directories.pop()
185 for name in os.listdir(directory):
186 fullpath = os.path.join(directory, name)
187 if os.path.isfile(fullpath):
188 files_list.append(fullpath)
189 elif os.path.isdir(fullpath):
190 directories.append(fullpath)
191
192 return files_list
Tan Gao2990a4d2010-09-22 09:34:27 -0700193
194
195def IsInsideChroot():
196 """Returns True if we are inside chroot."""
197 return os.path.exists('/etc/debian_chroot')
198
199
200def GetSrcRoot():
201 """Get absolute path to src/scripts/ directory.
202
203 Assuming test script will always be run from descendent of src/scripts.
204
205 Returns:
206 A string, absolute path to src/scripts directory. None if not found.
207 """
208 src_root = None
209 match_str = '/src/scripts/'
210 test_script_path = os.path.abspath('.')
211
212 path_list = re.split(match_str, test_script_path)
213 if path_list:
214 src_root = os.path.join(path_list[0], match_str.strip('/'))
215 Info ('src_root = %r' % src_root)
216 else:
217 Info ('No %r found in %r' % (match_str, test_script_path))
218
219 return src_root
220
221
222def GetChromeosVersion(str_obj):
223 """Helper method to parse output for CHROMEOS_VERSION_STRING.
224
225 Args:
226 str_obj: a string, which may contain Chrome OS version info.
227
228 Returns:
229 A string, value of CHROMEOS_VERSION_STRING environment variable set by
230 chromeos_version.sh. Or None if not found.
231 """
232 if str_obj is not None:
233 match = re.search('CHROMEOS_VERSION_STRING=([0-9_.]+)', str_obj)
234 if match and match.group(1):
235 Info ('CHROMEOS_VERSION_STRING = %s' % match.group(1))
236 return match.group(1)
237
238 Info ('CHROMEOS_VERSION_STRING NOT found')
239 return None
240
241
242def GetOutputImageDir(board, cros_version):
243 """Construct absolute path to output image directory.
244
245 Args:
246 board: a string.
247 cros_version: a string, Chrome OS version.
248
249 Returns:
250 a string: absolute path to output directory.
251 """
252 src_root = GetSrcRoot()
253 rel_path = 'build/images/%s' % board
254 # ASSUME: --build_attempt always sets to 1
255 version_str = '-'.join([cros_version, 'a1'])
256 output_dir = os.path.join(os.path.dirname(src_root), rel_path, version_str)
257 Info ('output_dir = %s' % output_dir)
258 return output_dir
Chris Sosa471532a2011-02-01 15:10:06 -0800259
260
261def FindRepoDir(path=None):
262 """Returns the nearest higher-level repo dir from the specified path.
263
264 Args:
265 path: The path to use. Defaults to cwd.
266 """
267 if path is None:
268 path = os.getcwd()
269 path = os.path.abspath(path)
270 while path != '/':
271 repo_dir = os.path.join(path, '.repo')
272 if os.path.isdir(repo_dir):
273 return repo_dir
274 path = os.path.dirname(path)
275 return None
276
277
278def ReinterpretPathForChroot(path):
279 """Returns reinterpreted path from outside the chroot for use inside.
280
281 Args:
282 path: The path to reinterpret. Must be in src tree.
283 """
284 root_path = os.path.join(FindRepoDir(path), '..')
285
286 path_abs_path = os.path.abspath(path)
287 root_abs_path = os.path.abspath(root_path)
288
289 # Strip the repository root from the path and strip first /.
290 relative_path = path_abs_path.replace(root_abs_path, '')[1:]
291
292 if relative_path == path_abs_path:
293 raise Exception('Error: path is outside your src tree, cannot reinterpret.')
294
295 new_path = os.path.join('/home', os.getenv('USER'), 'trunk', relative_path)
296 return new_path
297
298
David James645bf9e2011-05-26 23:50:49 -0700299def GetPushBranch(branch, cwd):
300 """Gets the appropriate push branch for the specified branch / directory.
301
302 If branch has a valid tracking branch, we should push to that branch. If
303 the tracking branch is a revision, we can't push to that, so we should look
304 at the default branch from the manifest.
305
306 Args:
307 branch: Branch to examine for tracking branch.
308 cwd: Directory to look in.
309 """
310 info = {}
311 for key in ('remote', 'merge'):
312 cmd = ['git', 'config', 'branch.%s.%s' % (branch, key)]
313 info[key] = RunCommand(cmd, redirect_stdout=True, cwd=cwd).output.strip()
314 if not info['merge'].startswith('refs/heads/'):
315 output = RunCommand(['repo', 'manifest', '-o', '-'], redirect_stdout=True,
316 cwd=cwd).output
317 m = re.search(r'<default[^>]*revision="(refs/heads/[^"]*)"', output)
318 assert m
319 info['merge'] = m.group(1)
320 assert info['merge'].startswith('refs/heads/')
321 return info['remote'], info['merge'].replace('refs/heads/', '')
322
323
324def GitPushWithRetry(branch, cwd, dryrun=False, retries=5):
325 """General method to push local git changes.
326
327 Args:
328 branch: Local branch to push. Branch should have already been created
329 with a local change committed ready to push to the remote branch. Must
330 also already be checked out to that branch.
331 cwd: Directory to push in.
332 dryrun: Git push --dry-run if set to True.
333 retries: The number of times to retry before giving up, default: 5
334
335 Raises:
336 GitPushFailed if push was unsuccessful after retries
337 """
338 remote, push_branch = GetPushBranch(branch, cwd)
339 for retry in range(1, retries + 1):
340 try:
341 RunCommand(['git', 'remote', 'update'], cwd=cwd)
David Jamesce4e4012011-06-08 11:57:38 -0700342 try:
343 RunCommand(['git', 'rebase', '%s/%s' % (remote, push_branch)], cwd=cwd)
344 except RunCommandError:
345 # Looks like our change conflicts with upstream. Cleanup our failed
346 # rebase.
347 RunCommand(['git', 'rebase', '--abort'], error_ok=True, cwd=cwd)
348 raise
David James645bf9e2011-05-26 23:50:49 -0700349 push_command = ['git', 'push', remote, '%s:%s' % (branch, push_branch)]
350 if dryrun:
351 push_command.append('--dry-run')
352
353 RunCommand(push_command, cwd=cwd)
354 break
355 except RunCommandError:
356 if retry < retries:
357 print 'Error pushing changes trying again (%s/%s)' % (retry, retries)
358 time.sleep(5 * retry)
359 else:
360 raise GitPushFailed('Failed to push change after %s retries' % retries)
361
362
Chris Sosa471532a2011-02-01 15:10:06 -0800363def GetCallerName():
364 """Returns the name of the calling module with __main__."""
365 top_frame = inspect.stack()[-1][0]
366 return os.path.basename(top_frame.f_code.co_filename)
367
368
369class RunCommandException(Exception):
370 """Raised when there is an error in OldRunCommand."""
Don Garrettb85946a2011-03-10 18:11:08 -0800371 def __init__(self, msg, cmd):
372 self.cmd = cmd
373 Exception.__init__(self, msg)
374
375 def __eq__(self, other):
376 return (type(self) == type(other) and
377 str(self) == str(other) and
378 self.cmd == other.cmd)
379
380 def __ne__(self, other):
381 return not self.__eq__(other)
Chris Sosa471532a2011-02-01 15:10:06 -0800382
383
384def OldRunCommand(cmd, print_cmd=True, error_ok=False, error_message=None,
385 exit_code=False, redirect_stdout=False, redirect_stderr=False,
386 cwd=None, input=None, enter_chroot=False, num_retries=0):
387 """Legacy run shell command.
388
389 Arguments:
390 cmd: cmd to run. Should be input to subprocess.POpen. If a string,
391 converted to an array using split().
392 print_cmd: prints the command before running it.
393 error_ok: does not raise an exception on error.
394 error_message: prints out this message when an error occurrs.
395 exit_code: returns the return code of the shell command.
396 redirect_stdout: returns the stdout.
397 redirect_stderr: holds stderr output until input is communicated.
398 cwd: the working directory to run this cmd.
399 input: input to pipe into this command through stdin.
400 enter_chroot: this command should be run from within the chroot. If set,
401 cwd must point to the scripts directory.
402 num_retries: the number of retries to perform before dying
403
404 Returns:
405 If exit_code is True, returns the return code of the shell command.
406 Else returns the output of the shell command.
407
408 Raises:
409 Exception: Raises RunCommandException on error with optional error_message.
410 """
411 # Set default for variables.
412 stdout = None
413 stderr = None
414 stdin = None
415 output = ''
416
417 # Modify defaults based on parameters.
418 if redirect_stdout: stdout = subprocess.PIPE
419 if redirect_stderr: stderr = subprocess.PIPE
420 if input: stdin = subprocess.PIPE
421 if enter_chroot: cmd = ['./enter_chroot.sh', '--'] + cmd
422
423 # Print out the command before running.
424 if print_cmd:
425 Info('PROGRAM(%s) -> RunCommand: %r in dir %s' %
426 (GetCallerName(), cmd, cwd))
427
428 for retry_count in range(num_retries + 1):
429 try:
430 proc = subprocess.Popen(cmd, cwd=cwd, stdin=stdin,
431 stdout=stdout, stderr=stderr)
432 (output, error) = proc.communicate(input)
433 if exit_code and retry_count == num_retries:
434 return proc.returncode
435
436 if proc.returncode == 0:
437 break
438
439 raise RunCommandException('Command "%r" failed.\n' % (cmd) +
Don Garrettb85946a2011-03-10 18:11:08 -0800440 (error_message or error or output or ''),
441 cmd)
Chris Sosa471532a2011-02-01 15:10:06 -0800442 except RunCommandException as e:
443 if not error_ok and retry_count == num_retries:
444 raise e
445 else:
446 Warning(str(e))
447 if print_cmd:
448 Info('PROGRAM(%s) -> RunCommand: retrying %r in dir %s' %
449 (GetCallerName(), cmd, cwd))
450
451 return output