• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""pw_ide CLI command handlers."""
15
16from glob import iglob
17import logging
18from pathlib import Path
19import shlex
20import shutil
21import subprocess
22import sys
23from typing import cast, Set
24
25from pw_cli.env import pigweed_environment
26from pw_cli.status_reporter import LoggingStatusReporter, StatusReporter
27
28from pw_ide.cpp import (
29    COMPDB_FILE_NAME,
30    ClangdSettings,
31    CppCompilationDatabase,
32    CppCompilationDatabaseFileHashes,
33    CppCompilationDatabaseFileTargets,
34    CppCompilationDatabasesMap,
35    CppIdeFeaturesState,
36    CppIdeFeaturesTarget,
37    find_cipd_installed_exe_path,
38)
39
40from pw_ide.exceptions import (
41    BadCompDbException,
42    InvalidTargetException,
43    MissingCompDbException,
44)
45
46from pw_ide.python import PythonPaths
47
48from pw_ide.settings import (
49    PigweedIdeSettings,
50    SupportedEditor,
51)
52
53from pw_ide import vscode
54from pw_ide.vscode import (
55    build_extension as build_vscode_extension,
56    VscSettingsManager,
57    VscSettingsType,
58)
59
60_LOG = logging.getLogger(__package__)
61env = pigweed_environment()
62
63
64def _inject_reporter(func):
65    """Inject a status reporter instance based on selected output type."""
66
67    def wrapped(*args, **kwargs):
68        output = kwargs.pop('output', 'stdout')
69        reporter = StatusReporter()
70
71        if output == 'log':
72            reporter = LoggingStatusReporter(_LOG)
73
74        kwargs['reporter'] = reporter
75        return func(*args, **kwargs)
76
77    # Hoist the decorated function's docstring onto the new function so that
78    # we can still access it to auto-generate CLI documentation.
79    wrapped.__doc__ = func.__doc__
80    return wrapped
81
82
83def _make_working_dir(
84    reporter: StatusReporter, settings: PigweedIdeSettings, quiet: bool = False
85) -> None:
86    if not settings.working_dir.exists():
87        settings.working_dir.mkdir(parents=True)
88        if not quiet:
89            reporter.new(
90                'Initialized the Pigweed IDE working directory at '
91                f'{settings.working_dir}'
92            )
93
94
95def _report_unrecognized_editor(reporter: StatusReporter, editor: str) -> None:
96    supported_editors = ', '.join(sorted([ed.value for ed in SupportedEditor]))
97    reporter.wrn(f'Unrecognized editor: {editor}')
98    reporter.wrn('This may not be an automatically-supported editor.')
99    reporter.wrn(f'Automatically-supported editors: {supported_editors}')
100
101
102@_inject_reporter
103def cmd_sync(
104    reporter: StatusReporter = StatusReporter(),
105    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
106) -> None:
107    """Setup or sync your Pigweed project IDE features.
108
109    This will automatically set up your development environment with all the
110    features that Pigweed IDE supports, with sensible defaults.
111
112    At minimum, this command will create the .pw_ide working directory and
113    create settings files for all supported editors. Projects can define
114    additional setup steps in .pw_ide.yaml.
115
116    When new IDE features are introduced in the future (either by Pigweed or
117    your downstream project), you can re-run this command to set up the new
118    features. It will not overwrite or break any of your existing configuration.
119    """
120    reporter.info('Syncing pw_ide...')
121    _make_working_dir(reporter, pw_ide_settings)
122
123    for command in pw_ide_settings.sync:
124        _LOG.debug("Running: %s", command)
125        subprocess.run(shlex.split(command))
126
127    if pw_ide_settings.editor_enabled('vscode'):
128        cmd_vscode()
129
130    reporter.info('Done')
131
132
133@_inject_reporter
134def cmd_setup(
135    reporter: StatusReporter = StatusReporter(),
136    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
137) -> None:
138    """Deprecated! Please use `pw ide sync`."""
139    reporter.wrn(
140        "The `setup` command is now `sync`. Next time, run `pw ide sync`."
141    )
142    cmd_sync(reporter, pw_ide_settings)
143
144
145@_inject_reporter
146def cmd_vscode(
147    include: list[VscSettingsType] | None = None,
148    exclude: list[VscSettingsType] | None = None,
149    build_extension: bool = False,
150    reporter: StatusReporter = StatusReporter(),
151    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
152) -> None:
153    """Configure support for Visual Studio Code.
154
155    This will replace your current Visual Studio Code (VSC) settings for this
156    project (in ``.vscode/settings.json``, etc.) with the following sets of
157    settings, in order:
158
159    - The Pigweed default settings
160    - Your project's settings, if any (in ``.vscode/pw_project_settings.json``)
161    - Your personal settings, if any (in ``.vscode/pw_user_settings.json``)
162
163    In other words, settings files lower on the list can override settings
164    defined by those higher on the list. Settings defined in the sources above
165    are not active in VSC until they are merged and output to the current
166    settings file by running:
167
168    .. code-block:: bash
169
170       pw ide vscode
171
172    Refer to the Visual Studio Code documentation for more information about
173    these settings: https://code.visualstudio.com/docs/getstarted/settings
174
175    This command also manages VSC tasks (``.vscode/tasks.json``) and extensions
176    (``.vscode/extensions.json``). You can explicitly control which of these
177    settings types ("settings", "tasks", and "extensions") is modified by
178    this command by using the ``--include`` or ``--exclude`` options.
179
180    Your current VSC settings will never change unless you run ``pw ide``
181    commands. Since the current VSC settings are an artifact built from the
182    three settings files described above, you should avoid manually editing
183    that file; it will be replaced the next time you run ``pw ide vscode``. A
184    backup of your previous settings file will be made, and you can diff it
185    against the new file to see what changed.
186
187    These commands will never modify your VSC user settings, which are
188    stored outside of the project repository and apply globally to all VSC
189    instances.
190
191    The settings files are system-specific and shouldn't be checked into the
192    repository, except for the project settings (those with ``pw_project_``),
193    which can be used to define consistent settings for everyone working on the
194    project.
195
196    Note that support for VSC can be disabled at the project level or the user
197    level by adding the following to .pw_ide.yaml or .pw_ide.user.yaml
198    respectively:
199
200    .. code-block:: yaml
201
202        editors:
203          vscode: false
204
205    Likewise, it can be enabled by setting that value to true. It is enabled by
206    default.
207    """
208    if build_extension:
209        reporter.info('Building the Visual Studio Code extension')
210
211        try:
212            build_vscode_extension(Path(env.PW_ROOT))
213        except subprocess.CalledProcessError:
214            reporter.err("Failed! See output for more info.")
215        else:
216            reporter.ok('Built successfully!')
217
218    if not pw_ide_settings.editor_enabled('vscode'):
219        reporter.wrn(
220            'Visual Studio Code support is disabled in settings! If this is '
221            'unexpected, see this page for information on enabling support: '
222            'https://pigweed.dev/pw_ide/'
223            '#pw_ide.settings.PigweedIdeSettings.editors'
224        )
225        sys.exit(1)
226
227    if not vscode.DEFAULT_SETTINGS_PATH.exists():
228        vscode.DEFAULT_SETTINGS_PATH.mkdir()
229
230    vsc_manager = VscSettingsManager(pw_ide_settings)
231
232    if include is None and exclude is None:
233        include_set = set(VscSettingsType.all())
234        exclude_set: Set[VscSettingsType] = set()
235
236    elif include is None:
237        include_set = set(VscSettingsType.all())
238        exclude_set = set(exclude if exclude is not None else [])
239
240    elif exclude is None:
241        include_set = set(include if include is not None else [])
242        exclude_set = set()
243
244    else:
245        include_set = set(include if include is not None else [])
246        exclude_set = set(exclude if exclude is not None else [])
247
248    types_to_update = cast(
249        list[VscSettingsType], tuple(include_set - exclude_set)
250    )
251
252    for settings_type in types_to_update:
253        prev_settings_hash = ''
254        active_settings_existed = vsc_manager.active(settings_type).is_present()
255
256        if active_settings_existed:
257            prev_settings_hash = vsc_manager.active(settings_type).hash()
258
259        with vsc_manager.active(settings_type).build() as active_settings:
260            vsc_manager.default(settings_type).sync_to(active_settings)
261            vsc_manager.project(settings_type).sync_to(active_settings)
262            vsc_manager.user(settings_type).sync_to(active_settings)
263            vsc_manager.active(settings_type).sync_to(active_settings)
264
265        new_settings_hash = vsc_manager.active(settings_type).hash()
266        settings_changed = new_settings_hash != prev_settings_hash
267
268        _LOG.debug(
269            'VS Code %s prev hash: %s',
270            settings_type.name.lower(),
271            prev_settings_hash,
272        )
273        _LOG.debug(
274            'VS Code %s curr hash: %s',
275            settings_type.name.lower(),
276            new_settings_hash,
277        )
278
279        if settings_changed:
280            verb = 'Updated' if active_settings_existed else 'Created'
281            reporter.new(
282                f'{verb} Visual Studio Code active ' f'{settings_type.value}'
283            )
284
285
286def _process_compdbs(  # pylint: disable=too-many-locals
287    reporter: StatusReporter,
288    pw_ide_settings: PigweedIdeSettings,
289    always_output_new: bool = False,
290):
291    """Find and process compilation databases in the project.
292
293    This essentially does four things:
294    - Find all the compilation databases it can in the build directory
295    - For any databases we've seen before and are unchanged, skip them
296    - For any we haven't seed before or are changed, process them
297    - Save the state to disk so that other commands can examine/change targets
298    """
299
300    state = CppIdeFeaturesState(pw_ide_settings)
301
302    # If a compilation database was seen before and is unchanged, or if it's new
303    # and we process it, it will end up in the new hashes dict. If we saw it
304    # in the past but it no longer exists, it will not move over to the new
305    # hashes dict.
306    prev_compdb_hashes = state.compdb_hashes
307    new_compdb_hashes: CppCompilationDatabaseFileHashes = {}
308    prev_compdb_targets = state.compdb_targets
309    new_compdb_targets: CppCompilationDatabaseFileTargets = {}
310
311    targets: list[CppIdeFeaturesTarget] = []
312    num_new_unprocessed_targets = 0
313    num_new_processed_targets = 0
314    num_carried_over_targets = 0
315    num_removed_targets = len(state.targets.values())
316
317    unprocessed_compdb_files: list[Path] = []
318    processed_compdb_files: list[Path] = []
319
320    # Associate processed compilation databases with their original sources
321    all_processed_compdbs: dict[Path, CppCompilationDatabasesMap] = {}
322
323    # Find all compilation databases in the paths defined in settings, and
324    # associate them with their target inference pattern.
325    compdb_file_paths: list[tuple[Path, Path, str]] = []
326    settings_search_paths = pw_ide_settings.compdb_search_paths
327
328    # Get all compdb_search_paths entries from settings
329    for search_path_glob, target_inference in settings_search_paths:
330        # Expand the search path globs to get all concrete search paths
331        for search_path in (Path(p) for p in iglob(str(search_path_glob))):
332            # Search each path for compilation database files
333            for compdb_file in search_path.rglob(str(COMPDB_FILE_NAME)):
334                compdb_file_paths.append(
335                    (Path(search_path), compdb_file, target_inference)
336                )
337
338    for (
339        compdb_root_dir,
340        compdb_file_path,
341        target_inference,
342    ) in compdb_file_paths:
343        # Load the compilation database
344        try:
345            compdb = CppCompilationDatabase.load(
346                compdb_to_load=compdb_file_path,
347                root_dir=compdb_root_dir,
348                target_inference=target_inference,
349            )
350        except MissingCompDbException:
351            reporter.err(f'File not found: {str(compdb_file_path)}')
352            sys.exit(1)
353        # TODO(chadnorvell): Recover more gracefully from errors.
354        except BadCompDbException:
355            reporter.err(
356                'File does not match compilation database format: '
357                f'{str(compdb_file_path)}'
358            )
359            sys.exit(1)
360
361        # Check the hash of the compilation database against our cache of
362        # database hashes. Have we see this before and is the hash the same?
363        # Then we can skip this database.
364        if (
365            compdb_file_path in prev_compdb_hashes
366            and compdb.file_hash == prev_compdb_hashes[compdb_file_path]
367        ):
368            # Store this hash in the new hash registry.
369            new_compdb_hashes[compdb_file_path] = compdb.file_hash
370            # Copy the targets associated with this file...
371            new_compdb_targets[compdb_file_path] = prev_compdb_targets[
372                compdb_file_path
373            ]
374            # ... and add them to the targets list.
375            targets.extend(new_compdb_targets[compdb_file_path])
376            num_carried_over_targets += len(
377                new_compdb_targets[compdb_file_path]
378            )
379            num_removed_targets -= len(new_compdb_targets[compdb_file_path])
380            continue
381
382        # We haven't seen this database before. Process it.
383        processed_compdbs = compdb.process(
384            settings=pw_ide_settings,
385            path_globs=pw_ide_settings.clangd_query_drivers(
386                find_cipd_installed_exe_path("clang++")
387            ),
388            always_output_new=always_output_new,
389        )
390
391        # The source database doesn't actually need processing, so use it as is.
392        if processed_compdbs is None:
393            # Infer the name of the target from the path
394            name = '_'.join(
395                compdb_file_path.relative_to(compdb_root_dir).parent.parts
396            )
397
398            target = CppIdeFeaturesTarget(
399                name=name,
400                compdb_file_path=compdb_file_path,
401                num_commands=len(
402                    CppCompilationDatabase.load(
403                        compdb_file_path, compdb_root_dir
404                    )
405                ),
406            )
407
408            # An unprocessed database will have only one target.
409            new_compdb_targets[compdb_file_path] = [target]
410            unprocessed_compdb_files.append(compdb_file_path)
411            targets.append(target)
412            num_new_unprocessed_targets += 1
413
414            # Remember that we've seen this database.
415            new_compdb_hashes[compdb_file_path] = compdb.file_hash
416
417        else:
418            # We need to use the processed databases, so store them for writing.
419            # We'll add the targets associated with the processed databases
420            # later.
421            all_processed_compdbs[compdb_file_path] = processed_compdbs
422            processed_compdb_files.append(compdb_file_path)
423
424    if len(all_processed_compdbs) > 0:
425        # Merge into one map of target names to compilation database.
426        merged_compdbs = CppCompilationDatabasesMap.merge(
427            *all_processed_compdbs.values()
428        )
429
430        # Write processed databases to files.
431        try:
432            merged_compdbs.write()
433        except TypeError:
434            reporter.err('Could not serialize file to JSON!')
435            reporter.wrn('pw_ide state will not be persisted.')
436            return False
437
438        # Grab the target and file info from the processed databases.
439        for target_name, compdb in merged_compdbs.items():
440            target = CppIdeFeaturesTarget(
441                name=target_name,
442                compdb_file_path=cast(Path, compdb.file_path),
443                num_commands=len(compdb),
444            )
445
446            targets.append(target)
447            num_new_processed_targets += 1
448
449            if (
450                source := cast(Path, compdb.source_file_path)
451            ) not in new_compdb_targets:
452                new_compdb_targets[source] = [target]
453                new_compdb_hashes[source] = cast(str, compdb.source_file_hash)
454            else:
455                new_compdb_targets[source].append(target)
456
457    # Write out state.
458    targets_dict = {target_data.name: target_data for target_data in targets}
459    state.targets = targets_dict
460    state.compdb_hashes = new_compdb_hashes
461    state.compdb_targets = new_compdb_targets
462
463    # If the current target is no longer valid, unset it.
464    if (
465        state.current_target is not None
466        and state.current_target.name not in targets_dict
467    ):
468        state.current_target = None
469
470    num_total_targets = len(targets)
471    num_new_targets = num_new_processed_targets + num_new_unprocessed_targets
472
473    # Report the results.
474    # Return True if anything meaningful changed as a result of the processing.
475    # If the new state is essentially identical to the old state, return False
476    # so the caller can avoid needlessly updating anything else.
477    if num_new_targets > 0 or num_removed_targets > 0:
478        found_compdb_text = (
479            f'Found {len(compdb_file_paths)} compilation database'
480        )
481
482        if len(compdb_file_paths) > 1:
483            found_compdb_text += 's'
484
485        reporter.ok(found_compdb_text)
486
487        reporter_lines = []
488
489        if len(unprocessed_compdb_files) > 0:
490            reporter_lines.append(
491                f'Linked {len(unprocessed_compdb_files)} '
492                'unmodified compilation databases'
493            )
494
495        if len(processed_compdb_files) > 0:
496            working_dir_path = pw_ide_settings.working_dir.relative_to(
497                Path(env.PW_PROJECT_ROOT)
498            )
499            reporter_lines.append(
500                f'Processed {len(processed_compdb_files)} to working dir at '
501                f'{working_dir_path}'
502            )
503
504        if len(reporter_lines) > 0:
505            reporter_lines.extend(
506                [
507                    f'{num_total_targets} targets are now available '
508                    f'({num_new_targets} are new, '
509                    f'{num_removed_targets} were removed)',
510                ]
511            )
512
513            reporter.new(reporter_lines)
514
515        return True
516    return False
517
518
519class TryAgainException(Exception):
520    """A signal to retry an action."""
521
522
523@_inject_reporter
524def cmd_cpp(  # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
525    should_list_targets: bool,
526    should_get_target: bool,
527    target_to_set: str | None,
528    process: bool = True,
529    use_default_target: bool = False,
530    clangd_command: bool = False,
531    clangd_command_system: str | None = None,
532    should_try_compdb_gen_cmd: bool = True,
533    reporter: StatusReporter = StatusReporter(),
534    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
535) -> None:
536    """Configure C/C++ code intelligence support.
537
538    Code intelligence can be provided by clangd or other language servers that
539    use the clangd compilation database format, defined at:
540    https://clang.llvm.org/docs/JSONCompilationDatabase.html
541
542    Pigweed projects define their build configuration(s) via a build system,
543    usually GN, Bazel, or CMake. Based on build configurations, the build
544    system generates commands to compile each translation unit in the project.
545    clangd uses those commands to parse the build graph and provide rich code
546    intelligence.
547
548    Pigweed projects often target multiple devices & architectures, and use
549    multiple compiler toolchains. As a result, there may be more than one way
550    to compile each translation unit. Your build system ensures that it only
551    invokes a single compiler command for each translation unit which is
552    consistent with the toolchain and target appropriate to that build, which
553    we refer to as a "target toolchain".
554
555    We need to do the same thing with the compilation database that clangd uses.
556    We handle this by:
557
558    - Processing the compilation database produced the build system into
559      multiple internally-consistent compilation databases, one for each
560      target toolchain.
561
562    - Providing commands to select which target toolchain you want to use for
563      code analysis.
564
565    Refer to the Pigweed documentation or your build system's documentation to
566    learn how to produce a clangd compilation database. Once you have one, run
567    this command to process it (or provide a glob to process multiple):
568
569    .. code-block:: bash
570
571        pw ide cpp --process {path to compile_commands.json}
572
573    You can now examine the target toolchains that are available to you:
574
575    .. code-block:: bash
576
577        pw ide cpp --list
578
579    ... and select the target toolchain you want to use:
580
581    .. code-block:: bash
582
583        pw ide cpp --set host_clang
584
585    As long as your editor or language server plugin is properly configured, you
586    will now get code intelligence features relevant to that particular target
587    toolchain.
588
589    You can see what target toolchain is selected by running:
590
591    .. code-block:: bash
592
593        pw ide cpp
594
595    Whenever you switch to a target toolchain you haven't used before, clangd
596    will index the build, which may take several minutes. This process is not
597    blocking, so you can take advantage of code analysis immediately even while
598    the indexing is in progress. These indexes are cached, so you can switch
599    between targets without re-indexing each time.
600
601    If your build configuration changes significantly (e.g. you add a new file
602    to the project), you will need to re-process the compilation database for
603    that change to be recognized and manifested in the target toolchain. Your
604    target toolchain selection will not change, and your index will only need to
605    be incrementally updated.
606
607    You can generate the clangd command your editor needs to run with:
608
609    .. code-block:: bash
610
611        pw ide cpp --clangd-command
612
613    If your editor uses JSON for configuration, you can export the same command
614    in that format:
615
616    .. code-block:: bash
617
618        pw ide cpp --clangd-command-for json
619    """
620    _make_working_dir(reporter, pw_ide_settings, quiet=True)
621
622    # If true, no arguments were provided so we do the default behavior.
623    default = True
624
625    state = CppIdeFeaturesState(pw_ide_settings)
626
627    if process:
628        default = False
629        _process_compdbs(reporter, pw_ide_settings)
630
631        if state.current_target is None:
632            use_default_target = True
633
634    if use_default_target:
635        defined_default = pw_ide_settings.default_target
636
637        if defined_default is None and state.max_commands_target is None:
638            reporter.err(
639                'Can\'t use default target toolchain because none is defined!'
640            )
641            reporter.wrn('Have you processed a compilation database yet?')
642            sys.exit(1)
643        else:
644            max_commands_target = cast(
645                CppIdeFeaturesTarget, state.max_commands_target
646            )
647
648        default_target = (
649            defined_default
650            if defined_default is not None
651            else max_commands_target.name
652        )
653
654        if state.current_target != default:
655            target_to_set = default_target
656
657    if target_to_set is not None:
658        default = False
659        reporter.info(
660            f'Setting C/C++ analysis target toolchain to: {target_to_set}'
661        )
662
663        try:
664            CppIdeFeaturesState(
665                pw_ide_settings
666            ).current_target = state.targets.get(target_to_set, None)
667
668            if str(CppIdeFeaturesState(pw_ide_settings).current_target) != str(
669                target_to_set
670            ):
671                reporter.err(
672                    f'Failed to set target toolchain to {target_to_set}!'
673                )
674                reporter.wrn(
675                    [
676                        'You have tried to set a target toolchain '
677                        'that is not available.',
678                        'Run `pw ide cpp --list` to show available '
679                        'target toolchains.',
680                        f'If you expected {target_to_set} to be in that list',
681                        'and it is not, you may need to use your build system',
682                        'generate a compilation database.',
683                    ]
684                )
685
686                if (
687                    should_try_compdb_gen_cmd
688                    and pw_ide_settings.compdb_gen_cmd is not None
689                ):
690                    raise TryAgainException
691
692                sys.exit(1)
693
694        except TryAgainException:
695            if pw_ide_settings.compdb_gen_cmd is not None:
696                reporter.info(
697                    'Will try to generate a compilation database with: '
698                    f'{pw_ide_settings.compdb_gen_cmd}'
699                )
700
701                subprocess.run(shlex.split(pw_ide_settings.compdb_gen_cmd))
702
703                cmd_cpp(
704                    should_list_targets=should_list_targets,
705                    should_get_target=should_get_target,
706                    target_to_set=target_to_set,
707                    process=process,
708                    use_default_target=use_default_target,
709                    clangd_command=clangd_command,
710                    clangd_command_system=clangd_command_system,
711                    should_try_compdb_gen_cmd=False,
712                )
713        except InvalidTargetException:
714            reporter.err(
715                f'Invalid target toolchain! {target_to_set} not among the '
716                'defined target toolchains.'
717            )
718            sys.exit(1)
719        except MissingCompDbException:
720            reporter.err(
721                f'File not found for target toolchain! {target_to_set}'
722            )
723            sys.exit(1)
724
725        reporter.new(
726            'Set C/C++ language server analysis target toolchain to: '
727            f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
728        )
729
730    if clangd_command:
731        default = False
732        reporter.info(
733            [
734                'Command to run clangd with Pigweed paths:',
735                ClangdSettings(pw_ide_settings).command(),
736            ]
737        )
738
739    if clangd_command_system is not None:
740        default = False
741        reporter.info(
742            [
743                'Command to run clangd with Pigweed paths for '
744                f'{clangd_command_system}:',
745                ClangdSettings(pw_ide_settings).command(clangd_command_system),
746            ]
747        )
748
749    if should_list_targets:
750        default = False
751        targets_list_status = [
752            'C/C++ target toolchains available for language server analysis:'
753        ]
754
755        for target in sorted(CppIdeFeaturesState(pw_ide_settings).targets):
756            targets_list_status.append(f'\t{target}')
757
758        reporter.info(targets_list_status)
759
760    if should_get_target or default:
761        current_target = CppIdeFeaturesState(pw_ide_settings).current_target
762        name = 'None' if current_target is None else current_target.name
763
764        reporter.info(
765            'Current C/C++ language server analysis '
766            f'target toolchain: {name}'
767        )
768
769
770def install_py_module_as_editable(
771    module_name: str,
772    reporter: StatusReporter,
773) -> None:
774    """Install a Pigweed Python module in editable mode."""
775    reporter.info(f'Installing {module_name} as an editable module')
776    try:
777        site_packages_path = [
778            path for path in sys.path if 'site-packages' in path
779        ][0]
780    except IndexError:
781        reporter.err(f'Could not find {module_name} in the Python path!')
782        sys.exit(1)
783
784    reporter.info(f'Found {module_name} at: {site_packages_path}')
785    shutil.rmtree(Path(site_packages_path) / module_name)
786    src_path = Path(env.PW_ROOT, module_name, 'py')
787
788    try:
789        subprocess.run(
790            [
791                'pip',
792                'install',
793                '--no-deps',
794                '-e',
795                str(src_path),
796            ],
797            check=True,
798            stdout=subprocess.PIPE,
799        )
800    except subprocess.CalledProcessError:
801        reporter.err(
802            [
803                f'Failed to install {module_name}!',
804                'You may need to re-bootstrap',
805            ]
806        )
807
808        sys.exit(1)
809
810    reporter.new('Success!')
811    reporter.wrn('Note that running bootstrap or building will reverse this.')
812
813
814@_inject_reporter
815def cmd_python(
816    should_print_venv: bool,
817    install_editable: str | None = None,
818    reporter: StatusReporter = StatusReporter(),
819) -> None:
820    """Configure Python code intelligence support.
821
822    You can generate the path to the Python virtual environment interpreter that
823    your editor/language server should use with:
824
825    .. code-block:: bash
826
827       pw ide python --venv
828
829    When working on Pigweed's Python modules, it can be convenient to install
830    them in editable mode to instantly realize code changes. You can do this by
831    running:
832
833    .. code-block:: bash
834
835       pw ide python --install-editable pw_{module name}
836
837    Just note that running bootstrap or building will override this.
838    """
839    # If true, no arguments were provided and we should do the default
840    # behavior.
841    default = True
842
843    if install_editable is not None:
844        default = False
845        install_py_module_as_editable(install_editable, reporter)
846
847    if should_print_venv or default:
848        reporter.info(
849            [
850                'Location of the Pigweed Python virtual environment:',
851                str(PythonPaths().interpreter),
852            ]
853        )
854