pw_build_mcuxpresso: Generate pw_source_set for MCUXpresso SDK targets

Provides a utility that parses the manifest XML files shipped inside
NXP MCUXpresso SDK packages detailing the SDK components and outputs
the list of sources, headers, etc. corresponding to an included and
excluded list of components.

Change-Id: I0e41cc046fe327860170e48bb561434730ebab55
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/61800
Commit-Queue: Scott James Remnant <keybuk@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index 7d35543..8272465 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -73,6 +73,7 @@
     "$dir_pw_boot_cortex_m:docs",
     "$dir_pw_build:docs",
     "$dir_pw_build_info:docs",
+    "$dir_pw_build_mcuxpresso:docs",
     "$dir_pw_bytes:docs",
     "$dir_pw_checksum:docs",
     "$dir_pw_chrono:docs",
diff --git a/modules.gni b/modules.gni
index b69ad03..9c23931 100644
--- a/modules.gni
+++ b/modules.gni
@@ -31,6 +31,7 @@
   dir_pw_boot_cortex_m = get_path_info("pw_boot_cortex_m", "abspath")
   dir_pw_build = get_path_info("pw_build", "abspath")
   dir_pw_build_info = get_path_info("pw_build_info", "abspath")
+  dir_pw_build_mcuxpresso = get_path_info("pw_build_mcuxpresso", "abspath")
   dir_pw_bytes = get_path_info("pw_bytes", "abspath")
   dir_pw_checksum = get_path_info("pw_checksum", "abspath")
   dir_pw_chrono = get_path_info("pw_chrono", "abspath")
