• 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 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