Build System#
Native Pre-Dependencies#
Some native extensions are written in Rust, Cython, or C++, requiring specific dependencies to build them.
For Rust, you need the rustc and cargo tools installed. Install them by following these steps:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable -y
For C++, requirements vary by system, but you’ll need a C++ compiler and cmake. Install them as follows:
On Ubuntu:
When using GCC, you need versions that have fixes for this bug
sudo apt-get install build-essential cmake
On macOS:
brew install cmake
xcode-select --install
Build Dependencies#
To see the current build dependencies, check the [build-system] section in the pyproject.toml file. For example, at the time of writing, the dependencies are:
[build-system]
requires = ["cython", "cmake>=3.24.2,<3.28; python_version>='3.8'", "setuptools-rust<2"]
To install all dependencies in one step, use:
pip install 'cython' 'cmake>=3.24.2,<3.28' 'setuptools-rust<2'
Note that pip install -e (described below) also installs these build dependencies automatically.
Local Installation from a Local Repository#
The simplest way to build and install the library in your Python environment is with pip:
pip install .
Local Installation in Development Mode#
To install the library in development mode, use the -e flag:
pip install -e .
This installs the library in “editable” mode, meaning changes to the source code are immediately reflected in the installed package. This works only if you run the command from the repository’s root directory. To use ddtrace from a different directory (e.g., another project running under ddtrace), add the repository path to your PYTHONPATH environment variable:
export PYTHONPATH=/path/to/ddtrace:$PYTHONPATH
If you skip pip install -e . and use PYTHONPATH, you must manually install the build dependencies (see above) and compile the native extensions with:
python setup.py build_ext --inplace
Then, if you want to run ddtrace from the repo with another project in another directory, instead of using ddtrace-run (which with an editable or PYTHONPATH install would not find the one in the repository), do:
python -m ddtrace.commands.ddtrace_run python my_traced_app.py
Using sccache to Speed Up Builds#
If you frequently rebuild native extensions or deploy to multiple containers, consider using sccache to accelerate compilation. sccache caches compilation results, reducing build times. Install it using cargo, which you should have if you followed the Rust installation steps earlier:
cargo install sccache
For the build system to locate sccache, either ensure its path is in your PATH environment variable or set the SCCACHE_PATH (or SCCACHE) environment variable to its location:
export SCCACHE_PATH=/home/doe/.cargo/bin/sccache
Additionally, enable sccache by setting the DD_USE_SCCACHE environment variable:
export DD_USE_SCCACHE=1
Then, build the tracer as usual.
To verify that sccache is working after a build, check its statistics:
sccache --show-stats
If cache-hits is greater than 0 (or increases after a cached build), sccache is functioning correctly.
To change the sccache cache directory—e.g., to share it with containers via copying or mounting a volume—set the SCCACHE_DIR environment variable:
export SCCACHE_DIR=/path/to/cache
The sccache –show-stats command also displays the current cache directory.
Configuration Environment Variables#
These environment variables modify aspects of the build process.
- DD_COMPILE_DEBUG(DEPRECATED)#
This deprecated variable will be removed in a future release. If set to 1, it compiles the tracer with debug symbols—useful for debugging the tracer itself but resulting in a slower and larger binary. This is not recommended for production. If set to 0, extensions are compiled in Release mode.
Type: Boolean
Default: (no value)
New in version v0.44.0.
- DD_USE_SCCACHE#
If set to 1, it enables sccache to accelerate native extension compilation (see above). This is beneficial for frequent rebuilds or multi-container deployments.
Type: Boolean
Default: (no value)
New in version v2.12.0.
- DD_COMPILE_MODE#
Specifies the compilation mode for native extensions. Depending on your CMake version, options typically include Release, Debug, RelWithDebInfo, and MinSizeRel. Note that Debug produces slower, larger binaries; RelWithDebInfo increases size but retains the performance of Release; and MinSizeRel reduces binary size at the cost of performance.
Type: String
Default: Release
New in version v3.3.0.
- DD_COMPILE_ABSEIL#
If set to 1, the tracer is compiled with the Abseil library, enhancing performance for Runtime Code Analysis features when active (DD_IAST_ENABLED=1). If set to 0, the Runtime Code Analysis extension is built without Abseil, speeding up the build process at the cost of some performance.
Type: Boolean
Default: True
New in version v3.3.0.
- DD_CYTHONIZE#
If enabled, then Cython extensions are included in the build. Disabling will exclude them. This is mostly useful for source distribution builds so we can skip calling
cythonizeon source files.Type: Boolean
Default: True
- DD_FAST_BUILD#
If set to 1, the tracer is compiled with minimal optimizations (-g0) and DD_COMPILE_ABSEIL is forced to 0. This is not recommended for production due to reduced performance.
Type: Boolean
Default: (no value)
New in version v3.3.0.
- DD_PROFILING_MEMALLOC_ASSERT_ON_REENTRY#
If set to 1, it enables a memalloc-specific native build guard that aborts on reentrant allocator hook calls (malloc -> malloc or malloc -> free). This is intended for memalloc testing and debugging builds, not for production use.
Type: Boolean
Default: (no value)
- DD_CMAKE_INCREMENTAL_BUILD#
Enables support for incremental builds of CMake extensions when doing an in-place install (e.g. editable mode).
Type: Boolean
Default: True
New in version v3.10.0.
- DD_SETUP_CACHE_DOWNLOADS#
Caches the download of artifacts needed by the build process.
Type: Boolean
Default: True
New in version v3.10.0.
- DD_DOWNLOAD_MAX_RETRIES#
Maximum number of retry attempts for transient download failures from GitHub. Retries are triggered by HTTP 429 (rate limit), 502/503/504 (server errors), and network timeouts. Uses exponential backoff with jitter between retries.
Type: Integer
Default: 10
New in version v4.1.0.
- DD_DOWNLOAD_INITIAL_DELAY#
Initial delay in seconds before the first retry attempt. Delay increases exponentially with backoff_factor=1.618 (Fibonacci-like). Useful for tuning retry behavior in different environments.
Type: Float
Default: 1.0
New in version v4.1.0.
- DD_DOWNLOAD_MAX_DELAY#
Maximum delay in seconds between retry attempts. Prevents excessive wait times during exponential backoff.
Type: Integer
Default: 120
New in version v4.1.0.
- _DD_DEBUG_EXT#
If set to any non-empty value, enables
DebugMetadatatiming output insetup.py. Per-phase and per-extension build times are written to the file specified by_DD_DEBUG_EXT_FILE(default:debug_ext_metadata.txt). Useful for diagnosing slow warm builds.Type: Boolean
Default: (no value)
- _DD_DEBUG_EXT_FILE#
Override the output filename for
DebugMetadatatiming data when_DD_DEBUG_EXTis set.Type: String
Default: debug_ext_metadata.txt
Using a system-provided libddwaf#
By default the build downloads the prebuilt libddwaf binaries from GitHub releases and bundles the one matching the target architecture into the package. Distribution packagers cannot do that: they build from source with no network access, and package libddwaf separately rather than vendoring it.
The build_py command therefore takes a --no-bundle-libddwaf option. With it, nothing is downloaded and no library is
bundled. Being a command option rather than an environment variable, it is set through setup.cfg, which is how it
reaches build_py through a PEP 517 frontend such as pip:
[build_py]
no_bundle_libddwaf = 1
It can also be passed on the command line for a direct python setup.py build_py --no-bundle-libddwaf invocation. Default
builds read no such section and bundle the library as before.
How the library is found at runtime#
The loader uses the bundled library when the package contains one. When it does not — which is what the option produces,
but also what a partial or damaged install looks like — it asks the dynamic linker instead, trying libddwaf.so.2 and
then libddwaf.so. Both names are tried because libddwaf’s own CMake sets no SOVERSION: an install built from
upstream sources is plain libddwaf.so, while a distribution that adds a SOVERSION ships libddwaf.so.2 in its
runtime package and keeps libddwaf.so in -devel. The versioned name is tried first so that the runtime package is
preferred over a development symlink.
Because an unversioned SONAME guarantees no ABI, the loader checks ddwaf_get_version() once the library is in: anything
that is not 2.x is refused.
What the packaging must guarantee#
The library is installed where the dynamic linker looks for it — a normal
/usr/lib64install registered inld.so.cacheis enough, and-develis not required since the versioned name is tried first.LD_LIBRARY_PATHalso works.The package declares a runtime dependency on libddwaf. The library is loaded at import time, not linked at build time, so the build succeeds whether or not libddwaf is installed.
libddwaf 2.x, at least 2.0.0: every
ddwaf_*symbol ddtrace resolves is present in 2.0.0, whose public header is identical to 2.0.1’s.LIBDDWAF_VERSIONinsetup.pyis the version ddtrace pins and tests against, so prefer that one; a 3.x will need a new ddtrace release.
If the library cannot be loaded, or is not 2.x, AppSec logs a warning and disables itself; the rest of the tracer is unaffected. The version actually loaded is reported in telemetry, so a mismatch is visible.
Linux only: elsewhere the runtime has no system library to fall back to, so the build fails rather than produce a package whose AppSec cannot load.
Debugging Build Performance#
The build system compiles many native extensions (CMake C++, Cython, Rust). In CI,
ext_cache.py caches compiled .so files between runs. On a warm run (extensions
already in cache), the entire pip install -e . should complete in under 30s.
How the Build Works#
scripts/run-tests
└─ pip install -e .
├─ build_py → LibraryDownloader.run()
│ ├─ CleanLibraries.remove_artifacts() ← SKIPPED when INCREMENTAL=1
│ └─ LibDDWafDownload.run()
└─ build_ext → CustomBuildExt.run()
├─ build_rust() → Rust _native extension
├─ build_libdd_wrapper() → libdd_wrapper.so (C++)
├─ build_shared_deps() → absl (once, cached by sentinel file)
└─ super().run() → build_extension() for every ext
├─ CMakeExtension → build_extension_cmake()
│ └─ skip if .so newer than sources (INCREMENTAL check)
└─ Cython/C ext → skip if .so newer than .pyx sources
ext_cache.py flow (CI and local testing):
ext_cache.py restore → copies .so files into source tree
pip install -e . → skip checks fire, nothing recompiles
ext_cache.py save → copies .so files into cache
Key Files#
File |
Purpose |
|---|---|
|
|
|
Cache/restore |
|
Standalone abseil build (shared between extensions, built once) |
|
CMake module included by consuming extensions to resolve abseil |
Known Root Causes of Warm Rebuilds#
``CleanLibraries.remove_artifacts()`` deletes restored ``.so`` files
setup.py→LibraryDownloader.run()→CleanLibraries.remove_artifacts()used to wipe all.sofiles unconditionally on every run, defeating any cache restore. Fixed by guarding withif not CustomBuildExt.INCREMENTAL:.CMakeExtension skip check gated on ``IS_EDITABLE``
The skip check
if IS_EDITABLE and self.INCREMENTALnever fired during the test environment’spip install -e .becauseIS_EDITABLEwas never set in that context. Fixed by removing theIS_EDITABLEguard.Cython-generated ``.c`` files poison CMakeExtension hashes
cythonize()writes.cfiles with the current timestamp during the samepip installrun. Those.cfiles appear newer than the just-restored.so, causing a spurious rebuild. Fixed by excluding.c,.so,.dylib,.dll, and.pydfromCMakeExtension.get_sources().No skip check for Cython/C extensions
Cython extensions had no mtime-based skip check, so they always rebuilt even when the
.sowas restored from cache. Fixed by adding anewer_groupcheck using.pyxfile mtimes (not.cfiles, which are touched bycythonize()), plus all.pxdfiles as invalidation inputs.Double extension processing
CustomBuildExt.run()calledsuper().run()then looped over extensions explicitly again. The second pass overwrote timing data inDebugMetadata, masking true rebuild costs. Fixed by removing the explicit loop.