• 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"""Pigweed Watch config file preferences loader."""
15
16import argparse
17import copy
18import shlex
19from pathlib import Path
20from typing import Any, Callable
21
22from pw_config_loader import yaml_config_loader_mixin
23
24_DEFAULT_CONFIG: dict[Any, Any] = {
25    # Config settings not available as a command line options go here.
26    'build_system_commands': {
27        'default': {
28            'commands': [
29                {
30                    'command': 'ninja',
31                    'extra_args': [],
32                },
33            ],
34        },
35    },
36}
37
38_DEFAULT_PROJECT_FILE = Path('$PW_PROJECT_ROOT/.pw_build.yaml')
39_DEFAULT_PROJECT_USER_FILE = Path('$PW_PROJECT_ROOT/.pw_build.user.yaml')
40_DEFAULT_USER_FILE = Path('$HOME/.pw_build.yaml')
41
42
43def load_defaults_from_argparse(
44    add_parser_arguments: Callable[
45        [argparse.ArgumentParser], argparse.ArgumentParser
46    ]
47) -> dict[Any, Any]:
48    parser = argparse.ArgumentParser(
49        description='', formatter_class=argparse.RawDescriptionHelpFormatter
50    )
51    parser = add_parser_arguments(parser)
52    default_namespace, _unknown_args = parser.parse_known_args(
53        [],  # Pass in blank arguments to avoid catching args from sys.argv.
54    )
55    defaults_flags = vars(default_namespace)
56    return defaults_flags
57
58
59class ProjectBuilderPrefs(yaml_config_loader_mixin.YamlConfigLoaderMixin):
60    """Pigweed Watch preferences storage class."""
61
62    def __init__(
63        self,
64        load_argparse_arguments: Callable[
65            [argparse.ArgumentParser], argparse.ArgumentParser
66        ],
67        project_file: Path | bool = _DEFAULT_PROJECT_FILE,
68        project_user_file: Path | bool = _DEFAULT_PROJECT_USER_FILE,
69        user_file: Path | bool = _DEFAULT_USER_FILE,
70    ) -> None:
71        self.load_argparse_arguments = load_argparse_arguments
72
73        self.config_init(
74            config_section_title='pw_build',
75            project_file=project_file,
76            project_user_file=project_user_file,
77            user_file=user_file,
78            default_config=_DEFAULT_CONFIG,
79            environment_var='PW_BUILD_CONFIG_FILE',
80        )
81
82    def reset_config(self) -> None:
83        super().reset_config()
84        self._update_config(
85            load_defaults_from_argparse(self.load_argparse_arguments),
86            yaml_config_loader_mixin.Stage.DEFAULT,
87        )
88
89    def _argparse_build_system_commands_to_prefs(  # pylint: disable=no-self-use
90        self, argparse_input: list[list[str]]
91    ) -> dict[str, Any]:
92        result = copy.copy(_DEFAULT_CONFIG['build_system_commands'])
93        for out_dir, command in argparse_input:
94            new_dir_spec = result.get(out_dir, {})
95            # Get existing commands list
96            new_commands = new_dir_spec.get('commands', [])
97
98            # Convert 'ninja -k 1' to 'ninja' and ['-k', '1']
99            extra_args = []
100            command_tokens = shlex.split(command)
101            if len(command_tokens) > 1:
102                extra_args = command_tokens[1:]
103                command = command_tokens[0]
104
105            # Append the command step
106            new_commands.append({'command': command, 'extra_args': extra_args})
107            new_dir_spec['commands'] = new_commands
108            result[out_dir] = new_dir_spec
109        return result
110
111    def apply_command_line_args(self, new_args: argparse.Namespace) -> None:
112        default_args = load_defaults_from_argparse(self.load_argparse_arguments)
113
114        # Only apply settings that differ from the defaults.
115        changed_settings: dict[Any, Any] = {}
116        for key, value in vars(new_args).items():
117            if key in default_args and value != default_args[key]:
118                if key == 'build_system_commands':
119                    value = self._argparse_build_system_commands_to_prefs(value)
120                changed_settings[key] = value
121
122        self._update_config(
123            changed_settings,
124            yaml_config_loader_mixin.Stage.DEFAULT,
125        )
126
127    @property
128    def run_commands(self) -> list[str]:
129        return self._config.get('run_command', [])
130
131    @property
132    def build_directories(self) -> dict[str, list[str]]:
133        """Returns build directories and the targets to build in each."""
134        build_directories: list[str] | dict[str, list[str]] = self._config.get(
135            'build_directories', {}
136        )
137        final_build_dirs: dict[str, list[str]] = {}
138
139        if isinstance(build_directories, dict):
140            final_build_dirs = build_directories
141        else:
142            # Convert list style command line arg to dict
143            for build_dir in build_directories:
144                # build_dir should be a list of strings from argparse
145                assert isinstance(build_dir, list)
146                assert isinstance(build_dir[0], str)
147                build_dir_name = build_dir[0]
148                new_targets = build_dir[1:]
149                # Append new targets in case out dirs are repeated on the
150                # command line. For example:
151                #   -C out python.tests -C out python.lint
152                existing_targets = final_build_dirs.get(build_dir_name, [])
153                existing_targets.extend(new_targets)
154                final_build_dirs[build_dir_name] = existing_targets
155
156        # If no build directory was specified fall back to 'out' with
157        # default_build_targets or empty targets. If run_commands were supplied,
158        # only run those by returning an empty final_build_dirs list.
159        if not final_build_dirs and not self.run_commands:
160            final_build_dirs['out'] = self._config.get(
161                'default_build_targets', []
162            )
163
164        return final_build_dirs
165
166    def _get_build_system_commands_for(self, build_dir: str) -> dict[str, Any]:
167        config_dict = self._config.get('build_system_commands', {})
168        if not config_dict:
169            config_dict = _DEFAULT_CONFIG['build_system_commands']
170        default_system_commands: dict[str, Any] = config_dict.get('default', {})
171        if default_system_commands is None:
172            default_system_commands = {}
173        build_system_commands = config_dict.get(build_dir)
174
175        # In case 'out:' is in the config but has no contents.
176        if not build_system_commands:
177            return default_system_commands
178
179        return build_system_commands
180
181    def build_system_commands(
182        self, build_dir: str
183    ) -> list[tuple[str, list[str]]]:
184        build_system_commands = self._get_build_system_commands_for(build_dir)
185
186        command_steps: list[tuple[str, list[str]]] = []
187        commands: list[dict[str, Any]] = build_system_commands.get(
188            'commands', []
189        )
190        for command_step in commands:
191            command_steps.append(
192                (command_step['command'], command_step['extra_args'])
193            )
194        return command_steps
195