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