blob: d726c6897950c11b87ffefc37fc5bb338cd491a5 [file] [log] [blame]
borenet1ed2ae42016-07-26 11:52:17 -07001#!/usr/bin/env python
Eric Borenf171e162016-11-14 12:18:34 -05002
Robert Iannucci3734c7d2017-05-09 11:06:48 -07003# Copyright 2017 The LUCI Authors. All rights reserved.
borenet1ed2ae42016-07-26 11:52:17 -07004# Use of this source code is governed under the Apache License, Version 2.0
5# that can be found in the LICENSE file.
Eric Borenf171e162016-11-14 12:18:34 -05006
borenet1ed2ae42016-07-26 11:52:17 -07007"""Bootstrap script to clone and forward to the recipe engine tool.
Eric Borenf171e162016-11-14 12:18:34 -05008
Robert Iannucci3734c7d2017-05-09 11:06:48 -07009*******************
10** DO NOT MODIFY **
11*******************
Eric Borenf171e162016-11-14 12:18:34 -050012
Eric Boren8e0c2c92017-09-27 13:03:35 -040013This is a copy of https://chromium.googlesource.com/infra/luci/recipes-py/+/master/doc/recipes.py.
14To fix bugs, fix in the googlesource repo then run the autoroller.
borenet1ed2ae42016-07-26 11:52:17 -070015"""
borenet1ed2ae42016-07-26 11:52:17 -070016
Robert Iannuccie8b50852017-03-15 01:24:56 -070017import argparse
Robert Iannucci2ba659e2017-03-15 18:04:05 -070018import json
borenet1ed2ae42016-07-26 11:52:17 -070019import logging
recipe-roller42e16b02017-05-11 04:27:01 -070020import os
borenet1ed2ae42016-07-26 11:52:17 -070021import random
borenet1ed2ae42016-07-26 11:52:17 -070022import subprocess
23import sys
24import time
Robert Iannucci342977c2017-03-24 17:45:40 -070025import urlparse
Eric Borenf171e162016-11-14 12:18:34 -050026
Robert Iannucci3734c7d2017-05-09 11:06:48 -070027from collections import namedtuple
28
Robert Iannucci2ba659e2017-03-15 18:04:05 -070029from cStringIO import StringIO
Eric Borenf171e162016-11-14 12:18:34 -050030
Robert Iannucci3734c7d2017-05-09 11:06:48 -070031# 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
Robert Iannucci2ba659e2017-03-15 18:04:05 -070052
53def parse(repo_root, recipes_cfg_path):
Robert Iannucci3734c7d2017-05-09 11:06:48 -070054 """Parse is a lightweight a recipes.cfg file parser.
Robert Iannucci2ba659e2017-03-15 18:04:05 -070055
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):
Eric Boren8e0c2c92017-09-27 13:03:35 -040062 engine_dep (EngineDep|None): The recipe_engine dependency, or None, if the
63 current repo IS the recipe_engine.
Robert Iannucci2ba659e2017-03-15 18:04:05 -070064 recipes_path (str) - native path to where the recipes live inside of the
65 current repo (i.e. the folder containing `recipes/` and/or
66 `recipe_modules`)
67 """
68 with open(recipes_cfg_path, 'rU') as fh:
Robert Iannucci654dfee2017-03-27 20:52:15 -070069 pb = json.load(fh)
Robert Iannucci2ba659e2017-03-15 18:04:05 -070070
Robert Iannucci3734c7d2017-05-09 11:06:48 -070071 try:
72 if pb['api_version'] != 2:
73 raise MalformedRecipesCfg('unknown version %d' % pb['api_version'],
74 recipes_cfg_path)
Robert Iannucci2ba659e2017-03-15 18:04:05 -070075
Eric Boren8e0c2c92017-09-27 13:03:35 -040076 # If we're running ./doc/recipes.py from the recipe_engine repo itself, then
77 # return None to signal that there's no EngineDep.
78 if pb['project_id'] == 'recipe_engine':
79 return None, pb.get('recipes_path', '')
80
Robert Iannucci3734c7d2017-05-09 11:06:48 -070081 engine = pb['deps']['recipe_engine']
82
83 if 'url' not in engine:
84 raise MalformedRecipesCfg(
85 'Required field "url" in dependency "recipe_engine" not found',
86 recipes_cfg_path)
87
88 engine.setdefault('revision', '')
89 engine.setdefault('path_override', '')
90 engine.setdefault('branch', 'refs/heads/master')
91 recipes_path = pb.get('recipes_path', '')
92
93 # TODO(iannucci): only support absolute refs
94 if not engine['branch'].startswith('refs/'):
95 engine['branch'] = 'refs/heads/' + engine['branch']
96
97 engine.setdefault('repo_type', 'GIT')
98 if engine['repo_type'] not in ('GIT', 'GITILES'):
99 raise MalformedRecipesCfg(
100 'Unsupported "repo_type" value in dependency "recipe_engine"',
101 recipes_cfg_path)
102
103 recipes_path = os.path.join(
104 repo_root, recipes_path.replace('/', os.path.sep))
105 return EngineDep(**engine), recipes_path
106 except KeyError as ex:
107 raise MalformedRecipesCfg(ex.message, recipes_cfg_path)
108
109
recipe-roller198498b2018-06-22 07:38:45 -0700110_BAT = '.bat' if sys.platform.startswith(('win', 'cygwin')) else ''
111GIT = 'git' + _BAT
112VPYTHON = 'vpython' + _BAT
Robert Iannucci2ba659e2017-03-15 18:04:05 -0700113
114
borenet1ed2ae42016-07-26 11:52:17 -0700115def _subprocess_call(argv, **kwargs):
116 logging.info('Running %r', argv)
117 return subprocess.call(argv, **kwargs)
Eric Borenf171e162016-11-14 12:18:34 -0500118
Robert Iannuccie8b50852017-03-15 01:24:56 -0700119
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700120def _git_check_call(argv, **kwargs):
121 argv = [GIT]+argv
borenet1ed2ae42016-07-26 11:52:17 -0700122 logging.info('Running %r', argv)
123 subprocess.check_call(argv, **kwargs)
Eric Borenf171e162016-11-14 12:18:34 -0500124
125
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700126def _git_output(argv, **kwargs):
127 argv = [GIT]+argv
128 logging.info('Running %r', argv)
129 return subprocess.check_output(argv, **kwargs)
130
131
132def parse_args(argv):
133 """This extracts a subset of the arguments that this bootstrap script cares
134 about. Currently this consists of:
135 * an override for the recipe engine in the form of `-O recipe_engin=/path`
136 * the --package option.
137 """
Robert Iannuccie8b50852017-03-15 01:24:56 -0700138 PREFIX = 'recipe_engine='
139
Eric Borenc1e96172017-04-19 11:12:20 +0000140 p = argparse.ArgumentParser(add_help=False)
Robert Iannuccie8b50852017-03-15 01:24:56 -0700141 p.add_argument('-O', '--project-override', action='append')
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700142 p.add_argument('--package', type=os.path.abspath)
Robert Iannuccie8b50852017-03-15 01:24:56 -0700143 args, _ = p.parse_known_args(argv)
144 for override in args.project_override or ():
145 if override.startswith(PREFIX):
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700146 return override[len(PREFIX):], args.package
147 return None, args.package
148
149
150def checkout_engine(engine_path, repo_root, recipes_cfg_path):
151 dep, recipes_path = parse(repo_root, recipes_cfg_path)
Eric Boren8e0c2c92017-09-27 13:03:35 -0400152 if dep is None:
153 # we're running from the engine repo already!
154 return os.path.join(repo_root, recipes_path)
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700155
156 url = dep.url
157
158 if not engine_path and url.startswith('file://'):
159 engine_path = urlparse.urlparse(url).path
160
161 if not engine_path:
162 revision = dep.revision
163 subpath = dep.path_override
164 branch = dep.branch
165
166 # Ensure that we have the recipe engine cloned.
167 engine = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine')
168 engine_path = os.path.join(engine, subpath)
169
170 with open(os.devnull, 'w') as NUL:
171 # Note: this logic mirrors the logic in recipe_engine/fetch.py
172 _git_check_call(['init', engine], stdout=NUL)
173
174 try:
175 _git_check_call(['rev-parse', '--verify', '%s^{commit}' % revision],
176 cwd=engine, stdout=NUL, stderr=NUL)
177 except subprocess.CalledProcessError:
178 _git_check_call(['fetch', url, branch], cwd=engine, stdout=NUL,
179 stderr=NUL)
180
181 try:
182 _git_check_call(['diff', '--quiet', revision], cwd=engine)
183 except subprocess.CalledProcessError:
184 _git_check_call(['reset', '-q', '--hard', revision], cwd=engine)
185
186 return engine_path
Robert Iannuccie8b50852017-03-15 01:24:56 -0700187
188
borenet1ed2ae42016-07-26 11:52:17 -0700189def main():
190 if '--verbose' in sys.argv:
191 logging.getLogger().setLevel(logging.INFO)
Eric Borenf171e162016-11-14 12:18:34 -0500192
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700193 args = sys.argv[1:]
194 engine_override, recipes_cfg_path = parse_args(args)
Robert Iannuccie8b50852017-03-15 01:24:56 -0700195
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700196 if recipes_cfg_path:
197 # calculate repo_root from recipes_cfg_path
198 repo_root = os.path.dirname(
199 os.path.dirname(
200 os.path.dirname(recipes_cfg_path)))
borenet1ed2ae42016-07-26 11:52:17 -0700201 else:
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700202 # find repo_root with git and calculate recipes_cfg_path
203 repo_root = (_git_output(
204 ['rev-parse', '--show-toplevel'],
205 cwd=os.path.abspath(os.path.dirname(__file__))).strip())
206 repo_root = os.path.abspath(repo_root)
207 recipes_cfg_path = os.path.join(repo_root, 'infra', 'config', 'recipes.cfg')
208 args = ['--package', recipes_cfg_path] + args
Eric Borenf171e162016-11-14 12:18:34 -0500209
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700210 engine_path = checkout_engine(engine_override, repo_root, recipes_cfg_path)
Eric Borenf171e162016-11-14 12:18:34 -0500211
borenet1ed2ae42016-07-26 11:52:17 -0700212 return _subprocess_call([
recipe-roller198498b2018-06-22 07:38:45 -0700213 VPYTHON, '-u',
Robert Iannucci2ba659e2017-03-15 18:04:05 -0700214 os.path.join(engine_path, 'recipes.py')] + args)
Eric Borenf171e162016-11-14 12:18:34 -0500215
Robert Iannucci3734c7d2017-05-09 11:06:48 -0700216
borenet1ed2ae42016-07-26 11:52:17 -0700217if __name__ == '__main__':
218 sys.exit(main())