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