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..14b892b2e2 --- /dev/null +++ b/.agent/plans/qdmi-installed-consumer-deployment-c3.md @@ -0,0 +1,37 @@ +# Installed QDMI runtime deployment + +Status: rebased on refreshed #2230; validation in progress. + +## 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, using QDMI #511 and current main APIs. + +## 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. + +Validation is being repeated against current main. + +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/cmake/AddMQTQDMIDevice.cmake b/cmake/AddMQTQDMIDevice.cmake index afc3d0466d..a9f83eeac0 100644 --- a/cmake/AddMQTQDMIDevice.cmake +++ b/cmake/AddMQTQDMIDevice.cmake @@ -147,105 +147,117 @@ 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(library_target ${target} ALIASED_TARGET) + if(NOT library_target) + set(library_target ${target}) + endif() + get_target_property(imported ${library_target} IMPORTED) + if(imported) + # CMake cannot evaluate TARGET_RUNTIME_DLLS on an imported target. An unbuilt local target + # lets it traverse the imported library's declared dependencies. + string(MAKE_C_IDENTIFIER "${library_target}-dependencies" dependency_target) + if(NOT TARGET ${dependency_target}) + add_library(${dependency_target} MODULE EXCLUDE_FROM_ALL + "${CMAKE_CURRENT_FUNCTION_LIST_FILE}") + set_property(TARGET ${dependency_target} PROPERTY LINKER_LANGUAGE CXX) + target_link_libraries(${dependency_target} PRIVATE ${target}) + endif() + set(runtime_files "$") + else() + list(APPEND runtime_files "$") + endif() + endif() + set(${result} + ${runtime_files} + PARENT_SCOPE) +endfunction() + +# Copy the shared QDMI libraries, device manifests, and assets beside an application. function(mqt_copy_qdmi_runtime target) if(NOT TARGET ${target}) - message(FATAL_ERROR "Unknown QDMI runtime consumer target: ${target}") + message(FATAL_ERROR "Unknown QDMI application target: ${target}") endif() set_property(TARGET ${target} PROPERTY BUILD_WITH_INSTALL_RPATH FALSE) - get_target_property(consumer_target ${target} ALIASED_TARGET) - if(NOT consumer_target) - set(consumer_target ${target}) - endif() - foreach(runtime_target IN ITEMS MQT::CoreQDMI MQT::CoreQDMIDriver) - if(TARGET ${runtime_target}) - get_target_property(runtime_concrete_target ${runtime_target} ALIASED_TARGET) - if(NOT runtime_concrete_target) - set(runtime_concrete_target ${runtime_target}) - endif() - get_target_property(runtime_imported ${runtime_concrete_target} IMPORTED) - get_target_property(library_type ${runtime_concrete_target} TYPE) - if(NOT runtime_imported - AND library_type STREQUAL "SHARED_LIBRARY" - AND NOT consumer_target STREQUAL runtime_concrete_target) - add_dependencies(${consumer_target} ${runtime_concrete_target}) - set(runtime_files "$") - if(WIN32) - list(APPEND runtime_files "$") - endif() - add_custom_command( - TARGET ${consumer_target} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${runtime_files} - "$" - COMMAND_EXPAND_LISTS) - endif() - endif() - endforeach() set(devices ${ARGN}) if(NOT devices) mqt_get_qdmi_device_targets(devices) endif() - if(NOT devices AND NOT TARGET MQT::CoreQDMIDriver) - message(FATAL_ERROR "mqt_copy_qdmi_runtime requires at least one QDMI device target") + set(libraries ${devices}) + foreach(library IN ITEMS MQT::CoreQDMI MQT::CoreQDMIDriver) + if(TARGET ${library}) + list(APPEND libraries ${library}) + endif() + endforeach() + if(NOT libraries) + message(FATAL_ERROR "mqt_copy_qdmi_runtime requires a QDMI driver or device target") endif() - foreach(device IN LISTS devices) - if(NOT TARGET ${device}) - message(FATAL_ERROR "Unknown QDMI device target: ${device}") + list(REMOVE_DUPLICATES libraries) + foreach(library IN LISTS libraries) + if(NOT TARGET ${library}) + message(FATAL_ERROR "Unknown QDMI library target: ${library}") endif() - get_target_property(device_target ${device} ALIASED_TARGET) - if(NOT device_target) - set(device_target ${device}) + get_target_property(library_target ${library} ALIASED_TARGET) + if(NOT library_target) + set(library_target ${library}) endif() - get_target_property(manifest_name ${device_target} QDMI_MANIFEST_NAME) - if(NOT manifest_name) - get_target_property(device_id ${device_target} QDMI_DEVICE_ID) - get_target_property(device_prefix ${device_target} QDMI_DEVICE_PREFIX) - if(NOT device_id OR NOT device_prefix) - message( - FATAL_ERROR - "QDMI device target '${device}' must define either QDMI_MANIFEST_NAME or both QDMI_DEVICE_ID and QDMI_DEVICE_PREFIX" + get_target_property(library_type ${library_target} TYPE) + if(library_target STREQUAL "${target}" OR NOT library_type MATCHES "^(SHARED|MODULE)_LIBRARY$") + continue() + endif() + _mqt_qdmi_runtime_files(files ${library}) + if(library IN_LIST devices) + get_target_property(manifest_name ${library_target} QDMI_MANIFEST_NAME) + if(NOT manifest_name) + get_target_property(device_id ${library_target} QDMI_DEVICE_ID) + get_target_property(device_prefix ${library_target} QDMI_DEVICE_PREFIX) + if(NOT device_id OR NOT device_prefix) + message( + FATAL_ERROR + "QDMI device target '${library}' must define either QDMI_MANIFEST_NAME or both QDMI_DEVICE_ID and QDMI_DEVICE_PREFIX" + ) + endif() + _mqt_qdmi_json_escape(device_id "${device_id}") + _mqt_qdmi_json_escape(device_prefix "${device_prefix}") + string(MAKE_C_IDENTIFIER "${target}-${library}" manifest_stem) + set(manifest_name "${manifest_stem}.qdmi.json") + set(manifest "${CMAKE_CURRENT_BINARY_DIR}/$/${manifest_name}") + file( + GENERATE + OUTPUT "${manifest}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${device_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n" ) + else() + set(manifest "$/${manifest_name}") + endif() + list(APPEND files "${manifest}") + get_target_property(assets ${library_target} QDMI_RUNTIME_FILES) + if(assets) + foreach(asset IN LISTS assets) + list(APPEND files "$/${asset}") + endforeach() endif() - _mqt_qdmi_json_escape(device_id "${device_id}") - _mqt_qdmi_json_escape(device_prefix "${device_prefix}") - string(MAKE_C_IDENTIFIER "${target}-${device}" manifest_stem) - set(manifest_name "${manifest_stem}.qdmi.json") - set(manifest "${CMAKE_CURRENT_BINARY_DIR}/$/${manifest_name}") - file( - GENERATE - OUTPUT "${manifest}" - CONTENT - "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${device_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n" - ) - else() - set(manifest "$/${manifest_name}") - endif() - get_target_property(device_imported ${device_target} IMPORTED) - if(NOT device_imported) - add_dependencies(${target} ${device}) endif() - set(device_files "$") - if(WIN32 AND NOT device_imported) - list(APPEND device_files "$") + get_target_property(imported ${library_target} IMPORTED) + if(NOT imported) + add_dependencies(${target} ${library_target}) endif() add_custom_command( TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${device_files} "$" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${manifest}" - "$/${manifest_name}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${files} "$" COMMAND_EXPAND_LISTS) - get_target_property(runtime_files ${device_target} QDMI_RUNTIME_FILES) - if(runtime_files) - foreach(runtime_file IN LISTS runtime_files) - add_custom_command( - TARGET ${target} - POST_BUILD - COMMAND - ${CMAKE_COMMAND} -E copy_if_different "$/${runtime_file}" - "$/${runtime_file}") - endforeach() + if(NOT WIN32 AND imported) + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND + ${CMAKE_COMMAND} "-DLIBRARY=$" + "-DDESTINATION=$" -P + "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CopyQDMISharedDependencies.cmake") endif() endforeach() endfunction() diff --git a/cmake/CopyQDMISharedDependencies.cmake b/cmake/CopyQDMISharedDependencies.cmake new file mode 100644 index 0000000000..05cf21fe1d --- /dev/null +++ b/cmake/CopyQDMISharedDependencies.cmake @@ -0,0 +1,21 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +# Copy dependencies shipped beside a shared library, leaving system libraries in place. +file(REAL_PATH "${LIBRARY}" library) +cmake_path(GET library PARENT_PATH library_directory) +file(GET_RUNTIME_DEPENDENCIES LIBRARIES "${library}" RESOLVED_DEPENDENCIES_VAR dependencies) +foreach(dependency IN LISTS dependencies) + cmake_path(GET dependency PARENT_PATH directory) + if(directory STREQUAL library_directory) + file( + COPY "${dependency}" + DESTINATION "${DESTINATION}" + FOLLOW_SYMLINK_CHAIN) + endif() +endforeach() diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index b054cc3ebd..81303a16e2 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -214,24 +214,11 @@ A cluster can configure more than one license for a device. For example, The count is a Slurm admission limit. It is not an access permission, a provider availability check, or a provider queue length. -## Relocatable packages and static consumers +## Installed C++ applications -Built-in targets generate manifests beside their runtime libraries in both build -and install trees. Library paths in those fragments contain only the target -filename, so moving an installed tree or Python wheel preserves discovery. -Automatic discovery searches relative to the MQT Core Driver, not every library -loaded by the process. A separately installed Python distribution can use the -`mqt.core.qdmi.manifests` entry point described above. Other applications copy -the manifest beside the Driver or register the definition by stable ID. - -A fully static executable has no portable shared-module location. Place the -fragments beside the executable, point `MQT_CORE_QDMI_CONFIG_FILE` at a complete -configuration, or use {cpp-api:func}`qdmi::Driver::registerDevice` and -{cpp-api:func}`qdmi::Driver::open`. No install prefix is compiled into the -manifests. - -An installed MQT Core CMake package provides a helper that colocates selected -device libraries and manifests with an executable: +The MQT Core Python distribution also supplies a CMake package. Use +`find_package(mqt-core)` to link its C++ QDMI library and copy the driver and +selected devices beside your application: ```cmake find_package(mqt-core CONFIG REQUIRED) @@ -240,12 +227,23 @@ target_link_libraries(my-application PRIVATE MQT::CoreQDMI) mqt_copy_qdmi_runtime(my-application MQT::CoreQDMIScDevice MQT::CoreQDMI_DDSIM_Device) ``` -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. +The helper copies shared libraries, device manifests, and configuration files. +It also copies DLL dependencies on Windows and dependencies shipped beside +installed libraries on Linux and macOS. Static libraries are linked into the +application and need no copy. The application uses its build RPATH during the +build. This also works with a source installation of MQT Core. + +Manifests contain library filenames relative to their own directory. Keep each +manifest beside its device library when moving an installation. The MQT Core +QDMI driver discovers manifests beside itself; an explicit +`MQT_CORE_QDMI_CONFIG_FILE` can instead select devices installed elsewhere. + +Inside a Core build, omitting the device list copies all devices registered +through `mqt_configure_qdmi_device`. An installed consumer selects the exported +targets it needs, as above. -An external device implementation does not need MQT Core as a build dependency. -It can export its stable ID and prefix as target metadata: +An external device implementation needs no Core build dependency. It can export +its stable ID and prefix as target metadata: ```cmake set_target_properties( @@ -258,8 +256,6 @@ set_property( PROPERTY EXPORT_PROPERTIES QDMI_DEVICE_ID QDMI_DEVICE_PREFIX) ``` -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. +For such a target, `mqt_copy_qdmi_runtime` generates the manifest. Targets with +an existing manifest can export `QDMI_MANIFEST_NAME` instead. Additional files +listed in `QDMI_RUNTIME_FILES` are copied from the device library's directory. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e7184b5a07..3484ae727a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -101,6 +101,7 @@ if(MQT_CORE_INSTALL) install( FILES ${PROJECT_SOURCE_DIR}/cmake/AddMQTPythonBinding.cmake ${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake + ${PROJECT_SOURCE_DIR}/cmake/CopyQDMISharedDependencies.cmake ${PROJECT_SOURCE_DIR}/cmake/Cache.cmake ${PROJECT_SOURCE_DIR}/cmake/PackageAddTest.cmake ${PROJECT_SOURCE_DIR}/cmake/PreventInSourceBuilds.cmake diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 664a7efaf2..7d8d1c5ca4 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -27,10 +27,6 @@ if(TARGET MQT::CoreQDMIDriver) TARGET mqt-core-qdmi-metadata-device APPEND PROPERTY EXPORT_PROPERTIES QDMI_DEVICE_ID QDMI_DEVICE_PREFIX QDMI_RUNTIME_FILES) - set(metadata_device_export - "${CMAKE_CURRENT_BINARY_DIR}/mqt-core-qdmi-metadata-device-targets.cmake") - export(TARGETS mqt-core-qdmi-metadata-device FILE "${metadata_device_export}") - add_library(mqt-core-qdmi-session-device SHARED session_device.cpp) target_link_libraries(mqt-core-qdmi-session-device PRIVATE qdmi::qdmi) set_target_properties( @@ -143,27 +139,6 @@ if(TARGET MQT::CoreQDMIDriver) COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$") - set(imported_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/imported-device-consumer") - 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") - if(CMAKE_GENERATOR_PLATFORM) - list(APPEND imported_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") - endif() - if(CMAKE_GENERATOR_TOOLSET) - list(APPEND imported_device_configure_command -T "${CMAKE_GENERATOR_TOOLSET}") - endif() - add_test(NAME mqt-core-qdmi-imported-device-configure - COMMAND ${imported_device_configure_command}) - add_test(NAME mqt-core-qdmi-imported-device-build - COMMAND ${CMAKE_COMMAND} --build "${imported_device_build_dir}" --config $) - set_tests_properties(mqt-core-qdmi-imported-device-configure - PROPERTIES FIXTURES_SETUP mqt-core-qdmi-imported-device) - set_tests_properties(mqt-core-qdmi-imported-device-build PROPERTIES FIXTURES_REQUIRED - mqt-core-qdmi-imported-device) - set(runtime_file_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/runtime-file-device") set(runtime_file_device_install_dir "${runtime_file_device_build_dir}/install") if(WIN32) diff --git a/test/qdmi/driver/imported_device/CMakeLists.txt b/test/qdmi/driver/imported_device/CMakeLists.txt deleted file mode 100644 index 3bb0d82b4c..0000000000 --- a/test/qdmi/driver/imported_device/CMakeLists.txt +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -cmake_minimum_required(VERSION 3.28) -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") -endif() - -include("${MQT_CORE_QDMI_DEVICE_TARGETS}") -include("${MQT_CORE_QDMI_HELPER}") - -set(device mqt-core-qdmi-metadata-device) -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) -if(NOT device_id STREQUAL "test.metadata-only" - OR NOT device_prefix STREQUAL "TEST_METADATA" - OR NOT runtime_files STREQUAL "metadata-runtime.json") - message(FATAL_ERROR "Exported QDMI device metadata was not preserved") -endif() - -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/main.cpp" "int main() { return 0; }\n") -add_executable(imported-device-consumer "${CMAKE_CURRENT_BINARY_DIR}/main.cpp") -mqt_copy_qdmi_runtime(imported-device-consumer ${device}) -add_custom_command( - TARGET imported-device-consumer - POST_BUILD - COMMAND - ${CMAKE_COMMAND} -E compare_files - "$/metadata-runtime.json" - "$/metadata-runtime.json")