pw_build/cmake: Enable separate backend variable declarations

Extends the CMake facade API to mirror GN in its ability to specify
facade backend variables separate from the facade library
declarations. Outside of making this more consistent with the GN
build, this also enables enable_if for unit tests in the future.

As part of this backend variables for facades are no longer set to
{NAME}.NO_BACKEND_SET by default and instead are set to empty
strings if unset.

Change-Id: Ie0669c702a15a462cef92d546ad28ca945b71027
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/117412
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
Commit-Queue: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_assert/CMakeLists.txt b/pw_assert/CMakeLists.txt
index 56b8e99..68972f8 100644
--- a/pw_assert/CMakeLists.txt
+++ b/pw_assert/CMakeLists.txt
@@ -130,10 +130,8 @@
     pw_assert
 )
 
-if((NOT "${pw_assert.assert_BACKEND}" STREQUAL
-    "pw_assert.assert.NO_BACKEND_SET") AND
-   (NOT "${pw_assert.check_BACKEND}" STREQUAL
-    "pw_assert.check.NO_BACKEND_SET"))
+if((NOT "${pw_assert.assert_BACKEND}" STREQUAL "") AND
+   (NOT "${pw_assert.check_BACKEND}" STREQUAL ""))
   pw_add_test(pw_assert.assert_backend_compile_test
     SOURCES
       assert_backend_compile_test.cc
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index f314d2a..a57bd4b 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -883,12 +883,13 @@
 
 .. code-block::
 
-  CMake Error at pw_build/pigweed.cmake:244 (add_custom_target):
-  Error evaluating generator expression:
+  CMake Error at pw_build/pigweed.cmake:257 (message):
+    my_module.my_facade's INTERFACE dep "my_nonexistent_backend" is not
+    a target.
+  Call Stack (most recent call first):
+    pw_build/pigweed.cmake:238:EVAL:1 (_pw_target_link_targets_deferred_check)
+    CMakeLists.txt:DEFERRED
 
-    $<TARGET_PROPERTY:my_backend_that_does_not_exist,TYPE>
-
-  Target "my_backend_that_does_not_exist" not found.
 
 Toolchain setup
 ---------------
diff --git a/pw_build/pigweed.cmake b/pw_build/pigweed.cmake
index f9e2955..7759ef0 100644
--- a/pw_build/pigweed.cmake
+++ b/pw_build/pigweed.cmake
@@ -537,45 +537,58 @@
 # pw_add_module_facade accepts the same arguments as pw_add_module_library,
 # except for IMPLEMENTS_FACADES. It also accepts the following argument:
 #
-#  DEFAULT_BACKEND - which backend to use by default
+#  BACKEND - The name of the facade's backend variable.
+#  DEFAULT_BACKEND - which backend to use by default, ignored if BACKEND is
+#                    specified.
 #
+# TODO(ewout, hepler): Deprecate DEFAULT_BACKEND and make BACKEND required.
 function(pw_add_module_facade NAME)
   _pw_add_library_multi_value_args(list_args)
-  pw_parse_arguments_strict(pw_add_module_facade 1 "" "DEFAULT_BACKEND"
+  pw_parse_arguments_strict(pw_add_module_facade 1 ""
+                            "DEFAULT_BACKEND;BACKEND"
                             "${list_args}")
 
-  # If no backend is set, a script that displays an error message is used
-  # instead. If the facade is used in the build, it fails with this error.
-  pw_add_error_target("${NAME}.NO_BACKEND_SET"
-    MESSAGE
-      "ERROR: Attempted to build the ${NAME} facade with no backend."
-      "Configure the ${NAME} backend using pw_set_backend or remove all "
-      "dependencies on it. See https://pigweed.dev/pw_build."
-  )
+  # TODO(ewout, hepler): Remove this conditional block and instead require
+  # BACKEND and assert that it is DEFINED.
+  if(NOT "${arg_BACKEND}" STREQUAL "")
+    string(REGEX MATCH ".+_BACKEND" backend_ends_in_backend "${arg_BACKEND}")
+    if(NOT backend_ends_in_backend)
+      message(FATAL_ERROR "The ${NAME} pw_add_module_facade's BACKEND argument "
+              "(${arg_BACKEND}) must end in _BACKEND (${name_ends_in_backend})")
+    endif()
 
-  # Set the default backend to the error message if no default is specified.
-  if("${arg_DEFAULT_BACKEND}" STREQUAL "")
-    set(arg_DEFAULT_BACKEND "${NAME}.NO_BACKEND_SET")
+
+    if(NOT "${arg_DEFAULT_BACKEND}" STREQUAL "")
+      message(FATAL_ERROR
+              "${NAME}'s DEFAULT_BACKEND is ignored as BACKEND was provided")
+    endif()
+  else()
+    set(arg_BACKEND "${NAME}_BACKEND")
+    # Declare the backend variable for this facade.
+    set("${NAME}_BACKEND" "${arg_DEFAULT_BACKEND}" CACHE STRING
+        "Backend for ${NAME}")
   endif()
 
-  # Declare the backend variable for this facade.
-  set("${NAME}_BACKEND" "${arg_DEFAULT_BACKEND}" CACHE STRING
-      "Backend for ${NAME}")
-
-  # This target is never used; it simply tests that the specified backend
-  # actually exists in the build. The generator expression will fail to evaluate
-  # if the target is not defined.
-  add_custom_target(_pw_check_that_backend_for_${NAME}_is_defined
-    COMMAND
-      ${CMAKE_COMMAND} -E echo "$<TARGET_PROPERTY:${${NAME}_BACKEND},TYPE>"
-  )
-
   # Define the facade library, which is used by the backend to avoid circular
   # dependencies.
   add_library("${NAME}.facade" INTERFACE)
   target_include_directories("${NAME}.facade" INTERFACE public)
   pw_target_link_targets("${NAME}.facade" INTERFACE ${arg_PUBLIC_DEPS})
 
+  set(backend_target "${${arg_BACKEND}}")
+  if ("${backend_target}" STREQUAL "")
+    # If no backend is set, a script that displays an error message is used
+    # instead. If the facade is used in the build, it fails with this error.
+    pw_add_error_target("${NAME}.NO_BACKEND_SET"
+      MESSAGE
+        "Attempted to build the ${NAME} facade with no backend. "
+        "Configure the ${NAME} backend using pw_set_backend or remove all "
+        "dependencies on it. See https://pigweed.dev/pw_build."
+    )
+
+    set(backend_target "${NAME}.NO_BACKEND_SET")
+  endif()
+
   # Define the public-facing library for this facade, which depends on the
   # header files in .facade target and exposes the dependency on the backend.
   pw_add_module_library("${NAME}"
@@ -585,13 +598,48 @@
       ${arg_HEADERS}
     PUBLIC_DEPS
       "${NAME}.facade"
-      "${${NAME}_BACKEND}"
+      "${backend_target}"
   )
 endfunction(pw_add_module_facade)
 
-# Sets which backend to use for the given facade.
-function(pw_set_backend FACADE BACKEND)
-  set("${FACADE}_BACKEND" "${BACKEND}" CACHE STRING "Backend for ${FACADE}" FORCE)
+# Declare a facade's backend variables which can be overriden later by using
+# pw_set_backend.
+#
+# Required Arguments:
+#   NAME - Name of the facade's backend variable.
+#
+# Optional Arguments:
+#   DEFAULT_BACKEND - Optional default backend selection for the facade.
+#
+function(pw_add_backend_variable NAME)
+  set(num_positional_args 1)
+  set(option_args)
+  set(one_value_args DEFAULT_BACKEND)
+  set(multi_value_args)
+  pw_parse_arguments_strict(
+      pw_add_backend_variable "${num_positional_args}" "${option_args}"
+      "${one_value_args}" "${multi_value_args}")
+
+  string(REGEX MATCH ".+_BACKEND" name_ends_in_backend "${NAME}")
+  if(NOT name_ends_in_backend)
+    message(FATAL_ERROR "The ${NAME} pw_add_backend_variable's NAME argument "
+            "must end in _BACKEND")
+  endif()
+  set("${NAME}" "${OPTIONAL_DEFAULT_BACKEND}" CACHE STRING
+      "${NAME} backend variable for a facade")
+endfunction()
+
+# Sets which backend to use for the given facade's backend variable.
+function(pw_set_backend NAME BACKEND)
+  # TODO(ewout, hepler): Deprecate this temporarily support which permits the
+  # direct facade name directly, instead of the facade's backend variable name.
+  # Also update this to later assert the variable is DEFINED to catch typos.
+  string(REGEX MATCH ".+_BACKEND" name_ends_in_backend "${NAME}")
+  if(NOT name_ends_in_backend)
+    set(NAME "${NAME}_BACKEND")
+  endif()
+
+  set("${NAME}" "${BACKEND}" CACHE STRING "backend variable for a facade" FORCE)
 endfunction(pw_set_backend)
 
 # Zephyr specific wrapper for pw_set_backend, selects the default zephyr backend based on a Kconfig while
diff --git a/pw_chrono/CMakeLists.txt b/pw_chrono/CMakeLists.txt
index bdc4df9..5e0cbe7 100644
--- a/pw_chrono/CMakeLists.txt
+++ b/pw_chrono/CMakeLists.txt
@@ -66,10 +66,8 @@
 # TODO(ewout): Renable this once we've resolved the backend variable definition
 # ordering issue, likely by mirroring GN's definition of variables in external
 # files which can be imported where needed.
-# if((NOT "${pw_chrono.system_clock_BACKEND}"
-#     STREQUAL "pw_chrono.system_clock.NO_BACKEND_SET") AND
-#    (NOT "${pw_sync.interrupt_spin_lock_BACKEND}"
-#     STREQUAL "pw_sync.interrupt_spin_lock.NO_BACKEND_SET"))
+# if((NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "") AND
+#    (NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL ""))
 #   pw_add_test(pw_chrono.simulated_system_clock_test
 #     SOURCES
 #       simulated_system_clock_test.cc
@@ -81,8 +79,7 @@
 #   )
 # endif()
 
-if(NOT "${pw_chrono.system_clock_BACKEND}"
-   STREQUAL "pw_chrono.system_clock.NO_BACKEND_SET")
+if(NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "")
   pw_add_test(pw_chrono.system_clock_facade_test
     SOURCES
       system_clock_facade_test.cc
@@ -96,8 +93,7 @@
   )
 endif()
 
-if(NOT "${pw_chrono.system_timer_BACKEND}"
-   STREQUAL "pw_chrono.system_timer.NO_BACKEND_SET")
+if(NOT "${pw_chrono.system_timer_BACKEND}" STREQUAL "")
   pw_add_test(pw_chrono.system_timer_facade_test
     SOURCES
       system_timer_facade_test.cc
diff --git a/pw_log/CMakeLists.txt b/pw_log/CMakeLists.txt
index 81e3ae2..6716b0a 100644
--- a/pw_log/CMakeLists.txt
+++ b/pw_log/CMakeLists.txt
@@ -84,7 +84,7 @@
     pw_tokenizer.proto
 )
 
-if(NOT "${pw_log_BACKEND}" STREQUAL "pw_log.NO_BACKEND_SET")
+if(NOT "${pw_log_BACKEND}" STREQUAL "")
   pw_add_test(pw_log.basic_log_test
     SOURCES
       basic_log_test.cc
diff --git a/pw_perf_test/CMakeLists.txt b/pw_perf_test/CMakeLists.txt
index 6068bfc..938f9df 100644
--- a/pw_perf_test/CMakeLists.txt
+++ b/pw_perf_test/CMakeLists.txt
@@ -54,8 +54,7 @@
     pw_perf_test.duration_unit
 )
 
-if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}"
-    STREQUAL "pw_perf_test.timer.NO_BACKEND_SET")
+if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
   pw_add_test(pw_perf_test.timer_test
     SOURCES
       timer_test.cc
diff --git a/pw_rpc/CMakeLists.txt b/pw_rpc/CMakeLists.txt
index 4b23de2..7ce8042 100644
--- a/pw_rpc/CMakeLists.txt
+++ b/pw_rpc/CMakeLists.txt
@@ -114,8 +114,7 @@
   zephyr_link_libraries(pw_rpc.common)
 endif()
 
-if (NOT "${pw_sync.mutex_BACKEND}" STREQUAL "pw_sync.mutex.NO_BACKEND_SET" AND
-    NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+if (NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
   target_link_libraries(pw_rpc.common PUBLIC pw_sync.mutex)
 endif()
 
diff --git a/pw_rpc/nanopb/CMakeLists.txt b/pw_rpc/nanopb/CMakeLists.txt
index 16fb466..871a043 100644
--- a/pw_rpc/nanopb/CMakeLists.txt
+++ b/pw_rpc/nanopb/CMakeLists.txt
@@ -125,10 +125,8 @@
 )
 
 if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
-   (NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL
-   "pw_sync.binary_semaphore.NO_BACKEND_SET") AND
-   (NOT "${pw_sync.mutex_BACKEND}" STREQUAL
-   "pw_sync.binary_semaphore.NO_BACKEND_SET"))
+   (NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
+   (NOT "${pw_sync.mutex_BACKEND}" STREQUAL ""))
   pw_add_test(pw_rpc.nanopb.client_server_context_threaded_test
     SOURCES
       client_server_context_threaded_test.cc
@@ -257,8 +255,7 @@
 # TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
 if((TARGET pw_work_queue.pw_work_queue) AND
    ("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
-   (NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL
-   "pw_sync.timed_thread_notification.NO_BACKEND_SET"))
+   (NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL ""))
   pw_add_test(pw_rpc.nanopb.synchronous_call_test
     SOURCES
       synchronous_call_test.cc
diff --git a/pw_rpc/pwpb/CMakeLists.txt b/pw_rpc/pwpb/CMakeLists.txt
index 8ca6722..994d291 100644
--- a/pw_rpc/pwpb/CMakeLists.txt
+++ b/pw_rpc/pwpb/CMakeLists.txt
@@ -107,10 +107,8 @@
 )
 
 if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
-   (NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL
-   "pw_sync.binary_semaphore.NO_BACKEND_SET") AND
-   (NOT "${pw_sync.mutex_BACKEND}" STREQUAL
-   "pw_sync.binary_semaphore.NO_BACKEND_SET"))
+   (NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
+   (NOT "${pw_sync.mutex_BACKEND}" STREQUAL ""))
   pw_add_test(pw_rpc.pwpb.client_server_context_threaded_test
     SOURCES
       client_server_context_threaded_test.cc
diff --git a/pw_sync/CMakeLists.txt b/pw_sync/CMakeLists.txt
index f5920d9..aa7e463 100644
--- a/pw_sync/CMakeLists.txt
+++ b/pw_sync/CMakeLists.txt
@@ -220,8 +220,7 @@
     pw_sync
 )
 
-if(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL
-   "pw_sync.binary_semaphore.NO_BACKEND_SET")
+if(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.binary_semaphore_facade_test
     SOURCES
       binary_semaphore_facade_test.cc
@@ -235,8 +234,7 @@
   )
 endif()
 
-if(NOT "${pw_sync.counting_semaphore_BACKEND}" STREQUAL
-   "pw_sync.counting_semaphore.NO_BACKEND_SET")
+if(NOT "${pw_sync.counting_semaphore_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.counting_semaphore_facade_test
     SOURCES
       counting_semaphore_facade_test.cc
@@ -250,7 +248,7 @@
   )
 endif()
 
-if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "pw_sync.mutex.NO_BACKEND_SET")
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.mutex_facade_test
     SOURCES
       mutex_facade_test.cc
