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