pw_build: Handle multiple artifacts in expressions

Toolchains may produce multiple files for a build command, such as an
ELF file and a .map file. Attempt to resolve this by filtering for
common compilation outputs.

Change-Id: I24bbf9ad667259148ef8c107135cba38dd9ffff8
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/38061
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index 6fff0e4..6135755 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -155,6 +155,11 @@
 BUILD.gn files. This allows build code to use GN labels without having to worry
 about converting them to files.
 
+.. note::
+
+  We intend to replace these expressions with native GN features when possible.
+  See `pwbug/347 <http://bugs.pigweed.dev/347>`_.
+
 The following expressions are supported:
 
 .. describe:: <TARGET_FILE(gn_target)>
diff --git a/pw_build/py/pw_build/python_runner.py b/pw_build/py/pw_build/python_runner.py
index 0bd5f96..7210757 100755
--- a/pw_build/py/pw_build/python_runner.py
+++ b/pw_build/py/pw_build/python_runner.py
@@ -160,6 +160,27 @@
 # Matches a non-phony build statement.
 _GN_NINJA_BUILD_STATEMENT = re.compile(r'^build (.+):[ \n](?!phony\b)')
 
+_MAIN_ARTIFACTS = '', '.elf', '.a', '.so'
+
+
+def _get_artifact(build_dir: Path, entries: List[str]) -> _Artifact:
+    """Attempts to resolve which artifact to use if there are multiple.
+
+    Selects artifacts based on extension. This will not work if a toolchain
+    creates multiple compilation artifacts from one command (e.g. .a and .elf).
+    """
+    assert entries, "There should be at least one entry here!"
+
+    if len(entries) != 1:
+        entries = [p for p in entries if Path(p).suffix in _MAIN_ARTIFACTS]
+
+    if len(entries) == 1:
+        return _Artifact(build_dir / entries[0], {})
+
+    raise ExpressionError(
+        f'Expected 1, but found {len(entries)} artifacts, after filtering for '
+        f'extensions {", ".join(repr(e) for e in _MAIN_ARTIFACTS)}: {entries}')
+
 
 def _parse_build_artifacts(build_dir: Path, fd) -> Iterator[_Artifact]:
     """Partially parses the build statements in a Ninja file."""
@@ -188,7 +209,7 @@
         else:
             match = _GN_NINJA_BUILD_STATEMENT.match(line)
             if match:
-                artifact = _Artifact(build_dir / match.group(1), {})
+                artifact = _get_artifact(build_dir, match.group(1).split())
 
             line = next_line()
 
@@ -364,6 +385,7 @@
         yield _ArgAction.EMIT_NEW, str(obj)
 
 
+# TODO(pwbug/347): Replace expressions with native GN features when possible.
 _FUNCTIONS: Dict['str', Callable[[GnPaths, _Expression], _Actions]] = {
     'TARGET_FILE': _target_file,
     'TARGET_FILE_IF_EXISTS': _target_file_if_exists,
diff --git a/pw_build/py/python_runner_test.py b/pw_build/py/python_runner_test.py
index 86497f4..1ea69b0 100755
--- a/pw_build/py/python_runner_test.py
+++ b/pw_build/py/python_runner_test.py
@@ -131,7 +131,7 @@
 build fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o: fake_toolchain_cxx ../fake_module/fake_test.cc
 build fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o: fake_toolchain_cc ../fake_module/fake_test_c.c
 
-build fake_toolchain/obj/fake_module/test/fake_test.elf: fake_toolchain_link fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o
+build fake_toolchain/obj/fake_module/test/fake_test.elf fake_toolchain/obj/fake_module/test/fake_test.map: fake_toolchain_link fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o
   ldflags = -Og -fdiagnostics-color
   libs =
   frameworks =