1# Copyright 2020 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Argument parsing code for presubmit checks.""" 15 16import argparse 17import logging 18from pathlib import Path 19import re 20import shutil 21from typing import Callable, Collection, Optional, Sequence 22 23from pw_presubmit import git_repo, presubmit 24 25_LOG = logging.getLogger(__name__) 26DEFAULT_PATH = Path('out', 'presubmit') 27 28_OUTPUT_PATH_README = '''\ 29This directory was created by pw_presubmit to run presubmit checks for the 30{repo} repository. This directory is not used by the regular GN or CMake Ninja 31builds. It may be deleted safely. 32''' 33 34 35def add_path_arguments(parser) -> None: 36 """Adds common presubmit check options to an argument parser.""" 37 38 parser.add_argument( 39 'paths', 40 metavar='pathspec', 41 nargs='*', 42 help=('Paths or patterns to which to restrict the checks. These are ' 43 'interpreted as Git pathspecs. If --base is provided, only ' 44 'paths changed since that commit are checked.')) 45 46 base = parser.add_mutually_exclusive_group() 47 base.add_argument( 48 '-b', 49 '--base', 50 metavar='commit', 51 default=git_repo.TRACKING_BRANCH_ALIAS, 52 help=('Git revision against which to diff for changed files. ' 53 'Default is the tracking branch of the current branch.')) 54 base.add_argument( 55 '--full', 56 dest='base', 57 action='store_const', 58 const=None, 59 help='Run presubmit on all files, not just changed files.') 60 61 parser.add_argument( 62 '-e', 63 '--exclude', 64 metavar='regular_expression', 65 default=[], 66 action='append', 67 type=re.compile, 68 help=('Exclude paths matching any of these regular expressions, ' 69 "which are interpreted relative to each Git repository's root.")) 70 71 72def _add_programs_arguments(exclusive: argparse.ArgumentParser, 73 programs: presubmit.Programs, default: str): 74 def presubmit_program(arg: str) -> presubmit.Program: 75 if arg not in programs: 76 raise argparse.ArgumentTypeError( 77 f'{arg} is not the name of a presubmit program') 78 79 return programs[arg] 80 81 exclusive.add_argument('-p', 82 '--program', 83 choices=programs.values(), 84 type=presubmit_program, 85 default=default, 86 help='Which presubmit program to run') 87 88 all_steps = programs.all_steps() 89 90 # The step argument appends steps to a program. No "step" argument is 91 # created on the resulting argparse.Namespace. 92 class AddToCustomProgram(argparse.Action): 93 def __call__(self, parser, namespace, values, unused_option=None): 94 if not isinstance(namespace.program, list): 95 namespace.program = [] 96 97 if values not in all_steps: 98 raise parser.error( 99 f'argument --step: {values} is not the name of a ' 100 'presubmit check\n\nValid values for --step:\n' 101 f'{{{",".join(sorted(all_steps))}}}') 102 103 namespace.program.append(all_steps[values]) 104 105 exclusive.add_argument( 106 '--step', 107 action=AddToCustomProgram, 108 default=argparse.SUPPRESS, # Don't create a "step" argument. 109 help='Provide explicit steps instead of running a predefined program.', 110 ) 111 112 113def add_arguments(parser: argparse.ArgumentParser, 114 programs: Optional[presubmit.Programs] = None, 115 default: str = '') -> None: 116 """Adds common presubmit check options to an argument parser.""" 117 118 add_path_arguments(parser) 119 parser.add_argument('-k', 120 '--keep-going', 121 action='store_true', 122 help='Continue instead of aborting when errors occur.') 123 parser.add_argument( 124 '--output-directory', 125 type=Path, 126 help=f'Output directory (default: {"<repo root>" / DEFAULT_PATH})', 127 ) 128 parser.add_argument( 129 '--package-root', 130 type=Path, 131 help='Package root directory (default: <output directory>/packages)', 132 ) 133 134 exclusive = parser.add_mutually_exclusive_group() 135 exclusive.add_argument( 136 '--clear', 137 '--clean', 138 action='store_true', 139 help='Delete the presubmit output directory and exit.', 140 ) 141 142 if programs: 143 if not default: 144 raise ValueError('A default must be provided with programs') 145 146 _add_programs_arguments(parser, programs, default) 147 148 # LUCI builders extract the list of steps from the program and run them 149 # individually for a better UX in MILO. 150 parser.add_argument( 151 '--only-list-steps', 152 action='store_true', 153 help=argparse.SUPPRESS, 154 ) 155 156 157def run( 158 program: Sequence[Callable], 159 output_directory: Optional[Path], 160 package_root: Path, 161 clear: bool, 162 root: Path = None, 163 repositories: Collection[Path] = (), 164 only_list_steps=False, 165 **other_args, 166) -> int: 167 """Processes arguments from add_arguments and runs the presubmit. 168 169 Args: 170 program: from the --program option 171 output_directory: from --output-directory option 172 package_root: from --package-root option 173 clear: from the --clear option 174 root: base path from which to run presubmit checks; defaults to the root 175 of the current directory's repository 176 repositories: roots of Git repositories on which to run presubmit checks; 177 defaults to the root of the current directory's repository 178 only_list_steps: list the steps that would be executed, one per line, 179 instead of executing them 180 **other_args: remaining arguments defined by by add_arguments 181 182 Returns: 183 exit code for sys.exit; 0 if succesful, 1 if an error occurred 184 """ 185 if root is None: 186 root = git_repo.root() 187 188 if not repositories: 189 repositories = [root] 190 191 if output_directory is None: 192 output_directory = root / DEFAULT_PATH 193 194 output_directory.mkdir(parents=True, exist_ok=True) 195 output_directory.joinpath('README.txt').write_text( 196 _OUTPUT_PATH_README.format(repo=root)) 197 198 if not package_root: 199 package_root = output_directory / 'packages' 200 201 _LOG.debug('Using environment at %s', output_directory) 202 203 if clear: 204 _LOG.info('Clearing presubmit output directory') 205 206 if output_directory.exists(): 207 shutil.rmtree(output_directory) 208 _LOG.info('Deleted %s', output_directory) 209 210 return 0 211 212 if presubmit.run(program, 213 root, 214 repositories, 215 only_list_steps=only_list_steps, 216 output_directory=output_directory, 217 package_root=package_root, 218 **other_args): 219 return 0 220 221 return 1 222