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 16 17import pathlib 18import sys 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: list[str], 395 exclude: list[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: list of component ids included in the project. 407 exclude: list of component ids explicitly excluded from the project. 408 device_core: name of core to filter sources for. 409 410 Returns: 411 True if dependencies are satisfied, False if not. 412 """ 413 xpath = f'./components/component[@id="{component_id}"]/dependencies/*' 414 for dependency in root.findall(xpath): 415 if not _check_dependency( 416 dependency, include, exclude=exclude, device_core=device_core 417 ): 418 return False 419 return True 420 421 422def _check_dependency( 423 dependency: xml.etree.ElementTree.Element, 424 include: list[str], 425 exclude: list[str] | None = None, 426 device_core: str | None = None, 427) -> bool: 428 """Check a dependency for a component. 429 430 Verifies that the given {dependency} is satisfied by components listed in 431 `include` or `exclude`. 432 433 Args: 434 dependency: XML Element of dependency. 435 include: list of component ids included in the project. 436 exclude: list of component ids explicitly excluded from the project. 437 device_core: name of core to filter sources for. 438 439 Returns: 440 True if dependencies are satisfied, False if not. 441 """ 442 if dependency.tag == 'component_dependency': 443 component_id = dependency.attrib['value'] 444 return component_id in include or ( 445 exclude is not None and component_id in exclude 446 ) 447 if dependency.tag == 'all': 448 for subdependency in dependency: 449 if not _check_dependency( 450 subdependency, include, exclude=exclude, device_core=device_core 451 ): 452 return False 453 return True 454 if dependency.tag == 'any_of': 455 for subdependency in dependency: 456 if _check_dependency( 457 subdependency, include, exclude=exclude, device_core=device_core 458 ): 459 return True 460 461 tree = xml.etree.ElementTree.tostring(dependency).decode('utf-8') 462 print(f'Unsatisfied dependency from: {tree}', file=sys.stderr) 463 return False 464 465 # Unknown dependency tag type. 466 return True 467 468 469def create_project( 470 root: xml.etree.ElementTree.Element, 471 include: list[str], 472 exclude: list[str] | None = None, 473 device_core: str | None = None, 474) -> tuple[ 475 list[str], 476 list[str], 477 list[pathlib.Path], 478 list[pathlib.Path], 479 list[pathlib.Path], 480 list[pathlib.Path], 481]: 482 """Create a project from a list of specified components. 483 484 Args: 485 root: root of element tree. 486 include: list of component ids included in the project. 487 exclude: list of component ids excluded from the project. 488 device_core: name of core to filter sources for. 489 490 Returns: 491 (component_ids, defines, include_paths, headers, sources, libs) for the 492 project. 493 """ 494 # Build the project list from the list of included components by expanding 495 # dependencies. 496 project_list = [] 497 pending_list = include 498 while len(pending_list) > 0: 499 component_id = pending_list.pop(0) 500 if component_id in project_list: 501 continue 502 if exclude is not None and component_id in exclude: 503 continue 504 505 project_list.append(component_id) 506 pending_list.extend(parse_dependencies(root, component_id)) 507 508 return ( 509 project_list, 510 sum( 511 ( 512 parse_defines(root, component_id, device_core=device_core) 513 for component_id in project_list 514 ), 515 [], 516 ), 517 sum( 518 ( 519 parse_include_paths(root, component_id, device_core=device_core) 520 for component_id in project_list 521 ), 522 [], 523 ), 524 sum( 525 ( 526 parse_headers(root, component_id, device_core=device_core) 527 for component_id in project_list 528 ), 529 [], 530 ), 531 sum( 532 ( 533 parse_sources(root, component_id, device_core=device_core) 534 for component_id in project_list 535 ), 536 [], 537 ), 538 sum( 539 ( 540 parse_libs(root, component_id, device_core=device_core) 541 for component_id in project_list 542 ), 543 [], 544 ), 545 ) 546 547 548class Project: 549 """Self-contained MCUXpresso project. 550 551 Properties: 552 component_ids: list of component ids compromising the project. 553 defines: list of compiler definitions to build the project. 554 include_dirs: list of include directory paths needed for the project. 555 headers: list of header paths exported by the project. 556 sources: list of source file paths built as part of the project. 557 libs: list of libraries linked to the project. 558 dependencies_satisfied: True if the project dependencies are satisfied. 559 """ 560 561 @classmethod 562 def from_file( 563 cls, 564 manifest_path: pathlib.Path, 565 include: list[str] | None = None, 566 exclude: list[str] | None = None, 567 device_core: str | None = None, 568 ): 569 """Create a self-contained project with the specified components. 570 571 Args: 572 manifest_path: path to SDK manifest XML. 573 include: list of component ids included in the project. 574 exclude: list of component ids excluded from the project. 575 device_core: name of core to filter sources for. 576 """ 577 tree = xml.etree.ElementTree.parse(manifest_path) 578 root = tree.getroot() 579 return cls( 580 root, include=include, exclude=exclude, device_core=device_core 581 ) 582 583 def __init__( 584 self, 585 manifest: xml.etree.ElementTree.Element, 586 include: list[str] | None = None, 587 exclude: list[str] | None = None, 588 device_core: str | None = None, 589 ): 590 """Create a self-contained project with the specified components. 591 592 Args: 593 manifest: parsed manifest XML. 594 include: list of component ids included in the project. 595 exclude: list of component ids excluded from the project. 596 device_core: name of core to filter sources for. 597 """ 598 assert ( 599 include is not None 600 ), "Project must include at least one component." 601 602 ( 603 self.component_ids, 604 self.defines, 605 self.include_dirs, 606 self.headers, 607 self.sources, 608 self.libs, 609 ) = create_project( 610 manifest, include, exclude=exclude, device_core=device_core 611 ) 612 613 for component_id in self.component_ids: 614 if not check_dependencies( 615 manifest, 616 component_id, 617 self.component_ids, 618 exclude=exclude, 619 device_core=device_core, 620 ): 621 self.dependencies_satisfied = False 622 return 623 624 self.dependencies_satisfied = True 625