Skip to content

Configuration

Config settings (-C)

Passed per-invocation via pip wheel … -C <key>=<value>:

Option Description Default
host-profile Conan profile for host context default
build-profile Conan profile for build context default
build-dir Persistent build directory temp dir

A persistent build-dir keeps Conan's build tree across runs so recompilation is incremental. The staging area that becomes the wheel is deleted and recreated on every build, so wheel contents never carry files from a previous build.

pyproject.toml

All project-level options live under [tool.conan-py-build]:

Option TOML section Description Default
conanfile-path [tool.conan-py-build] Path to the Conan recipe, relative to project root "."
extra-profile [tool.conan-py-build] Extra Conan profile composed on top of the active one (none)
extra-arguments [tool.conan-py-build] Extra Conan CLI flags appended to conan build and conan export-pkg []
clean-after-wheel [tool.conan-py-build] Remove the exported Conan reference from the cache after building the wheel true
local-recipes [tool.conan-py-build] Folders with Conan recipes to export to the cache before the build []
version.file [tool.conan-py-build.version] Python file with __version__ (none)
version.provider [tool.conan-py-build.version] "setuptools_scm" for version from git tags (none)
packages [tool.conan-py-build.wheel] Paths to Python packages in the wheel ["src/<name>"]
exclude [tool.conan-py-build.wheel] Patterns (.gitignore syntax) to drop files from the wheel []
include / exclude [tool.conan-py-build.sdist] Patterns (.gitignore syntax) to add/remove from the sdist [] / []

Variants for extra profiles: extra-profile-host, extra-profile-build, extra-profile-all. Paths relative to project root.

For one-off Conan overrides without shipping a separate profile file, use extra-arguments (see Extra Conan arguments below).

Default packages is src/<normalized_project_name> (hyphens → underscores).

Set packages = [] when there is no Python package directory to copy from the source tree, for example when the wheel contains a single top-level extension module. In that case the wheel payload comes entirely from what Conan's package() stages, so the extension has to be installed at the root of the Conan package folder (install(TARGETS mymod DESTINATION .) with CMake). Omitting packages keeps the default src/<name> behavior, including its validation.

Version

Declare it once, in [project], and read it back from installed metadata at runtime:

from importlib.metadata import version

__version__ = version("mypackage")  # [project].name, not the import name

A recipe used through conan-py-build doesn't need to declare it either: the backend passes the resolved version to Conan, so a recipe can read self.version directly (e.g. in conandata.yml lookups).

Dynamic version

When the version lives elsewhere, set dynamic = ["version"] and pick one:

[tool.conan-py-build.version]
file = "src/mypackage/__init__.py"
[tool.conan-py-build.version]
provider = "setuptools_scm"

version.file and version.provider are mutually exclusive. See [tool.setuptools_scm] for setuptools-scm options.

Profiles

The wheel is built against the interpreter running the build (your venv, or the one cibuildwheel provides); pick the Python version by running the build with it.

A custom host-profile is optional. The default profile works for plain native builds. Pass one when you need extra settings (the bundled macOS profile, for example, pins the deployment target and platform tag):

pip wheel . --no-build-isolation \
    -C host-profile=examples/profiles/macos.jinja \
    -C build-dir=./build \
    -w dist/

An extra-profile in pyproject.toml is applied on top of the active profile — useful for enforcing e.g. compiler.cppstd=17:

[tool.conan-py-build]
extra-profile = "cpp17.profile"

Wheel tags (WHEEL_PYVER, WHEEL_ABI, WHEEL_ARCH)

By default the backend reads the wheel filename tags (interpreter, ABI, platform) from the running Python interpreter. When you cross-compile or otherwise need to force the tags, set these three variables in the profile's [buildenv] to override them:

Variable Wheel tag Example
WHEEL_PYVER Interpreter cp312, py3
WHEEL_ABI ABI cp312, abi3, none
WHEEL_ARCH Platform manylinux_2_28_x86_64, macosx_11_0_arm64, win_amd64

Each variable independently overrides its auto-detected value. Auto-detection always runs from the current interpreter, so for a normal native build you do not need to set any of them. You only override the tag that the build interpreter cannot report correctly.

For example, building an x86_64 wheel on an arm64 macOS runner: the interpreter is arm64, so only its platform tag is wrong and needs pinning (the interpreter/ABI tags are still detected correctly):

[settings]
arch=x86_64

[buildenv]
WHEEL_ARCH=macosx_11_0_x86_64

