pw_presubmit: Add package_root to FormatContext

Add a package_root member to FormatContext, matching PresubmitContext.
This allows formatting steps to install packages if necessary.

Also moved around a couple things. The install_package function is moved
from build.py to presubmit.py, since it doesn't have anything to do with
building. And the FormatContext class is moved from format_code.py to
presubmit.py so functions in presubmit.py can operate on
Union[FormatContext, PresubmitContext] and not just PresubmitContext.

Bug: b/267680540
Change-Id: I0b57e0ead53c0c7de3a722b951f6617767ced65b
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/127410
Reviewed-by: Ben Lawson <benlawson@google.com>
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/pw_presubmit/py/pw_presubmit/build.py b/pw_presubmit/py/pw_presubmit/build.py
index 53ea123..15aee03 100644
--- a/pw_presubmit/py/pw_presubmit/build.py
+++ b/pw_presubmit/py/pw_presubmit/build.py
@@ -41,7 +41,6 @@
     Union,
 )
 
-from pw_package import package_manager
 from pw_presubmit import (
     bazel_parser,
     call,
@@ -49,6 +48,7 @@
     FileFilter,
     filter_paths,
     format_code,
+    install_package,
     Iterator,
     log_run,
     ninja_parser,
@@ -104,23 +104,6 @@
         raise exc
 
 
-def install_package(
-    ctx: PresubmitContext, name: str, force: bool = False
-) -> None:
-    """Install package with given name in given path."""
-    root = ctx.package_root
-    mgr = package_manager.PackageManager(root)
-
-    if not mgr.list():
-        raise PresubmitFailure(
-            'no packages configured, please import your pw_package '
-            'configuration module'
-        )
-
-    if not mgr.status(name) or force:
-        mgr.install(name, force=force)
-
-
 def _gn_value(value) -> str:
     if isinstance(value, bool):
         return str(value).lower()
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index 596808c..ceaf64e 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -21,7 +21,6 @@
 
 import argparse
 import collections
-import dataclasses
 import difflib
 import logging
 import os
@@ -56,36 +55,19 @@
 import pw_cli.color
 import pw_cli.env
 from pw_presubmit.presubmit import FileFilter
-from pw_presubmit import cli, git_repo, owners_checks, PresubmitContext
+from pw_presubmit import (
+    cli,
+    FormatContext,
+    git_repo,
+    owners_checks,
+    PresubmitContext,
+)
 from pw_presubmit.tools import exclude_paths, file_summary, log_run, plural
 
 _LOG: logging.Logger = logging.getLogger(__name__)
 _COLOR = pw_cli.color.colors()
 _DEFAULT_PATH = Path('out', 'format')
 
-
-@dataclasses.dataclass
-class FormatContext:
-    """Context passed into formatting helpers.
-
-    This class is a subset of PresubmitContext (from presubmit.py) containing
-    only what's needed by this function.
-
-    For full documentation on the members see the PresubmitContext section of
-    pw_presubmit/docs.rst.
-
-    Args:
-        root: Source checkout root directory
-        output_dir: Output directory for this specific language
-        paths: Modified files for the presubmit step to check (often used in
-            formatting steps but ignored in compile steps)
-    """
-
-    root: Optional[Path]
-    output_dir: Path
-    paths: Tuple[Path, ...]
-
-
 _Context = Union[PresubmitContext, FormatContext]
 
 
@@ -663,11 +645,13 @@
         files: Iterable[Path],
         output_dir: Path,
         code_formats: Collection[CodeFormat] = CODE_FORMATS_WITH_YAPF,
+        package_root: Optional[Path] = None,
     ):
         self.root = root
         self.paths = list(files)
         self._formats: Dict[CodeFormat, List] = collections.defaultdict(list)
         self.root_output_dir = output_dir
+        self.package_root = package_root or output_dir / 'packages'
 
         for path in self.paths:
             for code_format in code_formats:
@@ -688,6 +672,7 @@
             root=self.root,
             output_dir=outdir,
             paths=tuple(self._formats[code_format]),
+            package_root=self.package_root,
         )
 
     def check(self) -> Dict[Path, str]:
@@ -735,6 +720,7 @@
     base: str,
     code_formats: Collection[CodeFormat] = CODE_FORMATS_WITH_YAPF,
     output_directory: Optional[Path] = None,
+    package_root: Optional[Path] = None,
 ) -> int:
     """Checks or fixes formatting for files in a Git repo."""
 
@@ -778,6 +764,7 @@
         repo=repo,
         code_formats=code_formats,
         output_directory=output_directory,
+        package_root=package_root,
     )
 
 
@@ -787,6 +774,7 @@
     repo: Optional[Path] = None,
     code_formats: Collection[CodeFormat] = CODE_FORMATS,
     output_directory: Optional[Path] = None,
+    package_root: Optional[Path] = None,
 ) -> int:
     """Checks or fixes formatting for the specified files."""
 
@@ -813,6 +801,7 @@
         code_formats=code_formats,
         root=root,
         output_dir=output_dir,
+        package_root=package_root,
     )
 
     _LOG.info('Checking formatting for %s', plural(formatter.paths, 'file'))
@@ -894,6 +883,13 @@
         type=Path,
         help=f"Output directory (default: {'<repo root>' / _DEFAULT_PATH})",
     )
+    parser.add_argument(
+        '--package-root',
+        type=Path,
+        default=Path(os.environ['PW_PACKAGE_ROOT']),
+        help='Package root directory',
+    )
+
     return parser
 
 
diff --git a/pw_presubmit/py/pw_presubmit/presubmit.py b/pw_presubmit/py/pw_presubmit/presubmit.py
index 24f5309..a9e9917 100644
--- a/pw_presubmit/py/pw_presubmit/presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/presubmit.py
@@ -75,6 +75,7 @@
 
 import pw_cli.color
 import pw_cli.env
+from pw_package import package_manager
 from pw_presubmit import git_repo, tools
 from pw_presubmit.tools import plural
 
@@ -348,6 +349,30 @@
 
 
 @dataclasses.dataclass
+class FormatContext:
+    """Context passed into formatting helpers.
+
+    This class is a subset of PresubmitContext containing only what's needed by
+    formatters.
+
+    For full documentation on the members see the PresubmitContext section of
+    pw_presubmit/docs.rst.
+
+    Args:
+        root: Source checkout root directory
+        output_dir: Output directory for this specific language
+        paths: Modified files for the presubmit step to check (often used in
+            formatting steps but ignored in compile steps)
+        package_root: Root directory for pw package installations
+    """
+
+    root: Optional[Path]
+    output_dir: Path
+    paths: Tuple[Path, ...]
+    package_root: Path
+
+
+@dataclasses.dataclass
 class PresubmitContext:
     """Context passed into presubmit checks.
 
@@ -1248,3 +1273,22 @@
 
     if process.returncode:
         raise PresubmitFailure
+
+
+def install_package(
+    ctx: Union[FormatContext, PresubmitContext],
+    name: str,
+    force: bool = False,
+) -> None:
+    """Install package with given name in given path."""
+    root = ctx.package_root
+    mgr = package_manager.PackageManager(root)
+
+    if not mgr.list():
+        raise PresubmitFailure(
+            'no packages configured, please import your pw_package '
+            'configuration module'
+        )
+
+    if not mgr.status(name) or force:
+        mgr.install(name, force=force)