@@ -264,8 +262,7 @@
   )
 endif()
 
-if(NOT "${pw_sync.timed_mutex_BACKEND}" STREQUAL
-   "pw_sync.timed_mutex.NO_BACKEND_SET")
+if(NOT "${pw_sync.timed_mutex_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.timed_mutex_facade_test
     SOURCES
       timed_mutex_facade_test.cc
@@ -279,8 +276,7 @@
   )
 endif()
 
-if(NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL
-   "pw_sync.interrupt_spin_lock.NO_BACKEND_SET")
+if(NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.interrupt_spin_lock_facade_test
     SOURCES
       interrupt_spin_lock_facade_test.cc
@@ -294,8 +290,7 @@
   )
 endif()
 
-if(NOT "${pw_sync.thread_notification_BACKEND}" STREQUAL
-   "pw_sync.thread_notification.NO_BACKEND_SET")
+if(NOT "${pw_sync.thread_notification_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.thread_notification_facade_test
     SOURCES
       thread_notification_facade_test.cc
@@ -307,8 +302,7 @@
   )
 endif()
 
-if(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL
-   "pw_sync.timed_thread_notification.NO_BACKEND_SET")
+if(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL "")
   pw_add_test(pw_sync.timed_thread_notification_facade_test
     SOURCES
       timed_thread_notification_facade_test.cc
diff --git a/pw_thread/CMakeLists.txt b/pw_thread/CMakeLists.txt
index c4b9156..145e600 100644
--- a/pw_thread/CMakeLists.txt
+++ b/pw_thread/CMakeLists.txt
@@ -100,7 +100,7 @@
     pw_tokenizer.proto
 )
 
