pw_build: pw_build_assert template

Template for build-time asserts. These asserts are only evaluated if
they're depended on by another target. If they pass, nothing happens. If
they fail, they act like a pw_error.

Change-Id: Ic502966c7a9693abf6d8a89b0cb833e10c6acbbc
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/84360
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index 47dffe8..1918c11 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -659,6 +659,23 @@
 This is a result of transformations applied to strip absolute pathing prefixes,
 matching the behavior of pw_build's ``$dir_pw_build:relative_paths`` config.
 
+Build time errors: pw_error and pw_build_assert
+-----------------------------------------------
+In Pigweed's complex, multi-toolchain GN build it is not possible to build every
+target in every configuration. GN's ``assert`` statement is not ideal for
+enforcing the correct configuration because it may prevent the GN build files or
+targets from being referred to at all, even if they aren't used.
+
+The ``pw_error`` GN template results in an error if it is executed during the
+build. These error targets can exist in the build graph, but cannot be depended
+on without an error.
+
+``pw_build_assert`` evaluates to a ``pw_error`` if a condition fails or nothing
+(an empty group) if the condition passes. Targets can add a dependency on a
+``pw_build_assert`` to enforce a condition at build time.
+
+The templates for build time errors are defined in ``pw_build/error.gni``.
+
 CMake
 =====
 Pigweed's `CMake`_ support is provided primarily for projects that have an
diff --git a/pw_build/error.gni b/pw_build/error.gni
index 653b4a9..33804c9 100644
--- a/pw_build/error.gni
+++ b/pw_build/error.gni
@@ -18,8 +18,10 @@
 # or 'message_lines' must be specified, but not both.
 #
 # Args:
+#
 #   message: The message to print. Use \n for newlines.
 #   message_lines: List of lines to use for the message.
+#   visibility: GN visibility to apply to the underlying target.
 #
 template("pw_error") {
   assert(
@@ -48,5 +50,46 @@
 
     # This output file is never created.
     outputs = [ "$target_gen_dir/$target_name.build_error" ]
+
+    forward_variables_from(invoker, [ "visibility" ])
+  }
+}
+
+# An assert that is evaluated at build time. The assertion is only checked if
+# this target is depended on by another target. If the assertion passes, nothing
+# happens. If it fails, the target prints an error message with pw_error.
+#
+# To enforce a pw_build_assert, targets add a dependency on a pw_build_assert.
+# Multiple targets may depend on the same pw_build_assert if the same assertion
+# applies.
+#
+# Args:
+#
+#   condition: The assertion to verify.
+#   message: The message to print. Use \n for newlines.
+#   message_lines: List of lines to use for the message.
+#   visibility: GN visibility to apply to the underlying target.
+#
+template("pw_build_assert") {
+  assert(defined(invoker.condition),
+         "pw_build_assert requires a boolean condition")
+  assert(defined(invoker.message) != defined(invoker.message_lines),
+         "pw_build_assert requires either 'message' or 'message_lines'")
+
+  _pw_error_variables = [
+    "message",
+    "message_lines",
+    "visibility",
+  ]
+
+  if (invoker.condition) {
+    not_needed(invoker, _pw_error_variables)
+    group(target_name) {
+      forward_variables_from(invoker, [ "visibility" ])
+    }
+  } else {
+    pw_error(target_name) {
+      forward_variables_from(invoker, _pw_error_variables)
+    }
   }
 }
diff --git a/pw_sync_embos/BUILD.gn b/pw_sync_embos/BUILD.gn
index 043ae72..c399748 100644
--- a/pw_sync_embos/BUILD.gn
+++ b/pw_sync_embos/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
 import("$dir_pw_docgen/docs.gni")
