pw_build: Configurable Python and proto toolchains

- Rename pw_toolchain/dummy to pw_toolchain/default to make its intended
  purpose clear. Keep pw_toolchain/dummy around for compatibility.
- Create a template that generates non-C++ toolchains. This is used for
  non-C++ tasks like Python and proto. GN parses build files differently
  in the default toolchain (all targets are instantiated), so the
  these toolchains can be used when that is not desired.
- Make pw_python_package's toolchain configurable with the
  pw_build_PYTHON_TOOLCHAIN arg.
- Make pw_proto_library's toolchain configurable with the
  pw_protobuf_compiler_TOOLCHAIN arg.

Requires: pigweed-internal:11680
Change-Id: I0aad049c605906b77c58b93a08efc63a648947ee
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/40763
Reviewed-by: Armando Montanez <amontanez@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/BUILDCONFIG.gn b/BUILDCONFIG.gn
index dbf7ed4..1dbd4c5 100644
--- a/BUILDCONFIG.gn
+++ b/BUILDCONFIG.gn
@@ -22,7 +22,7 @@
   import("//build_overrides/pigweed.gni")
 }
 
-# The default toolchain is not used in Pigweed builds, so it is set to a dummy
-# toolchain. The top-level BUILD.gn should stamp a group with all of the build
-# targets and their toolchains.
-set_default_toolchain("${_pigweed_directory.dir_pw_toolchain}/dummy")
+# The default toolchain is not used in Pigweed builds, so it is set to a
+# toolchain that cannot compile C/C++ code. The top-level BUILD.gn should stamp
+# a group with all of the build targets and their toolchains.
+set_default_toolchain("${_pigweed_directory.dir_pw_toolchain}/default")
diff --git a/pw_build/python.gni b/pw_build/python.gni
index 038d9a2..c7ff3e4 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -17,6 +17,13 @@
 import("$dir_pw_build/input_group.gni")
 import("$dir_pw_build/mirror_tree.gni")
 import("$dir_pw_build/python_action.gni")
