• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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