blob: 51585aef2d3dbf7366433a3a659a395c827743d3 [file] [log] [blame]
Dennis Kempin58d9a742014-05-08 18:34:59 -07001# Copyright (c) 2014 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""" This module provides various tools for writing cros developer tools.
6
7This includes tools for regular expressions, path processing, chromium os git
8repositories, shell commands, and user interaction via command line.
9"""
Harry Cutts0edf1572020-01-21 15:42:10 -080010
11from __future__ import print_function
12
Dennis Kempin58d9a742014-05-08 18:34:59 -070013import os
14import re
15import shlex
16import shutil
17import subprocess
18import sys
19import time
20
21class Emerge(object):
22 """Provides tools for emerging and deploying packages to chromebooks."""
23
24 def __init__(self, board_variant):
25 self.cmd = "emerge-{}".format(board_variant)
26
27 def Emerge(self, package_name):
28 SafeExecute([self.cmd, package_name], verbose=True)
29
30 def Deploy(self, package_name, remote, emerge=True):
31 if emerge:
32 self.Emerge(package_name)
33 SafeExecute(["cros", "deploy", remote.ip, package_name], verbose=True)
34
35
36class RegexException(Exception):
37 """Describes a regular expression failure."""
38 def __init__(self, regex, string, match_type):
39 Exception.__init__(self)
40 self.regex = regex
41 self.string = string
42 self.match_type = match_type
43
44 def __str__(self):
45 capped = self.string
46 if len(capped) > 256:
47 capped = capped[1:251] + "[...]"
48 msg = "RequiredRegex \"{}\" failed to {} on:\n{}\n"
49 return msg.format(self.regex, self.match_type, capped)
50
51
52class RequiredRegex(object):
53 """Wrapper for regular expressions using exceptions.
54
55 Most regular expression calls in mttools are used for parsing and any
56 mismatches result in a program failure.
57 To reduce the amount of error checking done in place this wrapper throws
58 meaningful exceptions in case of mismatches.
59 """
60 cap_length = 30
61
62 def __init__(self, pattern):
63 self.pattern = pattern
64
65 def Search(self, string, must_succeed=True):
66 match = re.search(self.pattern, string)
67 return self.__Check(match, string, must_succeed, "search")
68
69 def Match(self, string, must_succeed=True):
70 match = re.match(self.pattern, string)
71 return self.__Check(match, string, must_succeed, "match")
72
73 def __Check(self, match, string, must_succeed, match_type):
74 if not match and must_succeed:
75 raise RegexException(self.pattern, string, match_type)
76 return match
77
78
79class Path(object):
80 """Wrapper for os.path functions enforcing absolute/real paths.
81
82 This wrapper helps processing file paths by enforcing paths to be
83 always absolute and with symlinks resolved. Being a class it also
84 allows a more compact syntax for common operations.
85 """
86 def __init__(self, path, *args):
87 if isinstance(path, Path):
88 self.path = path.path
89 else:
90 self.path = os.path.abspath(path)
91
92 for arg in args:
93 self.path = os.path.join(self.path, str(arg))
94
95 def ListDir(self):
96 for entry in os.listdir(self.path):
97 yield Path(self, entry)
98
99 def Read(self):
100 return open(self.path, "r").read()
101
102 def Open(self, props):
103 return open(self.path, props)
104
105 def Write(self, value):
106 self.Open("w").write(value)
107
108 def Join(self, other):
109 return Path(os.path.join(self.path, other))
110
111 def CopyTo(self, target):
112 shutil.copy(self.path, str(target))
113
114 def MoveTo(self, target):
115 target = Path(target)
116 shutil.move(self.path, str(target))
117
118 def RelPath(self, rel=os.curdir):
119 return os.path.relpath(self.path, str(rel))
120
121 def RmTree(self):
122 return shutil.rmtree(self.path)
123
124 def MakeDirs(self):
125 if not self.exists:
126 os.makedirs(self.path)
127
128 @property
129 def parent(self):
130 return Path(os.path.dirname(self.path))
131
132 @property
133 def exists(self):
134 return os.path.exists(self.path)
135
136 @property
137 def is_file(self):
138 return os.path.isfile(self.path)
139
140 @property
141 def is_link(self):
142 return os.path.islink(self.path)
143
144 @property
145 def is_dir(self):
146 return os.path.isdir(self.path)
147
148 @property
149 def basename(self):
150 return os.path.basename(self.path)
151
152 def __eq__(self, other):
153 return self.path == other.path
154
155 def __ne__(self, other):
156 return self.path != other.path
157
158 def __div__(self, other):
159 return self.Join(other)
160
161 def __bool__(self):
162 return self.exists
163
164 def __str__(self):
165 return self.path
166
167
168class ExecuteException(Exception):
169 """Describes a failure to run a shell command."""
170 def __init__(self, command, code, out, verbose=False):
171 Exception.__init__(self)
172 self.command = command
173 self.code = code
174 self.out = out
175 self.verbose = verbose
176
177 def __str__(self):
178 string = "$ %s\n" % " ".join(self.command)
179 if self.out:
180 string = string + str(self.out) + "\n"
181 string = string + "Command returned " + str(self.code)
182 return string
183
184
185def Execute(command, cwd=None, verbose=False, interactive=False,
186 must_succeed=False):
187 """Execute shell command.
188
189 Returns false if the command failed (i.e. return code != 0), otherwise
190 this command returns the stdout/stderr output of the command.
191 The verbose flag will print the executed command.
192 The interactive flag will not capture stdout/stderr, but allow direct
193 interaction with the command.
194 """
195 if must_succeed:
196 return SafeExecute(command, cwd, verbose, interactive)
197 (code, out) = __Execute(command, cwd, verbose, interactive)
198 if code != 0:
199 return False
200 return out
201
202
203def SafeExecute(command, cwd=None, verbose=False, interactive=False):
204 """Execute shell command and throw exception upon failure.
205
206 This method behaves the same as Execute, but throws an ExecuteException
207 if the command fails.
208 """
209 if isinstance(command, basestring):
210 command = shlex.split(command)
211 (code, out) = __Execute(command, cwd, verbose, interactive)
212 if code != 0:
213 raise ExecuteException(command, code, out, verbose)
214 return out
215
216def __Execute(command, cwd=None, verbose=False, interactive=False):
217 if isinstance(command, basestring):
218 command = shlex.split(command)
219
220 if cwd:
221 cwd = str(cwd)
222
223 if verbose:
Harry Cutts0edf1572020-01-21 15:42:10 -0800224 print("$", " ".join(command))
Dennis Kempin58d9a742014-05-08 18:34:59 -0700225
226 if interactive:
227 process = subprocess.Popen(command, cwd=cwd)
228 else:
229 process = subprocess.Popen(command,
230 stdout=subprocess.PIPE,
231 stderr=subprocess.STDOUT,
232 cwd=cwd)
Dennis Kempin7e296e92014-05-30 10:07:18 -0700233 if verbose and not interactive:
Dennis Kempin58d9a742014-05-08 18:34:59 -0700234 # print a . approximately every second to show progress
235 seconds = 0
236 while process.poll() is None:
237 seconds += 0.01
238 time.sleep(0.01)
239 if seconds > 1:
240 seconds = seconds - 1
241 sys.stdout.write(".")
242 sys.stdout.flush()
Harry Cutts0edf1572020-01-21 15:42:10 -0800243 print("")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700244 else:
245 process.wait()
246
247 out = None
248 if not interactive:
249 out = process.stdout.read().replace("\r", "").strip()
250
251 return (process.returncode, out)
252
253class GitRepo(object):
254 """A helper class to work with Git repositories.
255
256 This class is specialized to deal with chromium specific git workflows
257 and uses the git command line program to interface with the repository.
258 """
259 def __init__(self, repo_path, verbose=False):
260 self.path = Path(repo_path)
261 self.verbose = verbose
262 rel_path = self.path.RelPath("/mnt/host/source/src/")
263 self.review_url = ("https://chrome-internal.googlesource.com/chromeos/" +
264 rel_path)
265
266 def SafeExecute(self, command):
267 if isinstance(command, str):
268 command = "git " + command
269 else:
270 command = ["git"] + command
271 return SafeExecute(command, cwd=self.path, verbose=self.verbose)
272
273 def Execute(self, command):
274 if isinstance(command, str):
275 command = "git " + command
276 else:
277 command = ["git"] + command
278 return Execute("git " + command, cwd=self.path, verbose=self.verbose)
279
280 @property
281 def active_branch(self):
282 result = self.SafeExecute("branch")
283
284 regex = "\\* (\\S+)"
285 match = re.search(regex, result)
286 if match:
287 return match.group(1)
288 return None
289
290 @property
291 def branches(self):
292 branches = []
293 result = self.SafeExecute("branch")
294 regex = "(\\S+)$"
295 for match in re.finditer(regex, result):
296 branches.append(match.group(1))
Harry Cutts0edf1572020-01-21 15:42:10 -0800297 print("Branches:", branches)
Dennis Kempin58d9a742014-05-08 18:34:59 -0700298 return branches
299
300 @property
301 def working_directory_dirty(self):
302 result = self.SafeExecute("status")
303 return "Changes not staged for commit" in result
304
305 @property
306 def index_dirty(self):
307 result = self.SafeExecute("status")
308 return "Changes to be committed" in result
309
310 @property
311 def diverged(self):
312 result = self.SafeExecute("status")
313 return "Your branch is ahead of" in result
314
315 @property
316 def remote(self):
317 return self.SafeExecute("remote").strip()
318
319 @property
320 def status(self):
321 status = []
322 if self.index_dirty:
323 status.append("changes in index")
324 if self.diverged:
325 status.append("diverged from master")
326 if self.working_directory_dirty:
327 status.append("local changes")
328 if not status:
329 status.append("clean")
330 return ", ".join(status)
331
332 def Move(self, source, destination):
333 source = Path(source).RelPath(self.path)
334 destination = Path(destination).RelPath(self.path)
335 self.SafeExecute(["mv", source, destination])
336
337 def DeleteBranch(self, branch_name):
338 if branch_name not in self.branches:
339 return
340 self.SafeExecute("checkout -f m/master")
341 self.SafeExecute("branch -D " + branch_name)
342
343 def CreateBranch(self, branch_name, tracking=None):
344 if tracking:
345 self.SafeExecute("checkout -b " + branch_name + " " + tracking)
346 else:
347 self.SafeExecute("checkout -b " + branch_name)
348
349 def Checkout(self, name, force=False):
350 cmd = "checkout " + name
351 if force:
352 cmd = cmd + " -f"
353 self.SafeExecute(cmd)
354
355 def Stash(self):
356 self.SafeExecute("stash")
357
358 def Add(self, filepath):
359 self.SafeExecute(["add", Path(filepath).RelPath(self.path)])
360
361 def Commit(self, message, ammend=False, all_changes=False,
362 ammend_if_diverged=False):
363 cmd = "commit"
364 if ammend or (ammend_if_diverged and self.diverged):
365 existing = self.SafeExecute("log --format=%B -n 1")
366 regex = "Change-Id: I[a-f0-9]+"
367 match = re.search(regex, existing)
368 if match:
369 message = message + "\n" + match.group(0)
370 cmd = cmd + " --amend"
371 if all_changes:
372 cmd = cmd + " -a"
373 cmd = cmd + " -m \"" + message + "\""
374 self.SafeExecute(cmd)
375
376 def Upload(self):
377 result = self.SafeExecute("push %s HEAD:refs/for/master" % self.remote)
Dennis Kempin76f89c72014-05-15 15:53:41 -0700378 if "error" in result or "Error" in result:
379 raise Exception("Failed to upload:\n{}".format(result))
Dennis Kempin58d9a742014-05-08 18:34:59 -0700380 regex = "https://[a-zA-Z0-9\\-\\./]+"
381 match = re.search(regex, result)
382 if match:
383 return match.group(0)
384 return None
385
386class AskUser(object):
387 """Various static methods to ask for user input."""
388
389 @staticmethod
390 def Text(message=None, validate=None, default=None):
391 """Ask user to input a text."""
392 while True:
393 if message:
Harry Cutts0edf1572020-01-21 15:42:10 -0800394 print(message)
Dennis Kempin58d9a742014-05-08 18:34:59 -0700395 reply = sys.stdin.readline().strip()
396 if len(reply) == 0:
397 return default
398 if validate:
399 try:
400 reply = validate(reply)
401 except Exception, e:
Harry Cutts0edf1572020-01-21 15:42:10 -0800402 print(e)
Dennis Kempin58d9a742014-05-08 18:34:59 -0700403 continue
404 return reply
405
406 @staticmethod
407 def Continue():
408 """Ask user to press enter to continue."""
409 AskUser.Text("Press enter to continue.")
410
411 @staticmethod
412 def YesNo(message, default=False):
413 """Ask user to reply with yes or no."""
414 if default is True:
Harry Cutts0edf1572020-01-21 15:42:10 -0800415 print(message, "[Y/n]")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700416 else:
Harry Cutts0edf1572020-01-21 15:42:10 -0800417 print(message, "[y/N]")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700418 choice = sys.stdin.readline().strip()
419 if choice in ("y", "Y", "Yes", "yes"):
420 return True
421 elif choice in ("n", "N", "No", "no"):
422 return False
423 return default
424
425 @staticmethod
426 def Select(choices, msg, allow_none=False):
427 """Ask user to make a single choice from a list.
428
429 Returns the index of the item selected by the user.
430
431 allow_none allows the user to make an empty selection, which
432 will return None.
433
434 Note: Both None and the index 0 will evaluate to boolean False,
435 check the value explicitly with "if selection is None".
436 """
437 selection = AskUser.__Select(choices, msg, False, allow_none)
438 if len(selection) == 0:
439 return None
440 return selection[0]
441
442 @staticmethod
443 def SelectMulti(choices, msg, allow_none=False):
444 """Ask user to make a multiple choice from a list.
445
446 Returns the list of indices selected by the user.
447
448 allow_none allows the user to make an empty selection, which
449 will return an empty list.
450 """
451 return AskUser.__Select(choices, msg, True, allow_none)
452
453 @staticmethod
454 def __Select(choices, msg, allow_multi=False, allow_none=False):
455 # skip selection if there is only one option
456 if len(choices) == 1 and not allow_none:
457 return [0]
458
459 # repeat until user made a valid selection
460 while True:
461 # get user input (displays index + 1).
Harry Cutts0edf1572020-01-21 15:42:10 -0800462 print(msg)
Dennis Kempin58d9a742014-05-08 18:34:59 -0700463 for i, item in enumerate(choices):
Harry Cutts0edf1572020-01-21 15:42:10 -0800464 print(" ", str(i + 1) + ":", item)
Dennis Kempin58d9a742014-05-08 18:34:59 -0700465 if allow_multi:
Harry Cutts0edf1572020-01-21 15:42:10 -0800466 print("(Separate multiple selections with spaces)")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700467 if allow_none:
Harry Cutts0edf1572020-01-21 15:42:10 -0800468 print("(Press enter to select none)")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700469
470 sys.stdout.write('> ')
471 sys.stdout.flush()
472 selection = sys.stdin.readline()
473
474 if len(selection.strip()) == 0 and allow_none:
475 return []
476
477 if allow_multi:
478 selections = selection.split(" ")
479 else:
480 selections = [selection]
481
482 # validates single input value
483 def ProcessSelection(selection):
484 try:
485 idx = int(selection) - 1
486 if idx < 0 or idx >= len(choices):
Harry Cutts0edf1572020-01-21 15:42:10 -0800487 print("Number out of range")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700488 return None
489 except ValueError:
Harry Cutts0edf1572020-01-21 15:42:10 -0800490 print("Not a number")
Dennis Kempin58d9a742014-05-08 18:34:59 -0700491 return None
492 return idx
493
494 # validate list of values
495 selections = [ProcessSelection(s) for s in selections]
496 if None not in selections:
497 return selections
498