pw_ide: CLI tools for C++ comp. DBs
This surfaces a set of CLI commands for accessing the C++/clangd
compilation database management features.
This also scaffolds the git-subcommand-like structure of the `pw ide`
CLI.
Change-Id: Ia6cf86aa91ff51d10f5fc1cfaef742ce46eebdcf
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/110255
Commit-Queue: Chad Norvell <chadnorvell@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
diff --git a/PW_PLUGINS b/PW_PLUGINS
index 2c71655..e368610 100644
--- a/PW_PLUGINS
+++ b/PW_PLUGINS
@@ -13,6 +13,7 @@
# Pigweed's presubmit check script
format pw_presubmit.format_code _pigweed_upstream_main
heap-viewer pw_allocator.heap_viewer main
+ide pw_ide.__main__ main
package pw_package.pigweed_packages main
presubmit pw_presubmit.pigweed_presubmit main
requires pw_cli.requires main
diff --git a/pw_ide/docs.rst b/pw_ide/docs.rst
index 2663067..ebc11a8 100644
--- a/pw_ide/docs.rst
+++ b/pw_ide/docs.rst
@@ -22,3 +22,49 @@
target. For example, GN outputs build artifacts for the
``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.
+
+C++ Code Intelligence via ``clangd``
+====================================
+`clangd <https://clangd.llvm.org/>`_ is a language server that provides C/C++
+code intelligence features to any editor that supports the language server
+protocol (LSP). It uses a
+`compilation database <https://clang.llvm.org/docs/JSONCompilationDatabase.html>`_,
+a JSON file containing the compile commands for the project. Projects that have
+multiple targets and/or use multiple toolchains need separate compilation
+databases for each target/toolchain. ``pw_ide`` provides tools for managing
+those databases.
+
+Assuming you have a compilation database output from a build system, start with:
+
+.. code-block:: bash
+
+ pw ide cpp --process <path to your compile_commands.json>
+
+The ``pw_ide`` working directory will now contain one or more compilation
+database files, each for a separate target among the targets defined in
+``.pw_ide.yaml``. List the available targets with:
+
+.. code-block:: bash
+
+ pw ide cpp --list
+
+Then set the target that ``clangd`` should use with:
+
+.. code-block:: bash
+
+ pw ide cpp --set <selected target name>
+
+``clangd`` can now be configured to point to the ``compile_commands.json`` file
+in the ``pw_ide`` working directory and provide code intelligence for the
+selected target. If you select a new target, ``clangd`` *does not* need to be
+reconfigured to look at a new file (in other words, ``clangd`` can always be
+pointed at the same, stable ``compile_commands.json`` file). However,
+``clangd`` may need to be restarted when the target changes.
+
+``clangd`` must be run within the activated Pigweed environment in order for
+correct toolchain paths and sysroots to be detected. If you launch your editor
+from the terminal in an activated environment, then nothing special needs to be
+done (e.g. running ``vim`` from the terminal). But if you launch your editor
+outside of the activated environment (e.g. launching Visual Studio Code from
+your GUI shell's launcher), you will need to use the included wrapper
+``clangd.sh`` instead of directly using the ``clangd`` binary.
diff --git a/pw_ide/py/BUILD.gn b/pw_ide/py/BUILD.gn
index 7570575..b9ab35c 100644
--- a/pw_ide/py/BUILD.gn
+++ b/pw_ide/py/BUILD.gn
@@ -25,6 +25,7 @@
sources = [
"pw_ide/__init__.py",
"pw_ide/__main__.py",
+ "pw_ide/commands.py",
"pw_ide/cpp.py",
"pw_ide/exceptions.py",
"pw_ide/settings.py",
diff --git a/pw_ide/py/pw_ide/__main__.py b/pw_ide/py/pw_ide/__main__.py
index 5f6ba72..a7a8b82 100644
--- a/pw_ide/py/pw_ide/__main__.py
+++ b/pw_ide/py/pw_ide/__main__.py
@@ -13,12 +13,170 @@
# the License.
"""Configure IDE support for Pigweed projects."""
+import argparse
+from pathlib import Path
import sys
-from typing import NoReturn
+from typing import (Any, Callable, cast, Dict, Generic, NoReturn, Optional,
+ TypeVar, Union)
+from pw_ide.commands import cmd_cpp
+
+# TODO(chadnorvell): Move this docstring-as-argparse-docs functionality
+# to pw_cli.
+T = TypeVar('T')
+
+
+class Maybe(Generic[T]):
+ """A very rudimentary monadic option type.
+
+ This makes it easier to multiple chain T -> T and T -> T? functions when
+ dealing with Optional[T] values.
+
+ For example, rather than doing:
+
+ .. code-block:: python
+
+ def f(x: T) -> Optional[T]: ...
+ def g(x: T) -> T: ...
+
+ def foo(i: Optional[T]) -> Optional[T]:
+ if i is None:
+ return None
+
+ i = f(i)
+
+ if j is None:
+ return None
+
+ return g(j)
+
+ ... which becomes tedious as the number of functions increases, instead do:
+
+ .. code-block:: python
+
+ def f(x: T) -> Optional[T]: ...
+ def g(x: T) -> T: ...
+
+ def foo(i: Optional[T]) -> Optional[T]:
+ return Maybe(i)\
+ .and_then(f)
+ .and_then(g)
+ .to_optional()
+ """
+ def __init__(self, value: Optional[T]) -> None:
+ self.value: Optional[T] = value
+
+ def and_then(
+ self, func: Union[Callable[[T], T],
+ Callable[[T], 'Maybe[T]']]) -> 'Maybe[T]':
+ if self.value is None:
+ return self
+
+ result = func(self.value)
+
+ if isinstance(result, self.__class__):
+ return cast('Maybe[T]', result)
+
+ return self.__class__(cast(T, result))
+
+ def to_optional(self) -> Optional[T]:
+ return self.value
+
+
+def _reflow_multiline_string(doc: str) -> str:
+ """Reflow a multi-line string by removing formatting newlines.
+
+ This works on any string, but is intended for docstrings, where newlines
+ are added within sentences and paragraphs to satisfy the whims of line
+ length limits.
+
+ Paragraphs are assumed to be surrounded by blank lines. They will be joined
+ together without newlines, then each paragraph will be rejoined with blank
+ line separators.
+ """
+ return '\n\n'.join([para.replace('\n', ' ') for para in doc.split('\n\n')])
+
+
+def _multiline_string_summary(doc: str) -> str:
+ """Return the one-line summary from a multi-line string.
+
+ This works on any string, but is intended for docstrings, where you
+ typically have a single line summary.
+ """
+ return doc.split('\n\n', maxsplit=1)[0]
+
+
+def _reflow_docstring(doc: Optional[str]) -> Optional[str]:
+ """Reflow an docstring."""
+ return Maybe(doc)\
+ .and_then(_reflow_multiline_string)\
+ .to_optional()
+
+
+def _docstring_summary(doc: Optional[str]) -> Optional[str]:
+ """Return the one-line summary of a docstring."""
+ return Maybe(doc)\
+ .and_then(_reflow_multiline_string)\
+ .and_then(_multiline_string_summary)\
+ .to_optional()
+
+
+def _parse_args() -> argparse.Namespace:
+ parser_root = argparse.ArgumentParser(prog='pw ide', description=__doc__)
+
+ parser_root.set_defaults(
+ func=lambda *_args, **_kwargs: parser_root.print_help())
+
+ subcommand_parser = parser_root.add_subparsers(help='Subcommands')
+
+ parser_cpp = subcommand_parser.add_parser(
+ 'cpp',
+ description=_docstring_summary(cmd_cpp.__doc__),
+ help=_reflow_docstring(cmd_cpp.__doc__))
+ parser_cpp.add_argument('-l',
+ '--list',
+ dest='should_list_targets',
+ action='store_true',
+ help='List the targets available for C/C++ '
+ 'language analysis.')
+ parser_cpp.add_argument('-s',
+ '--set',
+ dest='target_to_set',
+ metavar='TARGET',
+ help='Set the target to use for C/C++ language '
+ 'server analysis.')
+ parser_cpp.add_argument('--no-override',
+ dest='override_current_target',
+ action='store_const',
+ const=False,
+ default=True,
+ help='If called with --set, don\'t override the '
+ 'current target if one is already set.')
+ parser_cpp.add_argument('-p',
+ '--process',
+ dest='compdb_file_path',
+ type=Path,
+ metavar='COMPILATION_DATABASE_FILE',
+ help='Process a file matching the clang '
+ 'compilation database format.')
+ parser_cpp.set_defaults(func=cmd_cpp)
+
+ args = parser_root.parse_args()
+ return args
+
+
+def _dispatch_command(func: Callable, **kwargs: Dict[str, Any]) -> int:
+ """Dispatch arguments to a subcommand handler.
+
+ Each CLI subcommand is handled by handler function, which is registered
+ with the subcommand parser with `parser.set_defaults(func=handler)`.
+ By calling this function with the parsed args, the appropriate subcommand
+ handler is called, and the arguments are passed to it as kwargs.
+ """
+ return func(**kwargs)
def main() -> NoReturn:
- sys.exit(0)
+ sys.exit(_dispatch_command(**vars(_parse_args())))
if __name__ == '__main__':
diff --git a/pw_ide/py/pw_ide/commands.py b/pw_ide/py/pw_ide/commands.py
new file mode 100644
index 0000000..17b55c6
--- /dev/null
+++ b/pw_ide/py/pw_ide/commands.py
@@ -0,0 +1,112 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""pw_ide CLI command implementations."""
+
+from pathlib import Path
+import sys
+from typing import Optional
+
+from pw_ide.cpp import (get_defined_available_targets, get_target,
+ process_compilation_database, set_target,
+ write_compilation_databases)
+
+from pw_ide.exceptions import (BadCompDbException, InvalidTargetException,
+ MissingCompDbException)
+
+from pw_ide.settings import IdeSettings
+
+
+def _print_current_target(settings: IdeSettings) -> None:
+ print('Current C/C++ language server analysis target: '
+ f'{get_target(settings)}\n')
+
+
+def _print_defined_available_targets(settings: IdeSettings) -> None:
+ print('C/C++ targets available for language server analysis:')
+
+ for toolchain in sorted(get_defined_available_targets(settings)):
+ print(f'\t{toolchain}')
+
+ print('')
+
+
+def cmd_cpp(
+ should_list_targets: bool,
+ target_to_set: Optional[str],
+ compdb_file_path: Optional[Path],
+ override_current_target: bool = True,
+ settings: IdeSettings = IdeSettings()
+) -> None:
+ """Configure C/C++ IDE support for Pigweed projects.
+
+ Provides tools for processing C/C++ compilation databases and setting the
+ particular target/toochain to use for code analysis."""
+ default = True
+
+ if should_list_targets:
+ default = False
+ _print_defined_available_targets(settings)
+
+ # Order of operations matters here. It should be possible to process a
+ # compilation database then set successfully set the target in a single
+ # command.
+ if compdb_file_path is not None:
+ default = False
+ try:
+ write_compilation_databases(
+ process_compilation_database(
+ compdb_file_path,
+ settings,
+ ),
+ settings,
+ )
+
+ print(
+ f'Processed {str(compdb_file_path)} to {settings.working_dir}')
+ except MissingCompDbException:
+ print(f'File not found: {str(compdb_file_path)}')
+ sys.exit(1)
+ except BadCompDbException:
+ print('File does not match compilation database format: '
+ f'{str(compdb_file_path)}')
+ sys.exit(1)
+
+ if target_to_set is not None:
+ default = False
+ should_set_target = get_target(settings) is None \
+ or override_current_target
+
+ if should_set_target:
+ try:
+ set_target(target_to_set, settings)
+ except InvalidTargetException:
+ print(f'Invalid target! {target_to_set} not among the '
+ 'available defined targets.\n\n'
+ 'Check .pw_ide.yaml or .pw_ide.user.yaml for defined '
+ 'targets.')
+ sys.exit(1)
+ except MissingCompDbException:
+ print(f'File not found for target! {target_to_set}\n'
+ 'Did you run pw ide cpp --process '
+ '{path to compile_commands.json}?')
+ sys.exit(1)
+
+ print('Set C/C++ language server analysis target to: '
+ f'{target_to_set}')
+ else:
+ print('Target already is set and will not be overridden.')
+ _print_current_target(settings)
+
+ if default:
+ _print_current_target(settings)