First commit of depot_tools/fetch.py
This is the bare minimum to have fetch.py working for chromium.
The DEPS file pulls the new tools/recipes repo in, which will contain
the recipe for chromium and all other checkout recipes eventually (followup
CL coming shortly). fetch and fetch.bat simply invoke fetch.py. fetch.py
takes a recipe and some arguments on the command line, invokes the recipe
to get its instructions, and performs the actual checkout.
Currently only supports Gclient-Git-Svn checkout (the kind needed for
Chromium), but other checkout types will be pulled from annotated_checkout.py
soon.
Review URL: https://codereview.chromium.org/13463006
git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@192130 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/fetch.py b/fetch.py
new file mode 100755
index 0000000..5973e05
--- /dev/null
+++ b/fetch.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+Tool to perform checkouts in one easy command line!
+
+Usage:
+ fetch <recipe> [--property=value [--property2=value2 ...]]
+
+This script is a wrapper around various version control and repository
+checkout commands. It requires a |recipe| name, fetches data from that
+recipe in depot_tools/recipes, and then performs all necessary inits,
+checkouts, pulls, fetches, etc.
+
+Optional arguments may be passed on the command line in key-value pairs.
+These parameters will be passed through to the recipe's main method.
+"""
+
+import json
+import os
+import subprocess
+import sys
+import pipes
+
+
+SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
+
+
+#################################################
+# Checkout class definitions.
+#################################################
+class Checkout(object):
+ """Base class for implementing different types of checkouts.
+
+ Attributes:
+ |base|: the absolute path of the directory in which this script is run.
+ |spec|: the spec for this checkout as returned by the recipe. Different
+ subclasses will expect different keys in this dictionary.
+ |root|: the directory into which the checkout will be performed, as returned
+ by the recipe. This is a relative path from |base|.
+ """
+ def __init__(self, spec, root):
+ self.base = os.getcwd()
+ self.spec = spec
+ self.root = root
+
+ def exists(self):
+ pass
+
+ def init(self):
+ pass
+
+ def sync(self):
+ pass
+
+
+class GclientCheckout(Checkout):
+
+ @staticmethod
+ def run_gclient(*cmd, **kwargs):
+ print 'Running: gclient %s' % ' '.join(pipes.quote(x) for x in cmd)
+ return subprocess.check_call(('gclient',) + cmd, **kwargs)
+
+
+class GitCheckout(Checkout):
+
+ @staticmethod
+ def run_git(*cmd, **kwargs):
+ print 'Running: git %s' % ' '.join(pipes.quote(x) for x in cmd)
+ return subprocess.check_call(('git',) + cmd, **kwargs)
+
+
+class GclientGitSvnCheckout(GclientCheckout, GitCheckout):
+
+ def __init__(self, spec, root):
+ super(GclientGitSvnCheckout, self).__init__(spec, root)
+ assert 'solutions' in self.spec
+ keys = ['solutions', 'target_os', 'target_os_only']
+ gclient_spec = '\n'.join('%s = %s' % (key, self.spec[key])
+ for key in self.spec if key in keys)
+ self.spec['gclient_spec'] = gclient_spec
+ assert 'svn_url' in self.spec
+ assert 'svn_branch' in self.spec
+ assert 'svn_ref' in self.spec
+
+ def exists(self):
+ return os.path.exists(os.path.join(os.getcwd(), self.root))
+
+ def init(self):
+ # Configure and do the gclient checkout.
+ self.run_gclient('config', '--spec', self.spec['gclient_spec'])
+ self.run_gclient('sync')
+
+ # Configure git.
+ wd = os.path.join(self.base, self.root)
+ self.run_git(
+ 'submodule', 'foreach',
+ 'git config -f $toplevel/.git/config submodule.$name.ignore all',
+ cwd=wd)
+ self.run_git('config', 'diff.ignoreSubmodules', 'all', cwd=wd)
+
+ # Configure git-svn.
+ self.run_git('svn', 'init', '--prefix=origin/', '-T',
+ self.spec['svn_branch'], self.spec['svn_url'], cwd=wd)
+ self.run_git('config', 'svn-remote.svn.fetch', self.spec['svn_branch'] +
+ ':refs/remotes/origin/' + self.spec['svn_ref'], cwd=wd)
+ self.run_git('svn', 'fetch', cwd=wd)
+
+ # Configure git-svn submodules, if any.
+ submodules = json.loads(self.spec.get('submodule_git_svn_spec', '{}'))
+ for path, subspec in submodules.iteritems():
+ subspec = submodules[path]
+ ospath = os.path.join(*path.split('/'))
+ wd = os.path.join(self.base, self.root, ospath)
+ self.run_git('svn', 'init', '--prefix=origin/', '-T',
+ subspec['svn_branch'], subspec['svn_url'], cwd=wd)
+ self.run_git('config', '--replace', 'svn-remote.svn.fetch',
+ subspec['svn_branch'] + ':refs/remotes/origin/' +
+ subspec['svn_ref'], cwd=wd)
+ self.run_git('svn', 'fetch', cwd=wd)
+
+
+CHECKOUT_TYPE_MAP = {
+ 'gclient': GclientCheckout,
+ 'gclient_git_svn': GclientGitSvnCheckout,
+ 'git': GitCheckout,
+}
+
+
+def CheckoutFactory(type_name, spec, root):
+ """Factory to build Checkout class instances."""
+ class_ = CHECKOUT_TYPE_MAP.get(type_name)
+ if not class_:
+ raise KeyError('unrecognized checkout type: %s' % type_name)
+ return class_(spec, root)
+
+
+#################################################
+# Utility function and file entry point.
+#################################################
+def usage(msg=None):
+ """Print help and exit."""
+ if msg:
+ print 'Error:', msg
+
+ print (
+"""
+usage: %s <recipe> [--property=value [--property2=value2 ...]]
+""" % os.path.basename(sys.argv[0]))
+ sys.exit(bool(msg))
+
+
+def handle_args(argv):
+ """Gets the recipe name from the command line arguments."""
+ if len(argv) <= 1:
+ usage('Must specify a recipe.')
+
+ def looks_like_arg(arg):
+ return arg.startswith('--') and arg.count('=') == 1
+
+ bad_parms = [x for x in argv[2:] if not looks_like_arg(x)]
+ if bad_parms:
+ usage('Got bad arguments %s' % bad_parms)
+
+ recipe = argv[1]
+ props = argv[2:]
+ return recipe, props
+
+
+def run_recipe_fetch(recipe, props, aliased=False):
+ """Invoke a recipe's fetch method with the passed-through args
+ and return its json output as a python object."""
+ recipe_path = os.path.abspath(os.path.join(SCRIPT_PATH, 'recipes', recipe))
+ cmd = [sys.executable, recipe_path + '.py', 'fetch'] + props
+ result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
+ spec = json.loads(result)
+ if 'alias' in spec:
+ assert not aliased
+ return run_recipe_fetch(
+ spec['alias']['recipe'], spec['alias']['props'] + props, aliased=True)
+ cmd = [sys.executable, recipe_path + '.py', 'root']
+ result = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
+ root = json.loads(result)
+ return spec, root
+
+
+def run(spec, root):
+ """Perform a checkout with the given type and configuration.
+
+ Args:
+ spec: Checkout configuration returned by the the recipe's fetch_spec
+ method (checkout type, repository url, etc.).
+ root: The directory into which the repo expects to be checkout out.
+ """
+ assert 'type' in spec
+ checkout_type = spec['type']
+ checkout_spec = spec['%s_spec' % checkout_type]
+ try:
+ checkout = CheckoutFactory(checkout_type, checkout_spec, root)
+ except KeyError:
+ return 1
+ if checkout.exists():
+ print 'You appear to already have this checkout.'
+ print 'Aborting to avoid clobbering your work.'
+ return 1
+ checkout.init()
+ return 0
+
+
+def main():
+ recipe, props = handle_args(sys.argv)
+ spec, root = run_recipe_fetch(recipe, props)
+ return run(spec, root)
+
+
+if __name__ == '__main__':
+ sys.exit(main())