3 # Copyright (c) 2012 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.
7 """This module uprevs a given package's ebuild to the next revision."""
13 from chromite.buildbot import constants
14 from chromite.buildbot import portage_utilities
15 from chromite.lib import cros_build_lib
16 from chromite.lib import git
17 from chromite.lib import osutils
18 from chromite.lib import parallel
21 # Commit message for uprevving Portage packages.
22 _GIT_COMMIT_MESSAGE = 'Marking 9999 ebuild for %s as stable.'
24 # Dictionary of valid commands with usage information.
25 COMMAND_DICTIONARY = {
26 'commit': 'Marks given ebuilds as stable locally',
27 'push': 'Pushes previous marking of ebuilds to remote repo',
31 # ======================= Global Helper Functions ========================
34 def CleanStalePackages(boards, package_atoms):
35 """Cleans up stale package info from a previous build.
38 boards: Boards to clean the packages from.
39 package_atoms: A list of package atoms to unmerge.
42 cros_build_lib.Info('Cleaning up stale packages %s.' % package_atoms)
44 # First unmerge all the packages for a board, then eclean it.
45 # We need these two steps to run in order (unmerge/eclean),
46 # but we can let all the boards run in parallel.
47 def _CleanStalePackages(board):
50 runcmd = cros_build_lib.RunCommand
53 runcmd = cros_build_lib.SudoRunCommand
55 emerge, eclean = 'emerge' + suffix, 'eclean' + suffix
56 if not osutils.FindMissingBinaries([emerge, eclean]):
58 # If nothing was found to be unmerged, emerge will exit(1).
59 result = runcmd([emerge, '-q', '--unmerge'] + package_atoms,
60 extra_env={'CLEAN_DELAY': '0'}, error_code_ok=True)
61 if not result.returncode in (0, 1):
62 raise cros_build_lib.RunCommandError('unexpected error', result)
63 runcmd([eclean, '-d', 'packages'],
64 redirect_stdout=True, redirect_stderr=True)
71 parallel.RunTasksInProcessPool(_CleanStalePackages, tasks)
74 # TODO(build): This code needs to be gutted and rebased to cros_build_lib.
75 def _DoWeHaveLocalCommits(stable_branch, tracking_branch, cwd):
76 """Returns true if there are local commits."""
77 current_branch = git.GetCurrentBranch(cwd)
79 if current_branch != stable_branch:
82 cwd, ['rev-parse', 'HEAD', tracking_branch]).output.split()
83 return output[0] != output[1]
86 def _CheckSaneArguments(command, options):
87 """Checks to make sure the flags are sane. Dies if arguments are not sane."""
88 if not command in COMMAND_DICTIONARY.keys():
89 _PrintUsageAndDie('%s is not a valid command' % command)
90 if not options.packages and command == 'commit' and not options.all:
91 _PrintUsageAndDie('Please specify at least one package')
93 cros_build_lib.AssertInsideChroot()
94 if not os.path.isdir(options.srcroot):
95 _PrintUsageAndDie('srcroot is not a valid path')
96 options.srcroot = os.path.abspath(options.srcroot)
99 def _PrintUsageAndDie(error_message=''):
100 """Prints optional error_message the usage and returns an error exit code."""
101 command_usage = 'Commands: \n'
102 # Add keys and usage information from dictionary.
103 commands = sorted(COMMAND_DICTIONARY.keys())
104 for command in commands:
105 command_usage += ' %s: %s\n' % (command, COMMAND_DICTIONARY[command])
106 commands_str = '|'.join(commands)
107 cros_build_lib.Warning('Usage: %s FLAGS [%s]\n\n%s' % (
108 sys.argv[0], commands_str, command_usage))
110 cros_build_lib.Die(error_message)
115 # ======================= End Global Helper Functions ========================
118 def PushChange(stable_branch, tracking_branch, dryrun, cwd):
119 """Pushes commits in the stable_branch to the remote git repository.
121 Pushes local commits from calls to CommitChange to the remote git
122 repository specified by current working directory. If changes are
123 found to commit, they will be merged to the merge branch and pushed.
124 In that case, the local repository will be left on the merge branch.
127 stable_branch: The local branch with commits we want to push.
128 tracking_branch: The tracking branch of the local branch.
129 dryrun: Use git push --dryrun to emulate a push.
130 cwd: The directory to run commands in.
133 OSError: Error occurred while pushing.
135 if not _DoWeHaveLocalCommits(stable_branch, tracking_branch, cwd):
136 cros_build_lib.Info('No work found to push in %s. Exiting', cwd)
139 # For the commit queue, our local branch may contain commits that were
140 # just tested and pushed during the CommitQueueCompletion stage. Sync
141 # and rebase our local branch on top of the remote commits.
142 remote, push_branch = git.GetTrackingBranch(cwd, for_push=True)
143 git.SyncPushBranch(cwd, remote, push_branch)
145 # Check whether any local changes remain after the sync.
146 if not _DoWeHaveLocalCommits(stable_branch, push_branch, cwd):
147 cros_build_lib.Info('All changes already pushed for %s. Exiting', cwd)
150 # Add a failsafe check here. Only CLs from the 'chrome-bot' user should
151 # be involved here. If any other CLs are found then complain.
152 # In dryruns extra CLs are normal, though, and can be ignored.
153 bad_cl_cmd = ['log', '--format=short', '--perl-regexp',
154 '--author', '^(?!chrome-bot)', '%s..%s' % (
155 push_branch, stable_branch)]
156 bad_cls = git.RunGit(cwd, bad_cl_cmd).output
157 if bad_cls.strip() and not dryrun:
158 cros_build_lib.Error('The Uprev stage found changes from users other'
159 ' than chrome-bot:\n\n%s', bad_cls)
160 raise AssertionError('Unexpected CLs found during uprev stage.')
162 description = git.RunGit(cwd,
163 ['log', '--format=format:%s%n%n%b', '%s..%s' % (
164 push_branch, stable_branch)]).output
165 description = 'Marking set of ebuilds as stable\n\n%s' % description
166 cros_build_lib.Info('For %s, using description %s', cwd, description)
167 git.CreatePushBranch(constants.MERGE_BRANCH, cwd)
168 git.RunGit(cwd, ['merge', '--squash', stable_branch])
169 git.RunGit(cwd, ['commit', '-m', description])
170 git.RunGit(cwd, ['config', 'push.default', 'tracking'])
171 git.PushWithRetry(constants.MERGE_BRANCH, cwd, dryrun=dryrun)
174 class GitBranch(object):
175 """Wrapper class for a git branch."""
177 def __init__(self, branch_name, tracking_branch, cwd):
178 """Sets up variables but does not create the branch.
181 branch_name: The name of the branch.
182 tracking_branch: The associated tracking branch.
183 cwd: The git repository to work in.
185 self.branch_name = branch_name
186 self.tracking_branch = tracking_branch
189 def CreateBranch(self):
192 def Checkout(self, branch=None):
193 """Function used to check out to another GitBranch."""
195 branch = self.branch_name
196 if branch == self.tracking_branch or self.Exists(branch):
197 git_cmd = ['git', 'checkout', '-f', branch]
199 git_cmd = ['repo', 'start', branch, '.']
200 cros_build_lib.RunCommand(git_cmd, print_cmd=False, cwd=self.cwd,
203 def Exists(self, branch=None):
204 """Returns True if the branch exists."""
206 branch = self.branch_name
207 branches = git.RunGit(self.cwd, ['branch']).output
208 return branch in branches.split()
212 parser = optparse.OptionParser('cros_mark_as_stable OPTIONS packages')
213 parser.add_option('--all', action='store_true',
214 help='Mark all packages as stable.')
215 parser.add_option('-b', '--boards', default='',
216 help='Colon-separated list of boards')
217 parser.add_option('--drop_file',
218 help='File to list packages that were revved.')
219 parser.add_option('--dryrun', action='store_true',
220 help='Passes dry-run to git push if pushing a change.')
221 parser.add_option('-o', '--overlays',
222 help='Colon-separated list of overlays to modify.')
223 parser.add_option('-p', '--packages',
224 help='Colon separated list of packages to rev.')
225 parser.add_option('-r', '--srcroot',
226 default=os.path.join(constants.SOURCE_ROOT, 'src'),
227 help='Path to root src directory.')
228 parser.add_option('--verbose', action='store_true',
229 help='Prints out debug info.')
230 (options, args) = parser.parse_args()
232 portage_utilities.EBuild.VERBOSE = options.verbose
235 _PrintUsageAndDie('Must specify a valid command [commit, push]')
240 package_list = options.packages.split(':')
242 _CheckSaneArguments(command, options)
245 for path in options.overlays.split(':'):
246 if not os.path.isdir(path):
247 cros_build_lib.Die('Cannot find overlay: %s' % path)
250 cros_build_lib.Warning('Missing --overlays argument')
252 '%s/private-overlays/chromeos-overlay' % options.srcroot: [],
253 '%s/third_party/chromiumos-overlay' % options.srcroot: []
256 manifest = git.ManifestCheckout.Cached(options.srcroot)
258 if command == 'commit':
259 portage_utilities.BuildEBuildDictionary(overlays, options.all, package_list)
261 # Contains the array of packages we actually revved.
263 new_package_atoms = []
265 # Slight optimization hack: process the chromiumos overlay before any other
266 # cros-workon overlay first so we can do background cache generation in it.
267 # A perfect solution would walk all the overlays, figure out any dependencies
268 # between them (with layout.conf), and then process them in dependency order.
269 # However, this operation isn't slow enough to warrant that level of
270 # complexity, so we'll just special case the main overlay.
272 # Similarly, generate the cache in the portage-stable tree asap. We know
273 # we won't have any cros-workon packages in there, so generating the cache
274 # is the only thing it'll be doing. The chromiumos overlay instead might
275 # have revbumping to do before it can generate the cache.
276 keys = overlays.keys()
277 for overlay in ('/third_party/chromiumos-overlay',
278 '/third_party/portage-stable'):
280 if k.endswith(overlay):
285 with parallel.BackgroundTaskRunner(portage_utilities.RegenCache) as queue:
287 ebuilds = overlays[overlay]
288 if not os.path.isdir(overlay):
289 cros_build_lib.Warning('Skipping %s' % overlay)
292 # Note we intentionally work from the non push tracking branch;
293 # everything built thus far has been against it (meaning, http mirrors),
294 # thus we should honor that. During the actual push, the code switches
295 # to the correct urls, and does an appropriate rebasing.
296 tracking_branch = git.GetTrackingBranchViaManifest(
297 overlay, manifest=manifest)[1]
299 if command == 'push':
300 PushChange(constants.STABLE_EBUILD_BRANCH, tracking_branch,
301 options.dryrun, cwd=overlay)
302 elif command == 'commit':
303 existing_commit = git.GetGitRepoRevision(overlay)
304 work_branch = GitBranch(constants.STABLE_EBUILD_BRANCH, tracking_branch,
306 work_branch.CreateBranch()
307 if not work_branch.Exists():
308 cros_build_lib.Die('Unable to create stabilizing branch in %s' %
311 # In the case of uprevving overlays that have patches applied to them,
312 # include the patched changes in the stabilizing branch.
313 git.RunGit(overlay, ['rebase', existing_commit])
316 for ebuild in ebuilds:
318 cros_build_lib.Info('Working on %s', ebuild.package)
320 new_package = ebuild.RevWorkOnEBuild(options.srcroot, manifest)
322 revved_packages.append(ebuild.package)
323 new_package_atoms.append('=%s' % new_package)
324 messages.append(_GIT_COMMIT_MESSAGE % ebuild.package)
325 except (OSError, IOError):
326 cros_build_lib.Warning(
328 'Note you will have to go into %s '
329 'and reset the git repo yourself.' % (ebuild.package, overlay))
333 portage_utilities.EBuild.CommitChange('\n\n'.join(messages), overlay)
335 if cros_build_lib.IsInsideChroot():
336 # Regenerate caches if need be. We do this all the time to
337 # catch when users make changes without updating cache files.
340 if command == 'commit':
341 if cros_build_lib.IsInsideChroot():
342 CleanStalePackages(options.boards.split(':'), new_package_atoms)
343 if options.drop_file:
344 osutils.WriteFile(options.drop_file, ' '.join(revved_packages))