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