-if(NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET")
+if(NOT "${pw_thread.id_BACKEND}" STREQUAL "")
   pw_add_test(pw_thread.id_facade_test
     SOURCES
       id_facade_test.cc
@@ -112,8 +112,8 @@
   )
 endif()
 
-if((NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET") AND
-   (NOT "${pw_thread.sleep_BACKEND}" STREQUAL "pw_thread.sleep.NO_BACKEND_SET"))
+if((NOT "${pw_thread.id_BACKEND}" STREQUAL "") AND
+   (NOT "${pw_thread.sleep_BACKEND}" STREQUAL ""))
   pw_add_test(pw_thread.sleep_facade_test
     SOURCES
       sleep_facade_test.cc
@@ -153,8 +153,8 @@
     pw_unit_test
 )
 
-if((NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET") AND
-   (NOT "${pw_thread.yield_BACKEND}" STREQUAL "pw_thread.yield.NO_BACKEND_SET"))
+if((NOT "${pw_thread.id_BACKEND}" STREQUAL "") AND
+   (NOT "${pw_thread.yield_BACKEND}" STREQUAL ""))
   pw_add_test(pw_thread.yield_facade_test
     SOURCES
       yield_facade_test.cc
diff --git a/pw_thread_stl/CMakeLists.txt b/pw_thread_stl/CMakeLists.txt
index dfb9b39..fb3b4e5 100644
--- a/pw_thread_stl/CMakeLists.txt
+++ b/pw_thread_stl/CMakeLists.txt
@@ -81,7 +81,7 @@
 )
 
 if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
-   (NOT "${pw_thread.sleep_BACKEND}" STREQUAL "pw_thread.sleep.NO_BACKEND_SET"))
+   (NOT "${pw_thread.sleep_BACKEND}" STREQUAL ""))
   pw_add_test(pw_thread_stl.thread_backend_test
     PRIVATE_DEPS
       pw_thread_stl.test_threads
diff --git a/pw_tokenizer/CMakeLists.txt b/pw_tokenizer/CMakeLists.txt
index c96e3ba..0c786c9 100644
--- a/pw_tokenizer/CMakeLists.txt
+++ b/pw_tokenizer/CMakeLists.txt
@@ -197,17 +197,20 @@
     pw_tokenizer
 )
 
-pw_add_test(pw_tokenizer.global_handlers_test
-  SOURCES
-    global_handlers_test_c.c
-    global_handlers_test.cc
-  PRIVATE_DEPS
-    pw_tokenizer.global_handler
-    pw_tokenizer.global_handler_with_payload
-  GROUPS
-    modules
-    pw_tokenizer
-)
+if((NOT "${pw_tokenizer.global_handler_with_payload_BACKEND}" STREQUAL "") AND
+   (NOT "${pw_tokenizer.global_handler_BACKEND}" STREQUAL ""))
+  pw_add_test(pw_tokenizer.global_handlers_test
+    SOURCES
+      global_handlers_test_c.c
+      global_handlers_test.cc
+    PRIVATE_DEPS
+      pw_tokenizer.global_handler
+      pw_tokenizer.global_handler_with_payload
+    GROUPS
+      modules
+      pw_tokenizer
+  )
+endif()
 
 pw_add_test(pw_tokenizer.hash_test
   SOURCES
diff --git a/pw_trace/CMakeLists.txt b/pw_trace/CMakeLists.txt
index 4ec89b9..fd2ed62 100644
--- a/pw_trace/CMakeLists.txt
+++ b/pw_trace/CMakeLists.txt
@@ -48,7 +48,7 @@
     pw_trace
 )
 
-if(NOT "${pw_trace_BACKEND}" STREQUAL "pw_trace.NO_BACKEND_SET")
+if(NOT "${pw_trace_BACKEND}" STREQUAL "")
   pw_add_test(pw_trace.trace_backend_compile_test
    SOURCES
     trace_backend_compile_test.cc