diff --git a/pw_build_mcuxpresso/BUILD.gn b/pw_build_mcuxpresso/BUILD.gn
new file mode 100644
index 0000000..9a6699a
--- /dev/null
+++ b/pw_build_mcuxpresso/BUILD.gn
@@ -0,0 +1,21 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
diff --git a/pw_build_mcuxpresso/docs.rst b/pw_build_mcuxpresso/docs.rst
new file mode 100644
index 0000000..1805871
--- /dev/null
+++ b/pw_build_mcuxpresso/docs.rst
@@ -0,0 +1,132 @@
+.. _module-pw_build_mcuxpresso:
+
+-------------------
+pw_build_mcuxpresso
+-------------------
+
+The ``pw_build_mcuxpresso`` module provides helper utilizies for building a
+target based on an NXP MCUXpresso SDK.
+
+The GN build files live in ``third_party/mcuxpresso`` but are documented here.
+The rationale for keeping the build files in ``third_party`` is that code
+depending on an MCUXpresso SDK can clearly see that their dependency is on
+third party, not pigweed code.
+
+Using an MCUXpresso SDK
+=======================
+An MCUXpresso SDK consists of a number of components, each of which has a set
+of sources, headers, pre-processor defines, and dependencies on other
+components. These are all described in an XML "manifest" file included in the
+SDK package.
+
+To use the SDK within a Pigweed project, the set of components you need must be
+combined into a ``pw_source_set`` that you can depend on. This source set will
+include all of the sources and headers, along with necessary pre-processor
+defines, for those components and their dependencies.
+
+The source set is defined using the ``pw_mcuxpresso_sdk`` template, providing
+the path to the ``manifest`` XML, along with the names of the components you
+wish to ``include``.
+
+.. code-block:: text
+
+   import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+
+   pw_mcuxpresso_sdk("sample_project_sdk") {
+     manifest = "$dir_pw_third_party/mcuxpresso/evkmimxrt595/EVK-MIMXRT595_manifest_v3_8.xml"
+     include = [
+       "component.serial_manager_uart.MIMXRT595S",
+       "project_template.evkmimxrt595.MIMXRT595S",
+       "utility.debug_console.MIMXRT595S",
+     ]
+   }
+
+   pw_executable("hello_world") {
+     sources = [ "hello_world.cc" ]
+     deps = [ ":sample_project_sdk" ]
+   }
+
+Where the components you include have optional dependencies, they must be
+satisfied by the set of components you include otherwise the GN generation will
+fail with an error.
+
+Excluding components
+--------------------
+Components can be excluded from the generated source set, for example to
+suppress errors about optional dependencies your project does not need, or to
+prevent an unwanted component dependency from being introduced into your
+project.
+
+This is accomplished by providing the list of components to ``exclude`` as an
+argument to the template.
+
+For example to replace the FreeRTOS kernel bundled with the MCUXpresso SDK with
+the Pigweed third-party target:
+
+.. code-block:: text
+
+   pw_mcuxpresso_sdk("freertos_project_sdk") {
+     // manifest and includes ommitted for clarity
+     exclude = [ "middleware.freertos-kernel.MIMXRT595S" ]
+     public_deps = [ "$dir_pw_third_party/freertos" ]
+   }
+
+Introducing dependencies
+------------------------
+As seen above, the generated source set can have dependencies added by passing
+the ``public_deps`` (or ``deps``) arguments to the template.
+
+You can also pass the ``allow_circular_includes_from``, ``configs``, and
+``public_configs`` arguments to augment the generated source set.
+
+For example it is very common to replace the ``project_template`` component with
+a source set of your own that provides modified copies of the files from the
+SDK.
+
+To resolve circular dependencies, in addition to the generated source set, two
+configs named with the ``__defines`` and ``__includes`` suffixes on the template
+name are generated, to provide the pre-processor defines and include paths that
+the source set uses.
+
+.. code-block:: text
+
+   pw_mcuxpresso_sdk("my_project_sdk") {
+     manifest = "$dir_pw_third_party/mcuxpresso/evkmimxrt595/EVK-MIMXRT595_manifest_v3_8.xml"
+     include = [
+       "component.serial_manager_uart.MIMXRT595S",
+       "utility.debug_console.MIMXRT595S",
+     ]
+     public_deps = [ ":my_project_config" ]
+     allow_circular_includes_from = [ ":my_project_config" ]
+   }
+
+   pw_source_set("my_project_config") {
+     sources = [ "board.c", "clock_config.c", "pin_mux.c" ]
+     public = [ "board.h", "clock_config.h", "pin_mux.h "]
+     public_configs = [
+       ":my_project_sdk__defines",
+       ":my_project_sdk__includes"
+     ]
+   }
+
+mcuxpresso_builder
+==================
+``mcuxpresso_builder`` is a utility that contains the backend scripts used by
+the GN build scripts in ``third_party/mcuxpresso``. You should only need to
+interact with ``mcuxpresso_builder`` directly if you are doing something custom.
+
+project
+-------
+This command outputs a GN scope describing the result of expanding the set of
+included and excluded components.
+
+The ``--prefix`` option specifies the GN location of the SDK files.
+
+.. code-block:: bash
+
+  mcuxpresso_builder project /path/to/manifest.xml \
+      --include project_template.evkmimxrt595.MIMXRT595S \
+      --include utility.debug_console.MIMXRT595S \
+      --include component.serial_manager_uart.MIMXRT595S \
+      --exclude middleware.freertos-kernel.MIMXRT595S \
+      --prefix //path/to/sdk
diff --git a/pw_build_mcuxpresso/py/BUILD.gn b/pw_build_mcuxpresso/py/BUILD.gn
new file mode 100644
index 0000000..8d7eb65
--- /dev/null
+++ b/pw_build_mcuxpresso/py/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+  setup = [
+    "pyproject.toml",
+    "setup.cfg",
+    "setup.py",
+  ]
+  sources = [
+    "pw_build_mcuxpresso/__init__.py",
+    "pw_build_mcuxpresso/__main__.py",
+    "pw_build_mcuxpresso/components.py",
+  ]
+  tests = [ "tests/components_test.py" ]
+  pylintrc = "$dir_pigweed/.pylintrc"
+}
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__init__.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__init__.py
new file mode 100644
index 0000000..e97d22e
--- /dev/null
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""This package provides tooling specific to MCUXpresso."""
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
new file mode 100644
index 0000000..1a39e50
--- /dev/null
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Command line interface for mcuxpresso_builder."""
+
+import argparse
+import pathlib
+import sys
+
+from pw_build_mcuxpresso import components
+
+
+def _parse_args() -> argparse.Namespace:
+    """Setup argparse and parse command line args."""
+    parser = argparse.ArgumentParser()
+
+    subparsers = parser.add_subparsers(dest='command',
+                                       metavar='<command>',
+                                       required=True)
+
+    project_parser = subparsers.add_parser(
+        'project', help='output components of an MCUXpresso project')
+    project_parser.add_argument('manifest_filename', type=pathlib.Path)
+    project_parser.add_argument('--include', type=str, action='append')
+    project_parser.add_argument('--exclude', type=str, action='append')
+    project_parser.add_argument('--prefix', dest='path_prefix', type=str)
+
+    return parser.parse_args()
+
+
+def main():
+    """Main command line function."""
+    args = _parse_args()
+
+    if args.command == 'project':
+        components.project(args.manifest_filename,
+                           include=args.include,
+                           exclude=args.exclude,
+                           path_prefix=args.path_prefix)
+
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
new file mode 100644
index 0000000..061f5a0
--- /dev/null
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
@@ -0,0 +1,474 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Finds components for a given manifest."""
+
+from typing import Any, List, Optional, Tuple
+
+import pathlib
+import sys
+import xml.etree.ElementTree
+
+
+def _gn_str_out(name: str, val: Any):
+    """Outputs scoped string in GN format."""
+    print(f'{name} = "{val}"')
+
+
+def _gn_list_str_out(name: str, val: List[Any]):
+    """Outputs list of strings in GN format with correct escaping."""
+    list_str = ','.join('"' + str(x).replace('"', r'\"').replace('$', r'\$') +
+                        '"' for x in val)
+    print(f'{name} = [{list_str}]')
+
+
+def _gn_list_path_out(name: str,
+                      val: List[pathlib.Path],
+                      path_prefix: Optional[str] = None):
+    """Outputs list of paths in GN format with common prefix."""
+    if path_prefix is not None:
+        str_val = list(f'{path_prefix}/{str(d)}' for d in val)
+    else:
+        str_val = list(str(d) for d in val)
+    _gn_list_str_out(name, str_val)
+
+
+def get_component(
+    root: xml.etree.ElementTree.Element, component_id: str
+) -> Tuple[Optional[xml.etree.ElementTree.Element], Optional[pathlib.Path]]:
+    """Parse <component> manifest stanza.
+
+    Schema:
+        <component id="{component_id}" package_base_path="component">
+        </component>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        (element, base_path) for the component, or (None, None).
+    """
+    xpath = f'./components/component[@id="{component_id}"]'
+    component = root.find(xpath)
+    if component is None:
+        return (None, None)
+
+    try:
+        base_path = pathlib.Path(component.attrib['package_base_path'])
+        return (component, base_path)
+    except KeyError:
+        return (component, None)
+
+
+def parse_defines(root: xml.etree.ElementTree.Element,
+                  component_id: str) -> List[str]:
+    """Parse pre-processor definitions for a component.
+
+    Schema:
+        <defines>
+          <define name="EXAMPLE" value="1"/>
+          <define name="OTHER"/>
+        </defines>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        list of str NAME=VALUE or NAME for the component.
+    """
+    xpath = f'./components/component[@id="{component_id}"]/defines/define'
+    return list(_parse_define(define) for define in root.findall(xpath))
+
+
+def _parse_define(define: xml.etree.ElementTree.Element) -> str:
+    """Parse <define> manifest stanza.
+
+    Schema:
+        <define name="EXAMPLE" value="1"/>
+        <define name="OTHER"/>
+
+    Args:
+        define: XML Element for <define>.
+
+    Returns:
+        str with a value NAME=VALUE or NAME.
+    """
+    name = define.attrib['name']
+    value = define.attrib.get('value', None)
+    if value is None:
+        return name
+
+    return f'{name}={value}'
+
+
+def parse_include_paths(root: xml.etree.ElementTree.Element,
+                        component_id: str) -> List[pathlib.Path]:
+    """Parse include directories for a component.
+
+    Schema:
+        <component id="{component_id}" package_base_path="component">
+          <include_paths>
+            <include_path relative_path="./" type="c_include"/>
+          </include_paths>
+        </component>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        list of include directories for the component.
+    """
+    (component, base_path) = get_component(root, component_id)
+    if component is None:
+        return []
+
+    include_paths: List[pathlib.Path] = []
+    for include_type in ('c_include', 'asm_include'):
+        include_xpath = f'./include_paths/include_path[@type="{include_type}"]'
+
+        include_paths.extend(
+            _parse_include_path(include_path, base_path)
+            for include_path in component.findall(include_xpath))
+    return include_paths
+
+
+def _parse_include_path(include_path: xml.etree.ElementTree.Element,
+                        base_path: Optional[pathlib.Path]) -> pathlib.Path:
+    """Parse <include_path> manifest stanza.
+
+    Schema:
+        <include_path relative_path="./" type="c_include"/>
+
+    Args:
+        include_path: XML Element for <input_path>.
+        base_path: prefix for paths.
+
+    Returns:
+        Path, prefixed with `base_path`.
+    """
+    path = pathlib.Path(include_path.attrib['relative_path'])
+    if base_path is None:
+        return path
+    return base_path / path
+
+
+def parse_headers(root: xml.etree.ElementTree.Element,
+                  component_id: str) -> List[pathlib.Path]:
+    """Parse header files for a component.
+
+    Schema:
+        <component id="{component_id}" package_base_path="component">
+          <source relative_path="./" type="c_include">
+            <files mask="example.h"/>
+          </source>
+        </component>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        list of header files for the component.
+    """
+    return _parse_sources(root, component_id, 'c_include')
+
+
+def parse_sources(root: xml.etree.ElementTree.Element,
+                  component_id: str) -> List[pathlib.Path]:
+    """Parse source files for a component.
+
+    Schema:
+        <component id="{component_id}" package_base_path="component">
+          <source relative_path="./" type="src">
+            <files mask="example.cc"/>
+          </source>
+        </component>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        list of source files for the component.
+    """
+    source_files = []
+    for source_type in ('src', 'src_c', 'src_cpp', 'asm_include'):
+        source_files.extend(_parse_sources(root, component_id, source_type))
+    return source_files
+
+
+def parse_libs(root: xml.etree.ElementTree.Element,
+               component_id: str) -> List[pathlib.Path]:
+    """Parse pre-compiled libraries for a component.
+
+    Schema:
+        <component id="{component_id}" package_base_path="component">
+          <source relative_path="./" type="lib">
+            <files mask="example.a"/>
+          </source>
+        </component>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        list of pre-compiler libraries for the component.
+    """
+    return _parse_sources(root, component_id, 'lib')
+
+
+def _parse_sources(root: xml.etree.ElementTree.Element, component_id: str,
+                   source_type: str) -> List[pathlib.Path]:
+    """Parse <source> manifest stanza.
+
+    Schema:
+        <component id="{component_id}" package_base_path="component">
+          <source relative_path="./" type="{source_type}">
+            <files mask="example.h"/>
+          </source>
+        </component>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+        source_type: type of source to search for.
+
+    Returns:
+        list of source files for the component.
+    """
+    (component, base_path) = get_component(root, component_id)
+    if component is None:
+        return []
+
+    sources: List[pathlib.Path] = []
+    source_xpath = f'./source[@type="{source_type}"]'
+    for source in component.findall(source_xpath):
+        relative_path = pathlib.Path(source.attrib['relative_path'])
+        if base_path is not None:
+            relative_path = base_path / relative_path
+
+        sources.extend(relative_path / files.attrib['mask']
+                       for files in source.findall('./files'))
+    return sources
+
+
+def parse_dependencies(root: xml.etree.ElementTree.Element,
+                       component_id: str) -> List[str]:
+    """Parse the list of dependencies for a component.
+
+    Optional dependencies are ignored for parsing since they have to be
+    included explicitly.
+
+    Schema:
+        <dependencies>
+          <all>
+            <component_dependency value="component"/>
+            <component_dependency value="component"/>
+            <any_of>
+              <component_dependency value="component"/>
+              <component_dependency value="component"/>
+            </any_of>
+          </all>
+        </dependencies>
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to return.
+
+    Returns:
+        list of component id dependencies of the component.
+    """
+    dependencies = []
+    xpath = f'./components/component[@id="{component_id}"]/dependencies/*'
+    for dependency in root.findall(xpath):
+        dependencies.extend(_parse_dependency(dependency))
+    return dependencies
+
+
+def _parse_dependency(dependency: xml.etree.ElementTree.Element) -> List[str]:
+    """Parse <all>, <any_of>, and <component_dependency> manifest stanzas.
+
+    Schema:
+        <all>
+          <component_dependency value="component"/>
+          <component_dependency value="component"/>
+          <any_of>
+            <component_dependency value="component"/>
+            <component_dependency value="component"/>
+          </any_of>
+        </all>
+
+    Args:
+        dependency: XML Element of dependency.
+
+    Returns:
+        list of component id dependencies.
+    """
+    if dependency.tag == 'component_dependency':
+        return [dependency.attrib['value']]
+    if dependency.tag == 'all':
+        dependencies = []
+        for subdependency in dependency:
+            dependencies.extend(_parse_dependency(subdependency))
+        return dependencies
+    if dependency.tag == 'any_of':
+        # Explicitly ignore.
+        return []
+
+    # Unknown dependency tag type.
+    return []
+
+
+def check_dependencies(root: xml.etree.ElementTree.Element,
+                       component_id: str,
+                       include: List[str],
+                       exclude: Optional[List[str]] = None) -> bool:
+    """Check the list of optional dependencies for a component.
+
+    Verifies that the optional dependencies for a component are satisfied by
+    components listed in `include` or `exclude`.
+
+    Args:
+        root: root of element tree.
+        component_id: id of component to check.
+        include: list of component ids included in the project.
+        exclude: list of component ids explicitly excluded from the project.
+
+    Returns:
+        True if dependencies are satisfied, False if not.
+    """
+    xpath = f'./components/component[@id="{component_id}"]/dependencies/*'
+    for dependency in root.findall(xpath):
+        if not _check_dependency(dependency, include, exclude=exclude):
+            return False
+    return True
+
+
+def _check_dependency(dependency: xml.etree.ElementTree.Element,
+                      include: List[str],
+                      exclude: Optional[List[str]] = None) -> bool:
+    """Check a dependency for a component.
+
+    Verifies that the given {dependency} is satisfied by components listed in
+    `include` or `exclude`.
+
+    Args:
+        dependency: XML Element of dependency.
+        include: list of component ids included in the project.
+        exclude: list of component ids explicitly excluded from the project.
+
+    Returns:
+        True if dependencies are satisfied, False if not.
+    """
+    if dependency.tag == 'component_dependency':
+        component_id = dependency.attrib['value']
+        return component_id in include or (exclude is not None
+                                           and component_id in exclude)
+    if dependency.tag == 'all':
+        for subdependency in dependency:
+            if not _check_dependency(subdependency, include, exclude=exclude):
+                return False
+        return True
+    if dependency.tag == 'any_of':
+        for subdependency in dependency:
+            if _check_dependency(subdependency, include, exclude=exclude):
+                return True
+
+        tree = xml.etree.ElementTree.tostring(dependency).decode('utf-8')
+        print(f'Unsatisfied dependency from: {tree}', file=sys.stderr)
+        return False
+
+    # Unknown dependency tag type.
+    return True
+
+
+def create_project(
+    root: xml.etree.ElementTree.Element,
+    include: List[str],
+    exclude: Optional[List[str]] = None
+) -> Tuple[List[str], List[str], List[pathlib.Path], List[pathlib.Path],
+           List[pathlib.Path], List[pathlib.Path]]:
+    """Create a project from a list of specified components.
+
+    Args:
+        root: root of element tree.
+        include: list of component ids included in the project.
+        exclude: list of component ids excluded from the project.
+
+    Returns:
+        (component_ids, defines, include_paths, headers, sources, libs) for the
+        project.
+    """
+    # Build the project list from the list of included components by expanding
+    # dependencies.
+    project_list = []
+    pending_list = include
+    while len(pending_list) > 0:
+        component_id = pending_list.pop(0)
+        if component_id in project_list:
+            continue
+        if exclude is not None and component_id in exclude:
+            continue
+
+        project_list.append(component_id)
+        pending_list.extend(parse_dependencies(root, component_id))
+
+    return (
+        project_list,
+        sum((parse_defines(root, component_id)
+             for component_id in project_list), []),
+        sum((parse_include_paths(root, component_id)
+             for component_id in project_list), []),
+        sum((parse_headers(root, component_id)
+             for component_id in project_list), []),
+        sum((parse_sources(root, component_id)
+             for component_id in project_list), []),
+        sum((parse_libs(root, component_id) for component_id in project_list),
+            []),
+    )
+
+
+def project(manifest_path: pathlib.Path,
+            include: Optional[List[str]] = None,
+            exclude: Optional[List[str]] = None,
+            path_prefix: Optional[str] = None):
+    """Output GN scope for a project with the specified components.
+
+    Args:
+        manifest_path: path to SDK manifest XML.
+        include: list of component ids included in the project.
+        exclude: list of component ids excluded from the project.
+        path_prefix: string prefix to prepend to all paths.
+    """
+    assert include is not None, "Project must include at least one component."
+
+    tree = xml.etree.ElementTree.parse(manifest_path)
+    root = tree.getroot()
+
+    (component_ids, defines, include_dirs, headers, sources, libs) = \
+        create_project(root, include, exclude=exclude)
+
+    for component_id in component_ids:
+        if not check_dependencies(
+                root, component_id, component_ids, exclude=exclude):
+            return
+
+    _gn_list_str_out('defines', defines)
+    _gn_list_path_out('include_dirs', include_dirs, path_prefix=path_prefix)
+    _gn_list_path_out('public', headers, path_prefix=path_prefix)
+    _gn_list_path_out('sources', sources, path_prefix=path_prefix)
+    _gn_list_path_out('libs', libs, path_prefix=path_prefix)
diff --git a/pw_build_mcuxpresso/py/pyproject.toml b/pw_build_mcuxpresso/py/pyproject.toml
new file mode 100644
index 0000000..798b747
--- /dev/null
+++ b/pw_build_mcuxpresso/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_build_mcuxpresso/py/setup.cfg b/pw_build_mcuxpresso/py/setup.cfg
new file mode 100644
index 0000000..6f093a4
--- /dev/null
+++ b/pw_build_mcuxpresso/py/setup.cfg
@@ -0,0 +1,31 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[metadata]
+name = pw_build_mcuxpresso
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Python scripts for MCUXpresso targets
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+
+[options.entry_points]
+console_scripts =
+    mcuxpresso_builder = pw_build_mcuxpresso.__main__:main
+
+[options.package_data]
+pw_build_mcuxpresso = py.typed
diff --git a/pw_build_mcuxpresso/py/setup.py b/pw_build_mcuxpresso/py/setup.py
new file mode 100644
index 0000000..694c286
--- /dev/null
+++ b/pw_build_mcuxpresso/py/setup.py
@@ -0,0 +1,18 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""pw_build_mcuxpresso"""
+
+import setuptools  # type: ignore
+
+setuptools.setup()  # Package definition in setup.cfg
diff --git a/pw_build_mcuxpresso/py/tests/components_test.py b/pw_build_mcuxpresso/py/tests/components_test.py
new file mode 100644
index 0000000..e3559a8
--- /dev/null
+++ b/pw_build_mcuxpresso/py/tests/components_test.py
@@ -0,0 +1,910 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Components Tests."""
+
+import pathlib
+import unittest
+import xml.etree.ElementTree
+
+from pw_build_mcuxpresso import components
+
+
+class GetComponentTest(unittest.TestCase):
+    """get_component tests."""
+    def test_without_basepath(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+
+        (component, base_path) = components.get_component(root, 'test')
+
+        self.assertIsInstance(component, xml.etree.ElementTree.Element)
+        self.assertEqual(component.tag, 'component')
+        self.assertEqual(base_path, None)
+
+    def test_with_basepath(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test" package_base_path="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+
+        (component, base_path) = components.get_component(root, 'test')
+
+        self.assertIsInstance(component, xml.etree.ElementTree.Element)
+        self.assertEqual(component.tag, 'component')
+        self.assertEqual(base_path, pathlib.Path('test'))
+
+    def test_component_not_found(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="other">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+
+        (component, base_path) = components.get_component(root, 'test')
+
+        self.assertEqual(component, None)
+        self.assertEqual(base_path, None)
+
+
+class ParseDefinesTest(unittest.TestCase):
+    """parse_defines tests."""
+    def test_parse_defines(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <defines>
+                <define name="TEST_WITH_VALUE" value="1"/>
+                <define name="TEST_WITHOUT_VALUE"/>
+              </defines>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        defines = components.parse_defines(root, 'test')
+
+        self.assertEqual(defines, ['TEST_WITH_VALUE=1', 'TEST_WITHOUT_VALUE'])
+
+    def test_no_defines(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        defines = components.parse_defines(root, 'test')
+
+        self.assertEqual(defines, [])
+
+
+class ParseIncludePathsTest(unittest.TestCase):
+    """parse_include_paths tests."""
+    def test_parse_include_paths(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <include_paths>
+                <include_path relative_path="example" type="c_include"/>
+                <include_path relative_path="asm" type="asm_include"/>
+              </include_paths>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        include_paths = components.parse_include_paths(root, 'test')
+
+        self.assertEqual(include_paths,
+                         [pathlib.Path('example'),
+                          pathlib.Path('asm')])
+
+    def test_with_base_path(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test" package_base_path="src">
+              <include_paths>
+                <include_path relative_path="example" type="c_include"/>
+                <include_path relative_path="asm" type="asm_include"/>
+              </include_paths>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        include_paths = components.parse_include_paths(root, 'test')
+
+        self.assertEqual(
+            include_paths,
+            [pathlib.Path('src/example'),
+             pathlib.Path('src/asm')])
+
+    def test_unknown_type(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test" package_base_path="src">
+              <include_paths>
+                <include_path relative_path="rust" type="rust_include"/>
+              </include_paths>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        include_paths = components.parse_include_paths(root, 'test')
+
+        self.assertEqual(include_paths, [])
+
+    def test_no_include_paths(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        include_paths = components.parse_include_paths(root, 'test')
+
+        self.assertEqual(include_paths, [])
+
+
+class ParseHeadersTest(unittest.TestCase):
+    """parse_headers tests."""
+    def test_parse_headers(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <source relative_path="include" type="c_include">
+                <files mask="test.h"/>
+                <files mask="test_types.h"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        headers = components.parse_headers(root, 'test')
+
+        self.assertEqual(headers, [
+            pathlib.Path('include/test.h'),
+            pathlib.Path('include/test_types.h')
+        ])
+
+    def test_with_base_path(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test" package_base_path="src">
+              <source relative_path="include" type="c_include">
+                <files mask="test.h"/>
+                <files mask="test_types.h"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        headers = components.parse_headers(root, 'test')
+
+        self.assertEqual(headers, [
+            pathlib.Path('src/include/test.h'),
+            pathlib.Path('src/include/test_types.h')
+        ])
+
+    def test_multiple_sets(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <source relative_path="include" type="c_include">
+                <files mask="test.h"/>
+              </source>
+              <source relative_path="internal" type="c_include">
+                <files mask="test_types.h"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        headers = components.parse_headers(root, 'test')
+
+        self.assertEqual(headers, [
+            pathlib.Path('include/test.h'),
+            pathlib.Path('internal/test_types.h')
+        ])
+
+    def test_no_headers(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        headers = components.parse_headers(root, 'test')
+
+        self.assertEqual(headers, [])
+
+
+class ParseSourcesTest(unittest.TestCase):
+    """parse_sources tests."""
+    def test_parse_sources(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <source relative_path="src" type="src">
+                <files mask="main.cc"/>
+                <files mask="test.cc"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        sources = components.parse_sources(root, 'test')
+
+        self.assertEqual(
+            sources,
+            [pathlib.Path('src/main.cc'),
+             pathlib.Path('src/test.cc')])
+
+    def test_with_base_path(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test" package_base_path="src">
+              <source relative_path="app" type="src">
+                <files mask="main.cc"/>
+                <files mask="test.cc"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        sources = components.parse_sources(root, 'test')
+
+        self.assertEqual(
+            sources,
+            [pathlib.Path('src/app/main.cc'),
+             pathlib.Path('src/app/test.cc')])
+
+    def test_multiple_sets(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <source relative_path="shared" type="src">
+                <files mask="test.cc"/>
+              </source>
+              <source relative_path="lib" type="src_c">
+                <files mask="test.c"/>
+              </source>
+              <source relative_path="app" type="src_cpp">
+                <files mask="main.cc"/>
+              </source>
+              <source relative_path="startup" type="asm_include">
+                <files mask="boot.s"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        sources = components.parse_sources(root, 'test')
+
+        self.assertEqual(sources, [
+            pathlib.Path('shared/test.cc'),
+            pathlib.Path('lib/test.c'),
+            pathlib.Path('app/main.cc'),
+            pathlib.Path('startup/boot.s')
+        ])
+
+    def test_unknown_type(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            <source relative_path="src" type="rust">
+                <files mask="test.rs"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        sources = components.parse_sources(root, 'test')
+
+        self.assertEqual(sources, [])
+
+    def test_no_sources(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        sources = components.parse_sources(root, 'test')
+
+        self.assertEqual(sources, [])
+
+
+class ParseLibsTest(unittest.TestCase):
+    """parse_libs tests."""
+    def test_parse_libs(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <source relative_path="gcc" type="lib">
+                <files mask="libtest.a"/>
+                <files mask="libtest_arm.a"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        libs = components.parse_libs(root, 'test')
+
+        self.assertEqual(
+            libs,
+            [pathlib.Path('gcc/libtest.a'),
+             pathlib.Path('gcc/libtest_arm.a')])
+
+    def test_with_base_path(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test" package_base_path="src">
+              <source relative_path="gcc" type="lib">
+                <files mask="libtest.a"/>
+                <files mask="libtest_arm.a"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        libs = components.parse_libs(root, 'test')
+
+        self.assertEqual(libs, [
+            pathlib.Path('src/gcc/libtest.a'),
+            pathlib.Path('src/gcc/libtest_arm.a')
+        ])
+
+    def test_multiple_sets(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <source relative_path="gcc" type="lib">
+                <files mask="libtest.a"/>
+              </source>
+              <source relative_path="arm" type="lib">
+                <files mask="libtest_arm.a"/>
+              </source>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        libs = components.parse_libs(root, 'test')
+
+        self.assertEqual(
+            libs,
+            [pathlib.Path('gcc/libtest.a'),
+             pathlib.Path('arm/libtest_arm.a')])
+
+    def test_no_libs(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        libs = components.parse_libs(root, 'test')
+
+        self.assertEqual(libs, [])
+
+
+class ParseDependenciesTest(unittest.TestCase):
+    """parse_dependencies tests."""
+    def test_component_dependency(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <component_dependency value="foo"/>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        dependencies = components.parse_dependencies(root, 'test')
+
+        self.assertEqual(dependencies, ['foo'])
+
+    def test_all(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <all>
+                  <component_dependency value="foo"/>
+                  <component_dependency value="bar"/>
+                  <component_dependency value="baz"/>
+                </all>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        dependencies = components.parse_dependencies(root, 'test')
+
+        self.assertEqual(dependencies, ['foo', 'bar', 'baz'])
+
+    def test_any_of_ignored(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <component_dependency value="foo"/>
+                  <component_dependency value="bar"/>
+                  <component_dependency value="baz"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        dependencies = components.parse_dependencies(root, 'test')
+
+        self.assertEqual(dependencies, [])
+
+    def test_any_of_inside_all_ignored(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <all>
+                  <component_dependency value="foo"/>
+                  <component_dependency value="bar"/>
+                  <component_dependency value="baz"/>
+                  <any_of>
+                    <all>
+                      <component_dependency value="frodo"/>
+                      <component_dependency value="bilbo"/>
+                    </all>
+                    <component_dependency value="gandalf"/>
+                  </any_of>
+                </all>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        dependencies = components.parse_dependencies(root, 'test')
+
+        self.assertEqual(dependencies, ['foo', 'bar', 'baz'])
+
+    def test_no_dependencies(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        dependencies = components.parse_dependencies(root, 'test')
+
+        self.assertEqual(dependencies, [])
+
+
+class CheckDependenciesTest(unittest.TestCase):
+    """check_dependencies tests."""
+    def test_any_of_satisfied(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <component_dependency value="foo"/>
+                  <component_dependency value="bar"/>
+                  <component_dependency value="baz"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test', ['test', 'foo'],
+                                                  exclude=None)
+
+        self.assertEqual(satisfied, True)
+
+    def test_any_of_not_satisfied(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <component_dependency value="foo"/>
+                  <component_dependency value="bar"/>
+                  <component_dependency value="baz"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test', ['test'],
+                                                  exclude=None)
+
+        self.assertEqual(satisfied, False)
+
+    def test_any_of_satisfied_by_exclude(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <component_dependency value="foo"/>
+                  <component_dependency value="bar"/>
+                  <component_dependency value="baz"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test', ['test'],
+                                                  exclude=['foo'])
+
+        self.assertEqual(satisfied, True)
+
+    def test_any_of_all_satisfied(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <all>
+                    <component_dependency value="foo"/>
+                    <component_dependency value="bar"/>
+                    <component_dependency value="baz"/>
+                  </all>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(
+            root, 'test', ['test', 'foo', 'bar', 'baz'], exclude=None)
+
+        self.assertEqual(satisfied, True)
+
+    def test_any_of_all_not_satisfied(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <all>
+                    <component_dependency value="foo"/>
+                    <component_dependency value="bar"/>
+                    <component_dependency value="baz"/>
+                  </all>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test',
+                                                  ['test', 'foo', 'bar'],
+                                                  exclude=None)
+
+        self.assertEqual(satisfied, False)
+
+    def test_any_of_all_satisfied_by_exclude(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <all>
+                    <component_dependency value="foo"/>
+                    <component_dependency value="bar"/>
+                    <component_dependency value="baz"/>
+                  </all>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test',
+                                                  ['test', 'foo', 'bar'],
+                                                  exclude=['baz'])
+
+        self.assertEqual(satisfied, True)
+
+    def test_any_of_all_or_one_satisfied(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <all>
+                    <component_dependency value="foo"/>
+                    <component_dependency value="bar"/>
+                    <component_dependency value="baz"/>
+                  </all>
+                  <component_dependency value="frodo"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test', ['test', 'frodo'],
+                                                  exclude=None)
+
+        self.assertEqual(satisfied, True)
+
+    def test_any_of_all_or_one_not_satisfied(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <all>
+                    <component_dependency value="foo"/>
+                    <component_dependency value="bar"/>
+                    <component_dependency value="baz"/>
+                  </all>
+                  <component_dependency value="frodo"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test', ['test'],
+                                                  exclude=None)
+
+        self.assertEqual(satisfied, False)
+
+    def test_any_of_all_or_one_satisfied_by_exclude(self):
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <any_of>
+                  <all>
+                    <component_dependency value="foo"/>
+                    <component_dependency value="bar"/>
+                    <component_dependency value="baz"/>
+                  </all>
+                  <component_dependency value="frodo"/>
+                </any_of>
+              </dependencies>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        satisfied = components.check_dependencies(root,
+                                                  'test', ['test'],
+                                                  exclude=['frodo'])
+
+        self.assertEqual(satisfied, True)
+
+
+class CreateProjectTest(unittest.TestCase):
+    """create_project tests."""
+    def test_create_project(self):
+        """test creating a project."""
+        test_manifest_xml = '''
+        <manifest>
+          <components>
+            <component id="test">
+              <dependencies>
+                <component_dependency value="foo"/>
+                <component_dependency value="bar"/>
+                <any_of>
+                  <component_dependency value="baz"/>
+                </any_of>
+              </dependencies>
+            </component>
+            <component id="foo" package_base_path="foo">
+              <defines>
+                <define name="FOO"/>
+              </defines>
+              <source relative_path="include" type="c_include">
+                <files mask="foo.h"/>
+              </source>
+              <source relative_path="src" type="src">
+                <files mask="foo.cc"/>
+              </source>
+              <include_paths>
+                <include_path relative_path="include" type="c_include"/>
+              </include_paths>
+            </component>
+            <component id="bar" package_base_path="bar">
+              <defines>
+                <define name="BAR"/>
+              </defines>
+              <source relative_path="include" type="c_include">
+                <files mask="bar.h"/>
+              </source>
+              <source relative_path="src" type="src">
+                <files mask="bar.cc"/>
+              </source>
+              <include_paths>
+                <include_path relative_path="include" type="c_include"/>
+              </include_paths>
+            </component>
+            <!-- baz should not be included in the output -->
+            <component id="baz" package_base_path="baz">
+              <defines>
+                <define name="BAZ"/>
+              </defines>
+              <source relative_path="include" type="c_include">
+                <files mask="baz.h"/>
+              </source>
+              <source relative_path="src" type="src">
+                <files mask="baz.cc"/>
+              </source>
+              <include_paths>
+                <include_path relative_path="include" type="c_include"/>
+              </include_paths>
+            </component>
+            <component id="frodo" package_base_path="frodo">
+              <dependencies>
+                <component_dependency value="bilbo"/>
+              </dependencies>
+              <defines>
+                <define name="FRODO"/>
+              </defines>
+              <source relative_path="include" type="c_include">
+                <files mask="frodo.h"/>
+              </source>
+              <source relative_path="src" type="src">
+                <files mask="frodo.cc"/>
+              </source>
+              <source relative_path="./" type="lib">
+                <files mask="libonering.a"/>
+              </source>
+              <include_paths>
+                <include_path relative_path="include" type="c_include"/>
+              </include_paths>
+            </component>
+            <!-- bilbo should be excluded from the project -->
+            <component id="bilbo" package_base_path="bilbo">
+              <defines>
+                <define name="BILBO"/>
+              </defines>
+              <source relative_path="include" type="c_include">
+                <files mask="bilbo.h"/>
+              </source>
+              <source relative_path="src" type="src">
+                <files mask="bilbo.cc"/>
+              </source>
+              <include_paths>
+                <include_path relative_path="include" type="c_include"/>
+              </include_paths>
+            </component>
+          </components>
+        </manifest>
+        '''
+        root = xml.etree.ElementTree.fromstring(test_manifest_xml)
+        (component_ids, defines, include_dirs, headers, sources, libs) = \
+            components.create_project(root, ['test', 'frodo'],
+                                      exclude=['bilbo'])
+
+        self.assertEqual(component_ids, ['test', 'frodo', 'foo', 'bar'])
+        self.assertEqual(defines, ['FRODO', 'FOO', 'BAR'])
+        self.assertEqual(include_dirs, [
+            pathlib.Path('frodo/include'),
+            pathlib.Path('foo/include'),
+            pathlib.Path('bar/include')
+        ])
+        self.assertEqual(headers, [
+            pathlib.Path('frodo/include/frodo.h'),
+            pathlib.Path('foo/include/foo.h'),
+            pathlib.Path('bar/include/bar.h')
+        ])
+        self.assertEqual(sources, [
+            pathlib.Path('frodo/src/frodo.cc'),
+            pathlib.Path('foo/src/foo.cc'),
+            pathlib.Path('bar/src/bar.cc')
+        ])
+        self.assertEqual(libs, [pathlib.Path('frodo/libonering.a')])
+
+
+if __name__ == '__main__':
+    unittest.main()