• 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
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