@@ -28,6 +29,15 @@
   visibility = [ ":*" ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_embos:system_clock"
+  message = "The embOS pw_sync backends only work with the " +
+            "embOS pw::chrono::SystemClock backend."
+  visibility = [ ":*" ]
+}
+
 # This target provides the backend for pw::sync::BinarySemaphore.
 pw_source_set("binary_semaphore") {
   public_configs = [
@@ -48,12 +58,10 @@
     "$dir_pw_third_party/embos",
   ]
   sources = [ "binary_semaphore.cc" ]
-  deps = [ "$dir_pw_sync:binary_semaphore.facade" ]
-  assert(
-      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_embos:system_clock",
-      "The embOS pw::sync::BinarySemaphore backend only works with the " +
-          "embOS pw::chrono::SystemClock backend.")
+  deps = [
+    ":check_system_clock_backend",
+    "$dir_pw_sync:binary_semaphore.facade",
+  ]
 }
 
 # This target provides the backend for pw::sync::CountingSemaphore.
@@ -76,12 +84,10 @@
     "$dir_pw_third_party/embos",
   ]
   sources = [ "counting_semaphore.cc" ]
-  deps = [ "$dir_pw_sync:counting_semaphore.facade" ]
-  assert(
-      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_embos:system_clock",
-      "The embOS pw::sync::CountingSemaphore backend only works with " +
-          "the embOS pw::chrono::SystemClock backend.")
+  deps = [
+    ":check_system_clock_backend",
+    "$dir_pw_sync:counting_semaphore.facade",
+  ]
 }
 
 # This target provides the backend for pw::sync::Mutex.
@@ -120,16 +126,12 @@
   ]
   sources = [ "timed_mutex.cc" ]
   deps = [
+    ":check_system_clock_backend",
     "$dir_pw_assert",
     "$dir_pw_chrono_embos:system_clock",
     "$dir_pw_interrupt:context",
     "$dir_pw_third_party/embos",
   ]
-  assert(
-      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_embos:system_clock",
-      "The embOS pw::sync::Mutex backend only works with the embOS " +
-          "pw::chrono::SystemClock backend.")
 }
 
 # This target provides the backend for pw::sync::InterruptSpinLock.
diff --git a/pw_sync_freertos/BUILD.gn b/pw_sync_freertos/BUILD.gn
index 82665f1..abb53c4 100644
--- a/pw_sync_freertos/BUILD.gn
+++ b/pw_sync_freertos/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/module_config.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
@@ -45,6 +46,15 @@
   ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_freertos:system_clock"
+  message = "The FreeRTOS pw_sync backends only work with the FreeRTOS " +
+            "pw::chrono::SystemClock backend."
+  visibility = [ ":*" ]
+}
+
 # This target provides the backend for pw::sync::BinarySemaphore.
 pw_source_set("binary_semaphore") {
   public_configs = [
@@ -65,12 +75,10 @@
     "$dir_pw_third_party/freertos",
   ]
   sources = [ "binary_semaphore.cc" ]
-  deps = [ "$dir_pw_sync:binary_semaphore.facade" ]
-  assert(pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-             pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                 "$dir_pw_chrono_freertos:system_clock",
-         "The FreeRTOS pw::sync::BinarySemaphore backend only works with the " +
-             "FreeRTOS pw::chrono::SystemClock backend.")
+  deps = [
+    ":check_system_clock_backend",
+    "$dir_pw_sync:binary_semaphore.facade",
+  ]
 }
 
 # This target provides the backend for pw::sync::CountingSemaphore.
@@ -93,12 +101,10 @@
     "$dir_pw_third_party/freertos",
   ]
   sources = [ "counting_semaphore.cc" ]
-  deps = [ "$dir_pw_sync:counting_semaphore.facade" ]
-  assert(pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-             pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                 "$dir_pw_chrono_freertos:system_clock",
-         "The FreeRTOS pw::sync::CountingSemaphore backend only works with " +
-             "the FreeRTOS pw::chrono::SystemClock backend.")
+  deps = [
+    ":check_system_clock_backend",
+    "$dir_pw_sync:counting_semaphore.facade",
+  ]
 }
 
 # This target provides the backend for pw::sync::Mutex.
@@ -174,17 +180,13 @@
   ]
   sources = [ "timed_thread_notification.cc" ]
   deps = [
+    ":check_system_clock_backend",
     ":config",
     "$dir_pw_assert",
     "$dir_pw_chrono_freertos:system_clock",
     "$dir_pw_interrupt:context",
     "$dir_pw_third_party/freertos",
   ]
