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 16import pathlib 17import sys 18from typing import Collection, Container 19import xml.etree.ElementTree 20 21 22def _element_is_compatible_with_device_core( 23 element: xml.etree.ElementTree.Element, device_core: str | None 24) -> bool: 25 """Check whether element is compatible with the given core. 26 27 Args: 28 element: element to check. 29 device_core: name of core to filter sources for. 30 31 Returns: 32 True if element can be used, False otherwise. 33 """ 34 if device_core is None: 35 return True 36 37 value = element.attrib.get('device_cores', None) 38 if value is None: 39 return True 40 41 device_cores = value.split() 42 return device_core in device_cores 43 44 45def get_component( 46 root: xml.etree.ElementTree.Element, 47 component_id: str, 48 device_core: str | None = None, 49) -> tuple[xml.etree.ElementTree.Element | None, pathlib.Path | None]: 50 """Parse <component> manifest stanza. 51 52 Schema: 53 <component id="{component_id}" package_base_path="component" 54 device_cores="{device_core}..."> 55 </component> 56 57 Args: 58 root: root of element tree. 59 component_id: id of component to return. 60 device_core: name of core to filter sources for. 61 62 Returns: 63 (element, base_path) for the component, or (None, None). 64 """ 65 xpath = f'./components/component[@id="{component_id}"]' 66 component = root.find(xpath) 67 if component is None or not _element_is_compatible_with_device_core( 68 component, device_core 69 ): 70 return (None, None) 71 72 try: 73 base_path = pathlib.Path(component.attrib['package_base_path']) 74 return (component, base_path) 75 except KeyError: 76 return (component, None) 77 78 79def parse_defines( 80 root: xml.etree.ElementTree.Element, 81 component_id: str, 82 device_core: str | None = None, 83) -> list[str]: 84 """Parse pre-processor definitions for a component. 85 86 Schema: 87 <defines> 88 <define name="EXAMPLE" value="1" device_cores="{device_core}..."/> 89 <define name="OTHER" device_cores="{device_core}..."/> 90 </defines> 91 92 Args: 93 root: root of element tree. 94 component_id: id of component to return. 95 device_core: name of core to filter sources for. 96 97 Returns: 98 list of str NAME=VALUE or NAME for the component. 99 """ 100 xpath = f'./components/component[@id="{component_id}"]/defines/define' 101 return list( 102 _parse_define(define) 103 for define in root.findall(xpath) 104 if _element_is_compatible_with_device_core(define, device_core) 105 ) 106 107 108def _parse_define(define: xml.etree.ElementTree.Element) -> str: 109 """Parse <define> manifest stanza. 110 111 Schema: 112 <define name="EXAMPLE" value="1"/> 113 <define name="OTHER"/> 114 115 Args: 116 define: XML Element for <define>. 117 118 Returns: 119 str with a value NAME=VALUE or NAME. 120 """ 121 name = define.attrib['name'] 122 value = define.attrib.get('value', None) 123 if value is None: 124 return name 125 126 return f'{name}={value}' 127 128 129def parse_include_paths( 130 root: xml.etree.ElementTree.Element, 131 component_id: str, 132 device_core: str | None = None, 133) -> list[pathlib.Path]: 134 """Parse include directories for a component. 135 136 Schema: 137 <component id="{component_id}" package_base_path="component"> 138 <include_paths> 139 <include_path relative_path="./" type="c_include" 140 device_cores="{device_core}..."/> 141 </include_paths> 142 </component> 143 144 Args: 145 root: root of element tree. 146 component_id: id of component to return. 147 device_core: name of core to filter sources for. 148 149 Returns: 150 list of include directories for the component. 151 """ 152 (component, base_path) = get_component(root, component_id) 153 if component is None: 154 return [] 155 156 include_paths: list[pathlib.Path] = [] 157 for include_type in ('c_include', 'asm_include'): 158 include_xpath = f'./include_paths/include_path[@type="{include_type}"]' 159 160 include_paths.extend( 161 _parse_include_path(include_path, base_path) 162 for include_path in component.findall(include_xpath) 163 if _element_is_compatible_with_device_core( 164 include_path, device_core 165 ) 166 ) 167 return include_paths 168 169 170def _parse_include_path( 171 include_path: xml.etree.ElementTree.Element, 172 base_path: pathlib.Path | None, 173) -> pathlib.Path: 174 """Parse <include_path> manifest stanza. 175 176 Schema: 177 <include_path relative_path="./" type="c_include"/> 178 179 Args: 180 include_path: XML Element for <input_path>. 181 base_path: prefix for paths. 182 183 Returns: 184 Path, prefixed with `base_path`. 185 """ 186 path = pathlib.Path(include_path.attrib['relative_path']) 187 if base_path is None: 188 return path 189 return base_path / path 190 191 192def parse_headers( 193 root: xml.etree.ElementTree.Element, 194 component_id: str, 195 device_core: str | None = None, 196) -> list[pathlib.Path]: 197 """Parse header files for a component. 198 199 Schema: 200 <component id="{component_id}" package_base_path="component"> 201 <source relative_path="./" type="c_include" 202 device_cores="{device_core}..."> 203 <files mask="example.h"/> 204 </source> 205 </component> 206 207 Args: 208 root: root of element tree. 209 component_id: id of component to return. 210 device_core: name of core to filter sources for. 211 212 Returns: 213 list of header files for the component. 214 """ 215 return _parse_sources( 216 root, component_id, 'c_include', device_core=device_core 217 ) 218 219 220def parse_sources( 221 root: xml.etree.ElementTree.Element, 222 component_id: str, 223 device_core: str | None = None, 224) -> list[pathlib.Path]: 225 """Parse source files for a component. 226 227 Schema: 228 <component id="{component_id}" package_base_path="component"> 229 <source relative_path="./" type="src" device_cores="{device_core}..."> 230 <files mask="example.cc"/> 231 </source> 232 </component> 233 234 Args: 235 root: root of element tree. 236 component_id: id of component to return. 237 device_core: name of core to filter sources for. 238 239 Returns: 240 list of source files for the component. 241 """ 242 source_files = [] 243 for source_type in ('src', 'src_c', 'src_cpp', 'asm_include'): 244 source_files.extend( 245 _parse_sources( 246 root, component_id, source_type, device_core=device_core 247 ) 248 ) 249 return source_files 250 251 252def parse_libs( 253 root: xml.etree.ElementTree.Element, 254 component_id: str, 255 device_core: str | None = None, 256) -> list[pathlib.Path]: 257 """Parse pre-compiled libraries for a component. 258 259 Schema: 260 <component id="{component_id}" package_base_path="component"> 261 <source relative_path="./" type="lib" device_cores="{device_core}..."> 262 <files mask="example.a"/> 263 </source> 264 </component> 265 266 Args: 267 root: root of element tree. 268 component_id: id of component to return. 269 device_core: name of core to filter sources for. 270 271 Returns: 272 list of pre-compiler libraries for the component. 273 """ 274 return _parse_sources(root, component_id, 'lib', device_core=device_core) 275 276 277def _parse_sources( 278 root: xml.etree.ElementTree.Element, 279 component_id: str, 280 source_type: str, 281 device_core: str | None = None, 282) -> list[pathlib.Path]: 283 """Parse <source> manifest stanza. 284 285 Schema: 286 <component id="{component_id}" package_base_path="component"> 287 <source relative_path="./" type="{source_type}" 288 device_cores="{device_core}..."> 289 <files mask="example.h"/> 290 </source> 291 </component> 292 293 Args: 294 root: root of element tree. 295 component_id: id of component to return. 296 source_type: type of source to search for. 297 device_core: name of core to filter sources for. 298 299 Returns: 300 list of source files for the component. 301 """ 302 (component, base_path) = get_component(root, component_id) 303 if component is None: 304 return [] 305 306 sources: list[pathlib.Path] = [] 307 source_xpath = f'./source[@type="{source_type}"]' 308 for source in component.findall(source_xpath): 309 if not _element_is_compatible_with_device_core(source, device_core): 310 continue 311 312 relative_path = pathlib.Path(source.attrib['relative_path']) 313 if base_path is not None: 314 relative_path = base_path / relative_path 315 316 sources.extend( 317 relative_path / files.attrib['mask'] 318 for files in source.findall('./files') 319 ) 320 return sources 321 322 323def parse_dependencies( 324 root: xml.etree.ElementTree.Element, component_id: str 325) -> list[str]: 326 """Parse the list of dependencies for a component. 327 328 Optional dependencies are ignored for parsing since they have to be 329 included explicitly. 330 331 Schema: 332 <dependencies> 333 <all> 334 <component_dependency value="component"/> 335 <component_dependency value="component"/> 336 <any_of> 337 <component_dependency value="component"/> 338 <component_dependency value="component"/> 339 </any_of> 340 </all> 341 </dependencies> 342 343 Args: 344 root: root of element tree. 345 component_id: id of component to return. 346 347 Returns: 348 list of component id dependencies of the component. 349 """ 350 dependencies = [] 351 xpath = f'./components/component[@id="{component_id}"]/dependencies/*' 352 for dependency in root.findall(xpath): 353 dependencies.extend(_parse_dependency(dependency)) 354 return dependencies 355 356 357def _parse_dependency(dependency: xml.etree.ElementTree.Element) -> list[str]: 358 """Parse <all>, <any_of>, and <component_dependency> manifest stanzas. 359 360 Schema: 361 <all> 362 <component_dependency value="component"/> 363 <component_dependency value="component"/> 364 <any_of> 365 <component_dependency value="component"/> 366 <component_dependency value="component"/> 367 </any_of> 368 </all> 369 370 Args: 371 dependency: XML Element of dependency. 372 373 Returns: 374 list of component id dependencies. 375 """ 376 if dependency.tag == 'component_dependency': 377 return [dependency.attrib['value']] 378 if dependency.tag == 'all': 379 dependencies = [] 380 for subdependency in dependency: 381 dependencies.extend(_parse_dependency(subdependency)) 382 return dependencies 383 if dependency.tag == 'any_of': 384 # Explicitly ignore. 385 return [] 386 387 # Unknown dependency tag type. 388 return [] 389 390 391def check_dependencies( 392 root: xml.etree.ElementTree.Element, 393 component_id: str, 394 include: Collection[str], 395 exclude: Container[str] | None = None, 396 device_core: str | None = None, 397) -> bool: 398 """Check the list of optional dependencies for a component. 399 400 Verifies that the optional dependencies for a component are satisfied by 401 components listed in `include` or `exclude`. 402 403 Args: 404 root: root of element tree. 405 component_id: id of component to check. 406 include: collection of component ids included in the project. 407 exclude: optional container of component ids explicitly excluded from 408 the project. 409 device_core: name of core to filter sources for. 410 411 Returns: 412 True if dependencies are satisfied, False if not. 413 """ 414 xpath = f'./components/component[@id="{component_id}"]/dependencies/*' 415 for dependency in root.findall(xpath): 416 if not _check_dependency( 417 dependency, include, exclude=exclude, device_core=device_core 418 ): 419 return False 420 return True 421 422 423def _check_dependency( 424 dependency: xml.etree.ElementTree.Element, 425 include: Collection[str], 426 exclude: Container[str] | None = None, 427 device_core: str | None = None, 428) -> bool: 429 """Check a dependency for a component. 430 431 Verifies that the given {dependency} is satisfied by components listed in 432 `include` or `exclude`. 433 434 Args: 435 dependency: XML Element of dependency. 436 include: collection of component ids included in the project. 437 exclude: optional container of component ids explicitly excluded from 438 the project. 439 device_core: name of core to filter sources for. 440 441 Returns: 442 True if dependencies are satisfied, False if not. 443 """ 444 if dependency.tag == 'component_dependency': 445 component_id = dependency.attrib['value'] 446 return component_id in include or ( 447 exclude is not None and component_id in exclude 448 ) 449 if dependency.tag == 'all': 450 for subdependency in dependency: 451 if not _check_dependency( 452 subdependency, include, exclude=exclude, device_core=device_core 453 ): 454 return False 455 return True 456 if dependency.tag == 'any_of': 457 for subdependency in dependency: 458 if _check_dependency( 459 subdependency, include, exclude=exclude, device_core=device_core 460 ): 461 return True 462 463 tree = xml.etree.ElementTree.tostring(dependency).decode('utf-8') 464 print(f'Unsatisfied dependency from: {tree}', file=sys.stderr) 465 return False 466 467 # Unknown dependency tag type. 468 return True 469 470 471def create_project( 472 root: xml.etree.ElementTree.Element, 473 include: Collection[str], 474 exclude: Container[str] | None = None, 475 device_core: str | None = None, 476) -> tuple[ 477 list[str], 478 list[str], 479 list[pathlib.Path], 480 list[pathlib.Path], 481 list[pathlib.Path], 482 list[pathlib.Path], 483]: 484 """Create a project from a list of specified components. 485 486 Args: 487 root: root of element tree. 488 include: collection of component ids included in the project. 489 exclude: container of component ids excluded from the project. 490 device_core: name of core to filter sources for. 491 492 Returns: 493 (component_ids, defines, include_paths, headers, sources, libs) for the 494 project. 495 """ 496 # Build the project list from the list of included components by expanding 497 # dependencies. 498 project_list = [] 499 pending_list = list(include) 500 while len(pending_list) > 0: 501 component_id = pending_list.pop(0) 502 if component_id in project_list: 503 continue 504 if exclude is not None and component_id in exclude: 505 continue 506 507 project_list.append(component_id) 508 pending_list.extend(parse_dependencies(root, component_id)) 509 510 return ( 511 project_list, 512 sum( 513 ( 514 parse_defines(root, component_id, device_core=device_core) 515 for component_id in project_list 516 ), 517 [], 518 ), 519 sum( 520 ( 521 parse_include_paths(root, component_id, device_core=device_core) 522 for component_id in project_list 523 ), 524 [], 525 ), 526 sum( 527 ( 528 parse_headers(root, component_id, device_core=device_core) 529 for component_id in project_list 530 ), 531 [], 532 ), 533 sum( 534 ( 535 parse_sources(root, component_id, device_core=device_core) 536 for component_id in project_list 537 ), 538 [], 539 ), 540 sum( 541 ( 542 parse_libs(root, component_id, device_core=device_core) 543 for component_id in project_list 544 ), 545 [], 546 ), 547 ) 548 549 550class Project: 551 """Self-contained MCUXpresso project. 552 553 Properties: 554 component_ids: list of component ids compromising the project. 555 defines: list of compiler definitions to build the project. 556 include_dirs: list of include directory paths needed for the project. 557 headers: list of header paths exported by the project. 558 sources: list of source file paths built as part of the project. 559 libs: list of libraries linked to the project. 560 dependencies_satisfied: True if the project dependencies are satisfied. 561 """ 562 563 @classmethod 564 def from_file( 565 cls, 566 manifest_path: pathlib.Path, 567 include: Collection[str], 568 exclude: Container[str] | None = None, 569 device_core: str | None = None, 570 ): 571 """Create a self-contained project with the specified components. 572 573 Args: 574 manifest_path: path to SDK manifest XML. 575 include: collection of component ids included in the project. 576 exclude: container of component ids excluded from the project. 577 device_core: name of core to filter sources for. 578 """ 579 tree = xml.etree.ElementTree.parse(manifest_path) 580 root = tree.getroot() 581 return cls( 582 root, include=include, exclude=exclude, device_core=device_core 583 ) 584 585 def __init__( 586 self, 587 manifest: xml.etree.ElementTree.Element, 588 include: Collection[str], 589 exclude: Container[str] | None = None, 590 device_core: str | None = None, 591 ): 592 """Create a self-contained project with the specified components. 593 594 Args: 595 manifest: parsed manifest XML. 596 include: collection of component ids included in the project. 597 exclude: container of component ids excluded from the project. 598 device_core: name of core to filter sources for. 599 """ 600 ( 601 self.component_ids, 602 self.defines, 603 self.include_dirs, 604 self.headers, 605 self.sources, 606 self.libs, 607 ) = create_project( 608 manifest, include, exclude=exclude, device_core=device_core 609 ) 610 611 for component_id in self.component_ids: 612 if not check_dependencies( 613 manifest, 614 component_id, 615 self.component_ids, 616 exclude=exclude, 617 device_core=device_core, 618 ): 619 self.dependencies_satisfied = False 620 return 621 622 self.dependencies_satisfied = True 623