• 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    if pb['project_id'] == 'recipe_engine':
73      return None, pb.get('recipes_path', '')
74
75    engine = pb['deps']['recipe_engine']
76
77    if 'url' not in engine:
78      raise MalformedRecipesCfg(
79        'Required field "url" in dependency "recipe_engine" not found',
80        recipes_cfg_path)
81
82    engine.setdefault('revision', '')
83    engine.setdefault('branch', 'refs/heads/master')
84    recipes_path = pb.get('recipes_path', '')
85
86    # TODO(iannucci): only support absolute refs
87    if not engine['branch'].startswith('refs/'):
88      engine['branch'] = 'refs/heads/' + engine['branch']
89
90    recipes_path = os.path.join(
91      repo_root, recipes_path.replace('/', os.path.sep))
92    return EngineDep(**engine), recipes_path
93  except KeyError as ex:
94    raise MalformedRecipesCfg(ex.message, recipes_cfg_path)
95
96
97_BAT = '.bat' if sys.platform.startswith(('win', 'cygwin')) else ''
98GIT = 'git' + _BAT
99VPYTHON = 'vpython' + _BAT
100
101
102def _subprocess_call(argv, **kwargs):
103  logging.info('Running %r', argv)
104  return subprocess.call(argv, **kwargs)
105
106
107def _git_check_call(argv, **kwargs):
108  argv = [GIT]+argv
109  logging.info('Running %r', argv)
110  subprocess.check_call(argv, **kwargs)
111
112
113def _git_output(argv, **kwargs):
114  argv = [GIT]+argv
115  logging.info('Running %r', argv)
116  return subprocess.check_output(argv, **kwargs)
117
118
119def parse_args(argv):
120  """This extracts a subset of the arguments that this bootstrap script cares
121  about. Currently this consists of:
122    * an override for the recipe engine in the form of `-O recipe_engine=/path`
123    * the --package option.
124  """
125  PREFIX = 'recipe_engine='
126
127  p = argparse.ArgumentParser(add_help=False)
128  p.add_argument('-O', '--project-override', action='append')
129  p.add_argument('--package', type=os.path.abspath)
130  args, _ = p.parse_known_args(argv)
131  for override in args.project_override or ():
132    if override.startswith(PREFIX):
133      return override[len(PREFIX):], args.package
134  return None, args.package
135
136
137def checkout_engine(engine_path, repo_root, recipes_cfg_path):
138  dep, recipes_path = parse(repo_root, recipes_cfg_path)
139  if dep is None:
140    # we're running from the engine repo already!
141    return os.path.join(repo_root, recipes_path)
142
143  url = dep.url
144
145  if not engine_path and url.startswith('file://'):
146    engine_path = urlparse.urlparse(url).path
147
148  if not engine_path:
149    revision = dep.revision
150    branch = dep.branch
151
152    # Ensure that we have the recipe engine cloned.
153    engine_path = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine')
154
155    with open(os.devnull, 'w') as NUL:
156      # Note: this logic mirrors the logic in recipe_engine/fetch.py
157      _git_check_call(['init', engine_path], stdout=NUL)
158
159      try:
160        _git_check_call(['rev-parse', '--verify', '%s^{commit}' % revision],
161                        cwd=engine_path, stdout=NUL, stderr=NUL)
162      except subprocess.CalledProcessError:
163        _git_check_call(['fetch', url, branch], cwd=engine_path, stdout=NUL,
164                        stderr=NUL)
165
166    try:
167      _git_check_call(['diff', '--quiet', revision], cwd=engine_path)
168    except subprocess.CalledProcessError:
169      _git_check_call(['reset', '-q', '--hard', revision], cwd=engine_path)
170
171  return engine_path
172
173
174def main():
175  if '--verbose' in sys.argv:
176    logging.getLogger().setLevel(logging.INFO)
177
178  args = sys.argv[1:]
179  engine_override, recipes_cfg_path = parse_args(args)
180
181  if recipes_cfg_path:
182    # calculate repo_root from recipes_cfg_path
183    repo_root = os.path.dirname(
184      os.path.dirname(
185        os.path.dirname(recipes_cfg_path)))
186  else:
187    # find repo_root with git and calculate recipes_cfg_path
188    repo_root = (_git_output(
189      ['rev-parse', '--show-toplevel'],
190      cwd=os.path.abspath(os.path.dirname(__file__))).strip())
191    repo_root = os.path.abspath(repo_root)
192    recipes_cfg_path = os.path.join(repo_root, 'infra', 'config', 'recipes.cfg')
193    args = ['--package', recipes_cfg_path] + args
194
195  engine_path = checkout_engine(engine_override, repo_root, recipes_cfg_path)
196
197  return _subprocess_call([
198      VPYTHON, '-u',
199      os.path.join(engine_path, 'recipe_engine', 'main.py')] + args)
200
201
202if __name__ == '__main__':
203  sys.exit(main())
204