• 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://github.com/luci/recipes-py/blob/master/doc/recipes.py.
14To fix bugs, fix in the github 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# path_override (str) - the subdirectory in the engine repo we should use to
36#   find it's recipes.py entrypoint. This is here for completeness, but will
37#   essentially always be empty. It would be used if the recipes-py repo was
38#   merged as a subdirectory of some other repo and you depended on that
39#   subdirectory.
40# branch (str) - the branch to fetch for the engine as an absolute ref (e.g.
41#   refs/heads/master)
42# repo_type ("GIT"|"GITILES") - An ignored enum which will be removed soon.
43EngineDep = namedtuple('EngineDep',
44                       'url revision path_override branch repo_type')
45
46
47class MalformedRecipesCfg(Exception):
48  def __init__(self, msg, path):
49    super(MalformedRecipesCfg, self).__init__('malformed recipes.cfg: %s: %r'
50                                              % (msg, path))
51
52
53def parse(repo_root, recipes_cfg_path):
54  """Parse is a lightweight a recipes.cfg file parser.
55
56  Args:
57    repo_root (str) - native path to the root of the repo we're trying to run
58      recipes for.
59    recipes_cfg_path (str) - native path to the recipes.cfg file to process.
60
61  Returns (as tuple):
62    engine_dep (EngineDep): The recipe_engine dependency.
63    recipes_path (str) - native path to where the recipes live inside of the
64      current repo (i.e. the folder containing `recipes/` and/or
65      `recipe_modules`)
66  """
67  with open(recipes_cfg_path, 'rU') as fh:
68    pb = json.load(fh)
69
70  try:
71    if pb['api_version'] != 2:
72      raise MalformedRecipesCfg('unknown version %d' % pb['api_version'],
73                                recipes_cfg_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('path_override', '')
84    engine.setdefault('branch', 'refs/heads/master')
85    recipes_path = pb.get('recipes_path', '')
86
87    # TODO(iannucci): only support absolute refs
88    if not engine['branch'].startswith('refs/'):
89      engine['branch'] = 'refs/heads/' + engine['branch']
90
91    engine.setdefault('repo_type', 'GIT')
92    if engine['repo_type'] not in ('GIT', 'GITILES'):
93      raise MalformedRecipesCfg(
94        'Unsupported "repo_type" value in dependency "recipe_engine"',
95        recipes_cfg_path)
96
97    recipes_path = os.path.join(
98      repo_root, recipes_path.replace('/', os.path.sep))
99    return EngineDep(**engine), recipes_path
100  except KeyError as ex:
101    raise MalformedRecipesCfg(ex.message, recipes_cfg_path)
102
103
104GIT = 'git.bat' if sys.platform.startswith(('win', 'cygwin')) else 'git'
105
106
107def _subprocess_call(argv, **kwargs):
108  logging.info('Running %r', argv)
109  return subprocess.call(argv, **kwargs)
110
111
112def _git_check_call(argv, **kwargs):
113  argv = [GIT]+argv
114  logging.info('Running %r', argv)
115  subprocess.check_call(argv, **kwargs)
116
117
118def _git_output(argv, **kwargs):
119  argv = [GIT]+argv
120  logging.info('Running %r', argv)
121  return subprocess.check_output(argv, **kwargs)
122
123
124def parse_args(argv):
125  """This extracts a subset of the arguments that this bootstrap script cares
126  about. Currently this consists of:
127    * an override for the recipe engine in the form of `-O recipe_engin=/path`
128    * the --package option.
129  """
130  PREFIX = 'recipe_engine='
131
132  p = argparse.ArgumentParser(add_help=False)
133  p.add_argument('-O', '--project-override', action='append')
134  p.add_argument('--package', type=os.path.abspath)
135  args, _ = p.parse_known_args(argv)
136  for override in args.project_override or ():
137    if override.startswith(PREFIX):
138      return override[len(PREFIX):], args.package
139  return None, args.package
140
141
142def checkout_engine(engine_path, repo_root, recipes_cfg_path):
143  dep, recipes_path = parse(repo_root, recipes_cfg_path)
144
145  url = dep.url
146
147  if not engine_path and url.startswith('file://'):
148    engine_path = urlparse.urlparse(url).path
149
150  if not engine_path:
151    revision = dep.revision
152    subpath = dep.path_override
153    branch = dep.branch
154
155    # Ensure that we have the recipe engine cloned.
156    engine = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine')
157    engine_path = os.path.join(engine, subpath)
158
159    with open(os.devnull, 'w') as NUL:
160      # Note: this logic mirrors the logic in recipe_engine/fetch.py
161      _git_check_call(['init', engine], stdout=NUL)
162
163      try:
164        _git_check_call(['rev-parse', '--verify', '%s^{commit}' % revision],
165                        cwd=engine, stdout=NUL, stderr=NUL)
166      except subprocess.CalledProcessError:
167        _git_check_call(['fetch', url, branch], cwd=engine, stdout=NUL,
168                        stderr=NUL)
169
170    try:
171      _git_check_call(['diff', '--quiet', revision], cwd=engine)
172    except subprocess.CalledProcessError:
173      _git_check_call(['reset', '-q', '--hard', revision], cwd=engine)
174
175  return engine_path
176
177
178def main():
179  if '--verbose' in sys.argv:
180    logging.getLogger().setLevel(logging.INFO)
181
182  args = sys.argv[1:]
183  engine_override, recipes_cfg_path = parse_args(args)
184
185  if recipes_cfg_path:
186    # calculate repo_root from recipes_cfg_path
187    repo_root = os.path.dirname(
188      os.path.dirname(
189        os.path.dirname(recipes_cfg_path)))
190  else:
191    # find repo_root with git and calculate recipes_cfg_path
192    repo_root = (_git_output(
193      ['rev-parse', '--show-toplevel'],
194      cwd=os.path.abspath(os.path.dirname(__file__))).strip())
195    repo_root = os.path.abspath(repo_root)
196    recipes_cfg_path = os.path.join(repo_root, 'infra', 'config', 'recipes.cfg')
197    args = ['--package', recipes_cfg_path] + args
198
199  engine_path = checkout_engine(engine_override, repo_root, recipes_cfg_path)
200
201  return _subprocess_call([
202      sys.executable, '-u',
203      os.path.join(engine_path, 'recipes.py')] + args)
204
205
206if __name__ == '__main__':
207  sys.exit(main())
208