1# Copyright 2021 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"""Finds components for a given manifest.""" 15 16from typing import Any, List, Optional, Tuple 17 18import pathlib 19import sys 20import xml.etree.ElementTree 21 22 23def _gn_str_out(name: str, val: Any): 24 """Outputs scoped string in GN format.""" 25 print(f'{name} = "{val}"') 26 27 28def _gn_list_str_out(name: str, val: List[Any]): 29 """Outputs list of strings in GN format with correct escaping.""" 30 list_str = ','.join('"' + str(x).replace('"', r'\"').replace('$', r'\$') + 31 '"' for x in val) 32 print(f'{name} = [{list_str}]') 33 34 35def _gn_list_path_out(name: str, 36 val: List[pathlib.Path], 37 path_prefix: Optional[str] = None): 38 """Outputs list of paths in GN format with common prefix.""" 39 if path_prefix is not None: 40 str_val = list(f'{path_prefix}/{str(d)}' for d in val) 41 else: 42 str_val = list(str(d) for d in val) 43 _gn_list_str_out(name, str_val) 44 45 46def get_component( 47 root: xml.etree.ElementTree.Element, component_id: str 48) -> Tuple[Optional[xml.etree.ElementTree.Element], Optional[pathlib.Path]]: 49 """Parse <component> manifest stanza. 50 51 Schema: 52 <component id="{component_id}" package_base_path="component"> 53 </component> 54 55 Args: 56 root: root of element tree. 57 component_id: id of component to return. 58 59 Returns: 60 (element, base_path) for the component, or (None, None). 61 """ 62 xpath = f'./components/component[@id="{component_id}"]' 63 component = root.find(xpath) 64 if component is None: 65 return (None, None) 66 67 try: 68 base_path = pathlib.Path(component.attrib['package_base_path']) 69 return (component, base_path) 70 except KeyError: 71 return (component, None) 72 73 74def parse_defines(root: xml.etree.ElementTree.Element, 75 component_id: str) -> List[str]: 76 """Parse pre-processor definitions for a component. 77 78 Schema: 79 <defines> 80 <define name="EXAMPLE" value="1"/> 81 <define name="OTHER"/> 82 </defines> 83 84 Args: 85 root: root of element tree. 86 component_id: id of component to return. 87 88 Returns: 89 list of str NAME=VALUE or NAME for the component. 90 """ 91 xpath = f'./components/component[@id="{component_id}"]/defines/define' 92 return list(_parse_define(define) for define in root.findall(xpath)) 93 94 95def _parse_define(define: xml.etree.ElementTree.Element) -> str: 96 """Parse <define> manifest stanza. 97 98 Schema: 99 <define name="EXAMPLE" value="1"/> 100 <define name="OTHER"/> 101 102 Args: 103 define: XML Element for <define>. 104 105 Returns: 106 str with a value NAME=VALUE or NAME. 107 """ 108 name = define.attrib['name'] 109 value = define.attrib.get('value', None) 110 if value is None: 111 return name 112 113 return f'{name}={value}' 114 115 116def parse_include_paths(root: xml.etree.ElementTree.Element, 117 component_id: str) -> List[pathlib.Path]: 118 """Parse include directories for a component. 119 120 Schema: 121 <component id="{component_id}" package_base_path="component"> 122 <include_paths> 123 <include_path relative_path="./" type="c_include"/> 124 </include_paths> 125 </component> 126 127 Args: 128 root: root of element tree. 129 component_id: id of component to return. 130 131 Returns: 132 list of include directories for the component. 133 """ 134 (component, base_path) = get_component(root, component_id) 135 if component is None: 136 return [] 137 138 include_paths: List[pathlib.Path] = [] 139 for include_type in ('c_include', 'asm_include'): 140 include_xpath = f'./include_paths/include_path[@type="{include_type}"]' 141 142 include_paths.extend( 143 _parse_include_path(include_path, base_path) 144 for include_path in component.findall(include_xpath)) 145 return include_paths 146 147 148def _parse_include_path(include_path: xml.etree.ElementTree.Element, 149 base_path: Optional[pathlib.Path]) -> pathlib.Path: 150 """Parse <include_path> manifest stanza. 151 152 Schema: 153 <include_path relative_path="./" type="c_include"/> 154 155 Args: 156 include_path: XML Element for <input_path>. 157 base_path: prefix for paths. 158 159 Returns: 160 Path, prefixed with `base_path`. 161 """ 162 path = pathlib.Path(include_path.attrib['relative_path']) 163 if base_path is None: 164 return path 165 return base_path / path 166 167 168def parse_headers(root: xml.etree.ElementTree.Element, 169 component_id: str) -> List[pathlib.Path]: 170 """Parse header files for a component. 171 172 Schema: 173 <component id="{component_id}" package_base_path="component"> 174 <source relative_path="./" type="c_include"> 175 <files mask="example.h"/> 176 </source> 177 </component> 178 179 Args: 180 root: root of element tree. 181 component_id: id of component to return. 182 183 Returns: 184 list of header files for the component. 185 """ 186 return _parse_sources(root, component_id, 'c_include') 187 188 189def parse_sources(root: xml.etree.ElementTree.Element, 190 component_id: str) -> List[pathlib.Path]: 191 """Parse source files for a component. 192 193 Schema: 194 <component id="{component_id}" package_base_path="component"> 195 <source relative_path="./" type="src"> 196 <files mask="example.cc"/> 197 </source> 198 </component> 199 200 Args: 201 root: root of element tree. 202 component_id: id of component to return. 203 204 Returns: 205 list of source files for the component. 206 """ 207 source_files = [] 208 for source_type in ('src', 'src_c', 'src_cpp', 'asm_include'): 209 source_files.extend(_parse_sources(root, component_id, source_type)) 210 return source_files 211 212 213def parse_libs(root: xml.etree.ElementTree.Element, 214 component_id: str) -> List[pathlib.Path]: 215 """Parse pre-compiled libraries for a component. 216 217 Schema: 218 <component id="{component_id}" package_base_path="component"> 219 <source relative_path="./" type="lib"> 220 <files mask="example.a"/> 221 </source> 222 </component> 223 224 Args: 225 root: root of element tree. 226 component_id: id of component to return. 227 228 Returns: 229 list of pre-compiler libraries for the component. 230 """ 231 return _parse_sources(root, component_id, 'lib') 232 233 234def _parse_sources(root: xml.etree.ElementTree.Element, component_id: str, 235 source_type: str) -> List[pathlib.Path]: 236 """Parse <source> manifest stanza. 237 238 Schema: 239 <component id="{component_id}" package_base_path="component"> 240 <source relative_path="./" type="{source_type}"> 241 <files mask="example.h"/> 242 </source> 243 </component> 244 245 Args: 246 root: root of element tree. 247 component_id: id of component to return. 248 source_type: type of source to search for. 249 250 Returns: 251 list of source files for the component. 252 """ 253 (component, base_path) = get_component(root, component_id) 254 if component is None: 255 return [] 256 257 sources: List[pathlib.Path] = [] 258 source_xpath = f'./source[@type="{source_type}"]' 259 for source in component.findall(source_xpath): 260 relative_path = pathlib.Path(source.attrib['relative_path']) 261 if base_path is not None: 262 relative_path = base_path / relative_path 263 264 sources.extend(relative_path / files.attrib['mask'] 265 for files in source.findall('./files')) 266 return sources 267 268 269def parse_dependencies(root: xml.etree.ElementTree.Element, 270 component_id: str) -> List[str]: 271 """Parse the list of dependencies for a component. 272 273 Optional dependencies are ignored for parsing since they have to be 274 included explicitly. 275 276 Schema: 277 <dependencies> 278 <all> 279 <component_dependency value="component"/> 280 <component_dependency value="component"/> 281 <any_of> 282 <component_dependency value="component"/> 283 <component_dependency value="component"/> 284 </any_of> 285 </all> 286 </dependencies> 287 288 Args: 289 root: root of element tree. 290 component_id: id of component to return. 291 292 Returns: 293 list of component id dependencies of the component. 294 """ 295 dependencies = [] 296 xpath = f'./components/component[@id="{component_id}"]/dependencies/*' 297 for dependency in root.findall(xpath): 298 dependencies.extend(_parse_dependency(dependency)) 299 return dependencies 300 301 302def _parse_dependency(dependency: xml.etree.ElementTree.Element) -> List[str]: 303 """Parse <all>, <any_of>, and <component_dependency> manifest stanzas. 304 305 Schema: 306 <all> 307 <component_dependency value="component"/> 308 <component_dependency value="component"/> 309 <any_of> 310 <component_dependency value="component"/> 311 <component_dependency value="component"/> 312 </any_of> 313 </all> 314 315 Args: 316 dependency: XML Element of dependency. 317 318 Returns: 319 list of component id dependencies. 320 """ 321 if dependency.tag == 'component_dependency': 322 return [dependency.attrib['value']] 323 if dependency.tag == 'all': 324 dependencies = [] 325 for subdependency in dependency: 326 dependencies.extend(_parse_dependency(subdependency)) 327 return dependencies 328 if dependency.tag == 'any_of': 329 # Explicitly ignore. 330 return [] 331 332 # Unknown dependency tag type. 333 return [] 334 335 336def check_dependencies(root: xml.etree.ElementTree.Element, 337 component_id: str, 338 include: List[str], 339 exclude: Optional[List[str]] = None) -> bool: 340 """Check the list of optional dependencies for a component. 341 342 Verifies that the optional dependencies for a component are satisfied by 343 components listed in `include` or `exclude`. 344 345 Args: 346 root: root of element tree. 347 component_id: id of component to check. 348 include: list of component ids included in the project. 349 exclude: list of component ids explicitly excluded from the project. 350 351 Returns: 352 True if dependencies are satisfied, False if not. 353 """ 354 xpath = f'./components/component[@id="{component_id}"]/dependencies/*' 355 for dependency in root.findall(xpath): 356 if not _check_dependency(dependency, include, exclude=exclude): 357 return False 358 return True 359 360 361def _check_dependency(dependency: xml.etree.ElementTree.Element, 362 include: List[str], 363 exclude: Optional[List[str]] = None) -> bool: 364 """Check a dependency for a component. 365 366 Verifies that the given {dependency} is satisfied by components listed in 367 `include` or `exclude`. 368 369 Args: 370 dependency: XML Element of dependency. 371 include: list of component ids included in the project. 372 exclude: list of component ids explicitly excluded from the project. 373 374 Returns: 375 True if dependencies are satisfied, False if not. 376 """ 377 if dependency.tag == 'component_dependency': 378 component_id = dependency.attrib['value'] 379 return component_id in include or (exclude is not None 380 and component_id in exclude) 381 if dependency.tag == 'all': 382 for subdependency in dependency: 383 if not _check_dependency(subdependency, include, exclude=exclude): 384 return False 385 return True 386 if dependency.tag == 'any_of': 387 for subdependency in dependency: 388 if _check_dependency(subdependency, include, exclude=exclude): 389 return True 390 391 tree = xml.etree.ElementTree.tostring(dependency).decode('utf-8') 392 print(f'Unsatisfied dependency from: {tree}', file=sys.stderr) 393 return False 394 395 # Unknown dependency tag type. 396 return True 397 398 399def create_project( 400 root: xml.etree.ElementTree.Element, 401 include: List[str], 402 exclude: Optional[List[str]] = None 403) -> Tuple[List[str], List[str], List[pathlib.Path], List[pathlib.Path], 404 List[pathlib.Path], List[pathlib.Path]]: 405 """Create a project from a list of specified components. 406 407 Args: 408 root: root of element tree. 409 include: list of component ids included in the project. 410 exclude: list of component ids excluded from the project. 411 412 Returns: 413 (component_ids, defines, include_paths, headers, sources, libs) for the 414 project. 415 """ 416 # Build the project list from the list of included components by expanding 417 # dependencies. 418 project_list = [] 419 pending_list = include 420 while len(pending_list) > 0: 421 component_id = pending_list.pop(0) 422 if component_id in project_list: 423 continue 424 if exclude is not None and component_id in exclude: 425 continue 426 427 project_list.append(component_id) 428 pending_list.extend(parse_dependencies(root, component_id)) 429 430 return ( 431 project_list, 432 sum((parse_defines(root, component_id) 433 for component_id in project_list), []), 434 sum((parse_include_paths(root, component_id) 435 for component_id in project_list), []), 436 sum((parse_headers(root, component_id) 437 for component_id in project_list), []), 438 sum((parse_sources(root, component_id) 439 for component_id in project_list), []), 440 sum((parse_libs(root, component_id) for component_id in project_list), 441 []), 442 ) 443 444 445def project(manifest_path: pathlib.Path, 446 include: Optional[List[str]] = None, 447 exclude: Optional[List[str]] = None, 448 path_prefix: Optional[str] = None): 449 """Output GN scope for a project with the specified components. 450 451 Args: 452 manifest_path: path to SDK manifest XML. 453 include: list of component ids included in the project. 454 exclude: list of component ids excluded from the project. 455 path_prefix: string prefix to prepend to all paths. 456 """ 457 assert include is not None, "Project must include at least one component." 458 459 tree = xml.etree.ElementTree.parse(manifest_path) 460 root = tree.getroot() 461 462 (component_ids, defines, include_dirs, headers, sources, libs) = \ 463 create_project(root, include, exclude=exclude) 464 465 for component_id in component_ids: 466 if not check_dependencies( 467 root, component_id, component_ids, exclude=exclude): 468 return 469 470 _gn_list_str_out('defines', defines) 471 _gn_list_path_out('include_dirs', include_dirs, path_prefix=path_prefix) 472 _gn_list_path_out('public', headers, path_prefix=path_prefix) 473 _gn_list_path_out('sources', sources, path_prefix=path_prefix) 474 _gn_list_path_out('libs', libs, path_prefix=path_prefix) 475