stm32cube: Add generic build script

This adds a generic stm32cube build script. The pw_package creates a
text file listing all of the relavent files in the repos. The python
script then reads the text file and lists the needed source files and
headers.

The python script also inserts a `pw_stm32cube_Init()` into the startup
scripts and converts .icf -> .ld linker scripts.

Although the API's are not guaranteed to be compatible across families,
many of the common API's (like GPIO) are identical. This CL adds a
common header, so that integrations targeting multiple families can be
built.

I have tested this integration on F2, F4, and L5 boards.

Change-Id: Ibc79cf7d5cf76b220bb193849863abc16f413967
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/42924
Commit-Queue: Varun Sharma <vars@google.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
Reviewed-by: Ali Zhang <alizhang@google.com>
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index 1126bd4..428ba25 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -117,6 +117,7 @@
     "$dir_pw_snapshot:docs",
     "$dir_pw_span:docs",
     "$dir_pw_status:docs",
+    "$dir_pw_stm32cube_build:docs",
     "$dir_pw_stream:docs",
     "$dir_pw_string:docs",
     "$dir_pw_sync:docs",
diff --git a/modules.gni b/modules.gni
index d0c9043..b8ac64f 100644
--- a/modules.gni
+++ b/modules.gni
@@ -79,6 +79,7 @@
   dir_pw_snapshot = get_path_info("pw_snapshot", "abspath")
   dir_pw_span = get_path_info("pw_span", "abspath")
   dir_pw_status = get_path_info("pw_status", "abspath")
+  dir_pw_stm32cube_build = get_path_info("pw_stm32cube_build", "abspath")
   dir_pw_stream = get_path_info("pw_stream", "abspath")
   dir_pw_string = get_path_info("pw_string", "abspath")
   dir_pw_sync = get_path_info("pw_sync", "abspath")
diff --git a/pw_env_setup/BUILD.gn b/pw_env_setup/BUILD.gn
index b097e68..48be4eb 100644
--- a/pw_env_setup/BUILD.gn
+++ b/pw_env_setup/BUILD.gn
@@ -43,6 +43,7 @@
     "$dir_pw_protobuf_compiler/py",
     "$dir_pw_rpc/py",
     "$dir_pw_status/py",
+    "$dir_pw_stm32cube_build/py",
     "$dir_pw_tokenizer/py",
     "$dir_pw_toolchain/py",
     "$dir_pw_trace/py",
diff --git a/pw_package/py/BUILD.gn b/pw_package/py/BUILD.gn
index d1bdd91..24bf438 100644
--- a/pw_package/py/BUILD.gn
+++ b/pw_package/py/BUILD.gn
@@ -31,5 +31,8 @@
     "pw_package/pigweed_packages.py",
   ]
   pylintrc = "$dir_pigweed/.pylintrc"
-  python_deps = [ "$dir_pw_arduino_build/py" ]
+  python_deps = [
+    "$dir_pw_arduino_build/py",
+    "$dir_pw_stm32cube_build/py",
+  ]
 }
diff --git a/pw_package/py/pw_package/packages/stm32cube.py b/pw_package/py/pw_package/packages/stm32cube.py
index 2d53175..e07b639 100644
--- a/pw_package/py/pw_package/packages/stm32cube.py
+++ b/pw_package/py/pw_package/packages/stm32cube.py
@@ -16,6 +16,7 @@
 import pathlib
 from typing import Sequence
 
+import pw_stm32cube_build.gen_file_list
 import pw_package.git_repo
 import pw_package.package_manager
 
@@ -94,7 +95,7 @@
     "wl": {
         "hal_driver_tag": "v1.0.0",
         "cmsis_device_tag": "v1.0.0",
-        "cmsis_core_tag": "V5.6.0_cm4",
+        "cmsis_core_tag": "v5.6.0_cm4",
     },
 }
 
@@ -102,12 +103,12 @@
 class Stm32Cube(pw_package.package_manager.Package):
     """Install and check status of stm32cube."""
     def __init__(self, family, tags, *args, **kwargs):
-        super().__init__(*args, name=f'stm32cube{family}', **kwargs)
+        super().__init__(*args, name=f'stm32cube_{family}', **kwargs)
 
         st_github_url = 'https://github.com/STMicroelectronics'
 
         self._hal_driver = pw_package.git_repo.GitRepo(
-            name=f'stm32{family}xx_hal_driver',
+            name='hal_driver',
             url=f'{st_github_url}/stm32{family}xx_hal_driver.git',
             tag=tags['hal_driver_tag'],
         )
@@ -119,7 +120,7 @@
         )
 
         self._cmsis_device = pw_package.git_repo.GitRepo(
-            name=f'cmsis_device_{family}',
+            name='cmsis_device',
             url=f'{st_github_url}/cmsis_device_{family}.git',
             tag=tags['cmsis_device_tag'],
         )
@@ -129,12 +130,15 @@
         self._cmsis_core.install(path / self._cmsis_core.name)
         self._cmsis_device.install(path / self._cmsis_device.name)
 
+        pw_stm32cube_build.gen_file_list.gen_file_list(path)
+
     def status(self, path: pathlib.Path) -> bool:
