diff --git a/.agent/plans/qdmi-installed-consumer-deployment-c3.md b/.agent/plans/qdmi-installed-consumer-deployment-c3.md new file mode 100644 index 0000000000..2671880fd6 --- /dev/null +++ b/.agent/plans/qdmi-installed-consumer-deployment-c3.md @@ -0,0 +1,40 @@ +# Installed QDMI runtime deployment + +Status: independently rebased and locally validated; hosted Windows CI pending. + +## Motivation and scope + +Installed CMake consumers need the same complete runtime layout as in-tree +applications. The existing mqt_copy_qdmi_runtime helper must stage imported +Client, driver, device libraries, manifests, provider assets and Windows DLLs. + +This is Core PR #2231 on #2230, targeting Core 4.1 / QDMI 1.4. It does not +depend on metadata removal, batching, or payload capabilities. No payload-format +header is introduced by the driver workstream. + +## Decisions + +Reuse the existing imported-device fixture as a real find_package consumer. Use +copy_if_different for local and imported runtime targets. Imported targets must +not become build dependencies. For Windows, retain the non-imported +linker-language-bearing closure used to compute transitive imported DLLs. +Preserve device metadata and asset copying, and use the build RPATH while +running staged build-tree applications. + +This changes deployment only. It does not add Client APIs, providers, compiler +behavior, or a second package-consumer harness. + +## Validation + +Run the release build, both imported-device fixture tests, and the full native +suite. The fixture must resolve installed Core targets, execute the consumer, +and compare staged libraries, manifest, assets and Windows dependency files. +Check that the helper disables BUILD_WITH_INSTALL_RPATH on its consumer. Run +repository lint; Windows hosted CI remains necessary for real DLL loading. + +The release build and native suite pass: 3,873 tests pass and one existing +optional-device test skips. Both installed-consumer fixture tests pass. + +Keep useful commits, human attribution and existing review threads. Do not +create archive branches or request reviews. Published artifacts require released +dependency pins. diff --git a/CHANGELOG.md b/CHANGELOG.md index 73921a5f86..f18f7260c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,9 +110,10 @@ releases may include breaking changes. open targeted sessions through the packaged driver's optional private configuration extension ([#2230]) ([**@burgholzer**]) - 💥 Load one replaceable QDMI 1.4 Client driver through a validated function - table, split `MQT::CoreQDMI` from the packaged `MQT::CoreQDMIDriver`, and use - stable Client device IDs across C++, Python, MLIR, Qiskit, PennyLane, and - Slurm ([#2229]) ([**@burgholzer**]) + table, split `MQT::CoreQDMI` from the packaged `MQT::CoreQDMIDriver`, and + stage their complete runtime dependencies for installed consumers. Use stable + Client device IDs across C++, Python, MLIR, Qiskit, PennyLane, and Slurm + ([#2229], [#2231]) ([**@burgholzer**]) - 💥 Drop support for x86 macOS and stop publishing the respective wheels ([#2259]) ([**@denialhaag**]) - ⬆️ Raise the macOS deployment target to 13.3 to enable `std::format` in libc++ @@ -914,6 +915,7 @@ for previous changelogs._ [#2257]: https://github.com/munich-quantum-toolkit/core/pull/2257 [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 [#2240]: https://github.com/munich-quantum-toolkit/core/pull/2240 +[#2231]: https://github.com/munich-quantum-toolkit/core/pull/2231 [#2230]: https://github.com/munich-quantum-toolkit/core/pull/2230 [#2229]: https://github.com/munich-quantum-toolkit/core/pull/2229 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 diff --git a/cmake/AddMQTQDMIDevice.cmake b/cmake/AddMQTQDMIDevice.cmake index 710a0f4fdd..fa79aa216d 100644 --- a/cmake/AddMQTQDMIDevice.cmake +++ b/cmake/AddMQTQDMIDevice.cmake @@ -147,7 +147,33 @@ function(mqt_get_qdmi_device_targets result) PARENT_SCOPE) endfunction() -# Copy in-tree QDMI runtime libraries and manifests beside a runtime consumer. +function(_mqt_qdmi_runtime_files result target) + set(runtime_files "$") + if(WIN32) + get_target_property(concrete_target ${target} ALIASED_TARGET) + if(NOT concrete_target) + set(concrete_target ${target}) + endif() + get_target_property(imported ${concrete_target} IMPORTED) + if(imported) + string(MAKE_C_IDENTIFIER "${concrete_target}-runtime-closure" runtime_closure_target) + if(NOT TARGET ${runtime_closure_target}) + add_library(${runtime_closure_target} MODULE EXCLUDE_FROM_ALL + "${CMAKE_CURRENT_FUNCTION_LIST_FILE}") + set_property(TARGET ${runtime_closure_target} PROPERTY LINKER_LANGUAGE CXX) + target_link_libraries(${runtime_closure_target} PRIVATE ${target}) + endif() + set(runtime_files "$") + else() + list(APPEND runtime_files "$") + endif() + endif() + set(${result} + ${runtime_files} + PARENT_SCOPE) +endfunction() + +# Copy the QDMI Client library, driver, device libraries, and manifests beside a runtime consumer. function(mqt_copy_qdmi_runtime target) if(NOT TARGET ${target}) message(FATAL_ERROR "Unknown QDMI runtime consumer target: ${target}") @@ -163,19 +189,22 @@ function(mqt_copy_qdmi_runtime target) if(NOT runtime_concrete_target) set(runtime_concrete_target ${runtime_target}) endif() - get_target_property(runtime_imported ${runtime_concrete_target} IMPORTED) - if(NOT runtime_imported AND NOT consumer_target STREQUAL runtime_concrete_target) - add_dependencies(${consumer_target} ${runtime_concrete_target}) - set(runtime_files "$") - if(WIN32) - list(APPEND runtime_files "$") + if(NOT consumer_target STREQUAL runtime_concrete_target) + string(MAKE_C_IDENTIFIER "${consumer_target}-${runtime_target}-copy" runtime_copy_target) + if(NOT TARGET ${runtime_copy_target}) + get_target_property(runtime_imported ${runtime_concrete_target} IMPORTED) + _mqt_qdmi_runtime_files(runtime_files ${runtime_target}) + add_custom_target( + ${runtime_copy_target} + COMMAND ${CMAKE_COMMAND} -E make_directory "$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${runtime_files} + "$" + COMMAND_EXPAND_LISTS) + if(NOT runtime_imported) + add_dependencies(${runtime_copy_target} ${runtime_concrete_target}) + endif() endif() - add_custom_command( - TARGET ${consumer_target} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${runtime_files} - "$" - COMMAND_EXPAND_LISTS) + add_dependencies(${consumer_target} ${runtime_copy_target}) endif() endif() endforeach() @@ -222,10 +251,7 @@ function(mqt_copy_qdmi_runtime target) if(NOT device_imported) add_dependencies(${target} ${device}) endif() - set(device_files "$") - if(WIN32 AND NOT device_imported) - list(APPEND device_files "$") - endif() + _mqt_qdmi_runtime_files(device_files ${device}) add_custom_command( TARGET ${target} POST_BUILD diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index a3d32c830c..06adbacbee 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -247,7 +247,12 @@ mqt_copy_qdmi_runtime(my-application MQT::CoreQDMIScDevice MQT::CoreQDMI_DDSIM_D Inside an MQT Core build, omitting the device list copies every device registered through `mqt_configure_qdmi_device`. Installed consumers select the -exported device targets they need, as shown above. +exported device targets they need, as shown above. The helper stages +`MQT::CoreQDMI` and `MQT::CoreQDMIDriver` when those targets are available, +whether they come from the current build or an installed CMake package. On +Windows, it also stages the transitive runtime DLLs of both Core libraries and +each selected device. During a build, the consumer uses its build RPATH rather +than an unrelated final install RPATH. An external device implementation does not need MQT Core as a build dependency. It can export its stable ID and prefix as target metadata: @@ -267,4 +272,5 @@ When `mqt_copy_qdmi_runtime` receives that built or imported target, it generates the relocatable manifest while copying the device. Device targets may also declare `RUNTIME_FILES` through `mqt_configure_qdmi_device`; their exported `QDMI_RUNTIME_FILES` basenames are copied beside the provider as part of the -same operation. +same operation. The result is one colocated directory with the Client, Driver, +provider, manifest, provider assets, and required Windows DLLs. diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 325e9c3d04..c5c300ce06 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -139,11 +139,13 @@ if(TARGET MQT::CoreQDMIDriver) add_dependencies(${TARGET_NAME} mqt-core-qdmi-session-device) set(imported_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/imported-device-consumer") + set(imported_device_install_dir "${imported_device_build_dir}/install") set(imported_device_configure_command ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/imported_device" -B "${imported_device_build_dir}" -G "${CMAKE_GENERATOR}" "-DMQT_CORE_QDMI_DEVICE_TARGETS=${metadata_device_export}" - "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake") + "-DMQT_CORE_BUILD_DIR=${CMAKE_BINARY_DIR}" "-DMQT_CORE_BUILD_CONFIG=$" + "-DMQT_CORE_INSTALL_DIR=${imported_device_install_dir}" "-DCMAKE_BUILD_TYPE=$") if(CMAKE_GENERATOR_PLATFORM) list(APPEND imported_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") endif() diff --git a/test/qdmi/driver/imported_device/CMakeLists.txt b/test/qdmi/driver/imported_device/CMakeLists.txt index bfe6c3990e..ec3aa41af6 100644 --- a/test/qdmi/driver/imported_device/CMakeLists.txt +++ b/test/qdmi/driver/imported_device/CMakeLists.txt @@ -9,14 +9,76 @@ cmake_minimum_required(VERSION 3.24) project(mqt-core-qdmi-imported-device-test LANGUAGES CXX) -if(NOT MQT_CORE_QDMI_DEVICE_TARGETS OR NOT MQT_CORE_QDMI_HELPER) - message(FATAL_ERROR "Device targets and the QDMI helper are required") +if(NOT MQT_CORE_BUILD_CONFIG + OR NOT MQT_CORE_BUILD_DIR + OR NOT MQT_CORE_INSTALL_DIR + OR NOT MQT_CORE_QDMI_DEVICE_TARGETS) + message(FATAL_ERROR "The MQT Core build, install prefix, and device target export are required") endif() +file(REMOVE_RECURSE "${MQT_CORE_INSTALL_DIR}") +foreach(component IN ITEMS qdmi_Runtime qdmi_Development mqt-core_Runtime mqt-core_Development) + execute_process( + COMMAND ${CMAKE_COMMAND} --install "${MQT_CORE_BUILD_DIR}" --config "${MQT_CORE_BUILD_CONFIG}" + --prefix "${MQT_CORE_INSTALL_DIR}" --component "${component}" + RESULT_VARIABLE install_result) + if(install_result) + message(FATAL_ERROR "Failed to stage installed component ${component}") + endif() +endforeach() + +list(PREPEND CMAKE_PREFIX_PATH "${MQT_CORE_INSTALL_DIR}") +find_package(mqt-core CONFIG REQUIRED PATHS "${MQT_CORE_INSTALL_DIR}" NO_DEFAULT_PATH) include("${MQT_CORE_QDMI_DEVICE_TARGETS}") -include("${MQT_CORE_QDMI_HELPER}") + +get_target_property(client_imported MQT::CoreQDMI IMPORTED) +get_target_property(driver_imported MQT::CoreQDMIDriver IMPORTED) +if(NOT client_imported OR NOT driver_imported) + message(FATAL_ERROR "The installed QDMI Client and Driver targets must be imported") +endif() set(device mqt-core-qdmi-metadata-device) +if(WIN32) + set(client_runtime_dependency "${CMAKE_CURRENT_BINARY_DIR}/client-runtime-dependency.dll") + set(client_runtime_import_library "${CMAKE_CURRENT_BINARY_DIR}/client-runtime-dependency.lib") + file(WRITE "${client_runtime_dependency}" "client runtime dependency\n") + file(WRITE "${client_runtime_import_library}" "") + add_library(qdmi-client-runtime-dependency SHARED IMPORTED) + set_target_properties( + qdmi-client-runtime-dependency PROPERTIES IMPORTED_IMPLIB "${client_runtime_import_library}" + IMPORTED_LOCATION "${client_runtime_dependency}") + set_property( + TARGET MQT::CoreQDMI + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES qdmi-client-runtime-dependency) + + set(driver_runtime_dependency "${CMAKE_CURRENT_BINARY_DIR}/driver-runtime-dependency.dll") + set(driver_runtime_import_library "${CMAKE_CURRENT_BINARY_DIR}/driver-runtime-dependency.lib") + file(WRITE "${driver_runtime_dependency}" "driver runtime dependency\n") + file(WRITE "${driver_runtime_import_library}" "") + add_library(qdmi-driver-runtime-dependency SHARED IMPORTED) + set_target_properties( + qdmi-driver-runtime-dependency PROPERTIES IMPORTED_IMPLIB "${driver_runtime_import_library}" + IMPORTED_LOCATION "${driver_runtime_dependency}") + set_property( + TARGET MQT::CoreQDMIDriver + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES qdmi-driver-runtime-dependency) + + set(device_runtime_dependency "${CMAKE_CURRENT_BINARY_DIR}/device-runtime-dependency.dll") + set(device_runtime_import_library "${CMAKE_CURRENT_BINARY_DIR}/device-runtime-dependency.lib") + file(WRITE "${device_runtime_dependency}" "device runtime dependency\n") + file(WRITE "${device_runtime_import_library}" "") + add_library(qdmi-device-runtime-dependency SHARED IMPORTED) + set_target_properties( + qdmi-device-runtime-dependency PROPERTIES IMPORTED_IMPLIB "${device_runtime_import_library}" + IMPORTED_LOCATION "${device_runtime_dependency}") + set_property( + TARGET ${device} + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES qdmi-device-runtime-dependency) +endif() + get_target_property(device_id ${device} QDMI_DEVICE_ID) get_target_property(device_prefix ${device} QDMI_DEVICE_PREFIX) get_target_property(runtime_files ${device} QDMI_RUNTIME_FILES) @@ -26,13 +88,95 @@ if(NOT device_id STREQUAL "test.metadata-only" message(FATAL_ERROR "Exported QDMI device metadata was not preserved") endif() -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/main.cpp" "int main() { return 0; }\n") +file( + WRITE "${CMAKE_CURRENT_BINARY_DIR}/main.cpp" + [=[#ifdef _WIN32 +#include + +int main(int argc, char** argv) { + if (argc != 3) { + return 1; + } + const auto client = LoadLibraryA(argv[1]); + const auto driver = LoadLibraryA(argv[2]); + if (driver != nullptr) { + FreeLibrary(driver); + } + if (client != nullptr) { + FreeLibrary(client); + } + return client != nullptr && driver != nullptr ? 0 : 1; +} +#else +#include "qdmi/Client.hpp" + +int main() { + const qdmi::Session session; + return 0; +} +#endif +]=]) add_executable(imported-device-consumer "${CMAKE_CURRENT_BINARY_DIR}/main.cpp") +if(NOT WIN32) + target_link_libraries(imported-device-consumer PRIVATE MQT::CoreQDMI) +endif() +set_property(TARGET imported-device-consumer PROPERTY BUILD_WITH_INSTALL_RPATH TRUE) mqt_copy_qdmi_runtime(imported-device-consumer ${device}) +get_target_property(build_with_install_rpath imported-device-consumer BUILD_WITH_INSTALL_RPATH) +if(build_with_install_rpath) + message(FATAL_ERROR "QDMI runtime staging must use the build RPATH") +endif() + +string(MAKE_C_IDENTIFIER "imported-device-consumer-${device}" manifest_stem) +set(manifest_name "${manifest_stem}.qdmi.json") add_custom_command( TARGET imported-device-consumer POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/$" + "$" + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/$" + "$" + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/$" + "$" COMMAND ${CMAKE_COMMAND} -E compare_files "$/metadata-runtime.json" - "$/metadata-runtime.json") + "$/metadata-runtime.json" + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/${manifest_name}" + "${CMAKE_CURRENT_BINARY_DIR}/$/${manifest_name}") + +if(WIN32) + add_custom_command( + TARGET imported-device-consumer + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/$" + "$" + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/$" + "$" + COMMAND + ${CMAKE_COMMAND} -E compare_files + "$/$" + "$") +endif() + +add_custom_command( + TARGET imported-device-consumer + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E env + "MQT_CORE_QDMI_DRIVER=$/$" + "$" + "$/$" + "$/$")