-  assert(pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-             pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                 "$dir_pw_chrono_freertos:system_clock",
-         "The FreeRTOS pw::sync::Mutex backend only works with the FreeRTOS " +
-             "pw::chrono::SystemClock backend.")
 }
 
 # This target provides the backend for pw::sync::TimedMutex.
@@ -203,16 +205,12 @@
   ]
   sources = [ "timed_mutex.cc" ]
   deps = [
+    ":check_system_clock_backend",
     "$dir_pw_assert",
     "$dir_pw_chrono_freertos:system_clock",
     "$dir_pw_interrupt:context",
     "$dir_pw_third_party/freertos",
   ]
-  assert(pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-             pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                 "$dir_pw_chrono_freertos:system_clock",
-         "The FreeRTOS pw::sync::Mutex backend only works with the FreeRTOS " +
-             "pw::chrono::SystemClock backend.")
 }
 
 # This target provides the backend for pw::sync::InterruptSpinLock.
diff --git a/pw_sync_stl/BUILD.gn b/pw_sync_stl/BUILD.gn
index f3f07c2..42aea99 100644
--- a/pw_sync_stl/BUILD.gn
+++ b/pw_sync_stl/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
 import("$dir_pw_docgen/docs.gni")
@@ -28,6 +29,15 @@
   visibility = [ ":*" ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_stl:system_clock"
+  message = "The STL pw_sync backends only work with the STL " +
+            "pw::chrono::SystemClock backend."
+  visibility = [ ":*" ]
+}
+
 # This target provides the backend for pw::sync::BinarySemaphore.
 pw_source_set("binary_semaphore_backend") {
   public_configs = [
@@ -42,15 +52,11 @@
   ]
   sources = [ "binary_semaphore.cc" ]
   deps = [
+    ":check_system_clock_backend",
     "$dir_pw_assert",
     "$dir_pw_chrono:system_clock",
     "$dir_pw_sync:binary_semaphore.facade",
   ]
-  assert(
-      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_stl:system_clock",
-      "The STL pw::sync::BinarySemaphore backend only works with the " +
-          "STL pw::chrono::SystemClock backend.")
 }
 
 # This target provides the backend for pw::sync::CountingSemaphore.
@@ -67,15 +73,11 @@
   ]
   sources = [ "counting_semaphore.cc" ]
   deps = [
+    ":check_system_clock_backend",
     "$dir_pw_assert",
     "$dir_pw_chrono:system_clock",
     "$dir_pw_sync:counting_semaphore.facade",
   ]
-  assert(
-      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_stl:system_clock",
-      "The STL pw::sync::CountingSemaphore backend only works with the " +
-          "STL pw::chrono::SystemClock backend.")
 }
 
 # This target provides the backend for pw::sync::Mutex.
@@ -108,11 +110,7 @@
     "public_overrides/pw_sync_backend/timed_mutex_inline.h",
   ]
   public_deps = [ "$dir_pw_chrono:system_clock" ]
-  assert(
-      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_stl:system_clock",
-      "The STL pw::sync::TimedMutex backend only works with the STL " +
-          "pw::chrono::SystemClock backend.")
+  deps = [ ":check_system_clock_backend" ]
 }
 
 # This target provides the backend for pw::sync::InterruptSpinLock.
diff --git a/pw_sync_threadx/BUILD.gn b/pw_sync_threadx/BUILD.gn
index afeebe3..524c478 100644
--- a/pw_sync_threadx/BUILD.gn
+++ b/pw_sync_threadx/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
 import("$dir_pw_docgen/docs.gni")
@@ -29,6 +30,15 @@
   visibility = [ ":*" ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_sync_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_threadx:system_clock"
+  message = "The ThreadX pw::sync::BinarySemaphore backend only works with " +
+            "the ThreadX pw::chrono::SystemClock backend."
+  visibility = [ ":*" ]
+}
+
 # This target provides the backend for pw::sync::Mutex.
 pw_source_set("mutex") {
   public_configs = [
@@ -70,15 +80,10 @@
     ]
     sources = [ "binary_semaphore.cc" ]
     deps = [
+      ":check_system_clock_backend",
       "$dir_pw_sync:binary_semaphore.facade",
       pw_chrono_SYSTEM_CLOCK_BACKEND,
     ]
-    assert(
-        pw_sync_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
-            pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                "$dir_pw_chrono_threadx:system_clock",
-        "The ThreadX pw::sync::BinarySemaphore backend only works with the " +
-            "ThreadX pw::chrono::SystemClock backend.")
   }
 
   # This target provides the backend for pw::sync::CountingSemaphore.