+import("$dir_pw_protobuf_compiler/toolchain.gni")
+
+declare_args() {
+  # Python tasks, such as running tests and Pylint, are done in a single GN
+  # toolchain to avoid unnecessary duplication in the build.
+  pw_build_PYTHON_TOOLCHAIN = "$dir_pw_build/python_toolchain:python"
+}
 
 # Python packages provide the following targets as $target_name.$subtarget.
 pw_python_package_subtargets = [
@@ -125,8 +132,9 @@
 #   - $name.install - Installs the package in a venv.
 #   - $name.wheel - Builds a Python wheel for the package.
 #
-# All Python packages are instantiated with the default toolchain, regardless of
-# the current toolchain.
+# All Python packages are instantiated with in pw_build_PYTHON_TOOLCHAIN,
+# regardless of the current toolchain. This prevents Python-specific work, like
+# running Pylint, from occurring multiple times in a build.
 #
 # Args:
 #   setup: List of setup file paths (setup.py or pyproject.toml & setup.cfg),
@@ -153,7 +161,7 @@
 #       directory will take precedence over others.
 #
 template("pw_python_package") {
-  # The Python targets are always instantiated in the default toolchain. Use
+  # The Python targets are always instantiated in pw_build_PYTHON_TOOLCHAIN. Use
   # fully qualified labels so that the toolchain is not lost.
   _other_deps = []
   if (defined(invoker.other_deps)) {
@@ -304,9 +312,10 @@
 
   _all_py_files = _sources + _test_sources + _setup_sources
 
-  # The pw_python_package subtargets are only instantiated in the default
-  # toolchain. Other toolchains just refer to targets in the default toolchain.
-  if (current_toolchain == default_toolchain) {
+  # The pw_python_package subtargets are only instantiated in
+  # pw_build_PYTHON_TOOLCHAIN. Targets in other toolchains just refer to the
+  # targets in this toolchain.
+  if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
     # Declare the main Python package group. This represents the Python files,
     # but does not take any actions. GN targets can depend on the package name
     # to run when any files in the package change.
@@ -330,8 +339,8 @@
       # Depend on the proto's _gen targets (from the default toolchain).
       _gen_protos = []
       foreach(proto, _import_protos) {
-        _gen_protos +=
-            [ get_label_info(proto, "label_no_toolchain") + ".python._gen" ]
+        _gen_protos += [ get_label_info(proto, "label_no_toolchain") +
+                         ".python._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
       }
 
       generated_file("$target_name._protos") {
@@ -387,9 +396,12 @@
           ":$target_name._protos_root",
         ]
 
+        # Each pw_proto_library generates a file that indicates which Python
+        # package it is nested in, if any. Locate those files.
         foreach(proto, _import_protos) {
           _tgt = get_label_info(proto, "label_no_toolchain")
-          _path = get_label_info("$_tgt($default_toolchain)", "target_gen_dir")
+          _path = get_label_info("$_tgt($pw_protobuf_compiler_TOOLCHAIN)",
+                                 "target_gen_dir")
           _name = get_label_info(_tgt, "name")
 
           args += [
@@ -398,7 +410,8 @@
             rebase_path("$_path/$_name.proto_library/python_package.txt"),
           ]
 
-          public_deps += [ "$_tgt.python._gen($default_toolchain)" ]
+          public_deps +=
+              [ "$_tgt.python._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
         }
 
         outputs = _setup_sources
@@ -575,12 +588,13 @@
     #
     # The $target_name.tests group is created separately below.
     group("$target_name") {
-      deps = [ ":$target_name($default_toolchain)" ]
+      deps = [ ":$target_name($pw_build_PYTHON_TOOLCHAIN)" ]
     }
 
     foreach(subtarget, pw_python_package_subtargets - [ "tests" ]) {
       group("$target_name.$subtarget") {
-        deps = [ ":${invoker.target_name}.$subtarget($default_toolchain)" ]
+        deps =
+            [ ":${invoker.target_name}.$subtarget($pw_build_PYTHON_TOOLCHAIN)" ]
       }
     }
 
@@ -602,7 +616,7 @@
 
     _test_target = "$target_name.tests." + string_replace(_name, "/", "_")
 
-    if (current_toolchain == default_toolchain) {
+    if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
       pw_python_action(_test_target) {
         script = test
         stamp = true
@@ -617,7 +631,7 @@
       # Create a public version of each test target, so tests can be executed as
       # //path/to:package.tests.foo.py.
       group(_test_target) {
-        deps = [ ":$_test_target($default_toolchain)" ]
+        deps = [ ":$_test_target($pw_build_PYTHON_TOOLCHAIN)" ]
       }
     }
 
diff --git a/pw_build/python.rst b/pw_build/python.rst
index 09b92a2..c1d25c9 100644
--- a/pw_build/python.rst
+++ b/pw_build/python.rst
@@ -31,6 +31,11 @@
   //path/to/my_python_package:my_python_package.tests
   //path/to/my_python_package:tests
 
+The actions in a ``pw_python_package`` (e.g. installing packages and running
+Pylint) are done within a single GN toolchain to avoid duplication in
+multi-toolchain builds. This toolchain can be set with the
+``pw_build_PYTHON_TOOLCHAIN`` GN arg, which defaults to a dummy toolchain.
+
 Arguments
 ---------
 - ``setup`` - List of setup file paths (setup.py or pyproject.toml & setup.cfg),
diff --git a/pw_build/python_toolchain/BUILD.gn b/pw_build/python_toolchain/BUILD.gn
new file mode 100644
index 0000000..56d2aba
--- /dev/null
+++ b/pw_build/python_toolchain/BUILD.gn
@@ -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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_toolchain/non_c_toolchain.gni")
+
+# A toolchain that provides no C/C++ compiler. It can be used for non-C/C++
+# languages or actions that should only happen once across all builds. This
+# toolchain cannot compile C/C++, and trying to use it to do so causes errors.
+pw_non_c_toolchain("python") {
+}
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index a6ec15e..c6348c9 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -21,6 +21,7 @@
 import("$dir_pw_build/python_action.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_third_party/nanopb/nanopb.gni")
+import("toolchain.gni")
 
 # Variables forwarded from the public pw_proto_library template to the final
 # pw_source_set.
@@ -35,7 +36,7 @@
 # This creates the internal GN target $target_name.$language._gen that compiles
 # proto files with protoc.
 template("_pw_invoke_protoc") {
-  if (current_toolchain == default_toolchain) {
+  if (current_toolchain == pw_protobuf_compiler_TOOLCHAIN) {
     if (defined(invoker.out_dir)) {
       _out_dir = invoker.out_dir
     } else {
@@ -101,7 +102,7 @@
       }
     }
   } else {
-    # protoc is only ever invoked from the default toolchain.
+    # protoc is only ever invoked from pw_protobuf_compiler_TOOLCHAIN.
     not_needed([ "target_name" ])
     not_needed(invoker, "*")
   }
@@ -127,7 +128,7 @@
   pw_source_set(target_name) {
     forward_variables_from(invoker, _forwarded_vars)
     public_configs = [ ":$target_name._include_path" ]
-    deps = [ ":$target_name._gen($default_toolchain)" ]
+    deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
     public_deps = [ dir_pw_protobuf ] + invoker.deps
     sources = invoker.outputs
     public = filter_include(sources, [ "*.pwpb.h" ])
@@ -156,7 +157,7 @@
   pw_source_set(target_name) {
     forward_variables_from(invoker, _forwarded_vars)
     public_configs = [ ":$target_name._include_path" ]
-    deps = [ ":$target_name._gen($default_toolchain)" ]
+    deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
     public_deps = [
                     ":${invoker.base_target}.nanopb",
                     "$dir_pw_rpc:server",
@@ -175,11 +176,14 @@
   # compiled internally, so skip recompiling it with protoc.
   if (rebase_path(invoker.sources, invoker.compile_dir) == [ "nanopb.proto" ]) {
     group("$target_name._gen") {
-      deps = [ ":${invoker.base_target}._sources" ]
+      deps = [
+        ":${invoker.base_target}._sources($pw_protobuf_compiler_TOOLCHAIN)",
+      ]
     }
 
     group("$target_name") {
-      deps = invoker.deps + [ ":$target_name._gen($default_toolchain)" ]
+      deps = invoker.deps +
+             [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
     }
   } else {
     # Create a target which runs protoc configured with the nanopb plugin to
@@ -199,7 +203,7 @@
     pw_source_set(target_name) {
       forward_variables_from(invoker, _forwarded_vars)
       public_configs = [ ":$target_name._include_path" ]
-      deps = [ ":$target_name._gen($default_toolchain)" ]
+      deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
       public_deps = [ "$dir_pw_third_party/nanopb" ] + invoker.deps
       sources = invoker.outputs
       public = filter_include(sources, [ "*.pb.h" ])
@@ -229,7 +233,7 @@
   pw_source_set(target_name) {
     forward_variables_from(invoker, _forwarded_vars)
     public_configs = [ ":$target_name._include_path" ]
-    deps = [ ":$target_name._gen($default_toolchain)" ]
+    deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
     public_deps = [
                     "$dir_pw_rpc:server",
                     "$dir_pw_rpc/raw:method_union",
@@ -259,7 +263,8 @@
   }
 
   group(target_name) {
-    deps = invoker.deps + [ ":$target_name._gen($default_toolchain)" ]
+    deps =
+        invoker.deps + [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
   }
 }
 
@@ -305,7 +310,7 @@
       sources = invoker.outputs
       strip_prefix = "${invoker.base_out_dir}/python"
       python_deps = invoker.deps
-      other_deps = [ ":$target_name._gen($default_toolchain)" ]
+      other_deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
       static_analysis = []
 
       _pw_module_as_package = invoker.module_as_package != ""
@@ -380,8 +385,8 @@
     # Sources are mirrored to "$base_out_dir/sources" and protoc puts outputs in
     # "$base_out_dir/$language" by default.
     base_out_dir =
-        get_label_info(":$target_name($default_toolchain)", "target_gen_dir") +
-        "/$target_name.proto_library"
+        get_label_info(":$target_name($pw_protobuf_compiler_TOOLCHAIN)",
+                       "target_gen_dir") + "/$target_name.proto_library"
 
     compile_dir = "$base_out_dir/sources"
 
@@ -468,15 +473,19 @@
   }
 
   # Mirror the proto sources to the output directory with the prefix added.
-  pw_mirror_tree("$target_name._sources") {
-    source_root = _source_root
-    sources = invoker.sources
+  if (current_toolchain == pw_protobuf_compiler_TOOLCHAIN) {
+    pw_mirror_tree("$target_name._sources") {
+      source_root = _source_root
+      sources = invoker.sources
 
-    if (defined(invoker.inputs)) {
-      sources += invoker.inputs
+      if (defined(invoker.inputs)) {
+        sources += invoker.inputs
+      }
+
+      directory = "${_common.compile_dir}/$_prefix"
     }
-
-    directory = "${_common.compile_dir}/$_prefix"
+  } else {
+    not_needed(invoker, [ "inputs" ])
   }
 
   # Enumerate all of the protobuf generator targets.
diff --git a/pw_protobuf_compiler/toolchain.gni b/pw_protobuf_compiler/toolchain.gni
new file mode 100644
index 0000000..606e69d
--- /dev/null
+++ b/pw_protobuf_compiler/toolchain.gni
@@ -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.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+  # Compiling protobufs involves mirroring .proto files to the output directory
+  # in a specific configuration and invoking protoc on them. This work is done
+  # in a single toolchain to avoid unnecessary duplication in the build.
+  pw_protobuf_compiler_TOOLCHAIN =
+      "$dir_pw_protobuf_compiler/toolchain:protocol_buffer"
+}
diff --git a/pw_protobuf_compiler/toolchain/BUILD.gn b/pw_protobuf_compiler/toolchain/BUILD.gn
new file mode 100644
index 0000000..6d2fd5b
--- /dev/null
+++ b/pw_protobuf_compiler/toolchain/BUILD.gn
@@ -0,0 +1,22 @@
+# 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_toolchain/non_c_toolchain.gni")
+
+# A toolchain used for compiling protocol buffers. This toolchain cannot compile
+# C/C++, and trying to use it to do so results in errors.
+pw_non_c_toolchain("protocol_buffer") {
+}
diff --git a/pw_toolchain/default/BUILD.gn b/pw_toolchain/default/BUILD.gn
new file mode 100644
index 0000000..e961c97
--- /dev/null
+++ b/pw_toolchain/default/BUILD.gn
@@ -0,0 +1,28 @@
+# 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_toolchain/non_c_toolchain.gni")
+
+# A toolchain that provides no C/C++ compiler for use as Pigweed's default
+# toolchain. This toolchain cannot compile C/C++, and trying to use it to do so
+# causes errors. The top-level BUILD.gn must explicitly specify the non-default
+# toolchains to use for each build.
+pw_non_c_toolchain("default") {
+  # If the user tries to build a target with the default toolchain, run a script
+  # printing out the error.
+  command = "python " +
+            rebase_path("$dir_pw_toolchain/py/pw_toolchain/bad_toolchain.py")
+}
diff --git a/pw_toolchain/docs.rst b/pw_toolchain/docs.rst
index 07e27fb..919eb88 100644
--- a/pw_toolchain/docs.rst
+++ b/pw_toolchain/docs.rst
@@ -3,10 +3,15 @@
 ------------
 pw_toolchain
 ------------
-The ``pw_toolchain`` module enumerates GN toolchain definitions that may be used
-to build pigweed.
+GN toolchains function both as a set of tools for compilation and as a workspace
+for evaluating build files. The same compilations and actions can be executed by
+different toolchains. Each toolchain maintains its own set of build args, and
+build steps from all toolchains can be executed in parallel.
 
-``pw_toolchain`` defines the following toolchains:
+Toolchains
+==========
+``pw_toolchain`` provides GN toolchains that may be used to build Pigweed. The
+following toolchains are defined:
 
  - arm_gcc_cortex_m4_og
  - arm_gcc_cortex_m4_o1
@@ -25,3 +30,21 @@
 
 .. note::
   The documentation for this module is currently incomplete.
+
+Non-C/C++ toolchains
+====================
+``pw_toolchain/non_c_toolchain.gni`` provides the ``pw_non_c_toolchain``
+template. This template creates toolchains that cannot compile C/C++ source
+code. These toolchains may only be used to execute GN actions or declare groups
+of targets in other toolchains. Attempting to compile C/C++ code with either of
+these toolchains results in errors.
+
+Non-C/C++ toolchains can be used to consolidate actions that should only occur
+once in a multi-toolchain build. Build targets from all toolchains can refer to
+these actions in a non-C/C++ toolchain so they only execute once instead of once
+per toolchain.
+
+For example, Pigweed runs protobuf compilation and Python package actions like
+installation and Pylint in toolchains created with ``pw_non_c_toolchain``. This
+allows all toolchains to cleanly share the same protobuf and Python declarations
+without any duplicated work.
diff --git a/pw_toolchain/dummy/BUILD.gn b/pw_toolchain/dummy/BUILD.gn
index ba0d738..a19f1d0 100644
--- a/pw_toolchain/dummy/BUILD.gn
+++ b/pw_toolchain/dummy/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# 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
@@ -14,55 +14,9 @@
 
 import("//build_overrides/pigweed.gni")
 
-import("$dir_pw_toolchain/universal_tools.gni")
+import("$dir_pw_toolchain/non_c_toolchain.gni")
 
-# A dummy toolchain which is set as the default for Pigweed. This is never used;
-# the top-level BUILD.gn enumerates the toolchains for each build.
-toolchain("dummy") {
-  tool("stamp") {
-    forward_variables_from(pw_universal_stamp, "*")
-  }
-
-  tool("copy") {
-    forward_variables_from(pw_universal_copy, "*")
-  }
-
-  # If the user tries to build a target with the default toolchain, run a script
-  # printing out the error.
-  _bad_toolchain_command =
-      "python " +
-      rebase_path("$dir_pw_toolchain/py/pw_toolchain/bad_toolchain.py")
-
-  tool("asm") {
-    command = _bad_toolchain_command
-    outputs =
-        [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
-  }
-
-  tool("cc") {
-    command = _bad_toolchain_command
-    outputs =
-        [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
-  }
-
-  tool("cxx") {
-    command = _bad_toolchain_command
-    outputs =
-        [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
-  }
-
-  tool("link") {
-    command = _bad_toolchain_command
-    outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
-  }
-
-  tool("alink") {
-    command = _bad_toolchain_command
-    outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
-  }
-
-  tool("solink") {
-    command = _bad_toolchain_command
-    outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
-  }
+# This toolchain has been DEPRECATED. Do not use it; use
+# "$dir_pw_toolchain/default:default" instead.
+pw_non_c_toolchain("dummy") {
 }
diff --git a/pw_toolchain/non_c_toolchain.gni b/pw_toolchain/non_c_toolchain.gni
new file mode 100644
index 0000000..04e7013
--- /dev/null
+++ b/pw_toolchain/non_c_toolchain.gni
@@ -0,0 +1,105 @@
+# 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")
+
+# Creates a toolchain that provides no C/C++ compiler. It can be used for
+# non-C/C++ languages or actions that should only happen once across all builds.
+# The toolchain cannot compile C/C++, and trying to use it to is an error.
+#
+# Args:
+#   command: Run this command if this toolchain is used to build C/C++ code.
+#
+template("pw_non_c_toolchain") {
+  # Import the universal stamp & copy tools.
+  import("$dir_pw_toolchain/universal_tools.gni")
+  _label = get_label_info(":$target_name", "label_no_toolchain")
+
+  # If the user tries to build a target with this toolchain, run a script that
+  # prints out an error.
+  _message =
+      "Attempted to use the $target_name toolchain to compile {{source}}.\n" +
+      "This toolchain cannot be used to compile C/C++ source code.\n\n" +
+      "This toolchain was either explicitly specified in a deps list with\n" +
+      "GN's :target($_label) syntax or was set as the\n" +
+      "default toolchain in the BUILDCONFIG.gn file.\n\n" +
+      "Ensure that no C/C++ GN targets are referred to with this toolchain,\n" +
+      "even transitively.\n\n" +
+      "See https://pigweed.dev/pw_toolchain for more information."
+
+  _command = string_join(" ",
+                         [
+                           "python",
+                           rebase_path("$dir_pw_build/py/pw_build/error.py"),
+                           "--message \"$_message\"",
+                           "--target",
+                           _label,
+                           "--root",
+                           rebase_path("//"),
+                           "--out",
+                           rebase_path(root_build_dir),
+                         ])
+
+  if (defined(invoker.command)) {
+    _command = invoker.command
+  } else {
+    not_needed([ "invoker" ])
+  }
+
+  toolchain(target_name) {
+    tool("stamp") {
+      forward_variables_from(pw_universal_stamp, "*")
+    }
+
+    tool("copy") {
+      forward_variables_from(pw_universal_copy, "*")
+    }
+
+    tool("asm") {
+      command = _command
+      outputs =
+          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
+    }
+
+    tool("cc") {
+      command = _command
+      outputs =
+          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
+    }
+
+    tool("cxx") {
+      command = _command
+      outputs =
+          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
+    }
+
+    # Can't use {{source}} for the linker, so replace it if it's in the command.
+    _command_no_source = string_replace(_command, "{{source}}", "C/C++ sources")
+
+    tool("link") {
+      command = _command_no_source
+      outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
+    }
+
+    tool("alink") {
+      command = _command_no_source
+      outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
+    }
+
+    tool("solink") {
+      command = _command_no_source
+      outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
+    }
+  }
+}