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 settings.""" 15 16import enum 17from inspect import cleandoc 18import glob 19import os 20from pathlib import Path 21from typing import Any, cast, Dict, List, Literal, Optional, Union 22import yaml 23 24from pw_cli.yaml_config_loader_mixin import YamlConfigLoaderMixin 25 26PW_IDE_DIR_NAME = '.pw_ide' 27PW_IDE_DEFAULT_DIR = ( 28 Path(os.path.expandvars('$PW_PROJECT_ROOT')) / PW_IDE_DIR_NAME 29) 30 31PW_PIGWEED_CIPD_INSTALL_DIR = Path( 32 os.path.expandvars('$PW_PIGWEED_CIPD_INSTALL_DIR') 33) 34 35PW_ARM_CIPD_INSTALL_DIR = Path(os.path.expandvars('$PW_ARM_CIPD_INSTALL_DIR')) 36 37_DEFAULT_BUILD_DIR_NAME = 'out' 38_DEFAULT_BUILD_DIR = ( 39 Path(os.path.expandvars('$PW_PROJECT_ROOT')) / _DEFAULT_BUILD_DIR_NAME 40) 41 42_DEFAULT_COMPDB_PATHS = [_DEFAULT_BUILD_DIR] 43_DEFAULT_TARGET_INFERENCE = '?' 44 45SupportedEditorName = Literal['vscode'] 46 47 48class SupportedEditor(enum.Enum): 49 VSCODE = 'vscode' 50 51 52_DEFAULT_SUPPORTED_EDITORS: Dict[SupportedEditorName, bool] = { 53 'vscode': True, 54} 55 56_DEFAULT_CONFIG: Dict[str, Any] = { 57 'clangd_additional_query_drivers': [], 58 'build_dir': _DEFAULT_BUILD_DIR, 59 'compdb_paths': _DEFAULT_BUILD_DIR_NAME, 60 'default_target': None, 61 'editors': _DEFAULT_SUPPORTED_EDITORS, 62 'setup': ['pw --no-banner ide cpp --gn --set-default --no-override'], 63 'targets': [], 64 'target_inference': _DEFAULT_TARGET_INFERENCE, 65 'working_dir': PW_IDE_DEFAULT_DIR, 66} 67 68_DEFAULT_PROJECT_FILE = Path('$PW_PROJECT_ROOT/.pw_ide.yaml') 69_DEFAULT_PROJECT_USER_FILE = Path('$PW_PROJECT_ROOT/.pw_ide.user.yaml') 70_DEFAULT_USER_FILE = Path('$HOME/.pw_ide.yaml') 71 72 73class PigweedIdeSettings(YamlConfigLoaderMixin): 74 """Pigweed IDE features settings storage class.""" 75 76 def __init__( 77 self, 78 project_file: Union[Path, bool] = _DEFAULT_PROJECT_FILE, 79 project_user_file: Union[Path, bool] = _DEFAULT_PROJECT_USER_FILE, 80 user_file: Union[Path, bool] = _DEFAULT_USER_FILE, 81 default_config: Optional[Dict[str, Any]] = None, 82 ) -> None: 83 self.config_init( 84 config_section_title='pw_ide', 85 project_file=project_file, 86 project_user_file=project_user_file, 87 user_file=user_file, 88 default_config=_DEFAULT_CONFIG 89 if default_config is None 90 else default_config, 91 environment_var='PW_IDE_CONFIG_FILE', 92 ) 93 94 @property 95 def working_dir(self) -> Path: 96 """Path to the ``pw_ide`` working directory. 97 98 The working directory holds C++ compilation databases and caches, and 99 other supporting files. This should not be a directory that's regularly 100 deleted or manipulated by other processes (e.g. the GN ``out`` 101 directory) nor should it be committed to source control. 102 """ 103 return Path(self._config.get('working_dir', PW_IDE_DEFAULT_DIR)) 104 105 @property 106 def build_dir(self) -> Path: 107 """The build system's root output directory. 108 109 We will use this as the output directory when automatically running 110 build system commands, and will use it to resolve target names using 111 target name inference when processing compilation databases. This can 112 be the same build directory used for general-purpose builds, but it 113 does not have to be. 114 """ 115 return Path(self._config.get('build_dir', _DEFAULT_BUILD_DIR)) 116 117 @property 118 def compdb_paths(self) -> str: 119 """A path glob to search for compilation databases. 120 121 These paths can be to files or to directories. Paths that are 122 directories will be appended with the default file name for 123 ``clangd`` compilation databases, ``compile_commands.json``. 124 """ 125 return self._config.get('compdb_paths', _DEFAULT_BUILD_DIR_NAME) 126 127 @property 128 def compdb_paths_expanded(self) -> List[Path]: 129 return [Path(node) for node in glob.iglob(self.compdb_paths)] 130 131 @property 132 def targets(self) -> List[str]: 133 """The list of targets that should be enabled for code analysis. 134 135 In this case, "target" is analogous to a GN target, i.e., a particular 136 build configuration. By default, all available targets are enabled. By 137 adding targets to this list, you can constrain the targets that are 138 enabled for code analysis to a subset of those that are available, which 139 may be useful if your project has many similar targets that are 140 redundant from a code analysis perspective. 141 142 Target names need to match the name of the directory that holds the 143 build system artifacts for the target. For example, GN outputs build 144 artifacts for the ``pw_strict_host_clang_debug`` target in a directory 145 with that name in its output directory. So that becomes the canonical 146 name for the target. 147 """ 148 return self._config.get('targets', list()) 149 150 @property 151 def target_inference(self) -> str: 152 """A glob-like string for extracting a target name from an output path. 153 154 Build systems and projects have varying ways of organizing their build 155 directory structure. For a given compilation unit, we need to know how 156 to extract the build's target name from the build artifact path. A 157 simple example: 158 159 .. code-block:: none 160 161 clang++ hello.cc -o host/obj/hello.cc.o 162 163 The top-level directory ``host`` is the target name we want. The same 164 compilation unit might be used with another build target: 165 166 .. code-block:: none 167 168 gcc-arm-none-eabi hello.cc -o arm_dev_board/obj/hello.cc.o 169 170 In this case, this compile command is associated with the 171 ``arm_dev_board`` target. 172 173 When importing and processing a compilation database, we assume by 174 default that for each compile command, the corresponding target name is 175 the name of the top level directory within the build directory root 176 that contains the build artifact. This is the default behavior for most 177 build systems. However, if your project is structured differently, you 178 can provide a glob-like string that indicates how to extract the target 179 name from build artifact path. 180 181 A ``*`` indicates any directory, and ``?`` indicates the directory that 182 has the name of the target. The path is resolved from the build 183 directory root, and anything deeper than the target directory is 184 ignored. For example, a glob indicating that the directory two levels 185 down from the build directory root has the target name would be 186 expressed with ``*/*/?``. 187 """ 188 return self._config.get('target_inference', _DEFAULT_TARGET_INFERENCE) 189 190 @property 191 def default_target(self) -> Optional[str]: 192 """The default target to use when calling ``--set-default``. 193 194 This target will be selected when ``pw ide cpp --set-default`` is 195 called. You can define an explicit default target here. If that command 196 is invoked without a default target definition, ``pw_ide`` will try to 197 infer the best choice of default target. Currently, it selects the 198 target with the broadest compilation unit coverage. 199 """ 200 return self._config.get('default_target', None) 201 202 @property 203 def setup(self) -> List[str]: 204 """A sequence of commands to automate IDE features setup. 205 206 ``pw ide setup`` should do everything necessary to get the project from 207 a fresh checkout to a working default IDE experience. This defines the 208 list of commands that makes that happen, which will be executed 209 sequentially in subprocesses. These commands should be idempotent, so 210 that the user can run them at any time to update their IDE features 211 configuration without the risk of putting those features in a bad or 212 unexpected state. 213 """ 214 return self._config.get('setup', list()) 215 216 @property 217 def clangd_additional_query_drivers(self) -> List[str]: 218 """Additional query driver paths that clangd should use. 219 220 By default, ``pw_ide`` supplies driver paths for the toolchains included 221 in Pigweed. If you are using toolchains that are not supplied by 222 Pigweed, you should include path globs to your toolchains here. These 223 paths will be given higher priority than the Pigweed toolchain paths. 224 """ 225 return self._config.get('clangd_additional_query_drivers', list()) 226 227 def clangd_query_drivers(self) -> List[str]: 228 return [ 229 *[str(Path(p)) for p in self.clangd_additional_query_drivers], 230 str(PW_PIGWEED_CIPD_INSTALL_DIR / 'bin' / '*'), 231 str(PW_ARM_CIPD_INSTALL_DIR / 'bin' / '*'), 232 ] 233 234 def clangd_query_driver_str(self) -> str: 235 return ','.join(self.clangd_query_drivers()) 236 237 @property 238 def editors(self) -> Dict[str, bool]: 239 """Enable or disable automated support for editors. 240 241 Automatic support for some editors is provided by ``pw_ide``, which is 242 accomplished through generating configuration files in your project 243 directory. All supported editors are enabled by default, but you can 244 disable editors by adding an ``'<editor>': false`` entry. 245 """ 246 return self._config.get('editors', _DEFAULT_SUPPORTED_EDITORS) 247 248 def editor_enabled(self, editor: SupportedEditorName) -> bool: 249 """True if the provided editor is enabled in settings. 250 251 This module will integrate the project with all supported editors by 252 default. If the project or user want to disable particular editors, 253 they can do so in the appropriate settings file. 254 """ 255 return self._config.get('editors', {}).get(editor, False) 256 257 258def _docstring_set_default( 259 obj: Any, default: Any, literal: bool = False 260) -> None: 261 """Add a default value annotation to a docstring. 262 263 Formatting isn't allowed in docstrings, so by default we can't inject 264 variables that we would like to appear in the documentation, like the 265 default value of a property. But we can use this function to add it 266 separately. 267 """ 268 if obj.__doc__ is not None: 269 default = str(default) 270 271 if literal: 272 lines = default.splitlines() 273 274 if len(lines) == 0: 275 return 276 if len(lines) == 1: 277 default = f'Default: ``{lines[0]}``' 278 else: 279 default = 'Default:\n\n.. code-block::\n\n ' + '\n '.join( 280 lines 281 ) 282 283 doc = cast(str, obj.__doc__) 284 obj.__doc__ = f'{cleandoc(doc)}\n\n{default}' 285 286 287_docstring_set_default( 288 PigweedIdeSettings.working_dir, PW_IDE_DIR_NAME, literal=True 289) 290_docstring_set_default( 291 PigweedIdeSettings.build_dir, _DEFAULT_BUILD_DIR_NAME, literal=True 292) 293_docstring_set_default( 294 PigweedIdeSettings.compdb_paths, 295 _DEFAULT_CONFIG['compdb_paths'], 296 literal=True, 297) 298_docstring_set_default( 299 PigweedIdeSettings.targets, _DEFAULT_CONFIG['targets'], literal=True 300) 301_docstring_set_default( 302 PigweedIdeSettings.default_target, 303 _DEFAULT_CONFIG['default_target'], 304 literal=True, 305) 306_docstring_set_default( 307 PigweedIdeSettings.target_inference, 308 _DEFAULT_CONFIG['target_inference'], 309 literal=True, 310) 311_docstring_set_default( 312 PigweedIdeSettings.setup, _DEFAULT_CONFIG['setup'], literal=True 313) 314_docstring_set_default( 315 PigweedIdeSettings.clangd_additional_query_drivers, 316 _DEFAULT_CONFIG['clangd_additional_query_drivers'], 317 literal=True, 318) 319_docstring_set_default( 320 PigweedIdeSettings.editors, 321 yaml.dump(_DEFAULT_SUPPORTED_EDITORS), 322 literal=True, 323) 324