• 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, Dict, List, Tuple, Union
21
22from pw_cli.toml_config_loader_mixin import YamlConfigLoaderMixin
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(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: Union[Path, bool] = _DEFAULT_PROJECT_FILE,
68        project_user_file: Union[Path, bool] = _DEFAULT_PROJECT_USER_FILE,
69        user_file: Union[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        )
87
88    def _argparse_build_system_commands_to_prefs(  # pylint: disable=no-self-use
89        self, argparse_input: List[List[str]]
90    ) -> Dict[str, Any]:
91        result = copy.copy(_DEFAULT_CONFIG['build_system_commands'])
92        for out_dir, command in argparse_input:
93            new_dir_spec = result.get(out_dir, {})
94            # Get existing commands list
95            new_commands = new_dir_spec.get('commands', [])
96
97            # Convert 'ninja -k 1' to 'ninja' and ['-k', '1']
98            extra_args = []
99            command_tokens = shlex.split(command)
100            if len(command_tokens) > 1:
101                extra_args = command_tokens[1:]
102                command = command_tokens[0]
103
104            # Append the command step
105            new_commands.append({'command': command, 'extra_args': extra_args})
106            new_dir_spec['commands'] = new_commands
107            result[out_dir] = new_dir_spec
108        return result
109
110    def apply_command_line_args(self, new_args: argparse.Namespace) -> None:
111        default_args = load_defaults_from_argparse(self.load_argparse_arguments)
112
113        # Only apply settings that differ from the defaults.
114        changed_settings: Dict[Any, Any] = {}
115        for key, value in vars(new_args).items():
116            if key in default_args and value != default_args[key]:
117                if key == 'build_system_commands':
118                    value = self._argparse_build_system_commands_to_prefs(value)
119                changed_settings[key] = value
120
121        self._update_config(changed_settings)
122
123    @property
124    def run_commands(self) -> List[str]:
125        return self._config.get('run_command', [])
126
127    @property
128    def build_directories(self) -> Dict[str, List[str]]:
129        """Returns build directories and the targets to build in each."""
130        build_directories: Union[
131            List[str], Dict[str, List[str]]
132        ] = self._config.get('build_directories', {})
133        final_build_dirs: Dict[str, List[str]] = {}
134
135        if isinstance(build_directories, dict):
136            final_build_dirs = build_directories
137        else:
138            # Convert list style command line arg to dict
139            for build_dir in build_directories:
140                # build_dir should be a list of strings from argparse
141                assert isinstance(build_dir, list)
142                assert isinstance(build_dir[0], str)
143                build_dir_name = build_dir[0]
144                new_targets = build_dir[1:]
145                # Append new targets in case out dirs are repeated on the
146                # command line. For example:
147                #   -C out python.tests -C out python.lint
148                existing_targets = final_build_dirs.get(build_dir_name, [])
149                existing_targets.extend(new_targets)
150                final_build_dirs[build_dir_name] = existing_targets
151
152        # If no build directory was specified fall back to 'out' with
153        # default_build_targets or empty targets. If run_commands were supplied,
154        # only run those by returning an empty final_build_dirs list.
155        if not final_build_dirs and not self.run_commands:
156            final_build_dirs['out'] = self._config.get(
157                'default_build_targets', []
158            )
159
160        return final_build_dirs
161
162    def _get_build_system_commands_for(self, build_dir: str) -> Dict[str, Any]:
163        config_dict = self._config.get('build_system_commands', {})
164        if not config_dict:
165            config_dict = _DEFAULT_CONFIG['build_system_commands']
166        default_system_commands: Dict[str, Any] = config_dict.get('default', {})
167        if default_system_commands is None:
168            default_system_commands = {}
169        build_system_commands = config_dict.get(build_dir)
170
171        # In case 'out:' is in the config but has no contents.
172        if not build_system_commands:
173            return default_system_commands
174
175        return build_system_commands
176
177    def build_system_commands(
178        self, build_dir: str
179    ) -> List[Tuple[str, List[str]]]:
180        build_system_commands = self._get_build_system_commands_for(build_dir)
181
182        command_steps: List[Tuple[str, List[str]]] = []
183        commands: List[Dict[str, Any]] = build_system_commands.get(
184            'commands', []
185        )
186        for command_step in commands:
187            command_steps.append(
188                (command_step['command'], command_step['extra_args'])
189            )
190        return command_steps
191