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