pw_presubmit: Add option to continue on failures

Add an option to continue with ninja/bazel invocations on compile
failures.

Bug: b/246966698
Change-Id: I89aa41656f170054d4af6be11f8c6c651f4dd41f
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/110473
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Ted Pudlik <tpudlik@google.com>
diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst
index f03d57d..4083837 100644
--- a/pw_presubmit/docs.rst
+++ b/pw_presubmit/docs.rst
@@ -115,6 +115,8 @@
 * ``override_gn_args``: Additional GN args processed by ``build.gn_gen()``
 * ``luci``: Information about the LUCI build or None if not running in LUCI
 * ``num_jobs``: Number of jobs to run in parallel
+* ``continue_after_build_error``: For steps that compile, don't exit on the
+  first compilation error
 
 The ``luci`` member is of type ``LuciContext`` and has the following members:
 
diff --git a/pw_presubmit/py/pw_presubmit/build.py b/pw_presubmit/py/pw_presubmit/build.py
index de72d92..6aca99a 100644
--- a/pw_presubmit/py/pw_presubmit/build.py
+++ b/pw_presubmit/py/pw_presubmit/build.py
@@ -53,6 +53,10 @@
     if ctx.num_jobs is not None:
         num_jobs.extend(('--jobs', str(ctx.num_jobs)))
 
+    keep_going: List[str] = []
+    if ctx.continue_after_build_error:
+        keep_going.append('--keep_going')
+
     call('bazel',
          cmd,
          '--verbose_failures',
@@ -60,6 +64,7 @@
          '--worker_verbose',
          f'--symlink_prefix={ctx.output_dir / ".bazel-"}',
          *num_jobs,
+         *keep_going,
          *args,
          cwd=ctx.root,
          env=env_with_clang_vars())
@@ -159,6 +164,10 @@
     if ctx.num_jobs is not None:
         num_jobs.extend(('-j', str(ctx.num_jobs)))
 
+    keep_going: List[str] = []
+    if ctx.continue_after_build_error:
+        keep_going.extend(('-k', '0'))
+
     if save_compdb:
         proc = subprocess.run(
             ['ninja', '-C', ctx.output_dir, '-t', 'compdb', *args],
@@ -173,7 +182,8 @@
             **kwargs)
         (ctx.output_dir / 'ninja.graph').write_bytes(proc.stdout)
 
-    call('ninja', '-C', ctx.output_dir, *num_jobs, *args, **kwargs)
+    call('ninja', '-C', ctx.output_dir, *num_jobs, *keep_going, *args,
+         **kwargs)
     (ctx.output_dir / '.ninja_log').rename(ctx.output_dir / 'ninja.log')
 
 
diff --git a/pw_presubmit/py/pw_presubmit/cli.py b/pw_presubmit/py/pw_presubmit/cli.py
index 31566b8..bda9a7b 100644
--- a/pw_presubmit/py/pw_presubmit/cli.py
+++ b/pw_presubmit/py/pw_presubmit/cli.py
@@ -133,10 +133,16 @@
     """Adds common presubmit check options to an argument parser."""
 
     add_path_arguments(parser)
-    parser.add_argument('-k',
-                        '--keep-going',
-                        action='store_true',
-                        help='Continue instead of aborting when errors occur.')
+    parser.add_argument(
+        '-k',
+        '--keep-going',
+        action='store_true',
+        help='Continue running presubmit steps after a failure.')
+    parser.add_argument(
+        '--continue-after-build-error',
+        action='store_true',
+        help=('Within presubmit steps, continue running build steps after a '
+              'failure.'))
     parser.add_argument(
         '--output-directory',
         type=Path,
diff --git a/pw_presubmit/py/pw_presubmit/presubmit.py b/pw_presubmit/py/pw_presubmit/presubmit.py
index dcfaf7b..68a6f46 100644
--- a/pw_presubmit/py/pw_presubmit/presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/presubmit.py
@@ -220,6 +220,7 @@
     luci: Optional[LuciContext]
     override_gn_args: Dict[str, str]
     num_jobs: Optional[int] = None
+    continue_after_build_error: bool = False
     _failed: bool = False
 
     @property
@@ -312,7 +313,8 @@
     """Runs a series of presubmit checks on a list of files."""
     def __init__(self, root: Path, repos: Sequence[Path],
                  output_directory: Path, paths: Sequence[Path],
-                 package_root: Path, override_gn_args: Dict[str, str]):
+                 package_root: Path, override_gn_args: Dict[str, str],
+                 continue_after_build_error: bool):
         self._root = root.resolve()
         self._repos = tuple(repos)
         self._output_directory = output_directory.resolve()
@@ -321,6 +323,7 @@
             tools.relative_paths(self._paths, self._root))
         self._package_root = package_root.resolve()
         self._override_gn_args = override_gn_args
+        self._continue_after_build_error = continue_after_build_error
 
     def run(self, program: Program, keep_going: bool = False) -> bool:
         """Executes a series of presubmit checks on the paths."""
@@ -430,6 +433,7 @@
                 paths=paths,
                 package_root=self._package_root,
                 override_gn_args=self._override_gn_args,
+                continue_after_build_error=self._continue_after_build_error,
                 luci=LuciContext.create_from_environment(),
             )
 
@@ -499,7 +503,8 @@
         package_root: Path = None,
         only_list_steps: bool = False,
         override_gn_args: Sequence[Tuple[str, str]] = (),
-        keep_going: bool = False) -> bool:
+        keep_going: bool = False,
+        continue_after_build_error: bool = False) -> bool:
     """Lists files in the current Git repo and runs a Presubmit with them.
 
     This changes the directory to the root of the Git repository after listing
@@ -524,7 +529,8 @@
         package_root: where to place package files
         only_list_steps: print step names instead of running them
         override_gn_args: additional GN args to set on steps
-        keep_going: whether to continue running checks if an error occurs
+        keep_going: continue running presubmit steps after a step fails
+        continue_after_build_error: continue building if a build step fails
 
     Returns:
         True if all presubmit checks succeeded
@@ -567,6 +573,7 @@
         paths=files,
         package_root=package_root,
         override_gn_args=dict(override_gn_args or {}),
+        continue_after_build_error=continue_after_build_error,
     )
 
     if only_list_steps: