pw_ide: add setup

The `setup` subcommand is designed to get you from zero to full IDE
feature support with sane defaults in a single command.

Change-Id: I91b01d08deaf9076abdebc09eb3d03f2f4295780
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/110258
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Chad Norvell <chadnorvell@google.com>
diff --git a/.pw_ide.yaml b/.pw_ide.yaml
new file mode 100644
index 0000000..131358f
--- /dev/null
+++ b/.pw_ide.yaml
@@ -0,0 +1,6 @@
+config_title: pw_ide
+
+setup:
+  - pw ide init
+  - gn gen out --export-compile-commands
+  - pw ide cpp --process out/compile_commands.json --set pw_strict_host_clang_debug --no-override
diff --git a/pw_ide/docs.rst b/pw_ide/docs.rst
index d6ed778..16322c8 100644
--- a/pw_ide/docs.rst
+++ b/pw_ide/docs.rst
@@ -8,8 +8,8 @@
 
 Configuration
 =============
-Pigweed IDE settings are stored in the project root in ``.pw_ide.yaml``, in which
-these options can be configured:
+Pigweed IDE settings are stored in the project root in ``.pw_ide.yaml``, in
+which these options can be configured:
 
 * ``working_dir``: The working directory for compilation databases and caches
   (by default this is `.pw_ide` in the project root). This directory shouldn't
@@ -23,11 +23,17 @@
   ``pw_strict_host_clang_debug`` target in a directory with that name in the
   ``out`` directory. So that becomes the canonical name for that target.
 
+* ``setup``: Projects can define a set of steps that automatically set up IDE
+  features with sensible defaults.
+
 Setup
 =====
-The working directory can be created by running ``pw ide init``, although it is
-rarely necessary to run this command manually; other subcommands will initialize
-if needed. You can also clear out the working directory with ``pw ide clean``.
+Most of the time, ``pw ide setup`` is all you need to get started.
+
+The working directory and other components can be created by running
+``pw ide init``, although it is rarely necessary to run this command manually;
+other subcommands will initialize if needed. You can also clear elements of the
+working directory with ``pw ide clean``.
 
 C++ Code Intelligence via ``clangd``
 ====================================
diff --git a/pw_ide/py/pw_ide/__main__.py b/pw_ide/py/pw_ide/__main__.py
index fdc11dd..3a79b65 100644
--- a/pw_ide/py/pw_ide/__main__.py
+++ b/pw_ide/py/pw_ide/__main__.py
@@ -18,7 +18,8 @@
 import sys
 from typing import (Any, Callable, cast, Dict, Generic, NoReturn, Optional,
                     TypeVar, Union)
-from pw_ide.commands import (cmd_info, cmd_init, cmd_cpp, cmd_python)
+from pw_ide.commands import (cmd_info, cmd_init, cmd_cpp, cmd_python,
+                             cmd_setup)
 
 # TODO(chadnorvell): Move this docstring-as-argparse-docs functionality
 # to pw_cli.
@@ -58,8 +59,8 @@
 
         def foo(i: Optional[T]) -> Optional[T]:
             return Maybe(i)\
-                .and_then(f)
-                .and_then(g)
+                .and_then(f)\
+                .and_then(g)\
                 .to_optional()
     """
     def __init__(self, value: Optional[T]) -> None:
@@ -225,6 +226,12 @@
                                'virtual environment.')
     parser_python.set_defaults(func=cmd_python)
 
+    parser_setup = subcommand_parser.add_parser(
+        'setup',
+        description=_docstring_summary(cmd_setup.__doc__),
+        help=_reflow_docstring(cmd_setup.__doc__))
+    parser_setup.set_defaults(func=cmd_setup)
+
     args = parser_root.parse_args()
     return args
 
diff --git a/pw_ide/py/pw_ide/commands.py b/pw_ide/py/pw_ide/commands.py
index e37eb7c..bea5af0 100644
--- a/pw_ide/py/pw_ide/commands.py
+++ b/pw_ide/py/pw_ide/commands.py
@@ -15,6 +15,8 @@
 
 from pathlib import Path
 import platform
+import shlex
+import subprocess
 import sys
 from typing import Callable, Optional
 
@@ -285,3 +287,20 @@
         except UnsupportedPlatformException:
             _print_unsupported_platform_error(
                 'find Python virtual environment')
+
+
+def cmd_setup(settings: IdeSettings = IdeSettings()):
+    """Set up or update your Pigweed project IDE features.
+
+    This will execute all the commands needed to set up your development
+    environment with all the features that Pigweed IDE supports, with sensible
+    defaults. This command is idempotent, so run it whenever you feel like
+    it."""
+
+    if len(settings.setup) == 0:
+        print('This project has no defined setup procedure :(\n'
+              'Refer to the the pw_ide docs to learn how to define a setup!')
+        sys.exit(1)
+
+    for command in settings.setup:
+        subprocess.run(shlex.split(command))
diff --git a/pw_ide/py/pw_ide/settings.py b/pw_ide/py/pw_ide/settings.py
index fb7cd40..ac6d5e4 100644
--- a/pw_ide/py/pw_ide/settings.py
+++ b/pw_ide/py/pw_ide/settings.py
@@ -25,6 +25,7 @@
     os.path.expandvars('$PW_PROJECT_ROOT')) / PW_IDE_DIR_NAME
 
 _DEFAULT_CONFIG = {
+    'setup': [],
     'targets': [],
     'working_dir': _PW_IDE_DEFAULT_DIR,
 }
@@ -72,3 +73,17 @@
         a compilation database.
         """
         return self._config.get('targets', list())
+
+    @property
+    def setup(self) -> List[str]:
+        """`pw ide setup` should do everything necessary to get the project from
+        a fresh checkout to a working default IDE experience. This defines the
+        list of commands that makes that happen.
+
+        Commands need to be formatted as lists in the way that Python's
+        subprocess.run expects, since that's exactly where they're going.
+
+        Note that this command must be idempotent, so that the user can run it
+        whenever they want without a care in the world.
+        """
+        return self._config.get('setup', list())