• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2
3# Copyright 2017 The LUCI Authors. All rights reserved.
4# Use of this source code is governed under the Apache License, Version 2.0
5# that can be found in the LICENSE file.
6
7"""Bootstrap script to clone and forward to the recipe engine tool.
8
9*******************
10** DO NOT MODIFY **
11*******************
12
13This is a copy of https://chromium.googlesource.com/infra/luci/recipes-py/+/master/recipes.py.
14To fix bugs, fix in the googlesource repo then run the autoroller.
15"""
16
17import argparse
18import json
19import logging
20import os
21import random
22import subprocess
23import sys
24import time
25import urlparse
26
27from collections import namedtuple
28
29from cStringIO import StringIO
30
31# The dependency entry for the recipe_engine in the client repo's recipes.cfg
32#
33# url (str) - the url to the engine repo we want to use.
34# revision (str) - the git revision for the engine to get.
35# branch (str) - the branch to fetch for the engine as an absolute ref (e.g.
36#   refs/heads/master)
37EngineDep = namedtuple('EngineDep',
38                       'url revision branch')
39
40
41class MalformedRecipesCfg(Exception):
42  def __init__(self, msg, path):
43    super(MalformedRecipesCfg, self).__init__('malformed recipes.cfg: %s: %r'
44                                              % (msg, path))
45
46
47def parse(repo_root, recipes_cfg_path):
48  """Parse is a lightweight a recipes.cfg file parser.
49
50  Args:
51    repo_root (str) - native path to the root of the repo we're trying to run
52      recipes for.
53    recipes_cfg_path (str) - native path to the recipes.cfg file to process.
54
55  Returns (as tuple):
56    engine_dep (EngineDep|None): The recipe_engine dependency, or None, if the
57      current repo IS the recipe_engine.
58    recipes_path (str) - native path to where the recipes live inside of the
59      current repo (i.e. the folder containing `recipes/` and/or
60      `recipe_modules`)
61  """
62  with open(recipes_cfg_path, 'rU') as fh:
63    pb = json.load(fh)
64
65  try:
66    if pb['api_version'] != 2:
67      raise MalformedRecipesCfg('unknown version %d' % pb['api_version'],
68                                recipes_cfg_path)
69
70    # If we're running ./recipes.py from the recipe_engine repo itself, then
71    # return None to signal that there's no EngineDep.
72    repo_name = pb.get('repo_name')
73    if not repo_name:
74      repo_name = pb['project_id']
75    if repo_name == 'recipe_engine':
76      return None, pb.get('recipes_path', '')
77
78    engine = pb['deps']['recipe_engine']
79
80    if 'url' not in engine:
81      raise MalformedRecipesCfg(
82        'Required field "url" in dependency "recipe_engine" not found',
83        recipes_cfg_path)
84
85    engine.setdefault('revision', '')
86    engine.setdefault('branch', 'refs/heads/master')
87    recipes_path = pb.get('recipes_path', '')
88
89    # TODO(iannucci): only support absolute refs
90    if not engine['branch'].startswith('refs/'):
91      engine['branch'] = 'refs/heads/' + engine['branch']
92
93    recipes_path = os.path.join(
94      repo_root, recipes_path.replace('/', os.path.sep))
95    return EngineDep(**engine), recipes_path
96  except KeyError as ex:
97    raise MalformedRecipesCfg(ex.message, recipes_cfg_path)
98
99
100_BAT = '.bat' if sys.platform.startswith(('win', 'cygwin')) else ''
101GIT = 'git' + _BAT
102VPYTHON = 'vpython' + _BAT
103
104
105def _subprocess_call(argv, **kwargs):
106  logging.info('Running %r', argv)
107  return subprocess.call(argv, **kwargs)
108
109
110def _git_check_call(argv, **kwargs):
111  argv = [GIT]+argv
112  logging.info('Running %r', argv)
113  subprocess.check_call(argv, **kwargs)
114
115
116def _git_output(argv, **kwargs):
117  argv = [GIT]+argv
118  logging.info('Running %r', argv)
119  return subprocess.check_output(argv, **kwargs)
120
121
122def parse_args(argv):
123  """This extracts a subset of the arguments that this bootstrap script cares
124  about. Currently this consists of:
125    * an override for the recipe engine in the form of `-O recipe_engine=/path`
126    * the --package option.
127  """
128  PREFIX = 'recipe_engine='
129
130  p = argparse.ArgumentParser(add_help=False)
131  p.add_argument('-O', '--project-override', action='append')
132  p.add_argument('--package', type=os.path.abspath)
133  args, _ = p.parse_known_args(argv)
134  for override in args.project_override or ():
135    if override.startswith(PREFIX):
136      return override[len(PREFIX):], args.package
137  return None, args.package
138
139
140def checkout_engine(engine_path, repo_root, recipes_cfg_path):
141  dep, recipes_path = parse(repo_root, recipes_cfg_path)
142  if dep is None:
143    # we're running from the engine repo already!
144    return os.path.join(repo_root, recipes_path)
145
146  url = dep.url
147
148  if not engine_path and url.startswith('file://'):
149    engine_path = urlparse.urlparse(url).path
150
151  if not engine_path:
152    revision = dep.revision
153    branch = dep.branch
154
155    # Ensure that we have the recipe engine cloned.
156    engine_path = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine')
157
158    with open(os.devnull, 'w') as NUL:
159      # Note: this logic mirrors the logic in recipe_engine/fetch.py
160      _git_check_call(['init', engine_path], stdout=NUL)
161
162      try:
163        _git_check_call(['rev-parse', '--verify', '%s^{commit}' % revision],
164                        cwd=engine_path, stdout=NUL, stderr=NUL)
165      except subprocess.CalledProcessError:
166        _git_check_call(['fetch', url, branch], cwd=engine_path, stdout=NUL,
167                        stderr=NUL)
168
169    try:
170      _git_check_call(['diff', '--quiet', revision], cwd=engine_path)
171    except subprocess.CalledProcessError:
172      _git_check_call(['reset', '-q', '--hard', revision], cwd=engine_path)
173
174  return engine_path
175
176
177def main():
178  if '--verbose' in sys.argv:
179    logging.getLogger().setLevel(logging.INFO)
180
181  args = sys.argv[1:]
182  engine_override, recipes_cfg_path = parse_args(args)
183
184  if recipes_cfg_path:
185    # calculate repo_root from recipes_cfg_path
186    repo_root = os.path.dirname(
187      os.path.dirname(
188        os.path.dirname(recipes_cfg_path)))
189  else:
190    # find repo_root with git and calculate recipes_cfg_path
191    repo_root = (_git_output(
192      ['rev-parse', '--show-toplevel'],
193      cwd=os.path.abspath(os.path.dirname(__file__))).strip())
194    repo_root = os.path.abspath(repo_root)
195    recipes_cfg_path = os.path.join(repo_root, 'infra', 'config', 'recipes.cfg')
196    args = ['--package', recipes_cfg_path] + args
197
198  engine_path = checkout_engine(engine_override, repo_root, recipes_cfg_path)
199
200  return _subprocess_call([
201      VPYTHON, '-u',
202      os.path.join(engine_path, 'recipe_engine', 'main.py')] + args)
203
204
205if __name__ == '__main__':
206  sys.exit(main())
207