From a05987359398f4c1e0e82e433ea89174b9dee143 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 16 Sep 2026 16:19:34 -0500 Subject: [PATCH 1/4] build(cython): link each extension module against the component it uses Every extension module set linked_libraries to cuopt::cuopt, so each one pulled both engines regardless of what it called. That makes the optional dependency groups in #1635 meaningless, since installing the routing group still drags in the LP solver. Each module now names the component it resolves symbols from: distance_engine routing grpc/client client lp/data_model mathopt + client lp/io client lp/solver mathopt + client lp/solver_settings mathopt + client routing routing + client lp/internals resolves no cuOpt symbols at all -- it is callback plumbing -- but still needs the public headers, so it names the client and --as-needed drops the dependency entirely. Uses the exported target names. Note cuopt_client is exported as cuopt::client while its in-tree alias is cuopt::cuopt_client, unlike routing and mathopt which match; find_package consumers must use the former. Co-Authored-By: Claude Opus 5 --- python/cuopt/cuopt/CMakeLists.txt | 3 +++ python/cuopt/cuopt/distance_engine/CMakeLists.txt | 4 ++-- python/cuopt/cuopt/grpc/client/CMakeLists.txt | 2 +- .../cuopt/linear_programming/data_model/CMakeLists.txt | 2 +- .../cuopt/linear_programming/internals/CMakeLists.txt | 7 +++++-- python/cuopt/cuopt/linear_programming/io/CMakeLists.txt | 4 ++-- .../cuopt/cuopt/linear_programming/solver/CMakeLists.txt | 2 +- .../linear_programming/solver_settings/CMakeLists.txt | 2 +- python/cuopt/cuopt/routing/CMakeLists.txt | 4 ++-- 9 files changed, 18 insertions(+), 12 deletions(-) diff --git a/python/cuopt/cuopt/CMakeLists.txt b/python/cuopt/cuopt/CMakeLists.txt index 0c12cfb04d..2c416735ef 100644 --- a/python/cuopt/cuopt/CMakeLists.txt +++ b/python/cuopt/cuopt/CMakeLists.txt @@ -12,6 +12,9 @@ add_subdirectory(grpc) add_subdirectory(routing) +# No sources here -- every extension module is created in a subdirectory, and each links +# the component it actually resolves symbols from. Anything added here would link the +# cuopt umbrella and pull in both engines, so add it to the owning subdirectory instead. set(cython_sources) set(linked_libraries cuopt::cuopt) diff --git a/python/cuopt/cuopt/distance_engine/CMakeLists.txt b/python/cuopt/cuopt/distance_engine/CMakeLists.txt index 4f3638f984..cec9db4945 100644 --- a/python/cuopt/cuopt/distance_engine/CMakeLists.txt +++ b/python/cuopt/cuopt/distance_engine/CMakeLists.txt @@ -1,10 +1,10 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on set(cython_sources waypoint_matrix_wrapper.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::routing) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) diff --git a/python/cuopt/cuopt/grpc/client/CMakeLists.txt b/python/cuopt/cuopt/grpc/client/CMakeLists.txt index 907b1cafee..883cc2b70b 100644 --- a/python/cuopt/cuopt/grpc/client/CMakeLists.txt +++ b/python/cuopt/cuopt/grpc/client/CMakeLists.txt @@ -7,7 +7,7 @@ # unit: they wrap the same C++ client object, and keeping them together lets the # client be detached from the engines as a single package later. set(cython_sources grpc_client.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::client) rapids_cython_create_modules( CXX diff --git a/python/cuopt/cuopt/linear_programming/data_model/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/data_model/CMakeLists.txt index a298752071..4598914852 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/data_model/CMakeLists.txt @@ -4,7 +4,7 @@ # cmake-format: on set(cython_sources data_model_wrapper.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::mathopt cuopt::client) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) diff --git a/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt index 08df120728..d818df4d21 100644 --- a/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt @@ -1,10 +1,13 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on set(cython_sources internals.pyx) -set(linked_libraries cuopt::cuopt) +# internals resolves no cuOpt symbols at all -- it is callback plumbing -- but still +# needs the public headers, which arrive through the target interface. The client is the +# leaf, so this gives headers without pulling in an engine. +set(linked_libraries cuopt::client) rapids_cython_create_modules( CXX SOURCE_FILES "${cython_sources}" diff --git a/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt index 43bd75ef68..a2c5f35fe4 100644 --- a/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt @@ -1,10 +1,10 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on set(cython_sources parser_wrapper.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::client) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) diff --git a/python/cuopt/cuopt/linear_programming/solver/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/solver/CMakeLists.txt index ab8c9c893a..18477c8ce7 100644 --- a/python/cuopt/cuopt/linear_programming/solver/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/solver/CMakeLists.txt @@ -4,7 +4,7 @@ # cmake-format: on set(cython_sources solver_wrapper.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::mathopt cuopt::client) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/solver_settings/CMakeLists.txt index 43c630341b..a746b25160 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/solver_settings/CMakeLists.txt @@ -4,7 +4,7 @@ # cmake-format: on set(cython_sources solver_settings.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::mathopt cuopt::client) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) diff --git a/python/cuopt/cuopt/routing/CMakeLists.txt b/python/cuopt/cuopt/routing/CMakeLists.txt index 48bf056626..3ce74b0120 100644 --- a/python/cuopt/cuopt/routing/CMakeLists.txt +++ b/python/cuopt/cuopt/routing/CMakeLists.txt @@ -1,10 +1,10 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on set(cython_sources vehicle_routing_wrapper.pyx utils_wrapper.pyx) -set(linked_libraries cuopt::cuopt) +set(linked_libraries cuopt::routing cuopt::client) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) From 46d644b448ed62f2260bd789c3987146ab73f307 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 17 Sep 2026 07:54:58 -0500 Subject: [PATCH 2/4] build(cython): skip the routing extensions when routing is not built distance_engine and routing now link cuopt::routing, which only exists inside if(NOT SKIP_ROUTING_BUILD). Linking the umbrella hid this before, since that target is defined either way, so a routing-less install would now fail at configure. Guarded on TARGET cuopt::routing rather than on SKIP_ROUTING_BUILD: this build only calls find_package(cuopt) and never sees that option, so testing the variable would always pass and leave the failure in place. Co-Authored-By: Claude Opus 5 --- python/cuopt/cuopt/CMakeLists.txt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/cuopt/cuopt/CMakeLists.txt b/python/cuopt/cuopt/CMakeLists.txt index 2c416735ef..f63f7a50aa 100644 --- a/python/cuopt/cuopt/CMakeLists.txt +++ b/python/cuopt/cuopt/CMakeLists.txt @@ -3,14 +3,22 @@ # SPDX-License-Identifier: Apache-2.0 # cmake-format: on -add_subdirectory(distance_engine) add_subdirectory(linear_programming/data_model) add_subdirectory(linear_programming/io) add_subdirectory(linear_programming/solver) add_subdirectory(linear_programming/solver_settings) add_subdirectory(grpc) -add_subdirectory(routing) +# These two link cuopt::routing, which a SKIP_ROUTING_BUILD install does not provide. +# Tested on the found package rather than on SKIP_ROUTING_BUILD: this build only does +# find_package(cuopt) and never sees that option, so the variable would be empty here +# and the guard would always pass. +if(TARGET cuopt::routing) + add_subdirectory(distance_engine) + add_subdirectory(routing) +else() + message(STATUS "cuopt::routing not found -- skipping the routing and distance_engine extension modules") +endif() # No sources here -- every extension module is created in a subdirectory, and each links # the component it actually resolves symbols from. Anything added here would link the From a92dea7bea1f0d85a795ee8a10fd126d62fd849e Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 17 Sep 2026 08:08:31 -0500 Subject: [PATCH 3/4] build(cython): skip the gRPC extension when cuOpt is built without gRPC The gRPC extension wraps cython_grpc_client, which is appended to the client sources only inside if(NOT SKIP_GRPC_BUILD). cuopt::client itself is defined in every configuration, so its presence tells a consumer nothing and the module would fail to link against a client built without that arm. rapids_export now emits CUOPT_HAS_GRPC into both the install and build configs, and the Python bindings skip the extension when it is off. Unlike the routing guard this is not a regression from this PR -- the subdirectory was unguarded on main too, where the umbrella target hid the same problem. Verified both paths by flipping the flag in the installed config: with it on the module builds, with it off the wheel builds cleanly and the extension is absent. Co-Authored-By: Claude Opus 5 --- cpp/CMakeLists.txt | 12 ++++++++++++ python/cuopt/cuopt/CMakeLists.txt | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index bdcc18b754..be76b4ed9d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1253,11 +1253,22 @@ It pulls both solver engines whether or not you use them, so prefer a component ]=]) +# cuopt::client exists in every configuration, but only carries the gRPC client when +# SKIP_GRPC_BUILD was off, so its presence alone tells a consumer nothing. Export the +# answer: the Python bindings use it to skip the gRPC extension module, which would +# otherwise fail to link against a client built without that arm. +if(SKIP_GRPC_BUILD) + set(cuopt_code_string "set(CUOPT_HAS_GRPC OFF)") +else() + set(cuopt_code_string "set(CUOPT_HAS_GRPC ON)") +endif() + rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string + FINAL_CODE_BLOCK cuopt_code_string ) # ############################################################################################### @@ -1267,6 +1278,7 @@ rapids_export(BUILD cuopt GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string + FINAL_CODE_BLOCK cuopt_code_string ) # ################################################################################################## diff --git a/python/cuopt/cuopt/CMakeLists.txt b/python/cuopt/cuopt/CMakeLists.txt index f63f7a50aa..5b3bf3ff15 100644 --- a/python/cuopt/cuopt/CMakeLists.txt +++ b/python/cuopt/cuopt/CMakeLists.txt @@ -7,7 +7,13 @@ add_subdirectory(linear_programming/data_model) add_subdirectory(linear_programming/io) add_subdirectory(linear_programming/solver) add_subdirectory(linear_programming/solver_settings) -add_subdirectory(grpc) +# The gRPC extension wraps cython_grpc_client, which is only compiled into cuopt::client +# when SKIP_GRPC_BUILD is off. The target exists either way, so ask the exported flag. +if(CUOPT_HAS_GRPC) + add_subdirectory(grpc) +else() + message(STATUS "cuOpt built without gRPC -- skipping the gRPC client extension module") +endif() # These two link cuopt::routing, which a SKIP_ROUTING_BUILD install does not provide. # Tested on the found package rather than on SKIP_ROUTING_BUILD: this build only does From a3abcbae962b4e16d4d11166559a3f7bfc5d2873 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 17 Sep 2026 08:23:45 -0500 Subject: [PATCH 4/4] build(cython): require the VRP arm for the gRPC extension too CUOPT_HAS_GRPC alone was not enough. One extension module covers both arms, and it calls submit_vrp/result_vrp, which live in cython_grpc_client_vrp.cpp. That file is appended to the client only under CUOPT_ENABLE_GRPC_ROUTING, so a SKIP_ROUTING_BUILD build with gRPC on left the module with unresolved VRP symbols. rapids_export now reports CUOPT_HAS_GRPC_ROUTING alongside CUOPT_HAS_GRPC, and the extension requires both. Reported as its own flag rather than inferred from TARGET cuopt::routing: CUOPT_ENABLE_GRPC_ROUTING is what gates those sources, and the two would diverge the moment the VRP arm gains an option of its own. Verified by flipping the flag in the installed config: with the VRP arm off the wheel builds cleanly and the extension is absent, with it on the module is built. Co-Authored-By: Claude Opus 5 --- cpp/CMakeLists.txt | 9 +++++++-- python/cuopt/cuopt/CMakeLists.txt | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index be76b4ed9d..2930d32a3d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1257,10 +1257,15 @@ It pulls both solver engines whether or not you use them, so prefer a component # SKIP_GRPC_BUILD was off, so its presence alone tells a consumer nothing. Export the # answer: the Python bindings use it to skip the gRPC extension module, which would # otherwise fail to link against a client built without that arm. +# CUOPT_HAS_GRPC_ROUTING is reported separately rather than inferred from cuopt::routing: +# it is CUOPT_ENABLE_GRPC_ROUTING that gates cython_grpc_client_vrp.cpp, and the two would +# diverge the moment the VRP arm gains an option of its own. if(SKIP_GRPC_BUILD) - set(cuopt_code_string "set(CUOPT_HAS_GRPC OFF)") + set(cuopt_code_string "set(CUOPT_HAS_GRPC OFF)\nset(CUOPT_HAS_GRPC_ROUTING OFF)") +elseif(CUOPT_ENABLE_GRPC_ROUTING) + set(cuopt_code_string "set(CUOPT_HAS_GRPC ON)\nset(CUOPT_HAS_GRPC_ROUTING ON)") else() - set(cuopt_code_string "set(CUOPT_HAS_GRPC ON)") + set(cuopt_code_string "set(CUOPT_HAS_GRPC ON)\nset(CUOPT_HAS_GRPC_ROUTING OFF)") endif() rapids_export(INSTALL cuopt diff --git a/python/cuopt/cuopt/CMakeLists.txt b/python/cuopt/cuopt/CMakeLists.txt index 5b3bf3ff15..7244102fc7 100644 --- a/python/cuopt/cuopt/CMakeLists.txt +++ b/python/cuopt/cuopt/CMakeLists.txt @@ -8,11 +8,13 @@ add_subdirectory(linear_programming/io) add_subdirectory(linear_programming/solver) add_subdirectory(linear_programming/solver_settings) # The gRPC extension wraps cython_grpc_client, which is only compiled into cuopt::client -# when SKIP_GRPC_BUILD is off. The target exists either way, so ask the exported flag. -if(CUOPT_HAS_GRPC) +# when SKIP_GRPC_BUILD is off. The target exists either way, so ask the exported flags. +# It needs the VRP arm too: one module covers both, and it calls submit_vrp/result_vrp, +# which live in cython_grpc_client_vrp.cpp and are dropped with the routing engine. +if(CUOPT_HAS_GRPC AND CUOPT_HAS_GRPC_ROUTING) add_subdirectory(grpc) else() - message(STATUS "cuOpt built without gRPC -- skipping the gRPC client extension module") + message(STATUS "cuOpt built without gRPC or without its VRP arm -- skipping the gRPC client extension module") endif() # These two link cuopt::routing, which a SKIP_ROUTING_BUILD install does not provide.