• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1.. _module-pw_presubmit:
2
3============
4pw_presubmit
5============
6.. pigweed-module::
7   :name: pw_presubmit
8
9The presubmit module provides Python tools for running presubmit checks and
10checking and fixing code format. It also includes the presubmit check script for
11the Pigweed repository, ``pigweed_presubmit.py``.
12
13Presubmit checks are essential tools, but they take work to set up, and
14projects don’t always get around to it. The ``pw_presubmit`` module provides
15tools for setting up high quality presubmit checks for any project. We use this
16framework to run Pigweed’s presubmit on our workstations and in our automated
17building tools.
18
19The ``pw_presubmit`` module also includes ``pw format``, a tool that provides a
20unified interface for automatically formatting code in a variety of languages.
21With ``pw format``, you can format Bazel, C, C++, Python, GN, and Go code
22according to configurations defined by your project. ``pw format`` leverages
23existing tools like ``clang-format``, and it’s simple to add support for new
24languages. (Note: Bazel formatting requires ``buildifier`` to be present on your
25system. If it's not Bazel formatting passes without checking.)
26
27.. image:: docs/pw_presubmit_demo.gif
28   :alt: ``pw format`` demo
29   :align: left
30
31The ``pw_presubmit`` package includes presubmit checks that can be used with any
32project. These checks include:
33
34.. todo-check: disable
35
36* Check code format of several languages including C, C++, and Python
37* Initialize a Python environment
38* Run all Python tests
39* Run pylint
40* Run mypy
41* Ensure source files are included in the GN and Bazel builds
42* Build and run all tests with GN
43* Build and run all tests with Bazel
44* Ensure all header files contain ``#pragma once`` (or, that they have matching
45  ``#ifndef``/``#define`` lines)
46* Ensure lists are kept in alphabetical order
47* Forbid non-inclusive language
48* Check format of TODO lines
49* Apply various rules to ``.gitmodules`` or ``OWNERS`` files
50* Ensure all source files are in the build
51
52.. todo-check: enable
53
54-------------
55Compatibility
56-------------
57Python 3
58
59-------------------------------------------
60Creating a presubmit check for your project
61-------------------------------------------
62Creating a presubmit check for a project using ``pw_presubmit`` is simple, but
63requires some customization. Projects must define their own presubmit check
64Python script that uses the ``pw_presubmit`` package.
65
66A project's presubmit script can be registered as a
67:ref:`pw_cli <module-pw_cli>` plugin, so that it can be run as ``pw
68presubmit``.
69
70Setting up the command-line interface
71=====================================
72The ``pw_presubmit.cli`` module sets up the command-line interface for a
73presubmit script. This defines a standard set of arguments for invoking
74presubmit checks. Its use is optional, but recommended.
75
76Common ``pw presubmit`` command line arguments
77----------------------------------------------
78.. argparse::
79   :module: pw_presubmit.cli
80   :func: _get_default_parser
81   :prog: pw presubmit
82   :nodefaultconst:
83   :nodescription:
84   :noepilog:
85
86
87``pw_presubmit.cli`` Python API
88-------------------------------
89.. automodule:: pw_presubmit.cli
90   :members: add_arguments, run
91
92
93Presubmit output directory
94--------------------------
95The ``pw_presubmit`` command line interface includes an ``--output-directory``
96option that specifies the working directory to use for presubmits. The default
97path is ``out/presubmit``.  A subdirectory is created for each presubmit step.
98This directory persists between presubmit runs and can be cleaned by deleting it
99or running ``pw presubmit --clean``.
100
101.. _module-pw_presubmit-presubmit-checks:
102
103Presubmit checks
104================
105A presubmit check is defined as a function or other callable. The function must
106accept one argument: a ``PresubmitContext``, which provides the paths on which
107to run. Presubmit checks communicate failure by raising an exception.
108
109Presubmit checks may use the ``filter_paths`` decorator to automatically filter
110the paths list for file types they care about.
111
112Either of these functions could be used as presubmit checks:
113
114.. code-block:: python
115
116   @pw_presubmit.filter_paths(endswith='.py')
117   def file_contains_ni(ctx: PresubmitContext):
118       for path in ctx.paths:
119           with open(path) as file:
120               contents = file.read()
121               if 'ni' not in contents and 'nee' not in contents:
122                   raise PresumitFailure('Files must say "ni"!', path=path)
123
124   def run_the_build(_):
125       subprocess.run(['make', 'release'], check=True)
126
127Presubmit checks functions are grouped into "programs" -- a named series of
128checks. Projects may find it helpful to have programs for different purposes,
129such as a quick program for local use and a full program for automated use. The
130:ref:`example script <example-script>` uses ``pw_presubmit.Programs`` to define
131``quick`` and ``full`` programs.
132
133By default, presubmit steps are only run on files changed since ``@{upstream}``.
134If all such files are filtered out by ``filter_paths``, then that step will be
135skipped. This can be overridden with the ``--base`` and ``--full`` arguments to
136``pw presubmit``. In automated testing ``--full`` is recommended, except for
137lint/format checks where ``--base HEAD~1`` is recommended.
138
139.. autoclass:: pw_presubmit.presubmit_context.PresubmitContext
140   :members:
141   :noindex:
142
143Additional members can be added by subclassing ``PresubmitContext`` and
144``Presubmit``. Then override ``Presubmit._create_presubmit_context()`` to
145return the subclass of ``PresubmitContext``. Finally, add
146``presubmit_class=PresubmitSubClass`` when calling ``cli.run()``.
147
148.. autoclass:: pw_presubmit.presubmit_context.LuciContext
149   :members:
150   :noindex:
151
152.. autoclass:: pw_presubmit.presubmit_context.LuciPipeline
153   :members:
154   :noindex:
155
156.. autoclass:: pw_presubmit.presubmit_context.LuciTrigger
157   :members:
158   :noindex:
159
160Substeps
161--------
162Presubmit steps can define substeps that can run independently in other tooling.
163These steps should subclass ``SubStepCheck`` and must define a ``substeps()``
164method that yields ``SubStep`` objects. ``SubStep`` objects have the following
165members:
166
167* ``name``: Name of the substep
168* ``_func``: Substep code
169* ``args``: Positional arguments for ``_func``
170* ``kwargs``: Keyword arguments for ``_func``
171
172``SubStep`` objects must have unique names. For a detailed example of a
173``SubStepCheck`` subclass see ``GnGenNinja`` in ``build.py``.
174
175Existing Presubmit Checks
176-------------------------
177A small number of presubmit checks are made available through ``pw_presubmit``
178modules.
179
180Code Formatting
181^^^^^^^^^^^^^^^
182Formatting checks for a variety of languages are available from
183``pw_presubmit.format_code``. These include C/C++, Java, Go, Python, GN, and
184others. All of these checks can be included by adding
185``pw_presubmit.format_code.presubmit_checks()`` to a presubmit program. These
186all use language-specific formatters like clang-format or black.
187
188Example changes demonstrating how to add formatters:
189
190* `CSS <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178810>`_
191* `JSON <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171991>`_
192* `reStructuredText <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168541>`_
193* `TypeScript <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164825>`_
194
195These will suggest fixes using ``pw format --fix``.
196
197Options for code formatting can be specified in the ``pigweed.json`` file
198(see also :ref:`SEED-0101 <seed-0101>`). These apply to both ``pw presubmit``
199steps that check code formatting and ``pw format`` commands that either check
200or fix code formatting.
201
202* ``python_formatter``: Choice of Python formatter. Options are ``black``
203  (default, used by Pigweed itself) and ``yapf``.
204* ``black_path``: If ``python_formatter`` is ``black``, use this as the
205  executable instead of ``black``.
206* ``black_config_file``: Set the config file for the black formatter.
207* ``exclude``: List of path regular expressions to ignore. Will be evaluated
208  against paths relative to the checkout root using ``re.search``.
209
210Example section from a ``pigweed.json`` file:
211
212.. code-block:: json
213
214   {
215     "pw": {
216       "pw_presubmit": {
217         "format": {
218           "python_formatter": "black",
219           "black_config_file": "$pw_env{PW_PROJECT_ROOT}/config/.black.toml"
220           "black_path": "black",
221           "exclude": [
222             "\\bthird_party/foo/src"
223           ]
224         }
225       }
226     }
227   }
228
229Sorted Blocks
230^^^^^^^^^^^^^
231Blocks of code can be required to be kept in sorted order using comments like
232the following:
233
234.. code-block:: python
235
236   # keep-sorted: start
237   bar
238   baz
239   foo
240   # keep-sorted: end
241
242This can be included by adding ``pw_presubmit.keep_sorted.presubmit_check`` to a
243presubmit program. Adding ``ignore-case`` to the start line will use
244case-insensitive sorting.
245
246By default, duplicates will be removed. Lines that are identical except in case
247are preserved, even with ``ignore-case``. To allow duplicates, add
248``allow-dupes`` to the start line.
249
250Prefixes can be ignored by adding ``ignore-prefix=`` followed by a
251comma-separated list of prefixes. The list below will be kept in this order.
252Neither commas nor whitespace are supported in prefixes.
253
254.. code-block:: python
255
256   # keep-sorted: start ignore-prefix=',"
257   'bar',
258   "baz",
259   'foo',
260   # keep-sorted: end
261
262Inline comments are assumed to be associated with the following line. For
263example, the following is already sorted. This can be disabled with
264``sticky-comments=no``.
265
266.. todo-check: disable
267
268.. code-block:: python
269
270   # keep-sorted: start
271   # TODO: b/1234 - Fix this.
272   bar,
273   # TODO: b/5678 - Also fix this.
274   foo,
275   # keep-sorted: end
276
277.. todo-check: enable
278
279By default, the prefix of the keep-sorted line is assumed to be the comment
280marker used by any inline comments. This can be overridden by adding lines like
281``sticky-comments=%,#`` to the start line.
282
283Lines indented more than the preceding line are assumed to be continuations.
284Thus, the following block is already sorted. keep-sorted blocks can not be
285nested, so there's no ability to add a keep-sorted block for the sub-items.
286
287.. code-block::
288
289   # keep-sorted: start
290   * abc
291     * xyz
292     * uvw
293   * def
294   # keep-sorted: end
295
296The presubmit check will suggest fixes using ``pw keep-sorted --fix``.
297
298Future versions may support additional multiline list items.
299
300.gitmodules
301^^^^^^^^^^^
302Various rules can be applied to .gitmodules files. This check can be included
303by adding ``pw_presubmit.gitmodules.create()`` to a presubmit program. This
304function takes an optional argument of type ``pw_presubmit.gitmodules.Config``.
305``Config`` objects have several properties.
306
307* ``allow_submodules: bool = True`` — If false, don't allow any submodules.
308* ``allow_non_googlesource_hosts: bool = False`` — If false, all submodule URLs
309  must be on a Google-managed Gerrit server.
310* ``allowed_googlesource_hosts: Sequence[str] = ()`` — If set, any
311  Google-managed Gerrit URLs for submodules most be in this list. Entries
312  should be like ``pigweed`` for ``pigweed-review.googlesource.com``.
313* ``require_relative_urls: bool = False`` — If true, all submodules must be
314  relative to the superproject remote.
315* ``allow_sso: bool = True`` — If false, ``sso://`` and ``rpc://`` submodule
316  URLs are prohibited.
317* ``allow_git_corp_google_com: bool = True`` — If false, ``git.corp.google.com``
318  submodule URLs are prohibited.
319* ``require_branch: bool = False`` — If true, all submodules must reference a
320  branch.
321* ``validator: Callable[[PresubmitContext, Path, str, dict[str, str]], None] = None``
322  — A function that can be used for arbitrary submodule validation. It's called
323  with the ``PresubmitContext``, the path to the ``.gitmodules`` file, the name
324  of the current submodule, and the properties of the current submodule.
325
326#pragma once
327^^^^^^^^^^^^
328There's a ``pragma_once`` check that confirms the first non-comment line of
329C/C++ headers is ``#pragma once``. This is enabled by adding
330``pw_presubmit.cpp_checks.pragma_once`` to a presubmit program.
331
332#ifndef/#define
333^^^^^^^^^^^^^^^
334There's an ``ifndef_guard`` check that confirms the first two non-comment lines
335of C/C++ headers are ``#ifndef HEADER_H`` and ``#define HEADER_H``. This is
336enabled by adding ``pw_presubmit.cpp_checks.include_guard_check()`` to a
337presubmit program. ``include_guard_check()`` has options for specifying what the
338header guard should be based on the path.
339
340This check is not used in Pigweed itself but is available to projects using
341Pigweed.
342
343.. todo-check: disable
344
345TODO(b/###) Formatting
346^^^^^^^^^^^^^^^^^^^^^^
347There's a check that confirms ``TODO`` lines match a given format. Upstream
348Pigweed expects these to look like ``TODO: https://pwbug.dev/### -
349Explanation``, but projects may define their own patterns instead.
350
351For information on supported TODO expressions, see Pigweed's
352:ref:`docs-pw-todo-style`.
353
354.. todo-check: enable
355
356Python Checks
357^^^^^^^^^^^^^
358There are two checks in the ``pw_presubmit.python_checks`` module, ``gn_pylint``
359and ``gn_python_check``. They assume there's a top-level ``python`` GN target.
360``gn_pylint`` runs Pylint and Mypy checks and ``gn_python_check`` runs Pylint,
361Mypy, and all Python tests.
362
363Inclusive Language
364^^^^^^^^^^^^^^^^^^
365.. inclusive-language: disable
366
367The inclusive language check looks for words that are typical of non-inclusive
368code, like using "master" and "slave" in place of "primary" and "secondary" or
369"sanity check" in place of "consistency check".
370
371.. inclusive-language: enable
372
373These checks can be disabled for individual lines with
374"inclusive-language: ignore" on the line in question or the line above it, or
375for entire blocks by using "inclusive-language: disable" before the block and
376"inclusive-language: enable" after the block.
377
378.. In case things get moved around in the previous paragraphs the enable line
379.. is repeated here: inclusive-language: enable.
380
381OWNERS
382^^^^^^
383There's a check that requires folders matching specific patterns contain
384``OWNERS`` files. It can be included by adding
385``module_owners.presubmit_check()`` to a presubmit program. This function takes
386a callable as an argument that indicates, for a given file, where a controlling
387``OWNERS`` file should be, or returns None if no ``OWNERS`` file is necessary.
388Formatting of ``OWNERS`` files is handled similary to formatting of other
389source files and is discussed in `Code Formatting`.
390
391JSON
392^^^^
393The JSON check requires all ``*.json`` files to be valid JSON files. It can be
394included by adding ``json_check.presubmit_check()`` to a presubmit program.
395
396Source in Build
397^^^^^^^^^^^^^^^
398Pigweed provides checks that source files are configured as part of the build
399for GN, Bazel, and CMake. These can be included by adding
400``source_in_build.gn(filter)`` and similar functions to a presubmit check. The
401CMake check additionally requires a callable that invokes CMake with appropriate
402options.
403
404pw_presubmit
405------------
406.. automodule:: pw_presubmit
407   :members: filter_paths, call, PresubmitFailure, Programs
408
409.. _example-script:
410
411
412Git hook
413--------
414You can run a presubmit program or step as a `git hook
415<https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks>`_ using
416``pw_presubmit.install_hook``.  This can be used to run certain presubmit
417checks before a change is pushed to a remote.
418
419We strongly recommend that you only run fast (< 15 seconds) and trivial checks
420as push hooks, and perform slower or more complex ones in CI. This is because,
421
422* Running slow checks in the push hook will force you to wait longer for
423  ``git push`` to complete, and
424* If your change fails one of the checks at this stage, it will not yet be
425  uploaded to the remote, so you'll have a harder time debugging any failures
426  (sharing the change with your colleagues, linking to it from an issue
427  tracker, etc).
428
429Example
430=======
431A simple example presubmit check script follows. This can be copied-and-pasted
432to serve as a starting point for a project's presubmit check script.
433
434See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
435
436.. code-block:: python
437
438   """Example presubmit check script."""
439
440   import argparse
441   import logging
442   import os
443   from pathlib import Path
444   import re
445   import sys
446
447   try:
448       import pw_cli.log
449   except ImportError:
450       print("ERROR: Activate the environment before running presubmits!", file=sys.stderr)
451       sys.exit(2)
452
453   import pw_presubmit
454   from pw_presubmit import (
455       build,
456       cli,
457       cpp_checks,
458       format_code,
459       inclusive_language,
460       python_checks,
461   )
462   from pw_presubmit.presubmit import filter_paths
463   from pw_presubmit.presubmit_context import PresubmitContext
464   from pw_presubmit.install_hook import install_git_hook
465
466   # Set up variables for key project paths.
467   PROJECT_ROOT = Path(os.environ["MY_PROJECT_ROOT"])
468   PIGWEED_ROOT = PROJECT_ROOT / "pigweed"
469
470   # Rerun the build if files with these extensions change.
471   _BUILD_EXTENSIONS = frozenset(
472       [".rst", ".gn", ".gni", *format_code.C_FORMAT.extensions]
473   )
474
475
476   #
477   # Presubmit checks
478   #
479   def release_build(ctx: PresubmitContext):
480       build.gn_gen(ctx, build_type="release")
481       build.ninja(ctx)
482       build.gn_check(ctx)  # Run after building to check generated files.
483
484
485   def host_tests(ctx: PresubmitContext):
486       build.gn_gen(ctx, run_host_tests="true")
487       build.ninja(ctx)
488       build.gn_check(ctx)
489
490
491   # Avoid running some checks on certain paths.
492   PATH_EXCLUSIONS = (
493       re.compile(r"^external/"),
494       re.compile(r"^vendor/"),
495   )
496
497
498   # Use the upstream pragma_once check, but apply a different set of path
499   # filters with @filter_paths.
500   @filter_paths(endswith=".h", exclude=PATH_EXCLUSIONS)
501   def pragma_once(ctx: PresubmitContext):
502       cpp_checks.pragma_once(ctx)
503
504
505   #
506   # Presubmit check programs
507   #
508   OTHER = (
509       # Checks not ran by default but that should be available. These might
510       # include tests that are expensive to run or that don't yet pass.
511       build.gn_gen_check,
512   )
513
514   QUICK = (
515       # List some presubmit checks to run
516       pragma_once,
517       host_tests,
518       # Use the upstream formatting checks, with custom path filters applied.
519       format_code.presubmit_checks(exclude=PATH_EXCLUSIONS),
520       # Include the upstream inclusive language check.
521       inclusive_language.presubmit_check,
522       # Include just the lint-related Python checks.
523       python_checks.gn_python_lint.with_filter(exclude=PATH_EXCLUSIONS),
524   )
525
526   FULL = (
527       QUICK,  # Add all checks from the 'quick' program
528       release_build,
529       # Use the upstream Python checks, with custom path filters applied.
530       # Checks listed multiple times are only run once.
531       python_checks.gn_python_check.with_filter(exclude=PATH_EXCLUSIONS),
532   )
533
534   PROGRAMS = pw_presubmit.Programs(other=OTHER, quick=QUICK, full=FULL)
535
536
537   #
538   # Allowlist of remote refs for presubmit. If the remote ref being pushed to
539   # matches any of these values (with regex matching), then the presubmits
540   # checks will be run before pushing.
541   #
542   PRE_PUSH_REMOTE_REF_ALLOWLIST = ("refs/for/main",)
543
544
545   def run(install: bool, remote_ref: str | None, **presubmit_args) -> int:
546       """Process the --install argument then invoke pw_presubmit."""
547
548       # Install the presubmit Git pre-push hook, if requested.
549       if install:
550           # '$remote_ref' will be replaced by the actual value of the remote ref
551           # at runtime.
552           install_git_hook(
553               "pre-push",
554               [
555                   "python",
556                   "-m",
557                   "tools.presubmit_check",
558                   "--base",
559                   "HEAD~",
560                   "--remote-ref",
561                   "$remote_ref",
562               ],
563           )
564           return 0
565
566       # Run the checks if either no remote_ref was passed, or if the remote ref
567       # matches anything in the allowlist.
568       if remote_ref is None or any(
569           re.search(pattern, remote_ref)
570           for pattern in PRE_PUSH_REMOTE_REF_ALLOWLIST
571       ):
572           return cli.run(root=PROJECT_ROOT, **presubmit_args)
573       return 0
574
575
576   def main() -> int:
577       """Run the presubmit checks for this repository."""
578       parser = argparse.ArgumentParser(description=__doc__)
579       cli.add_arguments(parser, PROGRAMS, "quick")
580
581       # Define an option for installing a Git pre-push hook for this script.
582       parser.add_argument(
583           "--install",
584           action="store_true",
585           help="Install the presubmit as a Git pre-push hook and exit.",
586       )
587
588       # Define an optional flag to pass the remote ref into this script, if it
589       # is run as a pre-push hook. The destination variable in the parsed args
590       # will be `remote_ref`, as dashes are replaced with underscores to make
591       # valid variable names.
592       parser.add_argument(
593           "--remote-ref",
594           default=None,
595           nargs="?",  # Make optional.
596           help="Remote ref of the push command, for use by the pre-push hook.",
597       )
598
599       return run(**vars(parser.parse_args()))
600
601
602   if __name__ == "__main__":
603       pw_cli.log.install(logging.INFO)
604       sys.exit(main())
605
606---------------------
607Code formatting tools
608---------------------
609The ``pw_presubmit.format_code`` module formats supported source files using
610external code format tools. The file ``format_code.py`` can be invoked directly
611from the command line or from ``pw`` as ``pw format``.
612
613Example
614=======
615A simple example of adding support for a custom format. This code wraps the
616built in formatter to add a new format. It could also be used to replace
617a formatter or remove/disable a PigWeed supplied one.
618
619.. code-block:: python
620
621   #!/usr/bin/env python
622   """Formats files in repository. """
623
624   import logging
625   import sys
626
627   import pw_cli.log
628   from pw_presubmit import format_code
629   from your_project import presubmit_checks
630   from your_project import your_check
631
632   YOUR_CODE_FORMAT = CodeFormat('YourFormat',
633                                 filter=FileFilter(suffix=('.your', )),
634                                 check=your_check.check,
635                                 fix=your_check.fix)
636
637   CODE_FORMATS = (*format_code.CODE_FORMATS, YOUR_CODE_FORMAT)
638
639   def _run(exclude, **kwargs) -> int:
640       """Check and fix formatting for source files in the repo."""
641       return format_code.format_paths_in_repo(exclude=exclude,
642                                               code_formats=CODE_FORMATS,
643                                               **kwargs)
644
645
646   def main():
647       return _run(**vars(format_code.arguments(git_paths=True).parse_args()))
648
649
650   if __name__ == '__main__':
651       pw_cli.log.install(logging.INFO)
652       sys.exit(main())
653
654.. pw_presubmit-nav-end
655
656.. toctree::
657   :maxdepth: 1
658   :hidden:
659
660   format
661