| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Helper script for fully-annotated builds. Performs checkouts of various | |
| 7 kinds. | |
| 8 | |
| 9 This script is part of the effort to move all builds to annotator-based systems. | |
| 10 Any builder configured to use the AnnotatorFactory uses run.py as its entry | |
| 11 point. If that builder's factory_properties include a spec for a checkout, then | |
| 12 the work of actually performing that checkout is done here. | |
| 13 """ | |
| 14 | |
| 15 import cStringIO as StringIO | |
| 16 import optparse | |
| 17 import os | |
| 18 import pipes | |
| 19 import subprocess | |
| 20 import sys | |
| 21 | |
| 22 from common import annotator | |
| 23 from common import chromium_utils | |
| 24 | |
| 25 | |
| 26 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
| 27 | |
| 28 | |
| 29 def get_args(): | |
| 30 """Process command-line arguments.""" | |
| 31 parser = optparse.OptionParser( | |
| 32 description='Checkout helper for annotated builds.') | |
| 33 parser.add_option('--type', | |
| 34 action='store', type='string', default='', | |
| 35 help='type of checkout (i.e. gclient, git, or svn)') | |
| 36 parser.add_option('--spec', | |
| 37 action='callback', callback=chromium_utils.convert_json, | |
| 38 type='string', default={}, | |
| 39 help='repository spec (url and metadata) to checkout') | |
| 40 return parser.parse_args() | |
| 41 | |
| 42 | |
| 43 class _CheckoutMetaclass(type): | |
| 44 """Automatically register Checkout subclasses for factory discoverability.""" | |
| 45 checkout_registry = {} | |
| 46 | |
| 47 def __new__(mcs, name, bases, attrs): | |
| 48 checkout_type = attrs['CHECKOUT_TYPE'] | |
| 49 | |
| 50 if checkout_type in mcs.checkout_registry: | |
| 51 raise ValueError('Duplicate checkout identifier "%s" found in: %s' % | |
| 52 (checkout_type, name)) | |
| 53 | |
| 54 # Only the base class is allowed to have no CHECKOUT_TYPE. The base class | |
| 55 # should be the only one to specify this metaclass. | |
| 56 if not checkout_type and attrs.get('__metaclass__') != mcs: | |
| 57 raise ValueError('"%s" CHECKOUT_TYPE cannot be empty or None.' % name) | |
| 58 | |
| 59 newcls = super(_CheckoutMetaclass, mcs).__new__(mcs, name, bases, attrs) | |
| 60 # Don't register the base class. | |
| 61 if checkout_type: | |
| 62 mcs.checkout_registry[checkout_type] = newcls | |
| 63 return newcls | |
| 64 | |
| 65 | |
| 66 class Checkout(object): | |
| 67 """Base class for implementing different types of checkouts. | |
| 68 | |
| 69 Attributes: | |
| 70 CHECKOUT_TYPE: String identifier used when selecting the type of checkout to | |
| 71 perform. All subclasses must specify a unique CHECKOUT_TYPE value. | |
| 72 """ | |
| 73 __metaclass__ = _CheckoutMetaclass | |
| 74 CHECKOUT_TYPE = None | |
| 75 | |
| 76 def __init__(self, spec): | |
| 77 self.spec = spec | |
| 78 | |
| 79 def setup(self): | |
| 80 pass | |
| 81 | |
| 82 def clean(self): | |
| 83 pass | |
| 84 | |
| 85 def checkout(self): | |
| 86 pass | |
| 87 | |
| 88 def root(self): | |
| 89 pass | |
| 90 | |
| 91 | |
| 92 def CheckoutFactory(type_name, spec): | |
| 93 """Factory to build Checkout class instances.""" | |
| 94 class_ = _CheckoutMetaclass.checkout_registry.get(type_name) | |
| 95 if not class_ or not issubclass(class_, Checkout): | |
| 96 raise KeyError('unrecognized checkout type: %s' % type_name) | |
| 97 return class_(spec) | |
| 98 | |
| 99 | |
| 100 class GclientCheckout(Checkout): | |
| 101 CHECKOUT_TYPE = 'gclient' | |
| 102 | |
| 103 gclient_path = os.path.abspath( | |
| 104 os.path.join(SCRIPT_PATH, '..', '..', '..', 'depot_tools', 'gclient')) | |
| 105 if sys.platform.startswith('win'): | |
| 106 gclient_path += '.bat' | |
| 107 | |
| 108 def __init__(self, *args, **kwargs): | |
| 109 super(GclientCheckout, self).__init__(*args, **kwargs) | |
| 110 assert 'solutions' in self.spec | |
| 111 | |
| 112 @classmethod | |
| 113 def run_gclient(cls, *cmd): | |
| 114 print 'Running: gclient %s' % ' '.join(pipes.quote(x) for x in cmd) | |
| 115 subprocess.check_call((cls.gclient_path,)+cmd) | |
| 116 | |
| 117 def setup(self): | |
| 118 spec_string = '' | |
| 119 for key in self.spec: | |
| 120 # We should be using json.dumps here, but gclient directly execs the dict | |
| 121 # that it receives as the argument to --spec, so we have to have True, | |
| 122 # False, and None instead of JSON's true, false, and null. | |
| 123 spec_string += '%s = %s\n' % (key, str(self.spec[key])) | |
| 124 self.run_gclient('config', '--spec', spec_string) | |
| 125 | |
| 126 def clean(self): | |
| 127 self.run_gclient('revert', '--nohooks') | |
| 128 | |
| 129 def checkout(self): | |
| 130 self.run_gclient('sync', '--nohooks') | |
| 131 | |
| 132 def root(self): | |
| 133 return os.path.abspath(self.spec['solutions'][0]['name']) | |
| 134 | |
| 135 | |
| 136 class GclientGitCheckout(GclientCheckout): | |
| 137 """A gclient checkout tuned for purely git-based DEPS.""" | |
| 138 CHECKOUT_TYPE = 'gclient_git' | |
| 139 def clean(self): | |
| 140 # clean() isn't used because the gclient sync flags passed in checkout() do | |
| 141 # much the same thing, and they're more correct than doing a separate | |
| 142 # 'gclient revert' because it makes sure the other args are correct when a | |
| 143 # repo was deleted and needs to be re-cloned (notably --with_branch_heads), | |
| 144 # whereas 'revert' uses default args for clone operations. | |
| 145 # | |
| 146 # TODO(mmoss): To be like current official builders, this step could just | |
| 147 # delete the whole <slave_name>/build/ directory and start each build from | |
| 148 # scratch. That might be the least bad solution, at least until we have a | |
| 149 # reliable gclient method to produce a pristine working dir for git-based | |
| 150 # builds (e.g. maybe some combination of 'git reset/clean -fx' and removing | |
| 151 # the 'out' directory). | |
| 152 pass | |
| 153 | |
| 154 def checkout(self): | |
| 155 self.run_gclient('sync', '--verbose', '--with_branch_heads', '--nohooks', | |
| 156 '--reset', '--delete_unversioned_trees', '--force') | |
| 157 | |
| 158 | |
| 159 class GitCheckout(Checkout): | |
| 160 """Git specs are a dictionary with up to four keys: |url|, |branch|, | |
| 161 |recursive|, and |directory|. Only |url| is required. The others default | |
| 162 to empty, which results in using the git-default values of HEAD, False, | |
| 163 and the 'humanish' interpretation of the url, respectively. Note that |url| | |
| 164 is the full git url of the repo, including username and port number if | |
| 165 necessary.""" | |
| 166 CHECKOUT_TYPE = 'git' | |
| 167 | |
| 168 def __init__(self, *args, **kwargs): | |
| 169 super(GitCheckout, self).__init__(*args, **kwargs) | |
| 170 assert 'url' in self.spec | |
| 171 assert os.pardir not in self.spec.get('directory', '') | |
| 172 | |
| 173 dir_path = self.spec.get('directory') | |
| 174 if not dir_path: | |
| 175 dir_path = self.spec['url'].rsplit('/', 1)[-1] | |
| 176 if dir_path.endswith('.git'): # ex: https://host/foobar.git | |
| 177 dir_path = dir_path[:-len('.git')] | |
| 178 if not dir_path: # ex: ssh://host:repo/foobar/.git | |
| 179 dir_path = dir_path.rsplit('/', 1)[-1] | |
| 180 self.cwd = os.path.abspath(os.path.join(os.curdir, dir_path)) | |
| 181 | |
| 182 def setup(self): | |
| 183 if not os.path.exists(self.cwd): | |
| 184 os.makedirs(self.cwd) | |
| 185 | |
| 186 try: | |
| 187 self.run_git('branch') | |
| 188 exists = True | |
| 189 except subprocess.CalledProcessError: | |
| 190 exists = False | |
| 191 if exists: | |
| 192 self.run_git('remote', 'rm', 'origin') | |
| 193 else: | |
| 194 self.run_git('init') | |
| 195 self.run_git('remote', 'add', 'origin', self.spec['url']) | |
| 196 # TODO(agable): add support for git crup. | |
| 197 if self.spec.get('recursive'): | |
| 198 self.run_git('fetch', 'origin', '--recurse-submodules') | |
| 199 else: | |
| 200 self.run_git('fetch', 'origin') | |
| 201 branch = self.spec.get('branch', 'master') | |
| 202 self.run_git('update-ref', 'refs/heads/%s' % branch, 'origin/%s' % branch) | |
| 203 | |
| 204 def run_git(self, *cmd): | |
| 205 cmd = ('git',) + cmd | |
| 206 print 'Running: %s' % (' '.join(pipes.quote(x) for x in cmd)) | |
| 207 subprocess.check_call(cmd, cwd=self.cwd) | |
| 208 | |
| 209 def clean(self): | |
| 210 self.run_git('clean', '-f', '-d', '-x') | |
| 211 | |
| 212 def checkout(self): | |
| 213 self.run_git('checkout', '-f', self.spec.get('branch', 'master')) | |
| 214 self.run_git('submodule', 'update', '--init', '--recursive') | |
| 215 | |
| 216 def root(self): | |
| 217 return self.cwd | |
| 218 | |
| 219 | |
| 220 class SvnCheckout(Checkout): | |
| 221 CHECKOUT_TYPE = 'svn' | |
| 222 | |
| 223 | |
| 224 def run(checkout_type, checkout_spec, test_mode=False): | |
| 225 """Perform a checkout with the given type and configuration. | |
| 226 | |
| 227 Args: | |
| 228 checkout_type: Type of checkout to perform (matching a Checkout subclass | |
| 229 CHECKOUT_TYPE attribute). | |
| 230 checkout_spec: Configuration values needed for the type of checkout | |
| 231 (repository url, etc.). | |
| 232 test_mode: If we're in test_mode, just return error code and root without | |
| 233 actually doing anything. | |
| 234 | |
| 235 Returns: | |
| 236 Tuple of (<retcode>, <root_path>) where root_path is the absolute path | |
| 237 to the 'root' of the checkout (as defined by |checkout_type|). | |
| 238 """ | |
| 239 stream = sys.stdout | |
| 240 if test_mode: | |
| 241 stream = StringIO.StringIO() | |
| 242 stream = annotator.StructuredAnnotationStream( | |
| 243 seed_steps=['checkout_setup', 'checkout_clean', 'checkout'], | |
| 244 stream=stream) | |
| 245 | |
| 246 with stream.step('checkout_setup') as s: | |
| 247 try: | |
| 248 checkout = CheckoutFactory(checkout_type, checkout_spec) | |
| 249 except KeyError as e: | |
| 250 s.step_text(e) | |
| 251 s.step_failure() | |
| 252 return (1, None) | |
| 253 if test_mode: | |
| 254 return (0, checkout.root()) | |
| 255 checkout.setup() | |
| 256 with stream.step('checkout_clean') as s: | |
| 257 checkout.clean() | |
| 258 with stream.step('checkout') as s: | |
| 259 checkout.checkout() | |
| 260 return (0, checkout.root()) | |
| 261 | |
| 262 | |
| 263 def main(): | |
| 264 opts, _ = get_args() | |
| 265 return run(opts.type, opts.spec)[0] | |
| 266 | |
| 267 | |
| 268 if __name__ == '__main__': | |
| 269 sys.exit(main()) | |
| OLD | NEW |