@@ -101,14 +106,10 @@
     ]
     sources = [ "counting_semaphore.cc" ]
     deps = [
+      ":check_system_clock_backend",
       "$dir_pw_sync:counting_semaphore.facade",
       pw_chrono_SYSTEM_CLOCK_BACKEND,
     ]
-    assert(pw_sync_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
-               pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                   "$dir_pw_chrono_threadx:system_clock",
-           "The ThreadX pw::sync::CountingSemaphore backend only works with " +
-               "the ThreadX pw::chrono::SystemClock backend.")
   }
 
   # This target provides the backend for pw::sync::TimedMutex.
@@ -127,16 +128,12 @@
     ]
     sources = [ "timed_mutex.cc" ]
     deps = [
+      ":check_system_clock_backend",
       "$dir_pw_assert",
       "$dir_pw_interrupt:context",
       "$dir_pw_third_party/threadx",
       pw_chrono_SYSTEM_CLOCK_BACKEND,
     ]
-    assert(pw_sync_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
-               pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                   "$dir_pw_chrono_threadx:system_clock",
-           "The ThreadX pw::sync::Mutex backend only works with the ThreadX " +
-               "pw::chrono::SystemClock backend.")
   }
 }
 
diff --git a/pw_thread_embos/BUILD.gn b/pw_thread_embos/BUILD.gn
index 300a46b..1b48750 100644
--- a/pw_thread_embos/BUILD.gn
+++ b/pw_thread_embos/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/module_config.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
@@ -67,6 +68,15 @@
   deps = [ "$dir_pw_thread:id.facade" ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_thread_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_embos:system_clock"
+  message = "The embOS pw::thread::sleep_{for,until} backend only works with " +
+            "the embOS pw::chrono::SystemClock backend."
+  visibility = [ ":*" ]
+}
+
 if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") {
   # This target provides the backend for pw::thread::sleep_{for,until}.
   pw_source_set("sleep") {
@@ -81,17 +91,13 @@
     public_deps = [ "$dir_pw_chrono:system_clock" ]
     sources = [ "sleep.cc" ]
     deps = [
+      ":check_system_clock_backend",
       "$dir_pw_assert",
       "$dir_pw_chrono_embos:system_clock",
       "$dir_pw_third_party/embos",
       "$dir_pw_thread:id",
       "$dir_pw_thread:sleep.facade",
     ]
-    assert(pw_thread_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
-               pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                   "$dir_pw_chrono_embos:system_clock",
-           "The embOS pw::thread::sleep_{for,until} backend only works with " +
-               "the embOS pw::chrono::SystemClock backend.")
   }
 }
 
diff --git a/pw_thread_freertos/BUILD.gn b/pw_thread_freertos/BUILD.gn
index 01d0a17..c1027cc 100644
--- a/pw_thread_freertos/BUILD.gn
+++ b/pw_thread_freertos/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/facade.gni")
 import("$dir_pw_build/module_config.gni")
 import("$dir_pw_build/target_types.gni")
