Building from Source
Build the Python bindings and the C++ unit-test tree, with or without MPI.
monoprop has one supported from-source build workflow:
- the Python bindings — the nanobind extension behind
import monoprop, built with scikit-build-core and driven byuv(orpip); - the C++ unit tests — built within the same
uv(orpip) invocation.
MPI is off by default in every build path; you enable it explicitly. The mechanism differs by build:
| Build | Enable MPI with |
|---|---|
Python bindings and C++ tests (scikit-build / uv / pip) | --config-settings=cmake.define.monoprop_ENABLE_MPI=ON (or export SKBUILD_CMAKE_ARGS="-Dmonoprop_ENABLE_MPI=ON") |
The prebuilt wheels published to PyPI (pip install monoprop) are also built
without MPI, so a from-source build is required for multi-rank runs.
Prerequisites
- a C++23-compliant compiler; on Linux the minimum supported versions are GCC 14 and Clang 18
- CMake and Ninja
- Python 3.11 or newer and the
uvpackage manager (for the bindings) - an MPI implementation such as Open MPI (only for MPI builds)
hwloc(version 2.9+) andpkg-config(required so CMake can locatehwloc)
The repository ships a DevContainer with all of the above pre-configured; opening the folder in VS Code and rebuilding the container is the quickest route to a working environment.
Building the Python bindings
uv creates a virtual environment, installs the Python dependencies, and
compiles the nanobind extension in editable mode. Re-run the sync command whenever
the dependency graph or the C++ sources change.
Without MPI (default)
uv sync --all-extras -vThis produces a single-process build with no MPI dependency.
With MPI
Pass a config-settings override to enable MPI:
uv sync --all-extras -v \
--config-settings=cmake.define.monoprop_ENABLE_MPI=ONThe same override works with pip when installing from a checkout:
pip install . --config-settings=cmake.define.monoprop_ENABLE_MPI=ONVerify the install
uv run python -c "import monoprop as mp; print(mp.__version__)"Running the bindings
A serial run is just a normal Python invocation:
uv run python your_script.pyFor a multi-rank run, launch the same script under mpiexec (requires an MPI
build) and pass comm=MPI.COMM_WORLD to the simulator:
mpiexec -n 8 uv run python your_script.pySee Parallelism and distribution for the communicator options and the operator-partitioning controls.
Building the C++ unit tests
The supported C++ workflow reuses the build tree produced by uv sync. Do not
run cmake --preset ... to configure this project directly: the top-level CMake
configuration expects scikit-build-core to provide Python, nanobind, and related
cache variables. Instead, first create the tree with uv sync, then invoke ctest
directly to run the C++ unit tests.
Release tree
uv sync --all-extras -v
ctest --test-dir build/editable/ReleaseThis uses the scikit-build-core Release tree at build/editable/Release and
runs bin/monoprop_unit_tests.x there, along with bin/monoprop_link_export_probe.x — a
link-time check that the installed shared monoprop library exports every detail/** free
function reachable from the public template chain.
Debug tree
uv sync --all-extras -v --config-settings=cmake.build-type=Debug
ctest --test-dir build/editable/DebugSanitizer trees
The QA workflow builds two sanitizer profiles, selected with monoprop_SANITIZER
(none, asan-ubsan, or tsan). Both require Linux and either GCC or Clang;
CI uses the default system GCC, which is what builds the released wheels.
Sanitizer trees intentionally skip the _core.pyi typing stubs — generating them
means importing the instrumented extension, which is not worth arranging for a
tree that is never packaged.
ASan + LSan + UBSan
SKBUILD_CMAKE_BUILD_TYPE=AsanUbsan \
SKBUILD_CMAKE_DEFINE="monoprop_SANITIZER=asan-ubsan" \
uv sync --group workspace-test --all-extras --reinstall-package monoprop --no-cache -v--reinstall-package monoprop --no-cache matters locally. uv decides whether to
rebuild from the cache-keys in pyproject.toml, which hash source files — not
environment variables. So if you already have an editable install, switching only
the build type leaves the sources unchanged, uv sync does nothing, and
build/editable/AsanUbsan is never created (the ctest below then fails with
"No such file or directory"). CI omits these flags because each job starts on a
clean runner with nothing installed.
The C++ test binary is fully instrumented, so every check applies to it, including leak detection:
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1:detect_invalid_pointer_pairs=1:check_initialization_order=1:strict_init_order=1:strict_string_checks=1:halt_on_error=1 \
LSAN_OPTIONS=suppressions=$PWD/.github/lsan.supp \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
ctest --test-dir build/editable/AsanUbsan --output-on-failureThe Python tests are a different situation: an instrumented _core is loaded
into an ordinary CPython, so the ASan runtime must be preloaded, and the checks
that assume the whole process is instrumented have to be switched off.
ASAN_OPTIONS=detect_leaks=0:detect_stack_use_after_return=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
LD_PRELOAD="$(g++ -print-file-name=libasan.so):$(g++ -print-file-name=libstdc++.so.6)" \
uv run pytestBoth preloads are required. Without libasan.so, the first import monoprop
aborts with ASan runtime does not come first in initial library list. Without
libstdc++.so.6, ASan initialises before libstdc++ is loaded — python is not
linked against it — so ASan's __cxa_throw interceptor never resolves the real
symbol, and the first C++ exception thrown out of the engine dies with
CHECK failed: real___cxa_throw != 0. monoprop reports validation errors as C++
exceptions, so that affects a large part of the suite.
Leak detection is off here because LSan cannot get a clean baseline from an uninstrumented interpreter — the C++ leg above is what covers leaks.
Sanitizer reports are hidden by pytest
pytest replaces the stderr file descriptor, so a sanitizer report printed from
native code is swallowed and the run looks like a bare exit code 1 with no
diagnostic. Add log_path to route reports to files instead, then read them:
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:log_path=$PWD/sanitizer-log \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1:log_path=$PWD/sanitizer-log \
LD_PRELOAD="$(g++ -print-file-name=libasan.so):$(g++ -print-file-name=libstdc++.so.6)" \
uv run pytest
cat sanitizer-log.*The QA workflow does this and dumps the files on failure.
ThreadSanitizer
SKBUILD_CMAKE_BUILD_TYPE=Tsan \
SKBUILD_CMAKE_DEFINE="monoprop_SANITIZER=tsan" \
uv sync --group workspace-test --all-extras --reinstall-package monoprop --no-cache -v
TSAN_OPTIONS=halt_on_error=1:history_size=4 \
ctest --test-dir build/editable/Tsan --output-on-failure -R "(partition_|shm_comm_)"TSan is scoped to the concurrency surface — the partition and ShmComm tests —
which is where its reports are worth the runtime cost.
There is no ThreadSanitizer equivalent of the ASan Python leg. TSan does not
support dlopen of instrumented libraries, so loading an instrumented _core
into a stock interpreter crashes during TSan initialisation; exercising the
Python API under TSan would require a TSan-instrumented CPython.
Related workflows
- Use
just test-widefor the 64-bitmonoprop_WIDE_TERM_INDEXconfiguration. - Use
just code-coveragefor the coverage build. - Use
ctest --test-dir build/editable/Release -L serialor-L mpi-2to filter the discovered C++ test set.
See also
- Getting Started — installing a prebuilt release from PyPI.
- Parallelism and distribution — running across MPI ranks and shared-memory threads.
- Testing — the full Python and C++ test workflow.
- How to Contribute — contributor workflow and documentation checks.