• 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
16import logging
17from pathlib import Path
18import shlex
19import shutil
20import subprocess
21import sys
22from typing import cast, Callable, List, Optional, Set, Tuple, Union
23
24from pw_cli.color import colors
25
26from pw_ide.cpp import (
27    ClangdSettings,
28    compdb_generate_file_path,
29    CppCompilationDatabase,
30    CppCompilationDatabasesMap,
31    CppIdeFeaturesState,
32    delete_compilation_databases,
33    delete_compilation_database_caches,
34    MAX_COMMANDS_TARGET_FILENAME,
35)
36
37from pw_ide.exceptions import (
38    BadCompDbException,
39    InvalidTargetException,
40    MissingCompDbException,
41)
42
43from pw_ide.python import PythonPaths
44
45from pw_ide.settings import (
46    PigweedIdeSettings,
47    SupportedEditor,
48    SupportedEditorName,
49)
50
51from pw_ide import vscode
52from pw_ide.vscode import VscSettingsManager, VscSettingsType
53
54
55def _no_color(msg: str) -> str:
56    return msg
57
58
59def _split_lines(msg: Union[str, List[str]]) -> Tuple[str, List[str]]:
60    """Turn a list of strings into a tuple of the first and list of rest."""
61    if isinstance(msg, str):
62        return (msg, [])
63
64    return (msg[0], msg[1:])
65
66
67class StatusReporter:
68    """Print user-friendly status reports to the terminal for CLI tools.
69
70    The output of ``demo()`` looks something like this, but more colorful:
71
72    .. code-block:: none
73
74       • FYI, here's some information:
75         Lorem ipsum dolor sit amet, consectetur adipiscing elit.
76         Donec condimentum metus molestie metus maximus ultricies ac id dolor.
77       ✓ This is okay, no changes needed.
78       ✓ We changed some things successfully!
79       ⚠ Uh oh, you might want to be aware of this.
80       ❌ This is bad! Things might be broken!
81
82    You can instead redirect these lines to logs without formatting by
83    substituting ``LoggingStatusReporter``. Consumers of this should be
84    designed to take any subclass and not make assumptions about where the
85    output will go. But the reason you would choose this over plain logging is
86    because you want to support pretty-printing to the terminal.
87
88    This is also "themable" in the sense that you can subclass this, override
89    the methods with whatever formatting you want, and supply the subclass to
90    anything that expects an instance of this.
91
92    Key:
93
94    - info: Plain ol' informational status.
95    - ok: Something was checked and it was okay.
96    - new: Something needed to be changed/updated and it was successfully.
97    - wrn: Warning, non-critical.
98    - err: Error, critical.
99
100    This doesn't expose the %-style string formatting that is used in idiomatic
101    Python logging, but this shouldn't be used for performance-critical logging
102    situations anyway.
103    """
104
105    def _report(  # pylint: disable=no-self-use
106        self,
107        msg: Union[str, List[str]],
108        color: Callable[[str], str],
109        char: str,
110        func: Callable,
111        silent: bool,
112    ) -> None:
113        """Actually print/log/whatever the status lines."""
114        first_line, rest_lines = _split_lines(msg)
115        first_line = color(f'{char} {first_line}')
116        spaces = ' ' * len(char)
117        rest_lines = [color(f'{spaces} {line}') for line in rest_lines]
118
119        if not silent:
120            for line in [first_line, *rest_lines]:
121                func(line)
122
123    def demo(self):
124        """Run this to see what your status reporter output looks like."""
125        self.info(
126            [
127                'FYI, here\'s some information:',
128                'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
129                'Donec condimentum metus molestie metus maximus ultricies '
130                'ac id dolor.',
131            ]
132        )
133        self.ok('This is okay, no changes needed.')
134        self.new('We changed some things successfully!')
135        self.wrn('Uh oh, you might want to be aware of this.')
136        self.err('This is bad! Things might be broken!')
137
138    def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
139        self._report(msg, _no_color, '\u2022', print, silent)
140
141    def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
142        self._report(msg, colors().blue, '\u2713', print, silent)
143
144    def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
145        self._report(msg, colors().green, '\u2713', print, silent)
146
147    def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
148        self._report(msg, colors().yellow, '\u26A0', print, silent)
149
150    def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
151        self._report(msg, colors().red, '\u274C', print, silent)
152
153
154class LoggingStatusReporter(StatusReporter):
155    """Print status lines to logs instead of to the terminal."""
156
157    def __init__(self, logger: logging.Logger) -> None:
158        self.logger = logger
159        super().__init__()
160
161    def _report(
162        self,
163        msg: Union[str, List[str]],
164        color: Callable[[str], str],
165        char: str,
166        func: Callable,
167        silent: bool,
168    ) -> None:
169        first_line, rest_lines = _split_lines(msg)
170
171        if not silent:
172            for line in [first_line, *rest_lines]:
173                func(line)
174
175    def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
176        self._report(msg, _no_color, '', self.logger.info, silent)
177
178    def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
179        self._report(msg, _no_color, '', self.logger.info, silent)
180
181    def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
182        self._report(msg, _no_color, '', self.logger.info, silent)
183
184    def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
185        self._report(msg, _no_color, '', self.logger.warning, silent)
186
187    def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
188        self._report(msg, _no_color, '', self.logger.error, silent)
189
190
191def _make_working_dir(
192    reporter: StatusReporter, settings: PigweedIdeSettings, quiet: bool = False
193) -> None:
194    if not settings.working_dir.exists():
195        settings.working_dir.mkdir()
196        reporter.new(
197            'Initialized the Pigweed IDE working directory at '
198            f'{settings.working_dir}'
199        )
200    else:
201        if not quiet:
202            reporter.ok(
203                'Pigweed IDE working directory already present at '
204                f'{settings.working_dir}'
205            )
206
207
208def _report_unrecognized_editor(reporter: StatusReporter, editor: str) -> None:
209    supported_editors = ', '.join(sorted([ed.value for ed in SupportedEditor]))
210    reporter.wrn(f'Unrecognized editor: {editor}')
211    reporter.wrn('This may not be an automatically-supported editor.')
212    reporter.wrn(f'Automatically-supported editors: {supported_editors}')
213
214
215def cmd_clear(
216    compdb: bool,
217    cache: bool,
218    editor: Optional[SupportedEditorName],
219    editor_backups: Optional[SupportedEditorName],
220    silent: bool = False,
221    reporter: StatusReporter = StatusReporter(),
222    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
223) -> None:
224    """Clear components of the IDE features.
225
226    In contrast to the ``reset`` subcommand, ``clear`` allows you to specify
227    components to delete. You will not need this command under normal
228    circumstances.
229    """
230    if compdb:
231        delete_compilation_databases(pw_ide_settings)
232        reporter.wrn('Cleared compilation databases', silent)
233
234    if cache:
235        delete_compilation_database_caches(pw_ide_settings)
236        reporter.wrn('Cleared compilation database caches', silent)
237
238    if editor is not None:
239        try:
240            validated_editor = SupportedEditor(editor)
241        except ValueError:
242            _report_unrecognized_editor(reporter, cast(str, editor))
243            sys.exit(1)
244
245        if validated_editor == SupportedEditor.VSCODE:
246            vsc_settings_manager = VscSettingsManager(pw_ide_settings)
247            vsc_settings_manager.delete_all_active_settings()
248
249        reporter.wrn(
250            f'Cleared active settings for {validated_editor.value}', silent
251        )
252
253    if editor_backups is not None:
254        try:
255            validated_editor = SupportedEditor(editor_backups)
256        except ValueError:
257            _report_unrecognized_editor(reporter, cast(str, editor))
258            sys.exit(1)
259
260        if validated_editor == SupportedEditor.VSCODE:
261            vsc_settings_manager = VscSettingsManager(pw_ide_settings)
262            vsc_settings_manager.delete_all_backups()
263
264        reporter.wrn(
265            f'Cleared backup settings for {validated_editor.value}',
266            silent=silent,
267        )
268
269
270def cmd_reset(
271    hard: bool = False,
272    reporter: StatusReporter = StatusReporter(),
273    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
274) -> None:
275    """Reset IDE settings.
276
277    This will clear your .pw_ide working directory and active settings for
278    supported editors, restoring your repository to a pre-"pw ide setup" state.
279    Any clangd caches in the working directory will not be removed, so that they
280    don't need to be generated again later. All backed up supported editor
281    settings will also be left in place.
282
283    Adding the --hard flag will completely delete the .pw_ide directory and all
284    supported editor backup settings, restoring your repository to a
285    pre-`pw ide setup` state.
286
287    This command does not affect this project's pw_ide and editor settings or
288    your own pw_ide and editor override settings.
289    """
290    delete_compilation_databases(pw_ide_settings)
291    vsc_settings_manager = VscSettingsManager(pw_ide_settings)
292    vsc_settings_manager.delete_all_active_settings()
293
294    if hard:
295        try:
296            shutil.rmtree(pw_ide_settings.working_dir)
297        except FileNotFoundError:
298            pass
299
300        vsc_settings_manager.delete_all_backups()
301
302    reporter.wrn('Pigweed IDE settings were reset!')
303
304
305def cmd_setup(
306    reporter: StatusReporter = StatusReporter(),
307    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
308) -> None:
309    """Set up or update your Pigweed project IDE features.
310
311    This will automatically set up your development environment with all the
312    features that Pigweed IDE supports, with sensible defaults.
313
314    At minimum, this command will create the .pw_ide working directory and
315    create settings files for all supported editors. Projects can define
316    additional setup steps in .pw_ide.yaml.
317
318    When new IDE features are introduced in the future (either by Pigweed or
319    your downstream project), you can re-run this command to set up the new
320    features. It will not overwrite or break any of your existing configuration.
321    """
322    _make_working_dir(reporter, pw_ide_settings)
323
324    if pw_ide_settings.editor_enabled('vscode'):
325        cmd_vscode()
326
327    for command in pw_ide_settings.setup:
328        subprocess.run(shlex.split(command))
329
330
331def cmd_vscode(
332    include: Optional[List[VscSettingsType]] = None,
333    exclude: Optional[List[VscSettingsType]] = None,
334    no_override: bool = False,
335    reporter: StatusReporter = StatusReporter(),
336    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
337) -> None:
338    """Configure support for Visual Studio Code.
339
340    This will replace your current Visual Studio Code (VSC) settings for this
341    project (in ``.vscode/settings.json``, etc.) with the following sets of
342    settings, in order:
343
344    - The Pigweed default settings
345    - Your project's settings, if any (in ``.vscode/pw_project_settings.json``)
346    - Your personal settings, if any (in ``.vscode/pw_user_settings.json``)
347
348    In other words, settings files lower on the list can override settings
349    defined by those higher on the list. Settings defined in the sources above
350    are not active in VSC until they are merged and output to the current
351    settings file by running:
352
353    .. code-block:: bash
354
355       pw ide vscode
356
357    Refer to the Visual Studio Code documentation for more information about
358    these settings: https://code.visualstudio.com/docs/getstarted/settings
359
360    This command also manages VSC tasks (``.vscode/tasks.json``) and extensions
361    (``.vscode/extensions.json``). You can explicitly control which of these
362    settings types ("settings", "tasks", and "extensions") is modified by
363    this command by using the ``--include`` or ``--exclude`` options.
364
365    Your current VSC settings will never change unless you run ``pw ide``
366    commands. Since the current VSC settings are an artifact built from the
367    three settings files described above, you should avoid manually editing
368    that file; it will be replaced the next time you run ``pw ide vscode``. A
369    backup of your previous settings file will be made, and you can diff it
370    against the new file to see what changed.
371
372    These commands will never modify your VSC user settings, which are
373    stored outside of the project repository and apply globally to all VSC
374    instances.
375
376    The settings files are system-specific and shouldn't be checked into the
377    repository, except for the project settings (those with ``pw_project_``),
378    which can be used to define consistent settings for everyone working on the
379    project.
380
381    Note that support for VSC can be disabled at the project level or the user
382    level by adding the following to .pw_ide.yaml or .pw_ide.user.yaml
383    respectively:
384
385    .. code-block:: yaml
386
387        editors:
388          vscode: false
389
390    Likewise, it can be enabled by setting that value to true. It is enabled by
391    default.
392    """
393    if not pw_ide_settings.editor_enabled('vscode'):
394        reporter.wrn('Visual Studio Code support is disabled in settings!')
395        sys.exit(1)
396
397    if not vscode.DEFAULT_SETTINGS_PATH.exists():
398        vscode.DEFAULT_SETTINGS_PATH.mkdir()
399
400    vsc_manager = VscSettingsManager(pw_ide_settings)
401
402    if include is None:
403        include_set = set(VscSettingsType.all())
404    else:
405        include_set = set(include)
406
407    if exclude is None:
408        exclude_set: Set[VscSettingsType] = set()
409    else:
410        exclude_set = set(exclude)
411
412    types_to_update = cast(
413        List[VscSettingsType], tuple(include_set - exclude_set)
414    )
415
416    for settings_type in types_to_update:
417        active_settings_existed = vsc_manager.active(settings_type).is_present()
418
419        if no_override and active_settings_existed:
420            reporter.ok(
421                f'Visual Studio Code active {settings_type.value} '
422                'already present; will not overwrite'
423            )
424
425        else:
426            with vsc_manager.active(settings_type).modify(
427                reinit=True
428            ) as active_settings:
429                vsc_manager.default(settings_type).sync_to(active_settings)
430                vsc_manager.project(settings_type).sync_to(active_settings)
431                vsc_manager.user(settings_type).sync_to(active_settings)
432
433            verb = 'Updated' if active_settings_existed else 'Created'
434            reporter.new(
435                f'{verb} Visual Studio Code active ' f'{settings_type.value}'
436            )
437
438
439# TODO(chadnorvell): Break up this function.
440# The linting errors are a nuisance but they're beginning to have a point.
441def cmd_cpp(  # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
442    should_list_targets: bool,
443    should_get_target: bool,
444    target_to_set: Optional[str],
445    compdb_file_paths: Optional[List[Path]],
446    build_dir: Optional[Path],
447    use_default_target: bool = False,
448    should_run_ninja: bool = False,
449    should_run_gn: bool = False,
450    override_current_target: bool = True,
451    clangd_command: bool = False,
452    clangd_command_system: Optional[str] = None,
453    reporter: StatusReporter = StatusReporter(),
454    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
455) -> None:
456    """Configure C/C++ code intelligence support.
457
458    Code intelligence can be provided by clangd or other language servers that
459    use the clangd compilation database format, defined at:
460    https://clang.llvm.org/docs/JSONCompilationDatabase.html
461
462    This command helps you use clangd with Pigweed projects, which use multiple
463    toolchains within a distinct environment, and often define multiple targets.
464    This means compilation units are likely have multiple compile commands, and
465    clangd is not equipped to deal with this out of the box. We handle this by:
466
467    - Processing the compilation database produced the build system into
468      multiple internally-consistent compilation databases, one for each target
469      (where a "target" is a particular build for a particular system using a
470      particular toolchain).
471
472    - Providing commands to select which target you want to use for code
473      analysis.
474
475    Refer to the Pigweed documentation or your build system's documentation to
476    learn how to produce a clangd compilation database. Once you have one, run
477    this command to process it (or provide a glob to process multiple):
478
479    .. code-block:: bash
480
481        pw ide cpp --process {path to compile_commands.json}
482
483    If you're using GN to generate the compilation database, you can do that and
484    process it in a single command:
485
486    .. code-block:: bash
487
488       pw ide cpp --gn
489
490    You can do the same for a Ninja build (whether it was generated by GN or
491    another way):
492
493    .. code-block:: bash
494
495       pw ide cpp --ninja
496
497    You can now examine the targets that are available to you:
498
499    .. code-block:: bash
500
501        pw ide cpp --list
502
503    ... and select the target you want to use:
504
505    .. code-block:: bash
506
507        pw ide cpp --set host_clang
508
509    As long as your editor or language server plugin is properly configured, you
510    will now get code intelligence features relevant to that particular target.
511
512    You can see what target is selected by running:
513
514    .. code-block:: bash
515
516        pw ide cpp
517
518    Whenever you switch to a target you haven't used before, clangd will need to
519    index the build, which may take several minutes. These indexes are cached,
520    so you can switch between targets without re-indexing each time.
521
522    If your build configuration changes significantly (e.g. you add a new file
523    to the project), you will need to re-process the compilation database for
524    that change to be recognized. Your target selection will not change, and
525    your index will only need to be incrementally updated.
526
527    You can generate the clangd command your editor needs to run with:
528
529    .. code-block:: bash
530
531        pw ide cpp --clangd-command
532
533    If your editor uses JSON for configuration, you can export the same command
534    in that format:
535
536    .. code-block:: bash
537
538        pw ide cpp --clangd-command-for json
539    """
540    _make_working_dir(reporter, pw_ide_settings, quiet=True)
541
542    # If true, no arguments were provided so we do the default behavior.
543    default = True
544
545    build_dir = (
546        build_dir if build_dir is not None else pw_ide_settings.build_dir
547    )
548
549    if compdb_file_paths is not None:
550        should_process = True
551
552        if len(compdb_file_paths) == 0:
553            compdb_file_paths = pw_ide_settings.compdb_paths_expanded
554    else:
555        should_process = False
556        # This simplifies typing in the rest of this method. We rely on
557        # `should_process` instead of the status of this variable.
558        compdb_file_paths = []
559
560    # Order of operations matters from here on. It should be possible to run
561    # a build system command to generate a compilation database, then process
562    # the compilation database, then successfully set the target in a single
563    # command.
564
565    # Use Ninja to generate the initial compile_commands.json
566    if should_run_ninja:
567        default = False
568
569        ninja_commands = ['ninja', '-t', 'compdb']
570        reporter.info(f'Running Ninja: {" ".join(ninja_commands)}')
571
572        output_compdb_file_path = build_dir / compdb_generate_file_path()
573
574        try:
575            # Ninja writes to STDOUT, so we capture to a file.
576            with open(output_compdb_file_path, 'w') as compdb_file:
577                result = subprocess.run(
578                    ninja_commands,
579                    cwd=build_dir,
580                    stdout=compdb_file,
581                    stderr=subprocess.PIPE,
582                )
583        except FileNotFoundError:
584            reporter.err(f'Could not open path! {str(output_compdb_file_path)}')
585
586        if result.returncode == 0:
587            reporter.info('Ran Ninja successfully!')
588            should_process = True
589            compdb_file_paths.append(output_compdb_file_path)
590        else:
591            reporter.err('Something went wrong!')
592            # Convert from bytes and remove trailing newline
593            err = result.stderr.decode().split('\n')[:-1]
594
595            for line in err:
596                reporter.err(line)
597
598            sys.exit(1)
599
600    # Use GN to generate the initial compile_commands.json
601    if should_run_gn:
602        default = False
603
604        gn_commands = ['gn', 'gen', str(build_dir), '--export-compile-commands']
605
606        try:
607            with open(build_dir / 'args.gn') as args_file:
608                gn_args = [
609                    line
610                    for line in args_file.readlines()
611                    if not line.startswith('#')
612                ]
613        except FileNotFoundError:
614            gn_args = []
615
616        gn_args_string = 'none' if len(gn_args) == 0 else ', '.join(gn_args)
617
618        reporter.info(
619            [f'Running GN: {" ".join(gn_commands)} (args: {gn_args_string})']
620        )
621
622        result = subprocess.run(gn_commands, capture_output=True)
623        gn_status_lines = ['Ran GN successfully!']
624
625        if result.returncode == 0:
626            # Convert from bytes and remove trailing newline
627            out = result.stdout.decode().split('\n')[:-1]
628
629            for line in out:
630                gn_status_lines.append(line)
631
632            reporter.info(gn_status_lines)
633            should_process = True
634            output_compdb_file_path = build_dir / compdb_generate_file_path()
635            compdb_file_paths.append(output_compdb_file_path)
636        else:
637            reporter.err('Something went wrong!')
638            # Convert from bytes and remove trailing newline
639            err = result.stderr.decode().split('\n')[:-1]
640
641            for line in err:
642                reporter.err(line)
643
644            sys.exit(1)
645
646    if should_process:
647        default = False
648        prev_targets = len(CppIdeFeaturesState(pw_ide_settings))
649        compdb_databases: List[CppCompilationDatabasesMap] = []
650        last_processed_path = Path()
651
652        for compdb_file_path in compdb_file_paths:
653            # If the path is a dir, append the default compile commands
654            # file name.
655            if compdb_file_path.is_dir():
656                compdb_file_path /= compdb_generate_file_path()
657
658            try:
659                compdb_databases.append(
660                    CppCompilationDatabase.load(
661                        Path(compdb_file_path), build_dir
662                    ).process(
663                        settings=pw_ide_settings,
664                        path_globs=pw_ide_settings.clangd_query_drivers(),
665                    )
666                )
667            except MissingCompDbException:
668                reporter.err(f'File not found: {str(compdb_file_path)}')
669
670                if '*' in str(compdb_file_path):
671                    reporter.wrn(
672                        'It looks like you provided a glob that '
673                        'did not match any files.'
674                    )
675
676                sys.exit(1)
677            # TODO(chadnorvell): Recover more gracefully from errors.
678            except BadCompDbException:
679                reporter.err(
680                    'File does not match compilation database format: '
681                    f'{str(compdb_file_path)}'
682                )
683                sys.exit(1)
684
685            last_processed_path = compdb_file_path
686
687        if len(compdb_databases) == 0:
688            reporter.err(
689                'No compilation databases found in: '
690                f'{str(compdb_file_paths)}'
691            )
692            sys.exit(1)
693
694        try:
695            CppCompilationDatabasesMap.merge(*compdb_databases).write()
696        except TypeError:
697            reporter.err('Could not serialize file to JSON!')
698
699        total_targets = len(CppIdeFeaturesState(pw_ide_settings))
700        new_targets = total_targets - prev_targets
701
702        if len(compdb_file_paths) == 1:
703            processed_text = str(last_processed_path)
704        else:
705            processed_text = f'{len(compdb_file_paths)} compilation databases'
706
707        reporter.new(
708            [
709                f'Processed {processed_text} '
710                f'to {pw_ide_settings.working_dir}',
711                f'{total_targets} targets are now available '
712                f'({new_targets} are new)',
713            ]
714        )
715
716    if use_default_target:
717        defined_default = pw_ide_settings.default_target
718        max_commands_target: Optional[str] = None
719
720        try:
721            with open(
722                pw_ide_settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
723            ) as max_commands_target_file:
724                max_commands_target = max_commands_target_file.readline()
725        except FileNotFoundError:
726            pass
727
728        if defined_default is None and max_commands_target is None:
729            reporter.err('Can\'t use default target because none is defined!')
730            reporter.wrn('Have you processed a compilation database yet?')
731            sys.exit(1)
732
733        target_to_set = (
734            defined_default
735            if defined_default is not None
736            else max_commands_target
737        )
738
739    if target_to_set is not None:
740        default = False
741
742        # Always set the target if it's not already set, but if it is,
743        # respect the --no-override flag.
744        should_set_target = (
745            CppIdeFeaturesState(pw_ide_settings).current_target is None
746            or override_current_target
747        )
748
749        if should_set_target:
750            try:
751                CppIdeFeaturesState(
752                    pw_ide_settings
753                ).current_target = target_to_set
754            except InvalidTargetException:
755                reporter.err(
756                    [
757                        f'Invalid target! {target_to_set} not among the '
758                        'defined targets.',
759                        'Check .pw_ide.yaml or .pw_ide.user.yaml for defined '
760                        'targets.',
761                    ]
762                )
763                sys.exit(1)
764            except MissingCompDbException:
765                reporter.err(
766                    [
767                        f'File not found for target! {target_to_set}',
768                        'Did you run pw ide cpp --process '
769                        '{path to compile_commands.json}?',
770                    ]
771                )
772                sys.exit(1)
773
774            reporter.new(
775                'Set C/C++ language server analysis target to: '
776                f'{target_to_set}'
777            )
778        else:
779            reporter.ok(
780                'Target already is set and will not be overridden: '
781                f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
782            )
783
784    if clangd_command:
785        default = False
786        reporter.info(
787            [
788                'Command to run clangd with Pigweed paths:',
789                ClangdSettings(pw_ide_settings).command(),
790            ]
791        )
792
793    if clangd_command_system is not None:
794        default = False
795        reporter.info(
796            [
797                'Command to run clangd with Pigweed paths for '
798                f'{clangd_command_system}:',
799                ClangdSettings(pw_ide_settings).command(clangd_command_system),
800            ]
801        )
802
803    if should_list_targets:
804        default = False
805        targets_list_status = [
806            'C/C++ targets available for language server analysis:'
807        ]
808
809        for target in sorted(
810            CppIdeFeaturesState(pw_ide_settings).enabled_available_targets
811        ):
812            targets_list_status.append(f'\t{target}')
813
814        reporter.info(targets_list_status)
815
816    if should_get_target or default:
817        reporter.info(
818            'Current C/C++ language server analysis target: '
819            f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
820        )
821
822
823def cmd_python(
824    should_print_venv: bool, reporter: StatusReporter = StatusReporter()
825) -> None:
826    """Configure Python code intelligence support.
827
828    You can generate the path to the Python virtual environment interpreter that
829    your editor/language server should use with:
830
831    .. code-block:: bash
832
833       pw ide python --venv
834    """
835    # If true, no arguments were provided and we should do the default
836    # behavior.
837    default = True
838
839    if should_print_venv or default:
840        reporter.info(
841            [
842                'Location of the Pigweed Python virtual environment:',
843                PythonPaths().interpreter,
844            ]
845        )
846