@@ -69,6 +70,17 @@
   deps = [ "$dir_pw_thread:id.facade" ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_freertos:system_clock"
+  message = "This FreeRTOS backend only works with the FreeRTOS " +
+            "pw::chrono::SystemClock backend " +
+            "(pw_chrono_SYSTEM_CLOCK_BACKEND = " +
+            "\"$dir_pw_chrono_freertos:system_clock\")"
+  visibility = [ ":*" ]
+}
+
 # This target provides the backend for pw::this_thread::sleep_{for,until}.
 pw_source_set("sleep") {
   public_configs = [
@@ -82,19 +94,13 @@
   public_deps = [ "$dir_pw_chrono:system_clock" ]
   sources = [ "sleep.cc" ]
   deps = [
+    ":check_system_clock_backend",
     "$dir_pw_assert",
     "$dir_pw_chrono_freertos:system_clock",
     "$dir_pw_third_party/freertos",
     "$dir_pw_thread:id",
     "$dir_pw_thread:sleep.facade",
   ]
-  assert(pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-             pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                 "$dir_pw_chrono_freertos:system_clock",
-         "The FreeRTOS pw::this_thread::sleep_{for,until} backend only works " +
-             "with the FreeRTOS pw::chrono::SystemClock backend " +
-             "(pw_chrono_SYSTEM_CLOCK_BACKEND = " +
-             "\"$dir_pw_chrono_freertos:system_clock\")")
 }
 
 # This target provides the backend for pw::thread::Thread and the headers needed
diff --git a/pw_thread_stl/BUILD.gn b/pw_thread_stl/BUILD.gn
index c4d4746..0e4e6b6 100644
--- a/pw_thread_stl/BUILD.gn
+++ b/pw_thread_stl/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
 import("$dir_pw_docgen/docs.gni")
@@ -63,6 +64,17 @@
   deps = [ "$dir_pw_thread:thread.facade" ]
 }
 
+pw_build_assert("check_system_clock_backend") {
+  condition =
+      pw_thread_SLEEP_BACKEND != "$dir_pw_thread_stl:sleep" ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
+      pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_stl:system_clock"
+  message = "The STL pw::this_thread::sleep_{for,until} backend only works " +
+            "with the STL pw::chrono::SystemClock backend " +
+            "(pw_chrono_SYSTEM_CLOCK_BACKEND = " +
+            "\"$dir_pw_chrono_stl:system_clock\")"
+}
+
 # This target provides the backend for pw::this_thread::sleep_{for,until}.
 pw_source_set("sleep") {
   public_configs = [
@@ -74,17 +86,10 @@
     "public_overrides/pw_thread_backend/sleep_inline.h",
   ]
   deps = [
+    ":check_system_clock_backend",
     "$dir_pw_chrono:system_clock",
     "$dir_pw_thread:sleep.facade",
   ]
-  assert(
-      pw_thread_SLEEP_BACKEND != "$dir_pw_thread_stl:sleep" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "" ||
-          pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_stl:system_clock",
-      "The STL pw::this_thread::sleep_{for,until} backend only works with " +
-          "the STL pw::chrono::SystemClock backend " +
-          "(pw_chrono_SYSTEM_CLOCK_BACKEND = " +
-          "\"$dir_pw_chrono_stl:system_clock\")")
 }
 
 # This target provides the backend for pw::this_thread::yield.
diff --git a/pw_thread_threadx/BUILD.gn b/pw_thread_threadx/BUILD.gn
index 892d2c8..2917147 100644
--- a/pw_thread_threadx/BUILD.gn
+++ b/pw_thread_threadx/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("//build_overrides/pigweed.gni")
 
+import("$dir_pw_build/error.gni")
 import("$dir_pw_build/module_config.gni")
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
@@ -68,6 +69,14 @@
 }
 
 if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") {
+  pw_build_assert("check_system_clock_backend") {
+    condition =
+        pw_thread_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
+        pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_threadx:system_clock"
+    message = "The ThreadX pw::this_thread::sleep_{for,until} backend only " +
+              "works with the ThreadX pw::chrono::SystemClock backend."
+  }
+
   # This target provides the backend for pw::this_thread::sleep_{for,until}.
   pw_source_set("sleep") {
     public_configs = [
@@ -81,16 +90,12 @@
     public_deps = [ "$dir_pw_chrono:system_clock" ]
     sources = [ "sleep.cc" ]
     deps = [
+      ":check_system_clock_backend",
       "$dir_pw_assert",
       "$dir_pw_chrono_threadx:system_clock",
       "$dir_pw_third_party/threadx",
       "$dir_pw_thread:id",
     ]
-    assert(
-        pw_thread_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK ||
-            pw_chrono_SYSTEM_CLOCK_BACKEND ==
-                "$dir_pw_chrono_threadx:system_clock",
-        "The ThreadX pw::this_thread::sleep_{for,until} backend only works with " + "the ThreadX pw::chrono::SystemClock backend.")
   }
 }