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