The resulting wheel filename will be, for example, mypackage-0.1.0-cp312-cp312-macosx_11_0_x86_64.whl. A working macOS profile lives under examples/profiles/.

Conan home

The backend uses Conan's default home (~/.conan2, or CONAN_HOME / .conanrc). Set CONAN_PY_BUILD_PROFILE_AUTODETECT=1 to autodetect the profile instead of requiring default.

Editable installs

An editable install, such as pip install -e . or the project install that uv sync and uv run do, builds and installs a normal wheel. The environment gets its own copy of the Python files, so changes to them, like changes to compiled code, apply only after installing again.

uv sync and uv run do not reinstall the project when you edit its sources, so run uv sync --reinstall-package <name> after a change. To have uv reinstall on its own, list your sources in cache-keys.

Each reinstall runs the whole build again. A persistent build-dir (see Config settings) keeps the build tree, so only what changed is recompiled. With uv, set it in [tool.uv] as config-settings = { build-dir = "build" }.

Extra Conan arguments

CLI flags appended to conan build and conan export-pkg. Symmetric with extra-profile, with higher precedence — CLI flags win against any profile entry.

Bump compiler.cppstd to match a transitive dep (e.g. gdal/3.12.1 requires C++17, MSVC defaults to 14):

[tool.conan-py-build]
extra-arguments = ["-s=compiler.cppstd=17"]

Disable an optional dep feature and pin parallelism:

[tool.conan-py-build]
extra-arguments = [
    "-o=gdal/*:with_arrow=False",
    "-c=tools.build:jobs=4",
]

For values with embedded double quotes (dict / list [conf] literals), use TOML literal strings ('...'):

extra-arguments = [
    '-c=tools.build:cflags+=["-O2", "-fPIC"]',
    '-c=tools.cmake.cmaketoolchain:extra_variables={"FOO": "bar"}',
]

Any Conan CLI flag works: -s / -o / -c (host), -s:b / -o:b / -c:b (build context), --build=..., --lockfile=.... Pair-form (["-s", "compiler.cppstd=17"]) is also accepted.

Local recipes

When a dependency has no recipe in any of your Conan remotes, ship the recipe with the project and list its folder in local-recipes:

[tool.conan-py-build]
local-recipes = ["recipes/hello"]
myproject/
├── pyproject.toml
├── conanfile.py          # self.requires("hello/0.1.0")
└── recipes/
    └── hello/
        ├── conanfile.py  # name = "hello", version = "0.1.0"
        └── conandata.yml # sources: url and sha256 for 0.1.0

Before the build, the backend runs conan export on each folder, so the requirement resolves from the Conan cache. conan build then builds it from source, as it does with any requirement that has no binary.

Each folder holds a conanfile.py that declares its name and version, next to any file it exports, such as conandata.yml or patches. Recipes from conan-center-index leave version out, so add it when you copy one, and list the folder that holds the conanfile.py (for example recipes/hello/all).

Folders must be inside the project, and the sdist includes them, so a build from the sdist exports them again. If a recipe copies files from outside its folder in export_sources(), add those files to sdist.include. Otherwise a build from the sdist exports the recipe without them and fails when it builds that dependency.

The exported recipes stay in the Conan cache after the build, as clean-after-wheel only removes the project's own package. Each build exports them again and makes them the latest revision of their reference. Another project in the same Conan home that requires the same reference gets this recipe too.

The local-recipes example builds bindings for a library that is not in ConanCenter.

Entry points (PEP 621)

[project.scripts], [project.gui-scripts] and [project.entry-points.*] from pyproject.toml are written to <pkg>.dist-info/entry_points.txt in the wheel, per the PyPA entry points specification. Installers create the corresponding console/GUI wrappers at install time; runtime tools like importlib.metadata.entry_points() read them from this file.

[project.scripts]
mycli = "mypackage.cli:main"

[project.gui-scripts]
mygui = "mypackage.gui:run"

[project.entry-points."myplugin.hooks"]
on_event = "mypackage.hooks:on_event"

The file is only written when at least one entry point is declared.

License files (PEP 639)

Set license-files in [project] (e.g. ["LICENSE"]) to include license files in the wheel .dist-info/licenses/ and sdist PKG-INFO.

Shared libraries

When your extension links to Conan-provided shared libraries, the backend:

  1. Deploys them to build/.conan-libs/ under the project root.
  2. Patches the extension RPATH so that repair tools can discover and bundle those libraries. On Linux this patching is done with patchelf --add-rpath during build_wheel() itself, before any repair tool runs, so patchelf must already be available on the build machine (it is preinstalled on manylinux images) rather than only at the later auditwheel step.

