1#!/usr/bin/env python 2# Copyright 2013 the V8 project authors. All rights reserved. 3# Redistribution and use in source and binary forms, with or without 4# modification, are permitted provided that the following conditions are 5# met: 6# 7# * Redistributions of source code must retain the above copyright 8# notice, this list of conditions and the following disclaimer. 9# * Redistributions in binary form must reproduce the above 10# copyright notice, this list of conditions and the following 11# disclaimer in the documentation and/or other materials provided 12# with the distribution. 13# * Neither the name of Google Inc. nor the names of its 14# contributors may be used to endorse or promote products derived 15# from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29import argparse 30import datetime 31import httplib 32import glob 33import imp 34import json 35import os 36import re 37import shutil 38import subprocess 39import sys 40import textwrap 41import time 42import urllib 43import urllib2 44 45from git_recipes import GitRecipesMixin 46from git_recipes import GitFailedException 47 48CHANGELOG_FILE = "ChangeLog" 49DAY_IN_SECONDS = 24 * 60 * 60 50PUSH_MSG_GIT_RE = re.compile(r".* \(based on (?P<git_rev>[a-fA-F0-9]+)\)$") 51PUSH_MSG_NEW_RE = re.compile(r"^Version \d+\.\d+\.\d+$") 52VERSION_FILE = os.path.join("include", "v8-version.h") 53 54# V8 base directory. 55V8_BASE = os.path.dirname( 56 os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 57 58 59def TextToFile(text, file_name): 60 with open(file_name, "w") as f: 61 f.write(text) 62 63 64def AppendToFile(text, file_name): 65 with open(file_name, "a") as f: 66 f.write(text) 67 68 69def LinesInFile(file_name): 70 with open(file_name) as f: 71 for line in f: 72 yield line 73 74 75def FileToText(file_name): 76 with open(file_name) as f: 77 return f.read() 78 79 80def MSub(rexp, replacement, text): 81 return re.sub(rexp, replacement, text, flags=re.MULTILINE) 82 83 84def Fill80(line): 85 # Replace tabs and remove surrounding space. 86 line = re.sub(r"\t", r" ", line.strip()) 87 88 # Format with 8 characters indentation and line width 80. 89 return textwrap.fill(line, width=80, initial_indent=" ", 90 subsequent_indent=" ") 91 92 93def MakeComment(text): 94 return MSub(r"^( ?)", "#", text) 95 96 97def StripComments(text): 98 # Use split not splitlines to keep terminal newlines. 99 return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n"))) 100 101 102def MakeChangeLogBody(commit_messages, auto_format=False): 103 result = "" 104 added_titles = set() 105 for (title, body, author) in commit_messages: 106 # TODO(machenbach): Better check for reverts. A revert should remove the 107 # original CL from the actual log entry. 108 title = title.strip() 109 if auto_format: 110 # Only add commits that set the LOG flag correctly. 111 log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:(?:Y(?:ES)?)|TRUE)" 112 if not re.search(log_exp, body, flags=re.I | re.M): 113 continue 114 # Never include reverts. 115 if title.startswith("Revert "): 116 continue 117 # Don't include duplicates. 118 if title in added_titles: 119 continue 120 121 # Add and format the commit's title and bug reference. Move dot to the end. 122 added_titles.add(title) 123 raw_title = re.sub(r"(\.|\?|!)$", "", title) 124 bug_reference = MakeChangeLogBugReference(body) 125 space = " " if bug_reference else "" 126 result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference)) 127 128 # Append the commit's author for reference if not in auto-format mode. 129 if not auto_format: 130 result += "%s\n" % Fill80("(%s)" % author.strip()) 131 132 result += "\n" 133 return result 134 135 136def MakeChangeLogBugReference(body): 137 """Grep for "BUG=xxxx" lines in the commit message and convert them to 138 "(issue xxxx)". 139 """ 140 crbugs = [] 141 v8bugs = [] 142 143 def AddIssues(text): 144 ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip()) 145 if not ref: 146 return 147 for bug in ref.group(1).split(","): 148 bug = bug.strip() 149 match = re.match(r"^v8:(\d+)$", bug) 150 if match: v8bugs.append(int(match.group(1))) 151 else: 152 match = re.match(r"^(?:chromium:)?(\d+)$", bug) 153 if match: crbugs.append(int(match.group(1))) 154 155 # Add issues to crbugs and v8bugs. 156 map(AddIssues, body.splitlines()) 157 158 # Filter duplicates, sort, stringify. 159 crbugs = map(str, sorted(set(crbugs))) 160 v8bugs = map(str, sorted(set(v8bugs))) 161 162 bug_groups = [] 163 def FormatIssues(prefix, bugs): 164 if len(bugs) > 0: 165 plural = "s" if len(bugs) > 1 else "" 166 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs))) 167 168 FormatIssues("", v8bugs) 169 FormatIssues("Chromium ", crbugs) 170 171 if len(bug_groups) > 0: 172 return "(%s)" % ", ".join(bug_groups) 173 else: 174 return "" 175 176 177def SortingKey(version): 178 """Key for sorting version number strings: '3.11' > '3.2.1.1'""" 179 version_keys = map(int, version.split(".")) 180 # Fill up to full version numbers to normalize comparison. 181 while len(version_keys) < 4: # pragma: no cover 182 version_keys.append(0) 183 # Fill digits. 184 return ".".join(map("{0:04d}".format, version_keys)) 185 186 187# Some commands don't like the pipe, e.g. calling vi from within the script or 188# from subscripts like git cl upload. 189def Command(cmd, args="", prefix="", pipe=True, cwd=None): 190 cwd = cwd or os.getcwd() 191 # TODO(machenbach): Use timeout. 192 cmd_line = "%s %s %s" % (prefix, cmd, args) 193 print "Command: %s" % cmd_line 194 print "in %s" % cwd 195 sys.stdout.flush() 196 try: 197 if pipe: 198 return subprocess.check_output(cmd_line, shell=True, cwd=cwd) 199 else: 200 return subprocess.check_call(cmd_line, shell=True, cwd=cwd) 201 except subprocess.CalledProcessError: 202 return None 203 finally: 204 sys.stdout.flush() 205 sys.stderr.flush() 206 207 208def SanitizeVersionTag(tag): 209 version_without_prefix = re.compile(r"^\d+\.\d+\.\d+(?:\.\d+)?$") 210 version_with_prefix = re.compile(r"^tags\/\d+\.\d+\.\d+(?:\.\d+)?$") 211 212 if version_without_prefix.match(tag): 213 return tag 214 elif version_with_prefix.match(tag): 215 return tag[len("tags/"):] 216 else: 217 return None 218 219 220def NormalizeVersionTags(version_tags): 221 normalized_version_tags = [] 222 223 # Remove tags/ prefix because of packed refs. 224 for current_tag in version_tags: 225 version_tag = SanitizeVersionTag(current_tag) 226 if version_tag != None: 227 normalized_version_tags.append(version_tag) 228 229 return normalized_version_tags 230 231 232# Wrapper for side effects. 233class SideEffectHandler(object): # pragma: no cover 234 def Call(self, fun, *args, **kwargs): 235 return fun(*args, **kwargs) 236 237 def Command(self, cmd, args="", prefix="", pipe=True, cwd=None): 238 return Command(cmd, args, prefix, pipe, cwd=cwd) 239 240 def ReadLine(self): 241 return sys.stdin.readline().strip() 242 243 def ReadURL(self, url, params=None): 244 # pylint: disable=E1121 245 url_fh = urllib2.urlopen(url, params, 60) 246 try: 247 return url_fh.read() 248 finally: 249 url_fh.close() 250 251 def ReadClusterFuzzAPI(self, api_key, **params): 252 params["api_key"] = api_key.strip() 253 params = urllib.urlencode(params) 254 255 headers = {"Content-type": "application/x-www-form-urlencoded"} 256 257 conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com") 258 conn.request("POST", "/_api/", params, headers) 259 260 response = conn.getresponse() 261 data = response.read() 262 263 try: 264 return json.loads(data) 265 except: 266 print data 267 print "ERROR: Could not read response. Is your key valid?" 268 raise 269 270 def Sleep(self, seconds): 271 time.sleep(seconds) 272 273 def GetDate(self): 274 return datetime.date.today().strftime("%Y-%m-%d") 275 276 def GetUTCStamp(self): 277 return time.mktime(datetime.datetime.utcnow().timetuple()) 278 279DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() 280 281 282class NoRetryException(Exception): 283 pass 284 285 286class VCInterface(object): 287 def InjectStep(self, step): 288 self.step=step 289 290 def Pull(self): 291 raise NotImplementedError() 292 293 def Fetch(self): 294 raise NotImplementedError() 295 296 def GetTags(self): 297 raise NotImplementedError() 298 299 def GetBranches(self): 300 raise NotImplementedError() 301 302 def MasterBranch(self): 303 raise NotImplementedError() 304 305 def CandidateBranch(self): 306 raise NotImplementedError() 307 308 def RemoteMasterBranch(self): 309 raise NotImplementedError() 310 311 def RemoteCandidateBranch(self): 312 raise NotImplementedError() 313 314 def RemoteBranch(self, name): 315 raise NotImplementedError() 316 317 def CLLand(self): 318 raise NotImplementedError() 319 320 def Tag(self, tag, remote, message): 321 """Sets a tag for the current commit. 322 323 Assumptions: The commit already landed and the commit message is unique. 324 """ 325 raise NotImplementedError() 326 327 328class GitInterface(VCInterface): 329 def Pull(self): 330 self.step.GitPull() 331 332 def Fetch(self): 333 self.step.Git("fetch") 334 335 def GetTags(self): 336 return self.step.Git("tag").strip().splitlines() 337 338 def GetBranches(self): 339 # Get relevant remote branches, e.g. "branch-heads/3.25". 340 branches = filter( 341 lambda s: re.match(r"^branch\-heads/\d+\.\d+$", s), 342 self.step.GitRemotes()) 343 # Remove 'branch-heads/' prefix. 344 return map(lambda s: s[13:], branches) 345 346 def MasterBranch(self): 347 return "master" 348 349 def CandidateBranch(self): 350 return "candidates" 351 352 def RemoteMasterBranch(self): 353 return "origin/master" 354 355 def RemoteCandidateBranch(self): 356 return "origin/candidates" 357 358 def RemoteBranch(self, name): 359 # Assume that if someone "fully qualified" the ref, they know what they 360 # want. 361 if name.startswith('refs/'): 362 return name 363 if name in ["candidates", "master"]: 364 return "refs/remotes/origin/%s" % name 365 try: 366 # Check if branch is in heads. 367 if self.step.Git("show-ref refs/remotes/origin/%s" % name).strip(): 368 return "refs/remotes/origin/%s" % name 369 except GitFailedException: 370 pass 371 try: 372 # Check if branch is in branch-heads. 373 if self.step.Git("show-ref refs/remotes/branch-heads/%s" % name).strip(): 374 return "refs/remotes/branch-heads/%s" % name 375 except GitFailedException: 376 pass 377 self.Die("Can't find remote of %s" % name) 378 379 def Tag(self, tag, remote, message): 380 # Wait for the commit to appear. Assumes unique commit message titles (this 381 # is the case for all automated merge and push commits - also no title is 382 # the prefix of another title). 383 commit = None 384 for wait_interval in [3, 7, 15, 35, 45, 60]: 385 self.step.Git("fetch") 386 commit = self.step.GitLog(n=1, format="%H", grep=message, branch=remote) 387 if commit: 388 break 389 print("The commit has not replicated to git. Waiting for %s seconds." % 390 wait_interval) 391 self.step._side_effect_handler.Sleep(wait_interval) 392 else: 393 self.step.Die("Couldn't determine commit for setting the tag. Maybe the " 394 "git updater is lagging behind?") 395 396 self.step.Git("tag %s %s" % (tag, commit)) 397 self.step.Git("push origin %s" % tag) 398 399 def CLLand(self): 400 self.step.GitCLLand() 401 402 403class Step(GitRecipesMixin): 404 def __init__(self, text, number, config, state, options, handler): 405 self._text = text 406 self._number = number 407 self._config = config 408 self._state = state 409 self._options = options 410 self._side_effect_handler = handler 411 self.vc = GitInterface() 412 self.vc.InjectStep(self) 413 414 # The testing configuration might set a different default cwd. 415 self.default_cwd = (self._config.get("DEFAULT_CWD") or 416 os.path.join(self._options.work_dir, "v8")) 417 418 assert self._number >= 0 419 assert self._config is not None 420 assert self._state is not None 421 assert self._side_effect_handler is not None 422 423 def __getitem__(self, key): 424 # Convenience method to allow direct [] access on step classes for 425 # manipulating the backed state dict. 426 return self._state.get(key) 427 428 def __setitem__(self, key, value): 429 # Convenience method to allow direct [] access on step classes for 430 # manipulating the backed state dict. 431 self._state[key] = value 432 433 def Config(self, key): 434 return self._config[key] 435 436 def Run(self): 437 # Restore state. 438 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"] 439 if not self._state and os.path.exists(state_file): 440 self._state.update(json.loads(FileToText(state_file))) 441 442 print ">>> Step %d: %s" % (self._number, self._text) 443 try: 444 return self.RunStep() 445 finally: 446 # Persist state. 447 TextToFile(json.dumps(self._state), state_file) 448 449 def RunStep(self): # pragma: no cover 450 raise NotImplementedError 451 452 def Retry(self, cb, retry_on=None, wait_plan=None): 453 """ Retry a function. 454 Params: 455 cb: The function to retry. 456 retry_on: A callback that takes the result of the function and returns 457 True if the function should be retried. A function throwing an 458 exception is always retried. 459 wait_plan: A list of waiting delays between retries in seconds. The 460 maximum number of retries is len(wait_plan). 461 """ 462 retry_on = retry_on or (lambda x: False) 463 wait_plan = list(wait_plan or []) 464 wait_plan.reverse() 465 while True: 466 got_exception = False 467 try: 468 result = cb() 469 except NoRetryException as e: 470 raise e 471 except Exception as e: 472 got_exception = e 473 if got_exception or retry_on(result): 474 if not wait_plan: # pragma: no cover 475 raise Exception("Retried too often. Giving up. Reason: %s" % 476 str(got_exception)) 477 wait_time = wait_plan.pop() 478 print "Waiting for %f seconds." % wait_time 479 self._side_effect_handler.Sleep(wait_time) 480 print "Retrying..." 481 else: 482 return result 483 484 def ReadLine(self, default=None): 485 # Don't prompt in forced mode. 486 if self._options.force_readline_defaults and default is not None: 487 print "%s (forced)" % default 488 return default 489 else: 490 return self._side_effect_handler.ReadLine() 491 492 def Command(self, name, args, cwd=None): 493 cmd = lambda: self._side_effect_handler.Command( 494 name, args, "", True, cwd=cwd or self.default_cwd) 495 return self.Retry(cmd, None, [5]) 496 497 def Git(self, args="", prefix="", pipe=True, retry_on=None, cwd=None): 498 cmd = lambda: self._side_effect_handler.Command( 499 "git", args, prefix, pipe, cwd=cwd or self.default_cwd) 500 result = self.Retry(cmd, retry_on, [5, 30]) 501 if result is None: 502 raise GitFailedException("'git %s' failed." % args) 503 return result 504 505 def Editor(self, args): 506 if self._options.requires_editor: 507 return self._side_effect_handler.Command( 508 os.environ["EDITOR"], 509 args, 510 pipe=False, 511 cwd=self.default_cwd) 512 513 def ReadURL(self, url, params=None, retry_on=None, wait_plan=None): 514 wait_plan = wait_plan or [3, 60, 600] 515 cmd = lambda: self._side_effect_handler.ReadURL(url, params) 516 return self.Retry(cmd, retry_on, wait_plan) 517 518 def GetDate(self): 519 return self._side_effect_handler.GetDate() 520 521 def Die(self, msg=""): 522 if msg != "": 523 print "Error: %s" % msg 524 print "Exiting" 525 raise Exception(msg) 526 527 def DieNoManualMode(self, msg=""): 528 if not self._options.manual: # pragma: no cover 529 msg = msg or "Only available in manual mode." 530 self.Die(msg) 531 532 def Confirm(self, msg): 533 print "%s [Y/n] " % msg, 534 answer = self.ReadLine(default="Y") 535 return answer == "" or answer == "Y" or answer == "y" 536 537 def DeleteBranch(self, name, cwd=None): 538 for line in self.GitBranch(cwd=cwd).splitlines(): 539 if re.match(r"\*?\s*%s$" % re.escape(name), line): 540 msg = "Branch %s exists, do you want to delete it?" % name 541 if self.Confirm(msg): 542 self.GitDeleteBranch(name, cwd=cwd) 543 print "Branch %s deleted." % name 544 else: 545 msg = "Can't continue. Please delete branch %s and try again." % name 546 self.Die(msg) 547 548 def InitialEnvironmentChecks(self, cwd): 549 # Cancel if this is not a git checkout. 550 if not os.path.exists(os.path.join(cwd, ".git")): # pragma: no cover 551 self.Die("This is not a git checkout, this script won't work for you.") 552 553 # Cancel if EDITOR is unset or not executable. 554 if (self._options.requires_editor and (not os.environ.get("EDITOR") or 555 self.Command( 556 "which", os.environ["EDITOR"]) is None)): # pragma: no cover 557 self.Die("Please set your EDITOR environment variable, you'll need it.") 558 559 def CommonPrepare(self): 560 # Check for a clean workdir. 561 if not self.GitIsWorkdirClean(): # pragma: no cover 562 self.Die("Workspace is not clean. Please commit or undo your changes.") 563 564 # Checkout master in case the script was left on a work branch. 565 self.GitCheckout('origin/master') 566 567 # Fetch unfetched revisions. 568 self.vc.Fetch() 569 570 def PrepareBranch(self): 571 # Delete the branch that will be created later if it exists already. 572 self.DeleteBranch(self._config["BRANCHNAME"]) 573 574 def CommonCleanup(self): 575 self.GitCheckout('origin/master') 576 self.GitDeleteBranch(self._config["BRANCHNAME"]) 577 578 # Clean up all temporary files. 579 for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]): 580 if os.path.isfile(f): 581 os.remove(f) 582 if os.path.isdir(f): 583 shutil.rmtree(f) 584 585 def ReadAndPersistVersion(self, prefix=""): 586 def ReadAndPersist(var_name, def_name): 587 match = re.match(r"^#define %s\s+(\d*)" % def_name, line) 588 if match: 589 value = match.group(1) 590 self["%s%s" % (prefix, var_name)] = value 591 for line in LinesInFile(os.path.join(self.default_cwd, VERSION_FILE)): 592 for (var_name, def_name) in [("major", "V8_MAJOR_VERSION"), 593 ("minor", "V8_MINOR_VERSION"), 594 ("build", "V8_BUILD_NUMBER"), 595 ("patch", "V8_PATCH_LEVEL")]: 596 ReadAndPersist(var_name, def_name) 597 598 def WaitForLGTM(self): 599 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " 600 "your change. (If you need to iterate on the patch or double check " 601 "that it's sane, do so in another shell, but remember to not " 602 "change the headline of the uploaded CL.") 603 answer = "" 604 while answer != "LGTM": 605 print "> ", 606 answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM") 607 if answer != "LGTM": 608 print "That was not 'LGTM'." 609 610 def WaitForResolvingConflicts(self, patch_file): 611 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " 612 "or resolve the conflicts, stage *all* touched files with " 613 "'git add', and type \"RESOLVED<Return>\"") 614 self.DieNoManualMode() 615 answer = "" 616 while answer != "RESOLVED": 617 if answer == "ABORT": 618 self.Die("Applying the patch failed.") 619 if answer != "": 620 print "That was not 'RESOLVED' or 'ABORT'." 621 print "> ", 622 answer = self.ReadLine() 623 624 # Takes a file containing the patch to apply as first argument. 625 def ApplyPatch(self, patch_file, revert=False): 626 try: 627 self.GitApplyPatch(patch_file, revert) 628 except GitFailedException: 629 self.WaitForResolvingConflicts(patch_file) 630 631 def GetVersionTag(self, revision): 632 tag = self.Git("describe --tags %s" % revision).strip() 633 return SanitizeVersionTag(tag) 634 635 def GetRecentReleases(self, max_age): 636 # Make sure tags are fetched. 637 self.Git("fetch origin +refs/tags/*:refs/tags/*") 638 639 # Current timestamp. 640 time_now = int(self._side_effect_handler.GetUTCStamp()) 641 642 # List every tag from a given period. 643 revisions = self.Git("rev-list --max-age=%d --tags" % 644 int(time_now - max_age)).strip() 645 646 # Filter out revisions who's tag is off by one or more commits. 647 return filter(lambda r: self.GetVersionTag(r), revisions.splitlines()) 648 649 def GetLatestVersion(self): 650 # Use cached version if available. 651 if self["latest_version"]: 652 return self["latest_version"] 653 654 # Make sure tags are fetched. 655 self.Git("fetch origin +refs/tags/*:refs/tags/*") 656 657 all_tags = self.vc.GetTags() 658 only_version_tags = NormalizeVersionTags(all_tags) 659 660 version = sorted(only_version_tags, 661 key=SortingKey, reverse=True)[0] 662 self["latest_version"] = version 663 return version 664 665 def GetLatestRelease(self): 666 """The latest release is the git hash of the latest tagged version. 667 668 This revision should be rolled into chromium. 669 """ 670 latest_version = self.GetLatestVersion() 671 672 # The latest release. 673 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version) 674 assert latest_hash 675 return latest_hash 676 677 def GetLatestReleaseBase(self, version=None): 678 """The latest release base is the latest revision that is covered in the 679 last change log file. It doesn't include cherry-picked patches. 680 """ 681 latest_version = version or self.GetLatestVersion() 682 683 # Strip patch level if it exists. 684 latest_version = ".".join(latest_version.split(".")[:3]) 685 686 # The latest release base. 687 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version) 688 assert latest_hash 689 690 title = self.GitLog(n=1, format="%s", git_hash=latest_hash) 691 match = PUSH_MSG_GIT_RE.match(title) 692 if match: 693 # Legacy: In the old process there's one level of indirection. The 694 # version is on the candidates branch and points to the real release 695 # base on master through the commit message. 696 return match.group("git_rev") 697 match = PUSH_MSG_NEW_RE.match(title) 698 if match: 699 # This is a new-style v8 version branched from master. The commit 700 # "latest_hash" is the version-file change. Its parent is the release 701 # base on master. 702 return self.GitLog(n=1, format="%H", git_hash="%s^" % latest_hash) 703 704 self.Die("Unknown latest release: %s" % latest_hash) 705 706 def ArrayToVersion(self, prefix): 707 return ".".join([self[prefix + "major"], 708 self[prefix + "minor"], 709 self[prefix + "build"], 710 self[prefix + "patch"]]) 711 712 def StoreVersion(self, version, prefix): 713 version_parts = version.split(".") 714 if len(version_parts) == 3: 715 version_parts.append("0") 716 major, minor, build, patch = version_parts 717 self[prefix + "major"] = major 718 self[prefix + "minor"] = minor 719 self[prefix + "build"] = build 720 self[prefix + "patch"] = patch 721 722 def SetVersion(self, version_file, prefix): 723 output = "" 724 for line in FileToText(version_file).splitlines(): 725 if line.startswith("#define V8_MAJOR_VERSION"): 726 line = re.sub("\d+$", self[prefix + "major"], line) 727 elif line.startswith("#define V8_MINOR_VERSION"): 728 line = re.sub("\d+$", self[prefix + "minor"], line) 729 elif line.startswith("#define V8_BUILD_NUMBER"): 730 line = re.sub("\d+$", self[prefix + "build"], line) 731 elif line.startswith("#define V8_PATCH_LEVEL"): 732 line = re.sub("\d+$", self[prefix + "patch"], line) 733 elif (self[prefix + "candidate"] and 734 line.startswith("#define V8_IS_CANDIDATE_VERSION")): 735 line = re.sub("\d+$", self[prefix + "candidate"], line) 736 output += "%s\n" % line 737 TextToFile(output, version_file) 738 739 740class BootstrapStep(Step): 741 MESSAGE = "Bootstrapping checkout and state." 742 743 def RunStep(self): 744 # Reserve state entry for json output. 745 self['json_output'] = {} 746 747 if os.path.realpath(self.default_cwd) == os.path.realpath(V8_BASE): 748 self.Die("Can't use v8 checkout with calling script as work checkout.") 749 # Directory containing the working v8 checkout. 750 if not os.path.exists(self._options.work_dir): 751 os.makedirs(self._options.work_dir) 752 if not os.path.exists(self.default_cwd): 753 self.Command("fetch", "v8", cwd=self._options.work_dir) 754 755 756class UploadStep(Step): 757 MESSAGE = "Upload for code review." 758 759 def RunStep(self): 760 if self._options.reviewer: 761 print "Using account %s for review." % self._options.reviewer 762 reviewer = self._options.reviewer 763 else: 764 print "Please enter the email address of a V8 reviewer for your patch: ", 765 self.DieNoManualMode("A reviewer must be specified in forced mode.") 766 reviewer = self.ReadLine() 767 self.GitUpload(reviewer, self._options.author, self._options.force_upload, 768 bypass_hooks=self._options.bypass_upload_hooks, 769 cc=self._options.cc) 770 771 772def MakeStep(step_class=Step, number=0, state=None, config=None, 773 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): 774 # Allow to pass in empty dictionaries. 775 state = state if state is not None else {} 776 config = config if config is not None else {} 777 778 try: 779 message = step_class.MESSAGE 780 except AttributeError: 781 message = step_class.__name__ 782 783 return step_class(message, number=number, config=config, 784 state=state, options=options, 785 handler=side_effect_handler) 786 787 788class ScriptsBase(object): 789 def __init__(self, 790 config=None, 791 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER, 792 state=None): 793 self._config = config or self._Config() 794 self._side_effect_handler = side_effect_handler 795 self._state = state if state is not None else {} 796 797 def _Description(self): 798 return None 799 800 def _PrepareOptions(self, parser): 801 pass 802 803 def _ProcessOptions(self, options): 804 return True 805 806 def _Steps(self): # pragma: no cover 807 raise Exception("Not implemented.") 808 809 def _Config(self): 810 return {} 811 812 def MakeOptions(self, args=None): 813 parser = argparse.ArgumentParser(description=self._Description()) 814 parser.add_argument("-a", "--author", default="", 815 help="The author email used for rietveld.") 816 parser.add_argument("--dry-run", default=False, action="store_true", 817 help="Perform only read-only actions.") 818 parser.add_argument("--json-output", 819 help="File to write results summary to.") 820 parser.add_argument("-r", "--reviewer", default="", 821 help="The account name to be used for reviews.") 822 parser.add_argument("-s", "--step", 823 help="Specify the step where to start work. Default: 0.", 824 default=0, type=int) 825 parser.add_argument("--work-dir", 826 help=("Location where to bootstrap a working v8 " 827 "checkout.")) 828 self._PrepareOptions(parser) 829 830 if args is None: # pragma: no cover 831 options = parser.parse_args() 832 else: 833 options = parser.parse_args(args) 834 835 # Process common options. 836 if options.step < 0: # pragma: no cover 837 print "Bad step number %d" % options.step 838 parser.print_help() 839 return None 840 841 # Defaults for options, common to all scripts. 842 options.manual = getattr(options, "manual", True) 843 options.force = getattr(options, "force", False) 844 options.bypass_upload_hooks = False 845 846 # Derived options. 847 options.requires_editor = not options.force 848 options.wait_for_lgtm = not options.force 849 options.force_readline_defaults = not options.manual 850 options.force_upload = not options.manual 851 852 # Process script specific options. 853 if not self._ProcessOptions(options): 854 parser.print_help() 855 return None 856 857 if not options.work_dir: 858 options.work_dir = "/tmp/v8-release-scripts-work-dir" 859 return options 860 861 def RunSteps(self, step_classes, args=None): 862 options = self.MakeOptions(args) 863 if not options: 864 return 1 865 866 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"] 867 if options.step == 0 and os.path.exists(state_file): 868 os.remove(state_file) 869 870 steps = [] 871 for (number, step_class) in enumerate([BootstrapStep] + step_classes): 872 steps.append(MakeStep(step_class, number, self._state, self._config, 873 options, self._side_effect_handler)) 874 875 try: 876 for step in steps[options.step:]: 877 if step.Run(): 878 return 0 879 finally: 880 if options.json_output: 881 with open(options.json_output, "w") as f: 882 json.dump(self._state['json_output'], f) 883 884 return 0 885 886 def Run(self, args=None): 887 return self.RunSteps(self._Steps(), args) 888