Repository: stephenberry/glaze (2026-06-08T15:07:15Z)

Issue:
Different priority for asio libraries in CMake configuration and glaze_asio.hpp includes
By default, Glaze does this in tests/CMakeLists.txt:

```cmake
# Setup ASIO dependency (used by networking tests)
# This must be defined here before glz_test_common/glz_test_exceptions
#
# Search order:
#   1. Boost.Asio (via find_package(Boost))
#   2. Standalone Asio (via find_package(Asio) using our FindAsio.cmake)
#   3. FetchContent (if glaze_USE_BUNDLED_ASIO is ON)

if(Boost_FOUND)
    message(STATUS "Using Boost.Asio")
    # ...
else()
    # Second, try standalone Asio
    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
    find_package(Asio QUIET)

    if(Asio_FOUND)
        ...
    elseif(glaze_USE_BUNDLED_ASIO)
        ...
    else()
        ...
    endif()
endif()
```

But at the same time, [`include/glaze/ext/glaze_asio.hpp`](https://github.com/stephenberry/glaze/blob/93d3e70ad41c1914e87ca71a8d34e1a0ea7f9d1b/include/glaze/ext/glaze_asio.hpp#L15) contains the following code:

```cpp
#if __has_include(<asio.hpp>) && !defined(GLZ_USE_BOOST_ASIO)
#include <asio.hpp>
#include <asio/signal_set.hpp>
#ifdef GLZ_ENABLE_SSL
#include <asio/ssl.hpp>
#endif
#elif __has_include(<boost/asio.hpp>)
#ifndef GLZ_USING_BOOST_ASIO
#define GLZ_USING_BOOST_ASIO
#endif
#include <boost/asio.hpp>
#include <boost/asio/signal_set.hpp>
#ifdef GLZ_ENABLE_SSL
#include <boost/asio/ssl.hpp>
#endif
#else
static_assert(false, "standalone or boost asio must be included to use glaze/ext/glaze_asio.hpp");
#endif
```

Which clearly prioritizes standalone-asio. This causes a problem when building the tests, since tests/CMakeLists.txt does not define `GLZ_USE_BOOST_ASIO` but still uses boost-asio. As a result, at the very least, LSP complains and cannot parse the code properly, generating hundreds of errors, and at worst, it can cause the build to fail.


Discussion:
Should be resolved with the merge of #2604 
Yes, everything's working great now, thank you!



Patch:
diff --git a/benchmarks/http_benchmark/CMakeLists.txt b/benchmarks/http_benchmark/CMakeLists.txt
index 15da32fdcb..71d5c36257 100644
--- a/benchmarks/http_benchmark/CMakeLists.txt
+++ b/benchmarks/http_benchmark/CMakeLists.txt
@@ -8,16 +8,21 @@ endif()
 
 message(STATUS "[bench] Boost found — building HTTP benchmark (Glaze vs Boost.Beast)")
 
+# Route Glaze's networking headers through the shared Asio selector. Boost is
+# required here (Boost.Beast), so glaze::asio resolves to the Boost backend and
+# carries GLZ_USE_BOOST_ASIO + Boost::headers. Linking it keeps this benchmark's
+# direct boost::asio/boost::beast usage and Glaze's headers on the same backend,
+# instead of each consumer remembering to set the macro itself (issue #2599).
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake")
+include(glaze-asio)
+glaze_setup_asio()
+
 add_executable(http_benchmark http_benchmark.cpp)
 target_link_libraries(http_benchmark PRIVATE
   glaze
+  glaze::asio
   bencher::bencher
 )
-if(TARGET Boost::headers)
-    target_link_libraries(http_benchmark PRIVATE Boost::headers)
-elseif(TARGET Boost::boost)
-    target_link_libraries(http_benchmark PRIVATE Boost::boost)
-endif()
 target_compile_options(http_benchmark PRIVATE
   $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-O3 -march=native>
   $<$<CXX_COMPILER_ID:MSVC>:/O2>
diff --git a/cmake/glaze-asio.cmake b/cmake/glaze-asio.cmake
new file mode 100644
index 0000000000..0af98ece13
--- /dev/null
+++ b/cmake/glaze-asio.cmake
@@ -0,0 +1,94 @@
+# glaze-asio.cmake
+#
+# Single source of truth for selecting the Asio backend used by Glaze's
+# networking headers (glaze/ext/glaze_asio.hpp, glaze/net/*).
+#
+# Including this module declares the glaze_USE_BUNDLED_ASIO option and provides:
+#
+#   glaze_setup_asio()
+#       Creates the `glaze_asio` INTERFACE target and the `glaze::asio` alias,
+#       wired to exactly one Asio backend. Link `glaze::asio` alongside
+#       `glaze::glaze` from any networking consumer.
+#
+# Selection order (unchanged from the historical tests/ behavior):
+#   1. Boost.Asio      - find_package(Boost CONFIG); also defines GLZ_USE_BOOST_ASIO
+#   2. Standalone Asio - find_package(Asio) via the FindAsio.cmake shipped here
+#   3. Bundled Asio    - FetchContent, when glaze_USE_BUNDLED_ASIO is ON
+#
+# Why the GLZ_USE_BOOST_ASIO define matters: glaze/ext/glaze_asio.hpp prefers
+# standalone Asio whenever <asio.hpp> is includable, the opposite of the order
+# above. On a system with both installed, CMake could select Boost while the
+# header silently compiled against standalone Asio. Defining GLZ_USE_BOOST_ASIO
+# on the target keeps the header in lockstep with the backend CMake actually
+# linked, so the two can never disagree (see issue #2599).
+
+include_guard(GLOBAL)
+
+include(FetchContent)
+
+option(glaze_USE_BUNDLED_ASIO
+    "Download Asio via FetchContent if no system Asio/Boost is found (disable for distro builds)"
+    ON
+)
+
+function(glaze_setup_asio)
+    # The target is global; only build it once per configure.
+    if(TARGET glaze_asio)
+        return()
+    endif()
+
+    # 1. Boost.Asio
+    find_package(Boost QUIET CONFIG)
+    if(Boost_FOUND)
+        message(STATUS "glaze: using Boost.Asio")
+        add_library(glaze_asio INTERFACE)
+        add_library(glaze::asio ALIAS glaze_asio)
+        # Pin glaze/ext/glaze_asio.hpp to the Boost backend it would not otherwise
+        # select when standalone <asio.hpp> is also visible (issue #2599).
+        target_compile_definitions(glaze_asio INTERFACE GLZ_USE_BOOST_ASIO)
+        # Boost.Asio is header-only; Boost::system was removed in Boost 1.89
+        # (header-only since 1.69), so Boost::headers alone is sufficient.
+        if(TARGET Boost::headers)
+            target_link_libraries(glaze_asio INTERFACE Boost::headers)
+        elseif(TARGET Boost::boost)
+            target_link_libraries(glaze_asio INTERFACE Boost::boost)
+        endif()
+        return()
+    endif()
+
+    # 2. Standalone Asio (FindAsio.cmake ships next to this module)
+    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_FUNCTION_LIST_DIR}")
+    find_package(Asio QUIET)
+    if(Asio_FOUND)
+        message(STATUS "glaze: using standalone Asio ${Asio_VERSION}")
+        add_library(glaze_asio INTERFACE)
+        add_library(glaze::asio ALIAS glaze_asio)
+        target_link_libraries(glaze_asio INTERFACE Asio::Asio)
+        return()
+    endif()
+
+    # 3. Bundled Asio via FetchContent
+    if(glaze_USE_BUNDLED_ASIO)
+        message(STATUS "glaze: fetching standalone Asio via FetchContent")
+        FetchContent_Declare(
+            asio
+            GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
+            GIT_TAG asio-1-36-0
+            GIT_SHALLOW TRUE
+        )
+        FetchContent_MakeAvailable(asio)
+        add_library(glaze_asio INTERFACE)
+        add_library(glaze::asio ALIAS glaze_asio)
+        target_include_directories(glaze_asio INTERFACE ${asio_SOURCE_DIR}/asio/include)
+        return()
+    endif()
+
+    message(FATAL_ERROR
+        "Asio not found and bundling is disabled (glaze_USE_BUNDLED_ASIO=OFF).\n"
+        "Options:\n"
+        "  - Install Boost with headers\n"
+        "  - Install standalone Asio (headers in a standard location)\n"
+        "  - Set Asio_INCLUDE_DIR to your Asio headers location\n"
+        "  - Set glaze_USE_BUNDLED_ASIO=ON to download via FetchContent"
+    )
+endfunction()
diff --git a/cmake/install-config.cmake.in b/cmake/install-config.cmake.in
index b1eddf7eb1..dbd6977f73 100644
--- a/cmake/install-config.cmake.in
+++ b/cmake/install-config.cmake.in
@@ -18,3 +18,14 @@ endif()
 unset(_glaze_ENABLE_SSL)
 
 include("${CMAKE_CURRENT_LIST_DIR}/glazeTargets.cmake")
+
+# Make the Asio backend selector available to networking consumers. This only
+# declares the glaze_USE_BUNDLED_ASIO option and the glaze_setup_asio() function;
+# nothing is resolved until the consumer calls glaze_setup_asio() and links
+# glaze::asio. Doing so picks the same backend and sets GLZ_USE_BOOST_ASIO exactly
+# as the in-tree build, so glaze/ext/glaze_asio.hpp can never disagree with the
+# linked Asio (issue #2599). Guarded so a source build without the installed
+# module (e.g. add_subdirectory) still configures.
+if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/glaze-asio.cmake")
+    include("${CMAKE_CURRENT_LIST_DIR}/glaze-asio.cmake")
+endif()
diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake
index 2e007a2434..cae895a72f 100644
--- a/cmake/install-rules.cmake
+++ b/cmake/install-rules.cmake
@@ -32,6 +32,20 @@ set(
 )
 mark_as_advanced(glaze_INSTALL_CMAKEDIR)
 
+# Ship the Asio backend selector alongside the package config so that
+# find_package(glaze) consumers can call glaze_setup_asio() and link
+# glaze::asio, getting the exact same backend + GLZ_USE_BOOST_ASIO bridge as the
+# in-tree builds. FindAsio.cmake travels with it because glaze_setup_asio()
+# locates it via CMAKE_CURRENT_FUNCTION_LIST_DIR (issue #2599). glazeConfig.cmake
+# includes glaze-asio.cmake so the function is available after find_package(glaze).
+install(
+    FILES
+        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/glaze-asio.cmake"
+        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/FindAsio.cmake"
+    DESTINATION "${glaze_INSTALL_CMAKEDIR}"
+    COMPONENT glaze_Development
+)
+
 # Read FindErlang.cmake content for embedding into config file
 # This avoids installing a separate FindErlang.cmake that could collide with other projects
 if(glaze_EETF_FORMAT)
diff --git a/docs/networking/asio-setup.md b/docs/networking/asio-setup.md
index 42e56b6ddb..3ebc678cc8 100644
--- a/docs/networking/asio-setup.md
+++ b/docs/networking/asio-setup.md
@@ -102,6 +102,36 @@ target_link_libraries(your_target PRIVATE Boost::system)
 
 ## CMake Configuration
 
+### Recommended: the `glaze::asio` target
+
+Glaze ships a CMake helper, `glaze_setup_asio()`, that selects exactly one Asio
+backend and exposes it as the `glaze::asio` target. Linking `glaze::asio` is the
+recommended way to wire networking, whether you consume Glaze via `find_package`
+or `FetchContent` / `add_subdirectory`:
+
+```cmake
+find_package(glaze CONFIG REQUIRED)   # or FetchContent_MakeAvailable(glaze)
+
+glaze_setup_asio()                    # picks Boost / standalone / bundled Asio
+
+add_executable(myapp main.cpp)
+target_link_libraries(myapp PRIVATE glaze::glaze glaze::asio)
+```
+
+`glaze_setup_asio()` searches in this order:
+
+1. **Boost.Asio** (`find_package(Boost CONFIG)`) - also defines `GLZ_USE_BOOST_ASIO`
+2. **Standalone Asio** (`find_package(Asio)`, bundled `FindAsio.cmake`)
+3. **Bundled Asio** via `FetchContent`, when `glaze_USE_BUNDLED_ASIO=ON` (the default)
+
+The key benefit: when Boost is selected, `glaze::asio` carries `GLZ_USE_BOOST_ASIO`,
+which pins `glaze/ext/glaze_asio.hpp` to the Boost backend. Without it, a system
+that has *both* standalone `<asio.hpp>` and Boost on the include path could link
+Boost while the header silently compiled against standalone Asio. Linking
+`glaze::asio` keeps CMake and the header in lockstep automatically, so you never
+have to set the macro by hand. Set `-Dglaze_USE_BUNDLED_ASIO=OFF` to require a
+system package instead of downloading one.
+
 ### Complete Example with ASIO
 
 ```cmake
diff --git a/include/glaze/ext/glaze_asio.hpp b/include/glaze/ext/glaze_asio.hpp
index 7dd1bfc6b4..94839158b8 100644
--- a/include/glaze/ext/glaze_asio.hpp
+++ b/include/glaze/ext/glaze_asio.hpp
@@ -12,6 +12,19 @@
 #define _WIN32_WINNT 0x0601
 #endif
 
+// Two distinct, intentionally similar macros govern the Asio backend. Do not confuse them:
+//
+//   GLZ_USE_BOOST_ASIO   - INPUT (you set it, or CMake's glaze::asio target sets it).
+//                          Suppresses the standalone <asio.hpp> branch below so the
+//                          Boost branch is taken even when standalone Asio is also on
+//                          the include path. This is the knob that forces Boost.
+//   GLZ_USING_BOOST_ASIO - OUTPUT (this header defines it; never set it yourself).
+//                          Reports that the Boost branch was actually taken. All code
+//                          downstream of this block keys off GLZ_USING_BOOST_ASIO.
+//
+// In short: define GLZ_USE_BOOST_ASIO to choose Boost; read GLZ_USING_BOOST_ASIO to
+// learn what was chosen. cmake/glaze-asio.cmake sets the former on the glaze::asio
+// target so the linked backend and this header can never disagree (issue #2599).
 #if __has_include(<asio.hpp>) && !defined(GLZ_USE_BOOST_ASIO)
 #include <asio.hpp>
 #include <asio/signal_set.hpp>


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 52ac62049e..5d039b28b9 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -15,66 +15,16 @@ set(CMAKE_SKIP_INSTALL_RULES OFF CACHE BOOL "" FORCE)
 set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL FALSE)
 message(STATUS "...finished fetching dependencies.")
 
-# Setup ASIO dependency (used by networking tests)
-# This must be defined here before glz_test_common/glz_test_exceptions
-#
-# Search order:
-#   1. Boost.Asio (via find_package(Boost))
-#   2. Standalone Asio (via find_package(Asio) using our FindAsio.cmake)
-#   3. FetchContent (if glaze_USE_BUNDLED_ASIO is ON)
+# Setup the Asio backend (Boost / standalone / bundled) used by networking tests.
+# Selection plus the GLZ_USE_BOOST_ASIO bridge live in cmake/glaze-asio.cmake, the
+# single source of truth shared with the benchmarks, so glaze/ext/glaze_asio.hpp and
+# the backend CMake links can never disagree (issue #2599). Must run before
+# glz_test_common/glz_test_exceptions, which link glaze::asio.
 #
 # Distro maintainers can set -Dglaze_USE_BUNDLED_ASIO=OFF to require system packages.
-
-option(glaze_USE_BUNDLED_ASIO
-    "Download ASIO via FetchContent if not found on system (disable for distro builds)"
-    ON
-)
-
-# First, try Boost.Asio
-find_package(Boost QUIET CONFIG)
-
-if(Boost_FOUND)
-    message(STATUS "Using Boost.Asio")
-    add_library(glz_asio INTERFACE)
-    # Boost.Asio is header-only, just need Boost headers
-    # Boost::system was removed in Boost 1.89 (it was header-only since 1.69)
-    if(TARGET Boost::headers)
-        target_link_libraries(glz_asio INTERFACE Boost::headers)
-    elseif(TARGET Boost::boost)
-        target_link_libraries(glz_asio INTERFACE Boost::boost)
-    endif()
-else()
-    # Second, try standalone Asio
-    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
-    find_package(Asio QUIET)
-
-    if(Asio_FOUND)
-        message(STATUS "Using standalone Asio ${Asio_VERSION}")
-        add_library(glz_asio INTERFACE)
-        target_link_libraries(glz_asio INTERFACE Asio::Asio)
-    elseif(glaze_USE_BUNDLED_ASIO)
-        # Third, fetch via FetchContent
-        message(STATUS "Fetching standalone Asio via FetchContent")
-        FetchContent_Declare(
-            asio
-            GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
-            GIT_TAG asio-1-36-0
-            GIT_SHALLOW TRUE
-        )
-        FetchContent_MakeAvailable(asio)
-        add_library(glz_asio INTERFACE)
-        target_include_directories(glz_asio INTERFACE ${asio_SOURCE_DIR}/asio/include)
-    else()
-        message(FATAL_ERROR
-            "ASIO not found and bundling is disabled (glaze_USE_BUNDLED_ASIO=OFF).\n"
-            "Options:\n"
-            "  - Install Boost with the 'system' component\n"
-            "  - Install standalone Asio (headers in standard location)\n"
-            "  - Set Asio_INCLUDE_DIR to your Asio headers location\n"
-            "  - Set glaze_USE_BUNDLED_ASIO=ON to download via FetchContent"
-        )
-    endif()
-endif()
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
+include(glaze-asio)
+glaze_setup_asio()
 
 include(../cmake/code-coverage.cmake)
 add_code_coverage_all_targets()
@@ -83,7 +33,7 @@ option(glaze_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSan
 
 add_library(glz_test_common INTERFACE)
 target_compile_features(glz_test_common INTERFACE cxx_std_23)
-target_link_libraries(glz_test_common INTERFACE ut::ut glaze::glaze glz_asio)
+target_link_libraries(glz_test_common INTERFACE ut::ut glaze::glaze glaze::asio)
 
 if(MSVC)
     target_compile_options(glz_test_common INTERFACE /GR- /bigobj)
@@ -107,7 +57,7 @@ endif()
 
 add_library(glz_test_exceptions INTERFACE)
 target_compile_features(glz_test_exceptions INTERFACE cxx_std_23)
-target_link_libraries(glz_test_exceptions INTERFACE ut::ut glaze::glaze glz_asio)
+target_link_libraries(glz_test_exceptions INTERFACE ut::ut glaze::glaze glaze::asio)
 if(MSVC)
     target_compile_options(glz_test_exceptions INTERFACE /GR- /bigobj)
     target_compile_options(glz_test_exceptions INTERFACE /W4 /wd4459 /wd4805)
diff --git a/tests/networking_tests/CMakeLists.txt b/tests/networking_tests/CMakeLists.txt
index 58d6adad5b..0a762bb057 100644
--- a/tests/networking_tests/CMakeLists.txt
+++ b/tests/networking_tests/CMakeLists.txt
@@ -1,4 +1,5 @@
-# glz_asio is defined in the parent tests/CMakeLists.txt
+# The glaze::asio target is created in the parent tests/CMakeLists.txt via
+# glaze_setup_asio() (cmake/glaze-asio.cmake).
 
 option(glaze_BUILD_SSL_TESTS "Build SSL/TLS tests (requires OpenSSL headers matching target architecture)" ON)
 
diff --git a/tests/networking_tests/rest_test/CMakeLists.txt b/tests/networking_tests/rest_test/CMakeLists.txt
index 13669d15ba..65b6036c32 100644
--- a/tests/networking_tests/rest_test/CMakeLists.txt
+++ b/tests/networking_tests/rest_test/CMakeLists.txt
@@ -4,7 +4,7 @@ project(rest_test)
 
 add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp)
 
-target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions glz_asio)
+target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions glaze::asio)
 
 add_executable(rest_registry_test rest_registry_test.cpp)