• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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