OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 |
| 3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. |
| 6 |
| 7 """This module uprevs a given package's ebuild to the next revision.""" |
| 8 |
| 9 |
| 10 import fileinput |
| 11 import gflags |
| 12 import os |
| 13 import re |
| 14 import shutil |
| 15 import subprocess |
| 16 import sys |
| 17 |
| 18 # TODO(sosa): Refactor Die into common library. |
| 19 sys.path.append(os.path.dirname(__file__)) |
| 20 import generate_test_report |
| 21 |
| 22 |
| 23 gflags.DEFINE_string('board', 'x86-generic', |
| 24 'Board for which the package belongs.', short_name='b') |
| 25 gflags.DEFINE_string('commit_ids', '', |
| 26 '''Optional list of commit ids for each package. |
| 27 This list must either be empty or have the same length as |
| 28 the packages list. If not set all rev'd ebuilds will have |
| 29 empty commit id's.''', |
| 30 short_name='i') |
| 31 gflags.DEFINE_string('packages', '', |
| 32 'Space separated list of packages to mark as stable.', |
| 33 short_name='p') |
| 34 gflags.DEFINE_boolean('push', False, |
| 35 'Creates, commits and pushes the stable ebuild.') |
| 36 gflags.DEFINE_boolean('verbose', False, |
| 37 'Prints out verbose information about what is going on.', |
| 38 short_name='v') |
| 39 |
| 40 |
| 41 # TODO(sosa): Remove hard-coding of overlays directory once there is a better |
| 42 # way. |
| 43 _CHROMIUMOS_OVERLAYS_DIRECTORY = \ |
| 44 '%s/trunk/src/third_party/chromiumos-overlay' % os.environ['HOME'] |
| 45 |
| 46 # Takes two strings, package_name and commit_id. |
| 47 _GIT_COMMIT_MESSAGE = \ |
| 48 'Marking 9999 ebuild for %s with commit %s as stable.' |
| 49 |
| 50 |
| 51 # ======================= Global Helper Functions ======================== |
| 52 |
| 53 |
| 54 def _Print(message): |
| 55 """Verbose print function.""" |
| 56 if gflags.FLAGS.verbose: |
| 57 print message |
| 58 |
| 59 |
| 60 def _CheckSaneArguments(package_list, commit_id_list): |
| 61 """Checks to make sure the flags are sane. Dies if arguments are not sane""" |
| 62 if not gflags.FLAGS.packages: |
| 63 generate_test_report.Die('Please specify at least one package') |
| 64 if not gflags.FLAGS.board: |
| 65 generate_test_report.Die('Please specify a board') |
| 66 if commit_id_list and (len(package_list) != len(commit_id_list)): |
| 67 print commit_id_list |
| 68 print len(commit_id_list) |
| 69 generate_test_report.Die( |
| 70 'Package list is not the same length as the commit id list') |
| 71 |
| 72 |
| 73 def _PrintUsageAndDie(): |
| 74 """Prints the usage and returns an error exit code.""" |
| 75 generate_test_report.Die('Usage: %s ARGS\n%s' % (sys.argv[0], gflags.FLAGS)) |
| 76 |
| 77 |
| 78 def _RunCommand(command): |
| 79 """Runs a shell command and returns stdout back to caller.""" |
| 80 _Print(' + %s' % command) |
| 81 proc_handle = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) |
| 82 return proc_handle.communicate()[0] |
| 83 |
| 84 |
| 85 # ======================= End Global Helper Functions ======================== |
| 86 |
| 87 |
| 88 class _GitBranch(object): |
| 89 """Wrapper class for a git branch.""" |
| 90 |
| 91 def __init__(self, branch_name): |
| 92 """Sets up variables but does not create the branch.""" |
| 93 self.branch_name = branch_name |
| 94 self._cleaned_up = False |
| 95 |
| 96 def __del__(self): |
| 97 """Ensures we're checked back out to the master branch.""" |
| 98 if not self._cleaned_up: |
| 99 self.CleanUp() |
| 100 |
| 101 def CreateBranch(self): |
| 102 """Creates a new git branch or replaces an existing one.""" |
| 103 if self.Exists(): |
| 104 self.Delete() |
| 105 self._Checkout(self.branch_name) |
| 106 |
| 107 def CleanUp(self): |
| 108 """Does a git checkout back to the master branch.""" |
| 109 self._Checkout('master', create=False) |
| 110 self._cleaned_up = True |
| 111 |
| 112 def _Checkout(self, target, create=True): |
| 113 """Function used internally to create and move between branches.""" |
| 114 if create: |
| 115 git_cmd = 'git checkout -b %s origin' % target |
| 116 else: |
| 117 git_cmd = 'git checkout %s' % target |
| 118 _RunCommand(git_cmd) |
| 119 |
| 120 def Exists(self): |
| 121 """Returns True if the branch exists.""" |
| 122 branch_cmd = 'git branch' |
| 123 branches = _RunCommand(branch_cmd) |
| 124 return self.branch_name in branches.split() |
| 125 |
| 126 def Delete(self): |
| 127 """Deletes the branch and returns the user to the master branch. |
| 128 |
| 129 Returns True on success. |
| 130 """ |
| 131 self._Checkout('master', create=False) |
| 132 delete_cmd = 'git branch -D %s' % self.branch_name |
| 133 _RunCommand(delete_cmd) |
| 134 |
| 135 |
| 136 class _EBuild(object): |
| 137 """Wrapper class for an ebuild.""" |
| 138 |
| 139 def __init__(self, package, commit_id=None): |
| 140 """Initializes all data about an ebuild. |
| 141 |
| 142 Uses equery to find the ebuild path and sets data about an ebuild for |
| 143 easy reference. |
| 144 """ |
| 145 self.package = package |
| 146 self.ebuild_path = self._FindEBuildPath(package) |
| 147 (self.ebuild_path_no_revision, |
| 148 self.ebuild_path_no_version, |
| 149 self.current_revision) = self._ParseEBuildPath(self.ebuild_path) |
| 150 self.commit_id = commit_id |
| 151 |
| 152 @classmethod |
| 153 def _FindEBuildPath(cls, package): |
| 154 """Static method that returns the full path of an ebuild.""" |
| 155 _Print('Looking for unstable ebuild for %s' % package) |
| 156 equery_cmd = 'equery-%s which %s 2> /dev/null' \ |
| 157 % (gflags.FLAGS.board, package) |
| 158 path = _RunCommand(equery_cmd) |
| 159 if path: |
| 160 _Print('Unstable ebuild found at %s' % path) |
| 161 return path |
| 162 |
| 163 @classmethod |
| 164 def _ParseEBuildPath(cls, ebuild_path): |
| 165 """Static method that parses the path of an ebuild |
| 166 |
| 167 Returns a tuple containing the (ebuild path without the revision |
| 168 string, without the version string, and the current revision number for |
| 169 the ebuild). |
| 170 """ |
| 171 # Get the ebuild name without the revision string. |
| 172 (ebuild_no_rev, _, rev_string) = ebuild_path.rpartition('-') |
| 173 |
| 174 # Verify the revision string starts with the revision character. |
| 175 if rev_string.startswith('r'): |
| 176 # Get the ebuild name without the revision and version strings. |
| 177 ebuild_no_version = ebuild_no_rev.rpartition('-')[0] |
| 178 rev_string = rev_string[1:].rpartition('.ebuild')[0] |
| 179 else: |
| 180 # Has no revision so we stripped the version number instead. |
| 181 ebuild_no_version = ebuild_no_rev |
| 182 ebuild_no_rev = ebuild_path.rpartition('.ebuild')[0] |
| 183 rev_string = "0" |
| 184 revision = int(rev_string) |
| 185 return (ebuild_no_rev, ebuild_no_version, revision) |
| 186 |
| 187 |
| 188 class EBuildStableMarker(object): |
| 189 """Class that revs the ebuild and commits locally or pushes the change.""" |
| 190 |
| 191 def __init__(self, ebuild): |
| 192 self._ebuild = ebuild |
| 193 |
| 194 def RevEBuild(self, commit_id="", redirect_file=None): |
| 195 """Revs an ebuild given the git commit id. |
| 196 |
| 197 By default this class overwrites a new ebuild given the normal |
| 198 ebuild rev'ing logic. However, a user can specify a redirect_file |
| 199 to redirect the new stable ebuild to another file. |
| 200 |
| 201 Args: |
| 202 commit_id: String corresponding to the commit hash of the developer |
| 203 package to rev. |
| 204 redirect_file: Optional file to write the new ebuild. By default |
| 205 it is written using the standard rev'ing logic. This file must be |
| 206 opened and closed by the caller. |
| 207 |
| 208 Raises: |
| 209 OSError: Error occurred while creating a new ebuild. |
| 210 IOError: Error occurred while writing to the new revved ebuild file. |
| 211 """ |
| 212 # TODO(sosa): Change to a check. |
| 213 if not self._ebuild: |
| 214 generate_test_report.Die('Invalid ebuild given to EBuildStableMarker') |
| 215 |
| 216 new_ebuild_path = '%s-r%d.ebuild' % (self._ebuild.ebuild_path_no_revision, |
| 217 self._ebuild.current_revision + 1) |
| 218 |
| 219 _Print('Creating new stable ebuild %s' % new_ebuild_path) |
| 220 shutil.copyfile('%s-9999.ebuild' % self._ebuild.ebuild_path_no_version, |
| 221 new_ebuild_path) |
| 222 |
| 223 for line in fileinput.input(new_ebuild_path, inplace=1): |
| 224 # Has to be done here to get changes to sys.stdout from fileinput.input. |
| 225 if not redirect_file: |
| 226 redirect_file = sys.stdout |
| 227 if line.startswith('KEYWORDS'): |
| 228 # Actually mark this file as stable by removing ~'s. |
| 229 redirect_file.write(line.replace("~", "")) |
| 230 elif line.startswith('EAPI'): |
| 231 # Always add new commit_id after EAPI definition. |
| 232 redirect_file.write(line) |
| 233 redirect_file.write('EGIT_COMMIT="%s"' % commit_id) |
| 234 elif not line.startswith('EGIT_COMMIT'): |
| 235 # Skip old EGIT_COMMIT definition. |
| 236 redirect_file.write(line) |
| 237 fileinput.close() |
| 238 |
| 239 _Print('Adding new stable ebuild to git') |
| 240 _RunCommand('git add %s' % new_ebuild_path) |
| 241 |
| 242 _Print('Removing old ebuild from git') |
| 243 _RunCommand('git rm %s' % self._ebuild.ebuild_path) |
| 244 |
| 245 def CommitChange(self, message): |
| 246 """Commits current changes in git locally. |
| 247 |
| 248 This method will take any changes from invocations to RevEBuild |
| 249 and commits them locally in the git repository that contains os.pwd. |
| 250 |
| 251 Args: |
| 252 message: the commit string to write when committing to git. |
| 253 |
| 254 Raises: |
| 255 OSError: Error occurred while committing. |
| 256 """ |
| 257 _Print('Committing changes for %s with commit message %s' % \ |
| 258 (self._ebuild.package, message)) |
| 259 git_commit_cmd = 'git commit -am "%s"' % message |
| 260 _RunCommand(git_commit_cmd) |
| 261 |
| 262 # TODO(sosa): This doesn't work yet. Want to directly push without a prompt. |
| 263 def PushChange(self): |
| 264 """Pushes changes to the git repository. |
| 265 |
| 266 Pushes locals commits from calls to CommitChange to the remote git |
| 267 repository specified by os.pwd. |
| 268 |
| 269 Raises: |
| 270 OSError: Error occurred while pushing. |
| 271 """ |
| 272 print 'Push currently not implemented' |
| 273 # TODO(sosa): Un-comment once PushChange works. |
| 274 # _Print('Pushing changes for %s' % self._ebuild.package) |
| 275 # git_commit_cmd = 'git push' |
| 276 # _RunCommand(git_commit_cmd) |
| 277 |
| 278 |
| 279 def main(argv): |
| 280 try: |
| 281 argv = gflags.FLAGS(argv) |
| 282 except gflags.FlagsError: |
| 283 _PrintUsageAndDie() |
| 284 |
| 285 package_list = gflags.FLAGS.packages.split(' ') |
| 286 if gflags.FLAGS.commit_ids: |
| 287 commit_id_list = gflags.FLAGS.commit_ids.split(' ') |
| 288 else: |
| 289 commit_id_list = None |
| 290 _CheckSaneArguments(package_list, commit_id_list) |
| 291 |
| 292 pwd = os.curdir |
| 293 os.chdir(_CHROMIUMOS_OVERLAYS_DIRECTORY) |
| 294 |
| 295 work_branch = _GitBranch('stabilizing_branch') |
| 296 work_branch.CreateBranch() |
| 297 if not work_branch.Exists(): |
| 298 generate_test_report.Die('Unable to create stabilizing branch') |
| 299 index = 0 |
| 300 try: |
| 301 for index in range(len(package_list)): |
| 302 # Gather the package and optional commit id to work on. |
| 303 package = package_list[index] |
| 304 commit_id = "" |
| 305 if commit_id_list: |
| 306 commit_id = commit_id_list[index] |
| 307 |
| 308 _Print('Working on %s' % package) |
| 309 worker = EBuildStableMarker(_EBuild(package, commit_id)) |
| 310 worker.RevEBuild(commit_id) |
| 311 worker.CommitChange(_GIT_COMMIT_MESSAGE % (package, commit_id)) |
| 312 if gflags.FLAGS.push: |
| 313 worker.PushChange() |
| 314 |
| 315 except (OSError, IOError): |
| 316 print 'An exception occurred %s' % sys.exc_info()[0] |
| 317 print 'Only the following packages were revved: %s' % package_list[:index] |
| 318 print '''Note you will have to go into the chromiumos-overlay directory and |
| 319 reset the git repo yourself. |
| 320 ''' |
| 321 finally: |
| 322 # Always run the last two cleanup functions. |
| 323 work_branch.CleanUp() |
| 324 os.chdir(pwd) |
| 325 |
| 326 |
| 327 if __name__ == '__main__': |
| 328 main(sys.argv) |
| 329 |
OLD | NEW |