-        return (
+        return all([
             self._hal_driver.status(path / self._hal_driver.name),
             self._cmsis_core.status(path / self._cmsis_core.name),
             self._cmsis_device.status(path / self._cmsis_device.name),
-        ) == (True, True, True)
+            (path / "files.txt").is_file(),
+        ])
 
     def info(self, path: pathlib.Path) -> Sequence[str]:
         return (
diff --git a/pw_stm32cube_build/BUILD.gn b/pw_stm32cube_build/BUILD.gn
new file mode 100644
index 0000000..9a6699a
--- /dev/null
+++ b/pw_stm32cube_build/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_stm32cube_build/docs.rst b/pw_stm32cube_build/docs.rst
new file mode 100644
index 0000000..6321495
--- /dev/null
+++ b/pw_stm32cube_build/docs.rst
@@ -0,0 +1,219 @@
+.. _module-pw_stm32cube_build:
+
+------------------
+pw_stm32cube_build
+------------------
+
+The ``pw_stm32cube_build`` module provides helper utilities for building a
+target with the stm32cube HAL and/or the stm32cube initialization code.
+
+The actual GN build files and headers live in ``third_party/stm32cube`` but
+are documented here. The rationale for keeping the build files in `third_party`
+is that code depending on stm32cube can clearly see that their dependency is on
+third party, not pigweed code.
+
+STM32Cube directory setup
+=========================
+Each stm32 product family (ex. F4, L5, etc.) has its own stm32cube libraries.
+This integration depends on ST's 3 core  `MCU Components`_ instead of their
+monolithic `MCU Package`. The components are the hal_driver, cmsis_core, and
+cmsis_device. All of these repos exist on `ST's GitHub page`_. Compatible
+version tags are specified on the ``README.md`` of each MCU component.
+Within a single directory, the following directory/file names are required.
+
+=============== =============================================
+Dir/File Name     Description
+=============== =============================================
+hal_driver/       checkout of ``stm32{family}xx_hal_driver``
+cmsis_device/     checkout of ``cmsis_device_{family}``
+cmsis_core/       checkout of ``cmsis_core``
+files.txt         list of files generated by `gen_file_list`_
+=============== =============================================
+
+pw_package
+----------
+The stm32cube directory can alternatively be setup using ``pw_package``. This
+will automatically download compatible repos into the expected folders and
+generate the ``files.txt``.
+
+.. code-block:: bash
+
+  pw package install stm32cube_{family}
+
+GN build
+========
+The primary ``pw_source_set`` for this integration is
+``$dir_pw_third_party/stm32cube:stm32cube``. This source set includes all of
+the HAL, init code, and templates, depending on value of the `GN args`_.
+
+Headers
+-------
+``$dir_pw_third_party/stm32cube:stm32cube`` contains the following primary
+headers that external targets / applications would care about.
+
+``{family}.h``
+^^^^^^^^^^^^^^
+ex. ``stm32f4xx.h``, ``stm32l5xx.h``
+
+This is the primary HAL header provided by stm32cube. It includes the entire
+HAL and all product specific defines.
+
+``stm32cube/stm32cube.h``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+This is a convenience define provided by this integration. It simply includes
+``{family}.h``.
+
+This is useful because there is a lot of commonality between the HAL's of the
+different stm32 families. Although the API's are not guaranteed to be
+compatible, many basic API's often are (ex. GPIO, UART, etc.). This common
+header allows for stm32 family agnostic modules (ex. ``pw_sys_io_stm32``, which
+could work with most, if not all families).
+
+``stm32cube/init.h``
+^^^^^^^^^^^^^^^^^^^^
+As described in the inject_init_ section, if you decide to use the built in
+init functionality, a pre main init function call, ``pw_stm32cube_Init()``, is
+injected into ST's startup scripts.
+
+This header contains the ``pw_stm32cube_Init()`` function declaration. It
+should be included and implemented by target init code.
+
+GN args
+-------
+The stm32cube GN build arguments are defined in
+``$dir_pw_third_party/stm32cube/stm32cube.gni``.
+
+``dir_pw_third_party_stm32cube_xx``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+These should be set to point to the stm32cube directory for each family that
+you need to build for. These are optional to set and are only provided for
+convenience if you need to build for multiple families in the same project.
+
+``dir_pw_third_party_stm32cube``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+This needs to point to the stm32cube directory for the current build.
+
+For multi target projects, the standard practice to set this for each target:
+
+.. code-block:: text
+
+  dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
+
+
+``pw_third_party_stm32cube_PRODUCT``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The product specified in as much detail as possible.
+ex. ``stm32f429zit``, ``stm32l552ze``, ``stm32f207zg``, etc.
+
+``pw_third_party_stm32cube_CONFIG``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The pw_source_set that provides ``stm32{family}xx_hal_conf.h``. The default
+uses the in-tree ``stm32{family}xx_hal_conf_template.h``.
+
+``pw_third_party_stm32cube_TIMEBASE``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The pw_source_set containing the timebase. The default uses the in-tree
+``stm32{family}xx_hal_timebase_tim_template.c``.
+
+``pw_third_party_stm32cube_CMSIS_INIT``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The pw_source_set containing the cmsis init logic. The default uses the in-tree
+``system_stm32{family}xx.c``.
+
+``pw_third_party_stm32cube_CORE_INIT``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+pw_source_set containing the core initialization logic. This normally includes
+a ``startup_stm32{...}.s`` + a dependent ``pw_linker_script``. The default
+``core_init_template`` uses the upstream startup and linker script matching
+``pw_third_party_stm32cube_PRODUCT``. If set to "", you must provide your own
+linker/startup logic somewhere else in the build.
+
+stm32cube_builder
+=================
+``stm32cube_builder`` is utility that contains the backend scripts used by
+``pw_package/stm32cube`` and the GN build scripts in ``third_party/stm32cube``
+to interact with the stm32cube repos. You should only need to interact with
+``stm32cube_builder`` directly if you are doing something custom, like
+using git submodules instead of pw_package, forking the stm32cube libraries,
+interfacing with a different build system, or using your own init.
+
+gen_file_list
+-------------
+Build systems like GN are unable to depend on arbitrary directories. Instead,
+they must have dependencies on specific files. The HAL for each stm32 product
+family has different filenames, so ``files.txt`` was created as a workaround.
+``files.txt`` is a basic list of all the files in the stm32cube directory with
+relavent file extensions. The build system only directly depends on this list,
+which must be updated everytime the underlying repos are updated.
+
+This command will generate ``files.txt`` for correctly structured stm32cube
+directories.
+
+.. code-block:: bash
+
+  stm32cube_builder gen_file_list /path/to/stm32cube_dir
+
+find_files
+----------
+Within each stm32 family, there are specific products. Although most of the
+HAL is common between products, the init code is almost always different.
+``find_files`` looks for all of the files relevant to a particular product
+within a stm32cube directory.
+
+The product string should be specified in as much detail as possible because
+there are sometimes different defines or init code for submembers of products.
+
+Ex. ``stm32f412cx``, ``stm32f412rx``, ``stm32f412vx``, and ``stm32f412zx`` all
+have different init logic, while all ``stm32f439xx`` have the same init.
+
+``find_files`` only ever looks for init (linker + startup scripts) if the
+``--init`` flag is provided.
+
+The output is currently only provided in the GN 'scope' format to stdout.
+The following variables are output: ``family``, ``product_define``,
+``sources``, ``headers``, ``include_dirs``, and the following three if
+``--init`` is specified: ``startup``, ``gcc_linker``, ``iar_linker``.
+
+.. code-block:: bash
+
+  stm32cube_builder find_files /path/to/stm32cube_dir stm32{family}{product} [--init]
+
+inject_init
+-----------
+ST provides init assembly files for every product in ``cmsis_device``. This is
+helpful for getting up and running quickly, but they directly call into
+``main()`` before initializing the hardware / peripherals. This is because they
+expect to do that initialization in ``main()``, then call into the user
+application. Upstream Pigweed unit tests expect at least ``sys_io`` to be
+initialized before ``main()`` is called.
+
+This command injects a call to ``pw_stm32cube_Init()`` immediately before the
+call to ``main()``. This function should be implemented by the target to do
+whatever init is necessary (hal init, sys_io init, clock configuration, etc.)
+
+``inject_init`` takes in an ST assembly script and outputs the same script with
+the pre main init call. The output is printed to stdout, or to the specified
+``--out-startup-path``.
+
+.. code-block:: bash
+
+  stm32cube_builder inject_init /path/to/startup.s [--out-startup-path /path/to/new_startup.s]
+
+icf_to_ld
+---------
+Pigweed primarily uses GCC for its Cortex-M builds. However, ST only provides
+IAR linker scripts in ``cmsis_device`` for most product families. This script
+converts from ST's IAR linker script format (.icf) to a basic GCC linker
+script (.ld). This is a very basic converter that only works with exactly how
+ST currently formats their .icf files.
+
+The output .ld files only contain ``RAM`` and ``FLASH`` sections. Anything more
+complicated will require hand customized .ld scripts. Output is printed to
+stdout or the specified ``--ld-path``.
+
+.. code-block:: bash
+
+  stm32cube_builder inject_init /path/to/iar_linker.icf [--ld-path /path/to/gcc_linker.ld]
+
+.. _`MCU Components`: https://github.com/STMicroelectronics/STM32Cube_MCU_Overall_Offer#stm32cube-mcu-components
+.. _`ST's GitHub page`: https://github.com/STMicroelectronics
diff --git a/pw_stm32cube_build/py/BUILD.gn b/pw_stm32cube_build/py/BUILD.gn
new file mode 100644
index 0000000..efcc869
--- /dev/null
+++ b/pw_stm32cube_build/py/BUILD.gn
@@ -0,0 +1,35 @@
+# 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 = [ "setup.py" ]
+  sources = [
+    "pw_stm32cube_build/__init__.py",
+    "pw_stm32cube_build/__main__.py",
+    "pw_stm32cube_build/find_files.py",
+    "pw_stm32cube_build/gen_file_list.py",
+    "pw_stm32cube_build/icf_to_ld.py",
+    "pw_stm32cube_build/inject_init.py",
+  ]
+  tests = [
+    "tests/find_files_test.py",
+    "tests/icf_to_ld_test.py",
+    "tests/inject_init_test.py",
+  ]
+  pylintrc = "$dir_pigweed/.pylintrc"
+}
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/__init__.py b/pw_stm32cube_build/py/pw_stm32cube_build/__init__.py
new file mode 100644
index 0000000..bdc367b
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/__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 the stm32cube."""
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py b/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py
new file mode 100644
index 0000000..fff71ba
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py
@@ -0,0 +1,83 @@
+#!/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 stm32cube_builder."""
+
+import argparse
+import pathlib
+import sys
+
+from pw_stm32cube_build import find_files, gen_file_list, icf_to_ld, inject_init
+
+
+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)
+
+    gen_file_list_parser = subparsers.add_parser(
+        'gen_file_list', help='generate files.txt for stm32cube directory')
+    gen_file_list_parser.add_argument('stm32cube_dir', type=pathlib.Path)
+
+    find_files_parser = subparsers.add_parser(
+        'find_files', help='find files in stm32cube directory')
+    find_files_parser.add_argument('stm32cube_dir', type=pathlib.Path)
+    find_files_parser.add_argument('product_str')
+    find_files_parser.add_argument('--init',
+                                   default=False,
+                                   action='store_true')
+
+    icf_to_ld_parser = subparsers.add_parser(
+        'icf_to_ld', help='convert stm32cube .icf linker files to .ld')
+    icf_to_ld_parser.add_argument('icf_path', type=pathlib.Path)
+    icf_to_ld_parser.add_argument('--ld-path',
+                                  nargs=1,
+                                  default=None,
+                                  type=pathlib.Path)
+
+    inject_init_parser = subparsers.add_parser(
+        'inject_init', help='inject `pw_stm32cube_Init()` into startup_*.s')
+    inject_init_parser.add_argument('in_startup_path', type=pathlib.Path)
+    inject_init_parser.add_argument('--out-startup-path',
+                                    nargs=1,
+                                    default=None,
+                                    type=pathlib.Path)
+
+    return parser.parse_args()
+
+
+def main():
+    """Main command line function."""
+    args = _parse_args()
+
+    if args.command == 'gen_file_list':
+        gen_file_list.gen_file_list(args.stm32cube_dir)
+    elif args.command == 'find_files':
+        find_files.find_files(args.stm32cube_dir, args.product_str, args.init)
+    elif args.command == 'icf_to_ld':
+        icf_to_ld.icf_to_ld(args.icf_path,
+                            args.ld_path[0] if args.ld_path else None)
+    elif args.command == 'inject_init':
+        inject_init.inject_init(
+            args.in_startup_path,
+            args.out_startup_path[0] if args.out_startup_path else None)
+
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py b/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
new file mode 100644
index 0000000..b6aa016
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
@@ -0,0 +1,299 @@
+# 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 files for a given product."""
+
+from typing import Any, List, Optional, Set, Tuple
+
+import pathlib
+import re
+
+
+def parse_product_str(product_str: str) -> Tuple[str, Set[str], str]:
+    """Parses provided product string.
+
+    Args:
+        product_str: target supplied product string.
+
+    Returns:
+        (family, defines, name) where
+            `family` is the stm32 product family (ex. 'stm32l5xx')
+            `defines` is a list of potential product defines for the HAL.
+                There can be multiple because some products use a subfamily
+                for their define.
+                (ex. The only stm32f411 define is `STM32F411xE`)
+                The correct define can be validated using `select_define()`
+            `name` is the standardized name for the product string.
+                (ex. product_str = 'STM32F429', name = 'stm32f429xx')
+                This is the product name expected by the filename matching
+                functions (`match_filename()`, etc.)
+
+    Raises:
+        ValueError if the product string does not start with 'stm32' or specify
+            at least the chip model (9 chars).
+    """
+    product_name = product_str.lower()
+    if not product_name.startswith('stm32'):
+        raise ValueError("Product string must start with 'stm32'")
+
+    if len(product_name) < 9:
+        raise ValueError(
+            "Product string too short. Must specify at least the chip model.")
+
+    family = product_name[:7] + 'xx'
+    name = product_name
+
+    # Pad the full name with 'x' to reach the max expected length.
+    name = product_name.ljust(11, 'x')
+
+    # This generates all potential define suffixes for a given product name
+    # This is required because some boards have more specific defines
+    # ex. STM32F411xE, while most others are generic, ex. STM32F439xx
+    # So if the user specifies `stm32f207zgt6u`, this should generate the
+    # following as potential defines
+    #  STM32F207xx, STM32F207Zx, STM32F207xG, STM32F207ZG
+    define_suffixes = ['xx']
+    if name[9] != 'x':
+        define_suffixes.append(name[9].upper() + 'x')
+    if name[10] != 'x':
+        define_suffixes.append('x' + name[10].upper())
+    if name[9] != 'x' and name[10] != 'x':
+        define_suffixes.append(name[9:11].upper())
+
+    defines = set(map(lambda x: product_name[:9].upper() + x, define_suffixes))
+    return (family, defines, name)
+
+
+def select_define(defines: Set[str], family_header: str) -> str:
+    """Selects valid define from set of potential defines.
+
+    Looks for the defines in the family header to pick the correct one.
+
+    Args:
+        defines: set of defines provided by `parse_product_str`
+        family_header: `{family}.h` read into a string
+
+    Returns:
+        A single valid define
+
+    Raises:
+        ValueError if exactly one define is not found.
+    """
+    valid_defines = list(
+        filter(
+            lambda x: f'defined({x})' in family_header or f'defined ({x})' in
+            family_header, defines))
+
+    if len(valid_defines) != 1:
+        raise ValueError("Unable to select a valid define")
+
+    return valid_defines[0]
+
+
+def match_filename(product_name: str, filename: str):
+    """Matches linker and startup filenames with product name.
+
+    Args:
+        product_name: the name standardized by `parse_product_str`
+        filename: a linker or startup filename
+
+    Returns:
+        True if the filename could be associated with the product.
+        False otherwise.
+    """
+    stm32_parts = list(
+        filter(lambda x: x.startswith('stm32'),
+               re.split(r'\.|_', filename.lower())))
+
+    if len(stm32_parts) != 1:
+        return False
+
+    pattern = stm32_parts[0].replace('x', '.')
+
+    return re.match(pattern, product_name) is not None
+
+
+def find_linker_files(
+    product_name: str, files: List[str], stm32cube_path: pathlib.Path
+) -> Tuple[Optional[pathlib.Path], Optional[pathlib.Path]]:
+    """Finds linker file for the given product.
+
+    This searches `files` for linker scripts by name.
+
+    Args:
+        product_name: the name standardized by `parse_product_str`
+        files: list of file paths
+        stm32cube_path: the root path that `files` entries are relative to
+
+    Returns:
+        (gcc_linker, iar_linker) where gcc_linker / iar_linker are paths to a
+            linker file or None
+
+    Raises:
+        ValueError if `product_name` matches with no linker files, or with
+            multiple .ld/.icf files.
+    """
+    linker_files = list(
+        filter(
+            lambda x:
+            (x.endswith('.ld') or x.endswith('.icf')) and '_flash.' in x.lower(
+            ), files))
+    matching_linker_files = list(
+        filter(lambda x: match_filename(product_name,
+                                        pathlib.Path(x).name), linker_files))
+
+    matching_ld_files = list(
+        filter(lambda x: x.endswith('.ld'), matching_linker_files))
+    matching_icf_files = list(
+        filter(lambda x: x.endswith('.icf'), matching_linker_files))
+
+    if len(matching_ld_files) > 1 or len(matching_icf_files) > 1:
+        raise ValueError(
+            f'Too many linker file matches for {product_name}.' +
+            ' Provide a more specific product string or your own linker script'
+        )
+    if not matching_ld_files and not matching_icf_files:
+        raise ValueError(f'No linker script matching {product_name} found')
+
+    return (stm32cube_path /
+            matching_ld_files[0] if matching_ld_files else None,
+            stm32cube_path /
+            matching_icf_files[0] if matching_icf_files else None)
+
+
+def find_startup_file(product_name: str, files: List[str],
+                      stm32cube_path: pathlib.Path) -> pathlib.Path:
+    """Finds startup file for the given product.
+
+    Searches for gcc startup files.
+
+    Args:
+        product_name: the name standardized by `parse_product_str`
+        files: list of file paths
+        stm32cube_path: the root path that `files` entries are relative to
+
+    Returns:
+        Path to matching startup file
+
+    Raises:
+        ValueError if no / > 1 matching startup files found.
+    """
+    # ST provides startup files for gcc, iar, and arm compilers. They have the
+    # same filenames, so this looks for a 'gcc' folder in the path.
+    matching_startup_files = list(
+        filter(
+            lambda f: '/gcc/' in f and f.endswith('.s') and match_filename(
+                product_name, f), files))
+
+    if not matching_startup_files:
+        raise ValueError(f'No matching startup file found for {product_name}')
+    if len(matching_startup_files) == 1:
+        return stm32cube_path / matching_startup_files[0]
+
+    raise ValueError(
+        f'Multiple matching startup files found for {product_name}')
+
+
+_INCLUDE_DIRS = [
+    'hal_driver/Inc',
+    'hal_driver/Inc/Legacy',
+    'cmsis_device/Include',
+    'cmsis_core/Include',
+]
+
+
+def get_include_dirs(stm32cube_path: pathlib.Path) -> List[pathlib.Path]:
+    """Get HAL include directories."""
+    return list(map(lambda f: stm32cube_path / f, _INCLUDE_DIRS))
+
+
+def get_sources_and_headers(
+        files: List[str],
+        stm32cube_path: pathlib.Path) -> Tuple[List[str], List[str]]:
+    """Gets list of all sources and headers needed to build the stm32cube hal.
+
+    Args:
+        files: list of file paths
+        stm32cube_path: the root path that `files` entries are relative to
+
+    Returns:
+        (sources, headers) where
+            `sources` is a list of absolute paths to all core (non-template)
+                sources needed for the hal
+            `headers` is a list of absolute paths to all needed headers
+    """
+    source_files = filter(
+        lambda f: f.startswith('hal_driver/Src') and f.endswith('.c') and
+        'template' not in f, files)
+
+    header_files = filter(
+        lambda f: (any([f.startswith(dir)
+                        for dir in _INCLUDE_DIRS])) and f.endswith('.h'),
+        files)
+
+    rebase_path = lambda f: str(stm32cube_path / f)
+    return list(map(rebase_path,
+                    source_files)), list(map(rebase_path, header_files))
+
+
+def parse_files_txt(stm32cube_path: pathlib.Path) -> List[str]:
+    """Reads files.txt into list."""
+    with open(stm32cube_path / 'files.txt', 'r') as files:
+        return list(
+            filter(lambda x: not x.startswith('#'),
+                   map(lambda f: f.strip(), files.readlines())))
+
+
+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 find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool):
+    """Generates and outputs the required GN args for the build."""
+    file_list = parse_files_txt(stm32cube_path)
+
+    include_dirs = get_include_dirs(stm32cube_path)
+    sources, headers = get_sources_and_headers(file_list, stm32cube_path)
+    (family, defines, name) = parse_product_str(product_str)
+
+    family_header_path = list(
+        filter(lambda p: p.endswith(f'/{family}.h'), headers))[0]
+
+    with open(family_header_path, 'rb') as family_header:
+        family_header_str = family_header.read().decode('utf-8',
+                                                        errors='ignore')
+
+    define = select_define(defines, family_header_str)
+
+    _gn_str_out('family', family)
+    _gn_str_out('product_define', define)
+    _gn_list_str_out('sources', sources)
+    _gn_list_str_out('headers', headers)
+    _gn_list_str_out('include_dirs', include_dirs)
+
+    if init:
+        startup_file_path = find_startup_file(name, file_list, stm32cube_path)
+        gcc_linker, iar_linker = find_linker_files(name, file_list,
+                                                   stm32cube_path)
+
+        _gn_str_out('startup', startup_file_path)
+        _gn_str_out('gcc_linker', gcc_linker if gcc_linker else '')
+        _gn_str_out('iar_linker', iar_linker if iar_linker else '')
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py b/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py
new file mode 100644
index 0000000..89d40d4
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py
@@ -0,0 +1,51 @@
+# 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.
+"""Generates a list of relevant files present in a stm32cube source package."""
+
+from typing import List
+
+import pathlib
+
+
+def gen_file_list(stm32cube_dir: pathlib.Path):
+    """Generates `files.txt` for stm32cube directories
+
+    The paths in `files.txt` are relative paths to the files in the 'posix'
+    path format.
+
+    Args:
+        stm32cube_dir: stm32cube directory containing 'hal_driver',
+            'cmsis_core' and 'cmsis_device
+
+    Raises
+        AssertionError if the provided directory is invalid
+    """
+
+    assert (stm32cube_dir / 'hal_driver').is_dir(), 'hal_driver not found'
+    assert (stm32cube_dir / 'cmsis_core').is_dir(), 'cmsis_core not found'
+    assert (stm32cube_dir / 'cmsis_device').is_dir(), 'cmsis_device not found'
+
+    file_paths: List[pathlib.Path] = []
+    file_paths.extend(stm32cube_dir.glob("**/*.h"))
+    file_paths.extend(stm32cube_dir.glob("**/*.c"))
+    file_paths.extend(stm32cube_dir.glob("**/*.s"))
+    file_paths.extend(stm32cube_dir.glob("**/*.ld"))
+    file_paths.extend(stm32cube_dir.glob("**/*.icf"))
+
+    #TODO: allow arbitrary path for generated file list
+    with open(stm32cube_dir / "files.txt", "w") as out_file:
+        out_file.write('# Generated by pw_stm32cube_build/gen_file_list\n')
+        for file_path in file_paths:
+            out_file.write(
+                file_path.relative_to(stm32cube_dir).as_posix() + '\n')
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py b/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py
new file mode 100644
index 0000000..fafede0
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py
@@ -0,0 +1,288 @@
+# 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.
+"""Converts ST generated .icf linker files into basic .ld linker files"""
+
+from typing import Dict, Optional, Tuple
+
+import pathlib
+
+
+def parse_icf(icf_file: str) -> Tuple[Dict, Dict]:
+    """Parse ICF linker file.
+
+    ST only provides .icf linker files for many products, so there is a need
+    to generate basic GCC compatible .ld files for all products.
+    This parses the basic features from the .icf format well enough to work
+    for the ST's .icf files that exist in `cmsis_device`
+
+    Args:
+        icf_file: .icf linker file read into a string
+
+    Returns:
+        (regions, blocks) where
+            `regions` is a map from region_name -> (start_hex, end_hex)
+            `blocks` is a map from block_name -> {feature_1: val_1,...}
+
+    Raises:
+        IndexError if .icf is malformed (at least compared to how ST makes them)
+    """
+    symbols = {}
+    regions = {}  # region: (start_addr, end_addr)
+    blocks = {}
+    for line in icf_file.split('\n'):
+        line = line.strip()
+        if line == '' or line.startswith('/*') or line.startswith('//'):
+            continue
+        tokens = line.split()
+        if len(tokens) < 2:
+            continue
+        if tokens[0] == 'define':
+            if tokens[1] == 'symbol':
+                symbols[tokens[2]] = tokens[4].strip(';')
+            elif tokens[1] == 'region':
+                regions[tokens[2].split('_')[0]] = (tokens[5],
+                                                    tokens[7].strip('];'))
+            elif tokens[1] == 'block':
+                blocks[tokens[2]] = {
+                    tokens[4]: tokens[6].strip(','),
+                    tokens[7]: tokens[9]
+                }
+    parsed_regions = {
+        region: (symbols[start] if start in symbols else start,
+                 symbols[end] if end in symbols else end)
+        for region, (start, end) in regions.items()
+    }
+
+    parsed_blocks = {
+        name:
+        {k: symbols[v] if v in symbols else v
+         for k, v in fields.items()}
+        for name, fields in blocks.items()
+    }
+
+    return (parsed_regions, parsed_blocks)
+
+
+def icf_regions_to_ld_regions(icf_regions: Dict) -> Dict:
+    """Converts .icf regions to .ld regions
+
+    The .icf format specifies the beginning and end of each region, while
+    .ld expects the beginning and a length string.
+
+    Args:
+        icf_regions: icf_regions parsed with `parse_icf()`
+
+    Returns:
+        A map from `region_name` -> (start_hex, length_str)
+    """
+    ld_regions = {}
+    for region, (start, end) in icf_regions.items():
+        start_dec = int(start, 16)
+        end_dec = int(end, 16)
+        length = end_dec - start_dec + 1
+        length_str = str(length)
+        if length % 1024 == 0:
+            length_str = f'{int(length/1024)}K'
+
+        # Some icf scripts incorrectly have an exclusive region end.
+        # This corrects for that.
+        elif (length - 1) % 1024 == 0:
+            length_str = f'{int((length-1)/1024)}K'
+
+        # ST's gcc linker scripts generally use FLASH instead of ROM
+        if region == 'ROM':
+            region = 'FLASH'
+
+        ld_regions[region] = (start, length_str)
+
+    return ld_regions
+
+
+def create_ld(ld_regions: Dict, blocks: Dict) -> str:
+    """Create .ld file from template.
+
+    This creates a barebones .ld file that *should* work for most single core
+    stm32 families. It only contains regions for RAM and FLASH.
+
+    This template can be bypassed in GN if a more sophisticated linker file
+    is required.
+
+    Args:
+        ld_regions: generated by `icf_regions_to_ld_regions()`
+        blocks: generated by `parse_icf`
+
+    Returns:
+        a string linker file with the RAM/FLASH specified by the given reginos.
+
+    Raises:
+        KeyError if ld_regions does not contain 'RAM' and 'FLASH'
+    """
+    return f"""\
+ENTRY(Reset_Handler)
+_estack = ORIGIN(RAM) + LENGTH(RAM);
+
+_Min_Heap_Size = {blocks['HEAP']['size']};
+_Min_Stack_Size = {blocks['CSTACK']['size']};
+
+MEMORY
+{{
+  RAM (xrw) : ORIGIN = {ld_regions['RAM'][0]}, LENGTH = {ld_regions['RAM'][1]}
+  FLASH (rx) : ORIGIN = {ld_regions['FLASH'][0]}, LENGTH = {ld_regions['FLASH'][1]}
+}}
+
+SECTIONS
+{{
+  .isr_vector :
+  {{
+    . = ALIGN(8);
+    KEEP(*(.isr_vector))
+    . = ALIGN(8);
+  }} >FLASH
+
+  .text :
+  {{
+    . = ALIGN(8);
+    *(.text)
+    *(.text*)
+    *(.glue_7)
+    *(.glue_7t)
+    *(.eh_frame)
+
+    KEEP (*(.init))
+    KEEP (*(.fini))
+
+    . = ALIGN(8);
+    _etext = .;
+  }} >FLASH
+
+  .rodata :
+  {{
+    . = ALIGN(8);
+    *(.rodata)
+    *(.rodata*)
+    . = ALIGN(8);
+  }} >FLASH
+
+  .ARM.extab   : {{
+    . = ALIGN(8);
+    *(.ARM.extab* .gnu.linkonce.armextab.*)
+    . = ALIGN(8);
+  }} >FLASH
+
+  .ARM : {{
+    . = ALIGN(8);
+    __exidx_start = .;
+    *(.ARM.exidx*)
+    __exidx_end = .;
+    . = ALIGN(8);
+  }} >FLASH
+
+  .preinit_array     :
+  {{
+    . = ALIGN(8);
+    PROVIDE_HIDDEN (__preinit_array_start = .);
+    KEEP (*(.preinit_array*))
+    PROVIDE_HIDDEN (__preinit_array_end = .);
+    . = ALIGN(8);
+  }} >FLASH
+
+  .init_array :
+  {{
+    . = ALIGN(8);
+    PROVIDE_HIDDEN (__init_array_start = .);
+    KEEP (*(SORT(.init_array.*)))
+    KEEP (*(.init_array*))
+    PROVIDE_HIDDEN (__init_array_end = .);
+    . = ALIGN(8);
+  }} >FLASH
+
+  .fini_array :
+  {{
+    . = ALIGN(8);
+    PROVIDE_HIDDEN (__fini_array_start = .);
+    KEEP (*(SORT(.fini_array.*)))
+    KEEP (*(.fini_array*))
+    PROVIDE_HIDDEN (__fini_array_end = .);
+    . = ALIGN(8);
+  }} >FLASH
+
+  _sidata = LOADADDR(.data);
+  .data :
+  {{
+    . = ALIGN(8);
+    _sdata = .;
+    *(.data)
+    *(.data*)
+    . = ALIGN(8);
+    _edata = .;
+  }} >RAM AT> FLASH
+
+  . = ALIGN(8);
+  .bss :
+  {{
+    _sbss = .;
+    __bss_start__ = _sbss;
+    *(.bss)
+    *(.bss*)
+    *(COMMON)
+
+    . = ALIGN(8);
+    _ebss = .;
+    __bss_end__ = _ebss;
+  }} >RAM
+
+  ._user_heap_stack :
+  {{
+    . = ALIGN(8);
+    PROVIDE ( end = . );
+    PROVIDE ( _end = . );
+    . = . + _Min_Heap_Size;
+    . = . + _Min_Stack_Size;
+    . = ALIGN(8);
+  }} >RAM
+
+  /DISCARD/ :
+  {{
+    libc.a ( * )
+    libm.a ( * )
+    libgcc.a ( * )
+  }}
+
+  .ARM.attributes 0 : {{ *(.ARM.attributes) }}
+}}
+    """
+
+
+def icf_to_ld(icf_path: pathlib.Path, ld_path: Optional[pathlib.Path]):
+    """Convert icf file into an ld file.
+
+    Note: This only works for ST generated .icf files.
+
+    Args:
+        icf_path: path to .icf file to convert
+        ld_path: path to write generated .ld file or None.
+                 If None, the .ld file is written to stdout.
+    """
+    with open(icf_path, 'rb') as icf_file:
+        icf_str = icf_file.read().decode('utf-8', errors='ignore')
+
+    icf_regions, blocks = parse_icf(icf_str)
+    ld_regions = icf_regions_to_ld_regions(icf_regions)
+    ld_str = create_ld(ld_regions, blocks)
+
+    if ld_path:
+        with open(ld_path, 'w') as ld_file:
+            ld_file.write(ld_str)
+    else:
+        print(ld_str)
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py b/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py
new file mode 100644
index 0000000..8a68cb6
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py
@@ -0,0 +1,67 @@
+# 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.
+"""Injects pre main init to ST startup scripts."""
+
+from typing import Optional
+
+import pathlib
+import re
+
+
+def add_pre_main_init(startup: str) -> str:
+    """Add pw_stm32cube_Init call to startup file
+
+    The stm32cube startup files directly call main(), while pigweed expects to
+    do some setup before main is called. This could include sys_io or system
+    clock initialization.
+
+    This adds a call to `pw_stm32cube_Init()` immediately before the call to
+    `main()`
+
+    Args:
+        startup: The startup script read into a string
+
+    Returns:
+        A new startup script with the `pw_stm32cube_Init()` call added.
+
+    Raises:
+        ValueError if the `main()` call is not found in `startup`
+    """
+    match = re.search(r'\s*bl\s+main', startup)
+    if match is None:
+        raise ValueError("`bl main` not found in startup script")
+
+    return startup[:match.start(
+    )] + '\nbl pw_stm32cube_Init' + startup[match.start():]
+
+
+def inject_init(startup_in: pathlib.Path, startup_out: Optional[pathlib.Path]):
+    """Injects pw_stm32cube_Init before main in given ST startup script.
+
+    Args:
+        startup_in: path to startup_*.s file
+        startup_out: path to write generated startup file or None.
+                    If None, output startup script printed to stdout
+    """
+    with open(startup_in, 'rb') as startup_in_file:
+        startup_in_str = startup_in_file.read().decode('utf-8',
+                                                       errors='ignore')
+
+    startup_out_str = add_pre_main_init(startup_in_str)
+
+    if startup_out:
+        with open(startup_out, 'w') as startup_out_file:
+            startup_out_file.write(startup_out_str)
+    else:
+        print(startup_out_str)
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/py.typed b/pw_stm32cube_build/py/pw_stm32cube_build/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/py.typed
diff --git a/pw_stm32cube_build/py/setup.py b/pw_stm32cube_build/py/setup.py
new file mode 100644
index 0000000..4a4f3b3
--- /dev/null
+++ b/pw_stm32cube_build/py/setup.py
@@ -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.
+"""pw_stm32cube_build"""
+
+import setuptools  # type: ignore
+
+setuptools.setup(
+    name='pw_stm32cube_build',
+    version='0.0.1',
+    author='Pigweed Authors',
+    author_email='pigweed-developers@googlegroups.com',
+    description='Python scripts for stm32cube targets',
+    packages=setuptools.find_packages(),
+    package_data={'pw_stm32cube_build': ['py.typed']},
+    zip_safe=False,
+    entry_points={
+        'console_scripts': [
+            'stm32cube_builder = pw_stm32cube_build.__main__:main',
+        ]
+    },
+    install_requires=[])
diff --git a/pw_stm32cube_build/py/tests/find_files_test.py b/pw_stm32cube_build/py/tests/find_files_test.py
new file mode 100644
index 0000000..882c3bf
--- /dev/null
+++ b/pw_stm32cube_build/py/tests/find_files_test.py
@@ -0,0 +1,303 @@
+#!/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.
+"""Generate File List Tests."""
+
+import pathlib
+import unittest
+
+from pw_stm32cube_build import find_files
+
+
+class ParseProductStringTest(unittest.TestCase):
+    """parse_product_str tests."""
+    def test_start_with_stm32(self):
+        with self.assertRaises(ValueError):
+            find_files.parse_product_str('f439zit')
+
+    def test_specify_chip(self):
+        with self.assertRaises(ValueError):
+            find_files.parse_product_str('stm32f43')
+
+    def test_stm32f412zx(self):
+        (family, defines, name) = find_files.parse_product_str('stm32f412zx')
+
+        self.assertEqual(family, 'stm32f4xx')
+        self.assertEqual(defines, {'STM32F412xx', 'STM32F412Zx'})
+        self.assertEqual(name, 'stm32f412zx')
+
+    def test_stm32f439xx(self):
+        (family, defines, name) = find_files.parse_product_str('STM32F439xx')
+
+        self.assertEqual(family, 'stm32f4xx')
+        self.assertEqual(defines, {'STM32F439xx'})
+        self.assertEqual(name, 'stm32f439xx')
+
+    def test_stm32f439(self):
+        (family, defines, name) = find_files.parse_product_str('STM32F439')
+
+        self.assertEqual(family, 'stm32f4xx')
+        self.assertEqual(defines, {'STM32F439xx'})
+        self.assertEqual(name, 'stm32f439xx')
+
+    def test_stm32f439xi(self):
+        (family, defines, name) = find_files.parse_product_str('STM32F439xI')
+
+        self.assertEqual(family, 'stm32f4xx')
+        self.assertEqual(defines, {'STM32F439xx', 'STM32F439xI'})
+        self.assertEqual(name, 'stm32f439xi')
+
+    def test_stm32f439zit6u(self):
+        (family, defines,
+         name) = find_files.parse_product_str('stm32f439zit6u')
+
+        self.assertEqual(family, 'stm32f4xx')
+        self.assertEqual(
+            defines,
+            {'STM32F439xx', 'STM32F439Zx', 'STM32F439xI', 'STM32F439ZI'})
+        self.assertEqual(name, 'stm32f439zit6u')
+
+    def test_stm32l552zet(self):
+        (family, defines, name) = find_files.parse_product_str('stm32l552zet')
+
+        self.assertEqual(family, 'stm32l5xx')
+        self.assertEqual(
+            defines,
+            {'STM32L552xx', 'STM32L552Zx', 'STM32L552xE', 'STM32L552ZE'})
+        self.assertEqual(name, 'stm32l552zet')
+
+    def test_stm32l552xc(self):
+        (family, defines, name) = find_files.parse_product_str('stm32l552xc')
+
+        self.assertEqual(family, 'stm32l5xx')
+        self.assertEqual(defines, {'STM32L552xx', 'STM32L552xC'})
+        self.assertEqual(name, 'stm32l552xc')
+
+    def test_stm32wb5m(self):
+        (family, defines, name) = find_files.parse_product_str('stm32wb5m')
+
+        self.assertEqual(family, 'stm32wbxx')
+        self.assertEqual(defines, {'STM32WB5Mxx'})
+        self.assertEqual(name, 'stm32wb5mxx')
+
+
+class SelectDefineTest(unittest.TestCase):
+    """select_define tests."""
+    def test_stm32f412zx_not_found(self):
+        with self.assertRaises(ValueError):
+            find_files.select_define({'STM32F412xx', 'STM32F412Zx'}, "")
+
+    def test_stm32f412zx_found(self):
+        define = find_files.select_define(
+            {'STM32F412xx', 'STM32F412Zx'},
+            "asdf\nfdas\n#if defined(STM32F412Zx)\n")
+        self.assertEqual(define, 'STM32F412Zx')
+
+    def test_stm32f412zx_multiple_found(self):
+        with self.assertRaises(ValueError):
+            find_files.select_define({
+                'STM32F412xx', 'STM32F412Zx'
+            }, "asdf\n#if defined (STM32F412xx)\n#elif defined(STM32F412Zx)\n")
+
+
+class MatchFilenameTest(unittest.TestCase):
+    """match_filename tests."""
+    def test_stm32f412zx(self):
+        # Match should fail if product name is not specific enough
+        self.assertTrue(
+            find_files.match_filename('stm32f412zx', 'stm32f412zx_flash.icf'))
+        self.assertFalse(
+            find_files.match_filename('stm32f412xx', 'stm32f412zx_flash.icf'))
+        self.assertTrue(
+            find_files.match_filename('stm32f412zx', 'startup_stm32f412zx.s'))
+        self.assertFalse(
+            find_files.match_filename('stm32f412xx', 'startup_stm32f429zx.s'))
+
+    def test_stm32f439xx(self):
+        self.assertTrue(
+            find_files.match_filename('stm32f439xx', 'stm32f439xx_flash.icf'))
+        self.assertFalse(
+            find_files.match_filename('stm32f439xx', 'stm32f429xx_flash.icf'))
+        self.assertTrue(
+            find_files.match_filename('stm32f439xx', 'startup_stm32f439xx.s'))
+        self.assertFalse(
+            find_files.match_filename('stm32f439xx', 'startup_stm32f429xx.s'))
+
+    def test_stm32f439xi(self):
+        self.assertTrue(
+            find_files.match_filename('stm32f439xi', 'stm32f439xx_flash.icf'))
+        self.assertFalse(
+            find_files.match_filename('stm32f439xi', 'stm32f429xx_flash.icf'))
+        self.assertTrue(
+            find_files.match_filename('stm32f439xi', 'startup_stm32f439xx.s'))
+        self.assertFalse(
+            find_files.match_filename('stm32f439xi', 'startup_stm32f429xx.s'))
+
+    def test_stm32l552zet(self):
+        self.assertTrue(
+            find_files.match_filename('stm32l552zet', 'STM32L552xE_FLASH.ld'))
+        self.assertTrue(
+            find_files.match_filename('stm32l552zet', 'STM32L552xx_FLASH.ld'))
+        self.assertFalse(
+            find_files.match_filename('stm32l552zet', 'STM32L552xC_FLASH.ld'))
+        self.assertTrue(
+            find_files.match_filename('stm32l552zet', 'stm32l552xe_flash.icf'))
+        self.assertFalse(
+            find_files.match_filename('stm32l552zet', 'stm32l552xc_flash.icf'))
+        self.assertTrue(
+            find_files.match_filename('stm32l552zet', 'startup_stm32l552xx.s'))
+        self.assertFalse(
+            find_files.match_filename('stm32l552zet', 'startup_stm32l562xx.s'))
+
+
+class FindLinkerFilesTest(unittest.TestCase):
+    """find_linker_files tests."""
+    TEST_PATH = pathlib.Path('/test/path')
+
+    def test_stm32f439xx(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xx_sram.icf'
+        ]
+        gcc_linker, iar_linker = find_files.find_linker_files(
+            'stm32f439xx', files, self.TEST_PATH)
+
+        self.assertEqual(gcc_linker, None)
+        self.assertEqual(iar_linker, self.TEST_PATH / files[0])
+
+    def test_stm32f439xx_find_ld(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xx_sram.icf',
+            'path/to/STM32F439xx_FLASH.ld',
+        ]
+        gcc_linker, iar_linker = find_files.find_linker_files(
+            'stm32f439xx', files, self.TEST_PATH)
+
+        self.assertEqual(gcc_linker, self.TEST_PATH / files[2])
+        self.assertEqual(iar_linker, self.TEST_PATH / files[0])
+
+    def test_stm32f439xc_error_multiple_matching_ld(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xx_sram.icf',
+            'other/path/to/STM32F439xI_FLASH.ld',
+            'path/to/STM32F439xx_FLASH.ld',
+        ]
+        with self.assertRaises(ValueError):
+            find_files.find_linker_files('stm32f439xi', files, self.TEST_PATH)
+
+    def test_stm32f439xc_error_multiple_matching_icf(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xc_flash.icf',
+        ]
+        with self.assertRaises(ValueError):
+            find_files.find_linker_files('stm32f439xc', files, self.TEST_PATH)
+
+    def test_stm32f439xc_error_none_found(self):
+        files = [
+            'path/to/stm32f439xc_flash.icf',
+            'other/path/to/stm32f439xc_flash.icf',
+        ]
+        with self.assertRaises(ValueError):
+            find_files.find_linker_files('stm32f439xx', files, self.TEST_PATH)
+
+    # ignore secure and nonsecure variants for the M33 boards
+    def test_stm32l552xe_ignore_s_ns(self):
+        files = [
+            'iar/linker/stm32l552xe_flash_ns.icf',
+            'iar/linker/stm32l552xe_flash_s.icf',
+            'iar/linker/stm32l552xe_flash.icf',
+            'gcc/linker/STM32L552xE_FLASH_ns.ld',
+            'gcc/linker/STM32L552xE_FLASH_s.ld',
+            'gcc/linker/STM32L552xE_FLASH.ld',
+        ]
+        gcc_linker, iar_linker = find_files.find_linker_files(
+            'stm32l552xe', files, self.TEST_PATH)
+
+        self.assertEqual(gcc_linker, self.TEST_PATH / files[-1])
+        self.assertEqual(iar_linker, self.TEST_PATH / files[2])
+
+
+class FindStartupFileTest(unittest.TestCase):
+    """find_startup_file tests."""
+    TEST_PATH = pathlib.Path('/test/path')
+
+    def test_stm32f439xx_none_found(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xx_sram.icf',
+            'path/iar/startup_stm32f439xx.s',
+        ]
+        with self.assertRaises(ValueError):
+            find_files.find_startup_file('stm32f439xx', files, self.TEST_PATH)
+
+    def test_stm32f439xx(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xx_sram.icf',
+            'path/iar/startup_stm32f439xx.s',
+            'path/gcc/startup_stm32f439xx.s',
+        ]
+        startup_file = find_files.find_startup_file('stm32f439xx', files,
+                                                    self.TEST_PATH)
+
+        self.assertEqual(startup_file, self.TEST_PATH / files[3])
+
+    def test_stm32f439xx_multiple_found(self):
+        files = [
+            'path/to/stm32f439xx_flash.icf',
+            'other/path/to/stm32f439xx_sram.icf',
+            'path/gcc/startup_stm32f439xc.s',
+            'path/gcc/startup_stm32f439xx.s',
+        ]
+        with self.assertRaises(ValueError):
+            find_files.find_startup_file('stm32f439xc', files, self.TEST_PATH)
+
+
+class GetSourceAndHeadersTest(unittest.TestCase):
+    """test_sources_and_headers tests."""
+    def test_sources_and_headers(self):
+        files = [
+            'random/header.h',
+            'random/source.c',
+            'cmsis_core/Include/core_cm4.h',
+            'cmsis_device/Include/stm32f4xx.h',
+            'cmsis_device/Include/stm32f439xx.h',
+            'hal_driver/Inc/stm32f4xx_hal_eth.h',
+            'hal_driver/Src/stm32f4xx_hal_adc.c',
+            'hal_driver/Inc/stm32f4xx_hal.h',
+            'hal_driver/Src/stm32f4xx_hal_timebase_tim_template.c',
+            'hal_driver/Src/stm32f4xx_hal_eth.c',
+        ]
+        path = pathlib.Path('/test/path/to/stm32cube')
+        sources, headers = find_files.get_sources_and_headers(files, path)
+        self.assertSetEqual(
+            set([
+                str(path / 'hal_driver/Src/stm32f4xx_hal_adc.c'),
+                str(path / 'hal_driver/Src/stm32f4xx_hal_eth.c'),
+            ]), set(sources))
+        self.assertSetEqual(
+            set([
+                str(path / 'cmsis_core/Include/core_cm4.h'),
+                str(path / 'cmsis_device/Include/stm32f4xx.h'),
+                str(path / 'cmsis_device/Include/stm32f439xx.h'),
+                str(path / 'hal_driver/Inc/stm32f4xx_hal_eth.h'),
+                str(path / 'hal_driver/Inc/stm32f4xx_hal.h'),
+            ]), set(headers))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pw_stm32cube_build/py/tests/icf_to_ld_test.py b/pw_stm32cube_build/py/tests/icf_to_ld_test.py
new file mode 100644
index 0000000..3d76d91
--- /dev/null
+++ b/pw_stm32cube_build/py/tests/icf_to_ld_test.py
@@ -0,0 +1,179 @@
+#!/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.
+"""Finds files for a given product."""
+
+import unittest
+
+from pw_stm32cube_build import icf_to_ld
+
+
+class ParseIcfTest(unittest.TestCase):
+    """parse_icf tests."""
+    TEST_ICF_1 = """
+/*test comments*/
+// some other comments
+define symbol __ICFEDIT_intvec_start__ = 0x08000000;
+/*-Memory Regions-*/
+define symbol __ICFEDIT_region_ROM_start__   = 0x08000000;
+define symbol __ICFEDIT_region_ROM_end__     = 0x0807FFFF;
+define symbol __ICFEDIT_region_RAM_start__   = 0x20000000;
+define symbol __ICFEDIT_region_RAM_end__     = 0x2002FFFF;
+
+/*-Sizes-*/
+define symbol __ICFEDIT_size_cstack__ = 0x400;
+define symbol __ICFEDIT_size_heap__   = 0x200;
+/**** End of ICF editor section. ###ICF###*/
+
+define symbol __region_SRAM1_start__  = 0x20000000;
+define symbol __region_SRAM1_end__    = 0x2002FFFF;
+define symbol __region_SRAM2_start__  = 0x20030000;
+define symbol __region_SRAM2_end__    = 0x2003FFFF;
+
+define memory mem with size = 4G;
+define region ROM_region      = mem:[from __ICFEDIT_region_ROM_start__   to __ICFEDIT_region_ROM_end__];
+define region RAM_region      = mem:[from __ICFEDIT_region_RAM_start__   to __ICFEDIT_region_RAM_end__];
+define region SRAM1_region    = mem:[from __region_SRAM1_start__   to __region_SRAM1_end__];
+define region SRAM2_region    = mem:[from __region_SRAM2_start__   to __region_SRAM2_end__];
+
+define block CSTACK    with alignment = 8, size = __ICFEDIT_size_cstack__   { };
+define block HEAP      with alignment = 8, size = __ICFEDIT_size_heap__     { };
+
+initialize by copy { readwrite };
+do not initialize  { section .noinit };
+
+place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
+
+place in ROM_region   { readonly };
+place in RAM_region   { readwrite,
+                        block CSTACK, block HEAP };
+place in SRAM1_region { };
+place in SRAM2_region { };
+"""
+
+    TEST_ICF_2 = """
+/*test comments*/
+// some other comments
+/*-Specials-*/
+define symbol __ICFEDIT_intvec_start__ = 0x08000000;
+/*-Memory Regions-*/
+define symbol __ICFEDIT_region_ROM_start__    = 0x08000000;
+define symbol __ICFEDIT_region_ROM_end__      = 0x081FFFFF;
+define symbol __ICFEDIT_region_RAM_start__    = 0x20000000;
+define symbol __ICFEDIT_region_RAM_end__      = 0x2002FFFF;
+define symbol __ICFEDIT_region_CCMRAM_start__ = 0x10000000;
+define symbol __ICFEDIT_region_CCMRAM_end__   = 0x1000FFFF;
+/*-Sizes-*/
+define symbol __ICFEDIT_size_cstack__ = 0x400;
+define symbol __ICFEDIT_size_heap__   = 0x200;
+/**** End of ICF editor section. ###ICF###*/
+
+
+define memory mem with size = 4G;
+define region ROM_region      = mem:[from __ICFEDIT_region_ROM_start__   to __ICFEDIT_region_ROM_end__];
+define region RAM_region      = mem:[from __ICFEDIT_region_RAM_start__   to __ICFEDIT_region_RAM_end__];
+define region CCMRAM_region   = mem:[from __ICFEDIT_region_CCMRAM_start__   to __ICFEDIT_region_CCMRAM_end__];
+
+define block CSTACK    with alignment = 8, size = __ICFEDIT_size_cstack__   { };
+define block HEAP      with alignment = 8, size = __ICFEDIT_size_heap__     { };
+
+initialize by copy { readwrite };
+do not initialize  { section .noinit };
+
+place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
+
+place in ROM_region   { readonly };
+place in RAM_region   { readwrite,
+                        block CSTACK, block HEAP };
+"""
+
+    def test_parse_icf_2(self):
+        regions, blocks = icf_to_ld.parse_icf(self.TEST_ICF_2)
+
+        self.assertEqual(
+            {
+                'ROM': ('0x08000000', '0x081FFFFF'),
+                'RAM': ('0x20000000', '0x2002FFFF'),
+                'CCMRAM': ('0x10000000', '0x1000FFFF'),
+            }, regions)
+
+        self.assertEqual(
+            {
+                'CSTACK': {
+                    'alignment': '8',
+                    'size': '0x400'
+                },
+                'HEAP': {
+                    'alignment': '8',
+                    'size': '0x200'
+                },
+            }, blocks)
+
+
+class IcfRegionsToLdRegionsTest(unittest.TestCase):
+    """icf_regions_to_ld_regions tests."""
+    def test_icf_region(self):
+        ld_regions = icf_to_ld.icf_regions_to_ld_regions({
+            'ROM': ('0x08000000', '0x081FFFFF'),
+            'RAM': ('0x20000000', '0x2002FFFF'),
+            'CCMRAM': ('0x10000000', '0x1000FFFF'),
+        })
+
+        self.assertEqual(
+            {
+                'FLASH': ('0x08000000', '2048K'),
+                'RAM': ('0x20000000', '192K'),
+                'CCMRAM': ('0x10000000', '64K'),
+            }, ld_regions)
+
+    def test_icf_region_off_by_one(self):
+        ld_regions = icf_to_ld.icf_regions_to_ld_regions({
+            'ROM': ('0x08000000', '0x080FFFFF'),
+            'RAM': ('0x20000000', '0x20020000'),
+        })
+
+        self.assertEqual(
+            {
+                'FLASH': ('0x08000000', '1024K'),
+                'RAM': ('0x20000000', '128K'),
+            }, ld_regions)
+
+
+class CreateLdTest(unittest.TestCase):
+    """create_ld tests."""
+    def test_create_ld(self):
+        ld_str = icf_to_ld.create_ld(
+            {
+                'FLASH': ('0x08000000', '2048K'),
+                'RAM': ('0x20000000', '192K'),
+                'CCMRAM': ('0x10000000', '64K'),
+            }, {
+                'CSTACK': {
+                    'alignment': '8',
+                    'size': '0x400'
+                },
+                'HEAP': {
+                    'alignment': '8',
+                    'size': '0x200'
+                },
+            })
+
+        self.assertTrue(
+            'RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K' in ld_str)
+        self.assertTrue(
+            'FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K' in ld_str)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pw_stm32cube_build/py/tests/inject_init_test.py b/pw_stm32cube_build/py/tests/inject_init_test.py
new file mode 100644
index 0000000..b08cf35
--- /dev/null
+++ b/pw_stm32cube_build/py/tests/inject_init_test.py
@@ -0,0 +1,103 @@
+#!/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.
+"""Finds files for a given product."""
+
+import unittest
+
+from pw_stm32cube_build import inject_init
+
+
+class AddPreMainInitTest(unittest.TestCase):
+    """add_pre_main_init tests."""
+    def test_spaces(self):
+        startup = '\n'.join([
+            '/* Call the clock system intitialization function.*/',
+            '  bl  SystemInit   ',
+            '/* Call static constructors */',
+            '    bl __libc_init_array',
+            '/* Call the application\'s entry point.*/',
+            '  bl  main',
+            '  bx  lr    ',
+            '.size  Reset_Handler, .-Reset_Handler',
+        ])
+
+        new_startup = inject_init.add_pre_main_init(startup)
+
+        self.assertEqual(
+            new_startup, '\n'.join([
+                '/* Call the clock system intitialization function.*/',
+                '  bl  SystemInit   ',
+                '/* Call static constructors */',
+                '    bl __libc_init_array',
+                '/* Call the application\'s entry point.*/',
+                'bl pw_stm32cube_Init',
+                '  bl  main',
+                '  bx  lr    ',
+                '.size  Reset_Handler, .-Reset_Handler',
+            ]))
+
+    def test_tabs(self):
+        startup = '\n'.join([
+            'LoopFillZerobss:',
+            '	ldr	r3, = _ebss',
+            '	cmp	r2, r3',
+            '	bcc	FillZerobss',
+            ''
+            '/* Call static constructors */',
+            '  bl __libc_init_array',
+            '/* Call the application\'s entry point.*/',
+            '	bl	main',
+            '',
+            'LoopForever:',
+            '    b LoopForever',
+        ])
+
+        new_startup = inject_init.add_pre_main_init(startup)
+
+        self.assertEqual(
+            new_startup, '\n'.join([
+                'LoopFillZerobss:',
+                '	ldr	r3, = _ebss',
+                '	cmp	r2, r3',
+                '	bcc	FillZerobss',
+                ''
+                '/* Call static constructors */',
+                '  bl __libc_init_array',
+                '/* Call the application\'s entry point.*/',
+                'bl pw_stm32cube_Init',
+                '	bl	main',
+                '',
+                'LoopForever:',
+                '    b LoopForever',
+            ]))
+
+    def test_main_not_found(self):
+        startup = '\n'.join([
+            '/* Call the clock system intitialization function.*/',
+            '  bl  SystemInit   ',
+            '/* Call static constructors */',
+            '    bl __libc_init_array',
+            '/* Call the application\'s entry point.*/',
+            '  bl  application_entry',
+            '  bx  lr    ',
+            '.size  Reset_Handler, .-Reset_Handler',
+        ])
+
+        with self.assertRaises(ValueError):
+            inject_init.add_pre_main_init(startup)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/third_party/stm32cube/BUILD b/third_party/stm32cube/BUILD
new file mode 100644
index 0000000..0441357
--- /dev/null
+++ b/third_party/stm32cube/BUILD
@@ -0,0 +1,30 @@
+# 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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])  # Apache License 2.0
+
+pw_cc_library(
+    name = "stm32cube",
+    hdrs = [
+        "public/stm32cube/init.h",
+        "public/stm32cube/stm32cube.h",
+    ],
+)
diff --git a/third_party/stm32cube/BUILD.gn b/third_party/stm32cube/BUILD.gn
new file mode 100644
index 0000000..b97bd87
--- /dev/null
+++ b/third_party/stm32cube/BUILD.gn
@@ -0,0 +1,156 @@
+# 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/linker_script.gni")
+import("$dir_pw_build/target_types.gni")
+
+import("$dir_pw_third_party/stm32cube/stm32cube.gni")
+
+assert(dir_pw_third_party_stm32cube != "",
+       "The target must specify the stm32cube directory.")
+
+stm32cube_builder_script =
+    "$dir_pw_stm32cube_build/py/pw_stm32cube_build/__main__.py"
+
+find_files_args = [
+  "find_files",
+  dir_pw_third_party_stm32cube,
+  pw_third_party_stm32cube_PRODUCT,
+]
+if (pw_third_party_stm32cube_CORE_INIT ==
+    "$dir_pw_third_party/stm32cube:core_init_template") {
+  find_files_args += [ "--init" ]
+}
+
+# This script finds the files relavent for the current product.
+files = exec_script(stm32cube_builder_script,
+                    find_files_args,
+                    "scope",
+                    [ "$dir_pw_third_party_stm32cube/files.txt" ])
+
+if (pw_third_party_stm32cube_CORE_INIT ==
+    "$dir_pw_third_party/stm32cube:core_init_template") {
+  assert(files.gcc_linker != "" || files.iar_linker != "",
+         "No linker file found")
+
+  gcc_linker = files.gcc_linker
+  if (gcc_linker == "") {
+    gcc_linker = "$target_gen_dir/linker.ld"
+    gcc_linker_str = exec_script(stm32cube_builder_script,
+                                 [
+                                   "icf_to_ld",
+                                   files.iar_linker,
+                                 ],
+                                 "string",
+                                 [ files.iar_linker ])
+    write_file(gcc_linker, gcc_linker_str)
+  }
+
+  startup_file = "$target_gen_dir/startup.s"
+  startup_file_str = exec_script(stm32cube_builder_script,
+                                 [
+                                   "inject_init",
+                                   files.startup,
+                                 ],
+                                 "string",
+                                 [ files.startup ])
+  write_file(startup_file, startup_file_str)
+
+  pw_linker_script("linker_script_template") {
+    linker_script = gcc_linker
+  }
+
+  pw_source_set("core_init_template") {
+    deps = [ ":linker_script_template" ]
+    sources = [ startup_file ]
+  }
+}
+
+pw_source_set("hal_timebase_template") {
+  deps = [ ":stm32cube_headers" ]
+  sources = [ "$dir_pw_third_party_stm32cube/hal_driver/Src/${files.family}_hal_timebase_tim_template.c" ]
+}
+
+pw_source_set("cmsis_init_template") {
+  deps = [ ":stm32cube_headers" ]
+  sources = [ "$dir_pw_third_party_stm32cube/cmsis_device/Source/Templates/system_${files.family}.c" ]
+}
+
+# Generate a stub config header that points to the correct template.
+write_file("$target_gen_dir/template_config/${files.family}_hal_conf.h",
+           "#include \"${files.family}_hal_conf_template.h\"")
+config("hal_config_template_includes") {
+  include_dirs = [ "$target_gen_dir/template_config" ]
+}
+pw_source_set("hal_config_template") {
+  public_configs = [ ":hal_config_template_includes" ]
+
+  # This is to make sure GN properly detects changes to these files. The
+  # generated file shouldn't change, but the file it redirects to might.
+  public = [ "$target_gen_dir/template_config/${files.family}_hal_conf.h" ]
+  inputs = [ "$dir_pw_third_party_stm32cube/hal_driver/Inc/${files.family}_hal_conf_template.h" ]
+}
+
+config("flags") {
+  cflags = [ "-Wno-unused-parameter" ]
+  cflags_c = [
+    "-Wno-redundant-decls",
+    "-Wno-sign-compare",
+    "-Wno-old-style-declaration",
+    "-Wno-maybe-uninitialized",
+  ]
+  defines = [
+    "USE_HAL_DRIVER",
+    files.product_define,
+    "STM32CUBE_HEADER=\"${files.family}.h\"",
+    "__ARMCC_VERSION=0",  # workaround for bug at stm32l552xx.h:1303
+  ]
+  visibility = [ ":*" ]
+}
+
+config("public_include_paths") {
+  include_dirs = files.include_dirs
+  include_dirs += [ "public" ]
+  visibility = [ ":*" ]
+}
+
+# Only libraries that implement parts of the stm32cube hal should depend on
+# this. If you just want to depend on the hal, depend on stm32cube directly.
+pw_source_set("stm32cube_headers") {
+  public_configs = [
+    ":flags",
+    ":public_include_paths",
+  ]
+  public = [
+    "public/stm32cube/init.h",
+    "public/stm32cube/stm32cube.h",
+  ]
+  public += files.headers
+  public_deps = [ pw_third_party_stm32cube_CONFIG ]
+  visibility = [ ":*" ]
+}
+
+pw_source_set("stm32cube") {
+  public_deps = [ ":stm32cube_headers" ]
+  sources = files.sources
+  deps = [
+    pw_third_party_stm32cube_CMSIS_INIT,
+    pw_third_party_stm32cube_TIMEBASE,
+  ]
+  if (pw_third_party_stm32cube_CORE_INIT != "") {
+    deps += [ pw_third_party_stm32cube_CORE_INIT ]
+  }
+}
diff --git a/third_party/stm32cube/public/stm32cube/init.h b/third_party/stm32cube/public/stm32cube/init.h
new file mode 100644
index 0000000..2e79f61
--- /dev/null
+++ b/third_party/stm32cube/public/stm32cube/init.h
@@ -0,0 +1,23 @@
+// 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.
+
+#pragma once
+
+#include "pw_preprocessor/util.h"
+
+PW_EXTERN_C_START
+
+void pw_stm32cube_Init(void);
+
+PW_EXTERN_C_END
\ No newline at end of file
diff --git a/third_party/stm32cube/public/stm32cube/stm32cube.h b/third_party/stm32cube/public/stm32cube/stm32cube.h
new file mode 100644
index 0000000..2cd24f1
--- /dev/null
+++ b/third_party/stm32cube/public/stm32cube/stm32cube.h
@@ -0,0 +1,17 @@
+// 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.
+
+#pragma once
+
+#include STM32CUBE_HEADER
diff --git a/third_party/stm32cube/stm32cube.gni b/third_party/stm32cube/stm32cube.gni
new file mode 100644
index 0000000..f1848df
--- /dev/null
+++ b/third_party/stm32cube/stm32cube.gni
@@ -0,0 +1,66 @@
+# 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")
+
+declare_args() {
+  # pw_package/stm32cube_xx install directories
+  dir_pw_third_party_stm32cube_f0 = ""
+  dir_pw_third_party_stm32cube_f1 = ""
+  dir_pw_third_party_stm32cube_f2 = ""
+  dir_pw_third_party_stm32cube_f3 = ""
+  dir_pw_third_party_stm32cube_f4 = ""
+  dir_pw_third_party_stm32cube_f7 = ""
+  dir_pw_third_party_stm32cube_g0 = ""
+  dir_pw_third_party_stm32cube_g4 = ""
+  dir_pw_third_party_stm32cube_h7 = ""
+  dir_pw_third_party_stm32cube_l0 = ""
+  dir_pw_third_party_stm32cube_l1 = ""
+  dir_pw_third_party_stm32cube_l4 = ""
+  dir_pw_third_party_stm32cube_l5 = ""
+  dir_pw_third_party_stm32cube_wb = ""
+  dir_pw_third_party_stm32cube_wl = ""
+
+  # The currently selected stm32cube_xx package
+  # This can be selected by the target by doing something like:
+  #  dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
+  dir_pw_third_party_stm32cube = ""
+
+  # The Product specified in as much detail as possible.
+  # i.e. "stm32f429zit", "stm32l552ze", "stm32f207zg", etc.
+  pw_third_party_stm32cube_PRODUCT = ""
+
+  # pw_source_set with `stm32{family}xx_hal_conf.h`
+  # The default uses the in-tree `stm32{family}xx_hal_conf_template.h`.
+  pw_third_party_stm32cube_CONFIG =
+      "$dir_pw_third_party/stm32cube:hal_config_template"
+
+  # pw_source_set containing timebase
+  # The default uses the in-tree `stm32{family}xx_hal_timebase_tim_template.c`
+  pw_third_party_stm32cube_TIMEBASE =
+      "$dir_pw_third_party/stm32cube:hal_timebase_template"
+
+  # pw_source_set containing cmsis init logic
+  # The default uses the in-tree `system_stm32{family}xx.c`
+  pw_third_party_stm32cube_CMSIS_INIT =
+      "$dir_pw_third_party/stm32cube:cmsis_init_template"
+
+  # pw_source_set containing the core initization logic. This normally includes
+  # a `startup_stm32{...}.s` + a dependent `pw_linker_script`. The default
+  # `core_init_template` uses the upstream startup and linker script matching
+  # $pw_third_party_stm32cube_PRODUCT. If set to "", you must provide your own
+  # linker/startup logic somewhere else in the build.
+  pw_third_party_stm32cube_CORE_INIT =
+      "$dir_pw_third_party/stm32cube:core_init_template"
+}