Important: when Conan shared libraries are deployed, the wheel returned by build_wheel() is an intermediate artifact. It contains an absolute RPATH pointing to the .conan-libs/ build directory but does not bundle the libraries themselves. Installing this wheel locally may appear to work as long as .conan-libs/ still exists on disk, but will fail with ImportError on any other machine or after the build directory is removed. Always run a repair step before installing or distributing.

Run the appropriate repair tool after building:

cibuildwheel runs the right tool automatically on Linux and macOS. On Windows, add this to your pyproject.toml:

[tool.cibuildwheel.windows]
before-all = "pip install delvewheel"
repair-wheel-command = 'delvewheel repair --add-path "{project}/build/.conan-libs" -w "{dest_dir}" "{wheel}"'

The .conan-libs/ directory is a build artifact and can be deleted after the repair step completes.

Static-only builds are unaffected: when no Conan shared libraries are deployed the backend is a no-op and the wheel is self-contained.

System libraries and ABI risk

Each repair tool excludes libraries it considers part of the host system and does not bundle them into the wheel:

  • auditwheel (Linux) uses an explicit allowlist of named libraries guaranteed on manylinux systems (e.g. libz.so.1, libstdc++.so.6, libm.so.6).
  • delocate (macOS) excludes any library whose install name starts with /usr/lib/ or /System/Library/ (e.g. /usr/lib/libz.dylib).
  • delvewheel (Windows) maintains a list of known Windows DLLs (e.g. kernel32.dll, user32.dll) that are assumed present on all supported Windows versions.

When an excluded library is found in .conan-libs/, the repair tool skips it and the wheel loads it from the target system at runtime instead.

This means that if you build against a Conan-provided version of a system library (e.g. zlib/1.3.2 with shared=True), the wheel will silently use the system zlib at runtime. If the system version is older and lacks a symbol your code depends on, the import will fail with undefined symbol, a bug that passes repair tools undetected.

If your code uses symbols that exist in the Conan-provided version but not in the system version on your target machines, the import will fail at runtime despite the wheel passing the repair step. Linking that library statically removes the dependency on the system version. You can inspect which symbols your extension imports from a given library with nm -D -u (Linux), nm -u (macOS), or dumpbin /imports (Windows). On Windows, /exports lists symbols the binary provides, not the ones it depends on.

Wheel exclude

Use wheel.exclude to drop files from the wheel that live inside a package directory but are only needed at build time (e.g. C/C++ binding sources co-located with the Python package):

[tool.conan-py-build.wheel]
packages = ["src/mypkg"]
exclude = ["binding/*.cpp", "binding/*.h"]

Patterns are relative to each package root and use the same .gitignore syntax as the sdist options (see Sdist defaults).

Sdist defaults

Included: pyproject.toml, conanfile.py, your build system's top-level file (CMakeLists.txt, meson.build), cmake/, src/, include/, README, LICENSE. Excluded: __pycache__, *.pyc, .git, build, dist, the tool environments .venv, .tox and .nox, and compiled extensions (*.so, *.pyd, *.dylib) left behind by an in-place or editable build.

Whatever the backend itself reads is added too, wherever it lives, so you never repeat it under include: every path in wheel.packages, version.file (or setuptools-scm's version_file), conandata.yml next to the recipe, every extra-profile variant, and every local-recipes folder.

Paths your own build reads, such as sources or patches outside src/, are still yours to add with include. Exclusions win over include: an entry never adds a file that exclude or a default exclusion drops. An entry that adds no files prints a warning, whether it matches nothing or only excluded files, such as anything under build/.

Patterns in include, exclude and wheel.exclude use .gitignore syntax:

  • A pattern with no /, or only a trailing one, matches at any level. tests or __pycache__ match those directories anywhere, and *.pyc matches src/foo.pyc.
  • A pattern with a / at the start or in the middle is relative to the root (the project root, or each package root for wheel.exclude). /NOTICE matches only the top-level file, and patches/*.patch only the patches directly in patches/.
  • * stays inside one directory, and ** crosses directories, as in third_party/**/*.h.
  • A pattern that matches a directory matches everything inside it.
  • A pattern starting with ! undoes what earlier patterns in the same list matched. To leave files out of the sdist, exclude is simpler, since it wins over include.
[tool.conan-py-build.sdist]
include = ["docs/"]
exclude = [".github", "tests"]