Build System and Project Layout
This page is the contributor-facing tour of Blackchirp’s build
plumbing. The bulk is CMake — the cmake/*.cmake modules, the
user-facing build options, the auto-generated hardware aggregator
headers that hold the runtime registration model together, the test
infrastructure, the documentation targets, and the CPack-based
packaging path — but the documentation pipeline and the Python module
have their own sections below for the pieces CMake does not own.
Installation covers the binary-package install
path; this page covers the from-source build that the installation
page links to.
Blackchirp produces three independently-buildable deliverables. The
C++ application (blackchirp and blackchirp-viewer) is
driven by CMake and packaged by CPack. The documentation under
doc/source/ is also a CMake target (docs), but its pipeline
chains four tools — Doxygen, Breathe, Sphinx, and nbsphinx — described
in Documentation build below. The Python module under
python/blackchirp/ is built outside CMake using pyproject.toml
and is versioned and released to PyPI on its own cadence; Python
module build below covers it.
Hardware selection is not part of the build. Every hardware
driver in src/hardware/ is always compiled into the
blackchirp-hardware library; the active set is decided at run time
by the registry and profile system. There is no compile-time flag that
filters drivers in or out.
CMake module map
The top-level CMakeLists.txt sets project-wide options, finds the
external dependencies (Qt6, GSL, Eigen3, Qwt), then include()-s one
cmake/*.cmake module per logical layer. Each module is
self-contained, guarded against double-inclusion, and produces exactly
one library or executable target.
BlackchirpData.cmake→blackchirp-data(STATIC)Data model, analysis (FT, peak finder), experiment configuration and validation, loadout system, file parsers, overlay processing, storage classes (
BlackchirpCSV,SettingsStorage,HeaderStorage,FidStorageBase), and logging. Linked into both the main app and the viewer.BlackchirpHardware.cmake→blackchirp-hardware(STATIC)All hardware base classes, every concrete driver, communication protocols (
rs232,tcp,virtual,gpib,custom), the registration machinery (HardwareManager,HardwareRegistry,HardwareProfileManager,RuntimeHardwareConfig), the Python trampolines, and the vendor library wrappers. Skipped whenBC_BUILD_VIEWER_ONLY=ON.BlackchirpGui.cmake→blackchirp-gui(STATIC)Full Qt Widgets layer: main window, dialogs, experiment-setup wizard pages, plots, overlay widgets, theme code. Main app only.
BlackchirpViewerGui.cmake→blackchirp-viewer-gui(STATIC)Lighter GUI subset for the viewer — plotting and experiment inspection without any hardware dependency. Always built.
BlackchirpApplication.cmake→blackchirp(executable)Glues data + GUI + hardware together, compiles
main.cppand the acquisition layer (AcquisitionManager,BatchManagerand friends), wires Qt resources, and registersblackchirp_deploy_qt(blackchirp). Skipped whenBC_BUILD_VIEWER_ONLY=ON.BlackchirpViewerApplication.cmake→blackchirp-viewer(executable)Viewer entry point; links data + viewer-gui only and registers
blackchirp_deploy_qt(blackchirp-viewer).BlackchirpDocumentation.cmake→docs,doxygen(custom targets)Sphinx HTML build and Doxygen XML/HTML build. Wired only when
BC_BUILD_DOCUMENTATION=ON.Packaging.cmake→ CPack configurationPer-platform generator selection (
DEB;RPM;TGZon Linux,DragNDrop;TGZon macOS,NSIS;ZIPon Windows), component restriction toApplications, and thepackage-deb,package-rpm,package-dmg,package-nsis, andpackage-allcustom targets.QtDeployment.cmake→blackchirp_deploy_qt(<target>)Install hook that runs
windeployqt(Windows) ormacdeployqt(macOS) against the installed binary so the CPack package is self-contained. No-op on Linux, where Qt comes from the system package manager.FindQWT.cmake→QWT::QWT(imported)Custom finder for the Qwt scientific-plotting library. No Qt6 Qwt Config file ships with most distributions, so this module probes a list of common include and lib paths, falls back to
pkg-config, and exposesQWT::QWT.
Prerequisites
Install the following before configuring a build:
A C++23-capable compiler (GCC 13+, Clang 16+, MSVC 19.35+).
CMake 3.16 or later.
Qt 6 6.4 or later with the Core, Gui, Widgets, Network, SerialPort, Concurrent, and Test modules. The Linux DEB release is built against the Qt that Ubuntu 24.04 (Noble) ships in
qt6-base-dev(6.4.2); the other jobs build against newer Qt releases. The C++ code is kept compatible with the 6.4 baseline.Qwt 6.2 or later (Qt6 build). Distributions without a Qt6-compatible Qwt package require an in-tree build; see
BC_BUNDLE_QWTin Packaging and Release CI.GNU Scientific Library (GSL) 2.1 or later.
Eigen3 3.3 or later (header-only).
Optional:
NVIDIA CUDA Toolkit — required only when
BC_ENABLE_CUDA=ON. See the warning underBC_ENABLE_CUDAbelow before turning this on.Doxygen plus a Python environment that satisfies
doc/source/requirements.txt— required only whenBC_BUILD_DOCUMENTATION=ON. See Documentation build below.
Configuring a build
Build directories live under build/ inside the source tree (so the
debug, release, and test trees are easy to find from the project root):
cmake . -B build/Desktop-Debug/
cmake --build build/Desktop-Debug/ -j$(nproc)
cmake . -B build/Desktop-Release/ -DCMAKE_BUILD_TYPE=Release
cmake --build build/Desktop-Release/ -j$(nproc)
Use cmake --build rather than make -C. CMake regenerates the
build system mid-invocation when CMakeLists.txt or any included
.cmake module changes; make aborts in that case with
“No rule to make target ‘CMakeFiles/Makefile2’”, whereas cmake
--build re-invokes the generator cleanly.
The default build type is Debug. A Release build switches on -O3
(GCC/Clang) or /O2 (MSVC) and suppresses qDebug() output.
The BuildConfig.cmake user-options file
User-facing build options are not declared in CMakeLists.txt
directly — they live in cmake/BuildConfig.cmake. This file is
git-ignored, so changes survive git pull. On the first cmake
configure, the top-level CMakeLists.txt notices the file is missing
and copies cmake/BuildConfig.cmake.template into place. After that,
edits to BuildConfig.cmake are yours to keep.
The four user-facing options are:
BC_BUILD_VIEWER_ONLY(defaultOFF)Build only
blackchirp-viewer: skip the hardware library, skip the main GUI library, and skip the main application executable. Useful on analysis machines without lab hardware attached. The main GUI library pulls inQt6::SerialPorttransitively, which is the reason for the hard split.BC_BUILD_TESTS(defaultON)Build the unit-test executables and the
testsaggregate custom target. Impliesenable_testing(), soctestworks in the build directory.BC_BUILD_DOCUMENTATION(defaultOFF)Wire the
docsanddoxygencustom targets. Requiressphinx-build(and Doxygen for the API reference) to be discoverable; otherwise the targets are silently omitted.BC_ENABLE_CUDA(defaultOFF)Enable the CUDA module under
src/modules/cuda/for GPU-side FID averaging. Requires the NVIDIA CUDA Toolkit. Turning this on enables theCUDAlanguage and adds a small set of.cusources to the main app target.Warning
The CUDA module is unmaintained and unlikely to build or run successfully without significant work. Developers may experiment with it at their own risk. Contributions are welcome for modernizing the code, configuring the build (PTX, fixed-architecture builds), and runtime discovery / integration with
ApplicationConfigManager.
There is no compile-time switch for hardware availability. If you add a
new driver under src/hardware/core/<type>/ or
src/hardware/optional/<type>/ matching one of the recognized name
patterns described below, it is picked up automatically on the next
cmake configure.
Building targets
Top-level targets you will use most often:
cmake --build build/Desktop-Debug/ --target blackchirp -j$(nproc)
cmake --build build/Desktop-Debug/ --target blackchirp-viewer -j$(nproc)
cmake --build build/Desktop-Debug/ --target tests -j$(nproc)
cmake --build build/Desktop-Debug/ --target docs -j$(nproc)
cmake --build build/Desktop-Debug/ --target doxygen -j$(nproc)
The tests aggregate target depends on every individual test
executable and is the convenient way to build the whole suite.
docs depends transitively on doxygen when both Sphinx and
Doxygen were located, so a single --target docs invocation
produces both the prose pages and the API reference.
Hardware aggregator headers
BlackchirpHardware.cmake writes three headers into
src/hardware/core/ at configure time:
hw_base.h— every hardware base type:clock.h,ftmwdigitizer.h,awg.h,pulsegenerator.h,flowcontroller.h,gpibcontroller.h,ioboard.h,pressurecontroller.h,temperaturecontroller.h,lifdigitizer.h,liflaser.h.hw_impl.h— every concrete driver header that the configure-timefile(GLOB)calls find undersrc/hardware/core/<type>/andsrc/hardware/optional/<type>/, plus every Python trampoline header undersrc/hardware/python/.hw_h.h— a one-line wrapper that#include-s both of the above. This is the header consumers refer to when they want “all hardware types and drivers.”
These are not just convenience headers. They exist because of how
CMAKE_AUTOMOC interacts with static libraries and Qt’s static
registration model.
Every concrete driver registers itself with
HardwareRegistry at static-initialization time via
REGISTER_HARDWARE_META and friends (see
HardwareRegistry). The registration code lives in the
driver’s .cpp translation unit at file scope. In a static-library
build, the linker is allowed to drop any object file whose symbols are
not referenced from the final executable — and a static initializer
counts as “unreferenced” for that purpose. Without an explicit symbol
reference into each driver, the registrations would silently
disappear at link time and the registry would come up empty.
The fix is to feed the driver headers to AUTOMOC. AUTOMOC
generates moc_<class>.cpp for every Q_OBJECT it finds, and the
generated meta_object_offsets references pull the corresponding
object file out of the static library at link time. Listing every
driver header in hw_impl.h (which is itself part of the
blackchirp-hardware source set) is what gives AUTOMOC the
visibility it needs.
This is also why the Python trampoline headers
(src/hardware/python/python*.h) get appended to
hw_impl.h even though they live outside the standard
hardware/<type>/ glob: the trampolines are Q_OBJECT subclasses
that register themselves the same way, and their meta-object code has
to be generated alongside the rest.
Glob-based source discovery
BlackchirpHardware.cmake discovers driver source files via
file(GLOB) against fixed name patterns under each hardware-type
directory. Dropping a new xxxx.cpp/xxxx.h pair into one of the
recognized directories under one of the recognized patterns is enough;
no edit to CMakeLists.txt or BlackchirpHardware.cmake is
needed, but you do need to re-run cmake (not just cmake --build)
so the glob is re-evaluated.
The recognized patterns, by hardware type:
Directory |
Patterns |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If a new driver does not match any existing pattern (for example, a
new vendor prefix), add the prefix to both the drivers glob and
the headers glob in BlackchirpHardware.cmake. The two lists are
parallel; keep them in sync.
Python hardware files are handled by a separate pair of globs against
src/hardware/python/:
src/hardware/python/*.cpp— the C++ trampolines (one per hardware type) compile straight intoblackchirp-hardware.src/hardware/python/python_hw_host.pyandsrc/hardware/python/python_*_template.py— the runtime files areconfigure_file-d into the build directory (so a source-tree-relative dev run finds them) andinstall-ed under${CMAKE_INSTALL_DATADIR}/blackchirp/(so a packaged install does too). The host script is the only Python file Blackchirp loads at runtime; the templates are seeds copied into the user’s data directory when they create a new Python driver profile.
See Adding a New Hardware Driver for the contributor-side recipe and Python Hardware for the Python hardware architecture.
Test infrastructure
When BC_BUILD_TESTS=ON (the default), CMakeLists.txt calls
enable_testing(), requires Qt6::Test, defines a series of
add_executable/target_link_libraries/add_test triples, and
collects them into the tests aggregate target.
Test executables and what each covers:
Executable |
Coverage |
|---|---|
|
|
|
|
|
The semicolon-delimited CSV reader/writer. |
|
|
|
The generic XY parser plus its test data set under |
|
Overlay-operation capability flags in isolation from the rest of the GUI layer. |
|
Concrete overlay operations against synthetic input. |
|
Catalog/spectrum parsers against fixture files. |
|
Registry behavior: registration macros, lookups, factory invocation. |
|
|
|
|
|
Static hardware-key declarations in |
|
Round-tripping a saved experiment through the data layer. |
|
The |
|
Concurrent access patterns on the plot layer; also offscreen. |
|
The |
|
|
Run the full suite with:
ctest --test-dir build/Desktop-Debug
Or build and run a single test directly:
cmake --build build/Desktop-Debug --target tst_settingsstoragetest -j$(nproc)
build/Desktop-Debug/tst_settingsstoragetest
The blackchirp-test-hardware library
A handful of tests need the hardware base classes plus the
virtual* drivers, but cannot link against the main
blackchirp-hardware library because that pulls in
HardwareManager, which in turn references symbols that
only the main application provides. CMakeLists.txt defines a
parallel static library, blackchirp-test-hardware, that includes
the hardware base classes, the communication protocols, and just the
virtual* drivers. Tests like tst_experimentloading
link against this instead so they can exercise the registration
machinery without dragging in the manager.
Adding a new test follows a four-step recipe:
Add
add_executable(tst_yourthing tests/tst_yourthing.cpp).target_link_libraries(tst_yourthing blackchirp-data Qt6::Test Qt6::Core)— substituteblackchirp-test-hardwareforblackchirp-dataif the test instantiates virtual hardware.add_blackchirp_definitions(tst_yourthing)and (if applicable)add_test(NAME YourThingTest COMMAND tst_yourthing).Append
tst_yourthingto theadd_custom_target(tests DEPENDS …)list near the bottom of the test block so the aggregate target picks it up.
If your test needs widgets or runs headless, set
QT_QPA_PLATFORM=offscreen via set_tests_properties(... PROPERTIES
ENVIRONMENT ...) — the existing widget-touching tests do this.
Documentation build
The documentation pipeline weaves four tools together: Doxygen reads
the C++ headers under src/ and emits an XML representation of every
class, struct, enum, and free function; Breathe (a Sphinx extension)
turns that XML into RST entities at Sphinx build time so the project’s
hand-written RST pages can pull C++ symbols in via
.. doxygenclass:: and friends; Sphinx renders all the RST sources
under doc/source/ into HTML (or PDF); and nbsphinx — together with
nbsphinx-link, which wires standalone notebook files into the doctree
— renders the example Jupyter notebooks under python/ into pages
alongside the rest of the documentation.
For Python entities, Sphinx’s built-in autodoc extension introspects
the blackchirp module on disk (conf.py puts
python/blackchirp/src on sys.path) and renders Google-style
docstrings via the napoleon extension. The contract between source
code, the generators, and the rendered API pages is documented in
API reference style.
CMake-side wiring
With BC_BUILD_DOCUMENTATION=ON, BlackchirpDocumentation.cmake
finds sphinx-build and Doxygen and registers the docs,
doxygen, docs-pdf (if LaTeX is found), docs-linkcheck, and
docs-clean custom targets. The Sphinx config (conf.py) and the
Doxyfile template (Doxyfile.in) live in doc/source/.
The docs target depends transitively on doxygen when both
Sphinx and Doxygen were located, so a single
cmake --build build --target docs invocation produces both the
prose pages and the API reference. conf.py also calls
doxygen Doxyfile directly at import time so that Read the Docs
(which does not use the CMake build) still gets fresh XML before
Sphinx parses anything; the call is idempotent in either environment.
Build environment
The full set of Python dependencies is in doc/source/requirements.txt:
sphinx— the documentation generator.sphinx_rtd_theme— the Read the Docs HTML theme.breathe— Doxygen-XML adapter for Sphinx.nbsphinxandnbsphinx-link— notebook rendering.ipython— required by nbsphinx for syntax highlighting.sphinxcontrib-lightbox2— image lightbox in rendered HTML.
System-level dependencies: doxygen itself, plus the LaTeX toolchain
(pdflatex, latexmk) if you want PDF output.
Activate an environment that satisfies these requirements before
invoking the docs target. The build target itself is environment-
agnostic; how a particular dev box satisfies the requirements (conda,
pip, virtualenv) is a per-checkout convention recorded in the local
AGENTS.local.md file at the project root.
Running the build
The reliable recipe is:
touch doc/source/index.rst
cmake --build build --target docs
The touch forces Sphinx to re-evaluate the toctree so that pages
added or removed since the last build are picked up; without it,
Sphinx may skip regeneration on the assumption that the toctree has
not changed.
A stale Doxygen tree is the other common gotcha: Breathe references
the XML that the previous Doxygen run produced, so editing a header
and rebuilding only the Sphinx side leaves the API pages out of sync.
cmake --build build --target doxygen followed by
cmake --build build --target docs refreshes both halves; the
docs target’s dependency on doxygen makes a single
--target docs invocation safe in most cases.
Read the Docs
The published documentation at https://blackchirp.readthedocs.io/ is
built from master (and PR previews from feature branches) by Read
the Docs, configured by .readthedocs.yaml at the project root. That
config pins Python 3.11 and Ubuntu 22.04, points at
doc/source/conf.py, and installs doc/source/requirements.txt
into the build environment. Doxygen runs via the
subprocess.call('doxygen Doxyfile') line in conf.py so that
Read the Docs does not need its own Doxygen install step. Changes to
the requirements file or to the conf.py Doxygen invocation flow
through to Read the Docs on the next push.
Output locations
build/docs/html/— Sphinx HTML output (theindex.htmlCMake actually depends on lives atbuild/docs/html/index.html).build/docs/doxygen/html/— Doxygen HTML browser, when thedoxygentarget has run.build/docs/doxygen/xml/— Doxygen XML, the input to Breathe.
Python module build
The standalone blackchirp Python package under
python/blackchirp/ is built independently of CMake. It uses the
standard PEP 517 / 518 toolchain driven by pyproject.toml, has its
own dependency list (numpy, scipy, pandas at runtime; pytest as a dev
extra), and is versioned and released to PyPI on its own cadence. CMake
does not touch the Python module: the C++ application’s build, test,
and packaging pipelines do not depend on it being installed, and
breaking the Python module does not break the CMake build.
Layout and pyproject.toml
The package follows the src/ layout:
python/
├── blackchirp/
│ ├── pyproject.toml # PEP 621 metadata, dependencies, version
│ ├── README.md # PyPI listing description
│ ├── LICENSE
│ ├── src/blackchirp/ # importable package
│ │ ├── __init__.py # public API surface (re-exports)
│ │ ├── bcfid.py
│ │ ├── bcftmw.py
│ │ ├── bclif.py
│ │ ├── blackchirpexperiment.py
│ │ └── coaverage.py
│ └── tests/ # pytest suite
├── single-fid.ipynb # example notebooks (not part of the package)
├── single-lif.ipynb
├── example-data/ # fixtures shared with the C++ test suite
├── environment.yml # conda env recipe (notebook-friendly)
└── requirements.txt # pip equivalents
The src/ layout means the package cannot be imported from
python/blackchirp/ itself; it must be installed (or installed in
editable mode with pip install -e .) to be importable. This is
deliberate — it prevents accidental imports from a half-built source
tree and matches how downstream users get the package from PyPI.
pyproject.toml declares the runtime dependencies (numpy, scipy,
pandas), the dev extra (pytest), the supported Python versions
(>=3.9), and the version string. The dependency list is
load-bearing; see Conventions and Style for the
minimal-dependency policy that constrains additions.
Build, test, install
Build the wheel and source distribution:
python -m build python/blackchirp
Output lands in python/blackchirp/dist/ as
blackchirp-<version>-py3-none-any.whl and
blackchirp-<version>.tar.gz.
For development, install in editable mode with the dev extra:
pip install -e "python/blackchirp[dev]"
This puts pytest on the path along with the package itself,
imported directly from the source tree so edits take effect without a
rebuild.
Run the tests:
pytest --rootdir python/blackchirp python/blackchirp/tests
The test fixtures live under python/example-data/. They are shared
with the C++ test suite (specifically tst_experimentloading), which
keeps the on-disk schema definitions consistent across the two
languages: a fixture-format change that breaks the C++ loader will
also break the Python loader.
Versioning
The Python module’s version is declared in
python/blackchirp/pyproject.toml under [project] version. It is
independent of the C++ application’s version (set in the top-level
CMakeLists.txt). The Python module ships its first release as
0.1.0rc1 while the C++ application is at 2.0.0 — the two
numbers do not track each other. Bumping the version is a one-line
edit to pyproject.toml and should be done in the same PR that
ships a user-visible Python change, with the rationale recorded in the
PR’s changelog entry under doc/source/changelog/.
Distribution
The package is published to PyPI as
blackchirp. There is no
automation in this repository for PyPI uploads: twine upload runs
from the maintainer’s machine after a manual review of the wheel and
sdist. Publishing requires PyPI credentials that are not in the
repository or in CI, and there is no GitHub Actions workflow that
pushes to PyPI on release.
Contributors and agents must not run twine upload, python -m
build followed by twine upload, or any other PyPI-publication
command without explicit user consent. Building the wheel locally for
inspection is fine; pushing to a public index is not.
Packaging
Binary distribution — the CPack per-platform generators, the Linux
AppImage built with linuxdeploy, Qt/Qwt redistribution, GPG
signing and build-provenance attestation, and the GitHub Actions
release workflow that drives all five platforms — is the topic of
Packaging and Release CI. On the CMake side it is the Packaging.cmake and
QtDeployment.cmake entries in the CMake module map above:
Packaging.cmake owns the CPack configuration and the
BC_BUNDLE_QWT option, and QtDeployment.cmake provides the
blackchirp_deploy_qt(<target>) install hook that each application
module calls after its install(TARGETS ...) rule.