This guide is for contributors changing QRM&CI itself. It covers the repository layout, the shipped deployment modes, the local build/test loop, and the documentation and container-build rules that keep the tree healthy.
If you only want to deploy the shipped daemons, use Getting Started. If you want to add your own entrypoint against qrmci, use Build custom QRM&CI workflow.
| Surface | Responsibility |
|---|---|
| Config.h / Config.cpp | configuration model, file discovery, defaults, and documented environment-variable overrides |
| CommunicationHandler.h / CommunicationHandler.cpp | RabbitMQ send/receive wrappers using std::expected and manual acknowledgement |
| BackendWrapper.h / BackendWrapper.cpp | backend capability snapshots from a live device or a published backend-status message |
| BackendRegistry.h / BackendRegistry.cpp | per-process backend liveness and deterministic iteration order |
| PublicationThrottle.h | header-only status-publication pacing, unrelated to and independent of registry liveness |
| ConstantsMapping.h / ConstantsMapping.cpp | pure translations between the submitter, mqss protocol, and mqss-ci enumerations |
| CircuitFormatPolicy.h / CircuitFormatPolicy.cpp | task/compiler/backend circuit-format admission policy: direct vs. compiled, and the one compiler target a backend offers |
| Runners.h / Runners.cpp | selection, compilation, submission, result collection, and cancellation helpers |
| Daemon.h / Daemon.cpp | daemon bring-up, device opening, own-backend registration and refresh, compile-and-schedule, plus the termination flag and SIGINT/SIGTERM handlers every entrypoint installs |
| DaemonMessaging.h | the queue I/O a work loop shares: receive, drain, best-effort send, failure and result replies |
| Error.h / Error.cpp | structured error kinds plus human-readable detail |
| Logger.h | the shared console-plus-file logger helper |
For the concept-level model, start with Overview; for the step-by-step composition, see Build custom QRM&CI workflow. What matters before you change apps/standalone/main.cpp is why its loop is ordered the way it is.
The daemon refreshes its own backend registry entry and expires stale entries before it receives work, and the receive itself is a non-blocking drain (poll the queue empty, then move on) rather than a bounded wait. Together those two facts mean a quiet intake queue cannot starve the refresh and let the daemon expire its own backend out from under itself, and a burst of tasks is processed in one turn instead of throttling to one task per stagePollInterval. Any change that makes the receive block, or that moves it ahead of the refresh, breaks that invariant.
Ready jobs are executed one at a time through executeQuantumTask(...), and the process exits cleanly on SIGINT and SIGTERM.
Distributed mode keeps the same pipeline stages but splits them across two kinds of process — one selector and one or more workers:
Two behavioral differences from standalone matter. The worker takes one task per turn rather than draining, so jobs it has already scheduled get submitted without waiting for the intake queue to run dry. And it submits every ready job before collecting any result, so several jobs can be in flight on the device at once. The selector never opens a QDMI connection, so every backend it can choose from comes from a worker's status message.
The repository ships a development container under .devcontainer/. Its Dockerfile installs the toolchain and system packages QRM&CI needs — compiler, CMake, Ninja, pre-commit, ccache, and the RabbitMQ/spdlog/protobuf development packages — and copies the prebuilt MQSS dependency tree from the same mqssci-deps image CI uses into /opt/deps. The devcontainer definition also preconfigures the VS Code C++/CMake/Doxygen extensions and the formatting settings this repository expects. Opening the repository in VS Code and choosing Reopen in Container is the quickest way to a working build environment, and it is the recommended one.
The container does not carry credentials. The MQSS components are fetched from GitHub at configure time, so git inside the container must be able to reach those repositories — forward your credentials or SSH agent into the container yourself.
Never put a token into QRMCI_MQSS_SUBMITTER_REPOSITORY or any other fetched repository URL: CMake cache variables land verbatim in CMakeCache.txt, and the value FetchContent uses to clone also ends up in the fetched repository's .git/config, so an embedded token persists in the build tree in two places. Authenticate instead with a process-scoped git config override, scoped to the cmake invocation and never written to disk:
or configure a standard git credential helper (git config --global credential.helper ...) so git itself resolves credentials outside of CMake entirely. If a token was ever placed in QRMCI_MQSS_SUBMITTER_REPOSITORY, treat it as exposed: rotate it and discard any local build directory that may have cached it.
ctest --test-dir build/tests runs every test target this build was configured for. CI configures with -DQRMCI_RUN_INTEGRATION_TESTS=ON and runs the unit and integration labels as two separate steps (ctest --test-dir build/tests -LE integration then -L integration) so failures in one are easy to tell apart from the other; running the plain command locally covers the same tests. Useful variants:
run only the unit tests:
run one CTest target:
reconfigure after toolchain or dependency changes:
Re-running configure reuses the existing cache. If a dependency revision changed in cmake/DependenciesVersion.cmake, delete build/ and configure fresh instead — the tree is disposable, and CI always builds from scratch.
run the integration tests. These need a reachable RabbitMQ broker and the real example QDMI driver/device (the same environment TestRunnersTaskExecution and TestDaemon need), and are registered DISABLED for ctest unless the configure step passed -DQRMCI_RUN_INTEGRATION_TESTS=ON. No deployment needs to be started by hand: each target owns its own daemon(s) via DaemonEnvironment/DaemonProcess (tests/integration/DaemonEnvironment.*, tests/integration/DaemonProcess.*), spawning them at the start of the run and tearing them down at the end.
This runs every registered integration target — TestSubmitTask, TestSubmitTaskDistributed, TestSubmitTaskUnsupportedBackend, TestSubmitTaskCompileFailure, TestSubmitTaskConcurrent, TestScheduleTask, and TestSubmitTaskMultiWorker. TestSubmitTaskDistributed and TestSubmitTaskMultiWorker are the only ones that spawn the distributed selector+worker(s) instead of the standalone daemon (TestSubmitTaskMultiWorker spawns two workers, each with a distinct device ID and dispatch queue, to exercise routing between them); every other target spawns qrmcid-standalone. Select a single target with -R, or every integration target with -L integration. Each target has a 120s TIMEOUT; one that exceeds it (the spawned daemon crashed, a broker that never delivers) is reported as a ctest timeout rather than hanging – check the spawned daemon's own log under build/tests/integration/daemon-logs/<target>/ first. Since every spawned daemon binds the same compiled-in queue names against the same broker, tests/integration/CMakeLists.txt gives all seven targets the same RESOURCE_LOCK so ctest -j never runs two of them at once.
Notes:
| Option | Default | Meaning |
|---|---|---|
| QRMCI_BUILD_UNIT_TESTS | ON | build the unit test targets |
| QRMCI_BUILD_INTEGRATION_TESTS | ON | build the integration test targets |
| QRMCI_RUN_INTEGRATION_TESTS | OFF | register the integration test targets for ctest |
| QRMCI_BUILD_APPS | ON | build the standalone and distributed daemon binaries |
| BUILD_QRMCI_DOCS | OFF | build the Doxygen docs target |
| QRMCI_WARNINGS_AS_ERRORS | OFF | treat QRM-owned target warnings as errors |
qrmci links with LINKER:--no-as-needed (see the comment above that target_link_options call in CMakeLists.txt) to work around an --as-needed-default linker dropping a shared library MQSSCI's static archives still need. The configure step probes linker support for that flag with check_linker_flag() first and only applies it when supported, so a linker that rejects GNU-style --no-as-needed configures without it instead of failing; this is a capability check, not a claim that QRM&CI is supported on non-Linux/non-GNU toolchains.
Pinned dependency revisions live in cmake/DependenciesVersion.cmake.
MQSSSubmitter (the CMake target mqss_submitter) lives in its own repository, MQSS-Submitter, fetched by cmake/FindMQSSSubmitter.cmake. It is a plain FetchContent fetch: the library ships no install()/package config, so there is no installed-package mode to prefer. The revision is pinned by QRMCI_MQSS_SUBMITTER_GIT_TAG in cmake/DependenciesVersion.cmake; point that at a local checkout's commit, or add a SOURCE_DIR to the FetchContent_Declare, to build against a working copy.
MQSSSubmitter's own cmake_minimum_required(VERSION 3.40) raises the CMake version this project can actually be configured with above the 3.25 its own cmake_minimum_required declares.
A deployment does not declare the driver's QDMI version anywhere: MQSSSubmitter detects it from the driver, and a version this build cannot speak comes back as an openDevice() failure. The only submitter timing knob is SubmitterConfig::jobWaitTimeout ([submitter] jobWaitTimeout, seconds, file-only, default 0 = wait indefinitely).
pre-commit run -a is the same check CI runs first; a failure there blocks the build and static-analysis jobs. It covers formatting, spelling, license headers, and the capitalization checks configured in .pre-commit-config.yaml.
For static analysis, CI configures with compile commands and runs run-clang-tidy over everything under include/, src/, apps/, and tests/. To reproduce it exactly:
To check a single file while iterating, run clang-tidy -p build src/Runners.cpp instead.
Build the docs target locally with:
Generated HTML lands at build/docs/html/index.html.
Two rules matter when you add or rename documentation pages:
The glob in docs/CMakeLists.txt already makes docs/*.md part of the build dependencies, but INPUT is an explicit list. A page that is not added there silently does not build.
The docs are gated by .github/workflows/publish-docs.yml, which builds the docs target and fails if build/docs/doxygen_warnings.txt is non-empty. That workflow runs on pushes to develop, not on pull requests, so build the docs target locally before you open a PR — a dangling @ref or an unlisted page will otherwise break develop after the merge.
The standalone and distributed images are built from separate multi-stage Dockerfiles:
Build targets from the repository root:
The distributed images go through apps/distributed/docker-bake.hcl rather than a plain docker build, because the two runtime stages share one builder stage: the selector and worker bake targets select the runtime-selector and runtime-worker stages of the same Dockerfile, and the default bake group builds both. The bake file reads DOCKERFILE_DISTRIBUTED, IMAGE_NAME_DISTRIBUTED_SELECTOR, IMAGE_NAME_DISTRIBUTED_WORKER, and IMAGE_TAG_DISTRIBUTED from the environment, which is why the Makefile exports the variables it overrides.
Image tags are not hardcoded. IMAGE_TAG_STANDALONE and IMAGE_TAG_DISTRIBUTED are extracted in the Makefile from the project(... VERSION x.y.z ...) line of the matching apps/*/CMakeLists.txt, so bumping an app's project version changes the tag the docker-run-* targets look for — rebuild the image before running it.
Each Dockerfile's builder stage runs the same cmake configure that pulls the private Munich-Quantum-Software-Stack dependencies (see cmake/DependenciesVersion.cmake) via FetchContent over HTTPS. Unlike the development container described above, which forwards your host SSH agent, these image builds run without any ambient credential, so the clone fails unless you pass one in. Set GITHUB_TOKEN to a token with read access to those repositories and the Makefile forwards it as a BuildKit secret (--secret id=github_token,env=GITHUB_TOKEN); the builder stage only uses it to authenticate that one git clone/cmake invocation, never writing it to disk, so it can't end up in an image layer:
Omitting GITHUB_TOKEN reproduces the original failure: fatal: could not read Username for 'https://github.com': No such device or address.