xref: /aosp_15_r20/external/pigweed/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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