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:
- Deploys them to
build/.conan-libs/under the project root. - Patches the extension RPATH so that repair tools can discover and bundle
those libraries. On Linux this patching is done with
patchelf --add-rpathduringbuild_wheel()itself, before any repair tool runs, sopatchelfmust already be available on the build machine (it is preinstalled on manylinux images) rather than only at the laterauditwheelstep.
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 withImportErroron 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:
- Linux —
auditwheel repair - macOS —
delocate-wheel - Windows —
delvewheel repair
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.testsor__pycache__match those directories anywhere, and*.pycmatchessrc/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 forwheel.exclude)./NOTICEmatches only the top-level file, andpatches/*.patchonly the patches directly inpatches/. *stays inside one directory, and**crosses directories, as inthird_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,excludeis simpler, since it wins overinclude.
[tool.conan-py-build.sdist]
include = ["docs/"]
exclude = [".github", "tests"]