Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .agent/plans/qdmi-installed-consumer-deployment-c3.md
Original file line number Diff line number Diff line change
@@ -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.
172 changes: 92 additions & 80 deletions cmake/AddMQTQDMIDevice.cmake
Comment thread
burgholzer marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$<TARGET_FILE:${target}>")
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 "$<TARGET_RUNTIME_DLLS:${dependency_target}>")
else()
list(APPEND runtime_files "$<TARGET_RUNTIME_DLLS:${target}>")
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 "$<TARGET_FILE:${runtime_target}>")
if(WIN32)
list(APPEND runtime_files "$<TARGET_RUNTIME_DLLS:${runtime_target}>")
endif()
add_custom_command(
TARGET ${consumer_target}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${runtime_files}
"$<TARGET_FILE_DIR:${consumer_target}>"
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}/$<CONFIG>/${manifest_name}")
file(
GENERATE
OUTPUT "${manifest}"
CONTENT
"{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${device_id}\",\n \"library\": \"$<TARGET_FILE_NAME:${library}>\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n"
)
else()
set(manifest "$<TARGET_FILE_DIR:${library}>/${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 "$<TARGET_FILE_DIR:${library}>/${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}/$<CONFIG>/${manifest_name}")
file(
GENERATE
OUTPUT "${manifest}"
CONTENT
"{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${device_id}\",\n \"library\": \"$<TARGET_FILE_NAME:${device}>\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n"
)
else()
set(manifest "$<TARGET_FILE_DIR:${device}>/${manifest_name}")
endif()
get_target_property(device_imported ${device_target} IMPORTED)
if(NOT device_imported)
add_dependencies(${target} ${device})
endif()
set(device_files "$<TARGET_FILE:${device}>")
if(WIN32 AND NOT device_imported)
list(APPEND device_files "$<TARGET_RUNTIME_DLLS:${device}>")
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} "$<TARGET_FILE_DIR:${target}>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${manifest}"
"$<TARGET_FILE_DIR:${target}>/${manifest_name}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${files} "$<TARGET_FILE_DIR:${target}>"
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 "$<TARGET_FILE_DIR:${device}>/${runtime_file}"
"$<TARGET_FILE_DIR:${target}>/${runtime_file}")
endforeach()
if(NOT WIN32 AND imported)
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND
${CMAKE_COMMAND} "-DLIBRARY=$<TARGET_FILE:${library}>"
"-DDESTINATION=$<TARGET_FILE_DIR:${target}>" -P
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CopyQDMISharedDependencies.cmake")
endif()
endforeach()
endfunction()
21 changes: 21 additions & 0 deletions cmake/CopyQDMISharedDependencies.cmake
Original file line number Diff line number Diff line change
@@ -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()
50 changes: 23 additions & 27 deletions docs/qdmi/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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.
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 0 additions & 25 deletions test/qdmi/driver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -143,27 +139,6 @@ if(TARGET MQT::CoreQDMIDriver)
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:mqt-core-qdmi-session-device>"
"$<TARGET_FILE_DIR:${TARGET_NAME}>")

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 $<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)
Expand Down
Loading
Loading