Dynamic metadata

Scikit-build-core supports dynamic metadata with four built-in plugins.

Note

Beyond the built-in plugins, your package and third parties can provide their own; they are fully supported through the standard [[tool.dynamic-metadata]] interface described below.

The [[tool.dynamic-metadata]] table

Added in version 1.0: Support for the standard dynamic-metadata 0.4 specification.

The standard, cross-backend way to configure plugins is the top-level [[tool.dynamic-metadata]] ordered array of tables. Each entry names a provider: either the name an installed plugin registers in the dynamic_metadata.provider entry-point group (scikit-build-core’s built-ins register their module path, e.g. scikit_build_core.metadata.regex), or an inline { path, module } table for a plugin living inside your own project. Every other key is passed to that plugin as its settings. Entries run in order, so a later entry sees every field an earlier one produced (no dependency graph, no cycles), and a plugin can read an already-resolved field with project[...].

[project]
name = "mypackage"
dynamic = ["version"]

[[tool.dynamic-metadata]]
provider = "scikit_build_core.metadata.regex"
field = "version"
input = "src/mypackage/__init__.py"

The field-agnostic plugins (regex, template) can target any field, chosen with a field setting. An inline provider = { path, module } table (see Custom plugins) lets a plugin live inside your own project. Following PEP 808, a list or table field can be given a static value in [project] and listed in dynamic, in which case a provider only adds to it; the single-value fields (version, description, requires-python, license, readme) cannot be both static and dynamic.

Warning

The older [tool.scikit-build.metadata.<field>] table (a field-keyed mapping rather than an ordered array) is superseded by [[tool.dynamic-metadata]], and since 1.0 it emits a deprecation warning unless minimum-version is set below 1.0. It still works, but the two forms cannot be combined in one project, and plugins not shipped with scikit-build-core additionally require tool.scikit-build.experimental = true in the legacy form. The plugin examples below show both spellings as Modern/Classic tabs; the example above would be spelled:

[tool.scikit-build.metadata.version]
provider = "scikit_build_core.metadata.regex"
input = "src/mypackage/__init__.py"

Built-in plugins

We provide some built-in plugins in scikit_build_core.metadata. These work in either mode with the same provider string (e.g. scikit_build_core.metadata.regex). A field = key is normally required, but it can be omitted for fixed-target plugins that declare a default: setuptools_scm defaults to field = "version" and fancy_pypi_readme defaults to field = "readme".

version: Setuptools-scm

You can use setuptools-scm to pull the version from VCS:

[project]
name = "mypackage"
dynamic = ["version"]

[tool.scikit-build]
sdist.include = ["src/package/_version.py"]

[[tool.dynamic-metadata]]
provider = "scikit_build_core.metadata.setuptools_scm"
field = "version"

[tool.setuptools_scm]  # Section required
write_to = "src/package/_version.py"
[project]
name = "mypackage"
dynamic = ["version"]

[tool.scikit-build]
metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
sdist.include = ["src/package/_version.py"]

[tool.setuptools_scm]  # Section required
write_to = "src/package/_version.py"

This sets the python project version according to git tags or a .git_archival.txt file, or equivalents for other VCS systems.

Tip

If you need to set the CMake project version without scikit-build-core (which provides ${SKBUILD_PROJECT_VERSION}), you can use something like DynamicVersion module from github.com/LecrisUT/CMakeExtraUtils:

# Import `CMakeExtraUtils` or bundle `DynamicVersion.cmake` from there
include(DynamicVersion)

# Set ${PROJECT_VERSION} according to git tag or `.git_archival.txt`
dynamic_version()

project(MyPackage VERSION ${PROJECT_VERSION})

Regex

If you want to pull a string-valued expression (usually version) from an existing file, you can use the integrated regex plugin, as shown in the first example on this page.

You can set a custom regex with regex=. By default when targeting version, you get a reasonable regex for python files, '(?i)^(__version__|VERSION)(?: ?\: ?str)? *= *([\'"])v?(?P<value>.+?)\2'. You can set result to a format string to process the matches; the default is "{value}". You can also specify a regex for remove= which will strip any matches from the final result. A more complex example:

[[tool.dynamic-metadata]]
provider = "scikit_build_core.metadata.regex"
field = "version"
input = "src/mypackage/version.hpp"
regex = '''(?sx)
\#define \s+ VERSION_MAJOR \s+ (?P<major>\d+) .*?
\#define \s+ VERSION_MINOR \s+ (?P<minor>\d+) .*?
\#define \s+ VERSION_PATCH \s+ (?P<patch>\d+) .*?
\#define \s+ VERSION_DEV   \s+ (?P<dev>\d+)   .*?
'''
result = "{major}.{minor}.{patch}dev{dev}"
remove = "dev0"
[tool.scikit-build.metadata.version]
provider = "scikit_build_core.metadata.regex"
input = "src/mypackage/version.hpp"
regex = '''(?sx)
\#define \s+ VERSION_MAJOR \s+ (?P<major>\d+) .*?
\#define \s+ VERSION_MINOR \s+ (?P<minor>\d+) .*?
\#define \s+ VERSION_PATCH \s+ (?P<patch>\d+) .*?
\#define \s+ VERSION_DEV   \s+ (?P<dev>\d+)   .*?
'''
result = "{major}.{minor}.{patch}dev{dev}"
remove = "dev0"

This will remove the “dev” tag when it is equal to 0.

Changed in version 0.10: Support for result and remove added.

readme: Fancy-pypi-readme

You can use hatch-fancy-pypi-readme to render your README:

[project]
name = "mypackage"
dynamic = ["readme"]

[[tool.dynamic-metadata]]
provider = "scikit_build_core.metadata.fancy_pypi_readme"
field = "readme"

# tool.hatch.metadata.hooks.fancy-pypi-readme options here
[project]
name = "mypackage"
dynamic = ["readme"]

[tool.scikit-build]
metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme"

# tool.hatch.metadata.hooks.fancy-pypi-readme options here

In order to use the version number in readme feature, this must be listed after the version in the [[tool.dynamic-metadata]] mode.

Changed in version 0.11.2: The version number feature now works.

Template

You can access other metadata fields and produce templated outputs.

[[tool.dynamic-metadata]]
provider = "scikit_build_core.metadata.template"
field = "optional-dependencies"
result = {"dev" = ["{project[name]}=={project[version]}"]}
[tool.scikit-build.metadata.optional-dependencies]
provider = "scikit_build_core.metadata.template"
result = {"dev" = ["{project[name]}=={project[version]}"]}

You can use project to access the current metadata values. You can use result to specify the output. The result must match the type of the metadata field you are writing to.

You can reference other dynamic metadata fields. With the legacy tool.scikit-build.metadata table they are resolved on demand, so the order in the file does not matter; with [[tool.dynamic-metadata]] entries run top to bottom, so any referenced field must be produced by an earlier entry (or be static in [project]).

Added in version 0.11.2.

Custom plugins

Added in version 1.0: Writing a custom dynamic-metadata plugin through the standard dynamic-metadata 0.4 interface.

You can write your own plugins. Full details are in the dynamic metadata docs. Here’s a quick overview.

There’s one required hook:

def dynamic_metadata(
    settings: Mapping[str, Any],
    project: Mapping[str, Any],
) -> dict[str, Any]: ...  # return a fragment of [project], e.g. {"version": ...}

And several optional ones (build_state, dynamic_wheel, and get_requires_for_dynamic_metadata). You can optionally use a class, as well. Fields a plugin reports via dynamic_wheel as possibly changing between the SDist and a wheel built from it are marked Dynamic in the SDist’s PKG-INFO (METADATA 2.2, or 2.6 when the field also has a static value in [project], per PEP 808); version may never change in this sense.

A plugin distributed as a package is referenced by the name it registers in the dynamic_metadata.provider entry-point group. A plugin that lives inside your own project instead uses an inline { path, module } table, no packaging or entry point required:

[[tool.dynamic-metadata]]
provider = {path = "helpers/plugins", module = "my_plugin"}

module may also be "my_plugin:MyClass" to load a class (instantiated with no arguments, so its hooks share state through self).

build-system.requires: Scikit-build-core’s build.requires

If you need to inject and manipulate additional build-system.requires, you can use the build.requires. This is intended to be used in combination with Overrides.

This is not project metadata (nothing is listed in [project] dynamic), but like the plugins above it resolves at build time, injecting the additional build-system.requires.

[project]
name = "mypackage"

[tool.scikit-build]
build.requires = ["foo"]

[[tool.scikit-build.overrides]]
if.from-sdist = false
build.requires = ["foo @ {root:uri}/foo"]

This example shows a common use-case where the package has a default build-system.requires pointing to the package foo in the PyPI index, but when built from the original git checkout or equivalent, the local folder is used as dependency instead by resolving the {root:uri} to a file uri pointing to the folder where the pyproject.toml is located.

Note

In order to be compliant with the package index, when building from sdist, the build.requires MUST NOT have any @ redirects. This rule may be later enforced explicitly.

Added in version 0.11.

Generate files with dynamic metadata

You can write out metadata to file(s) as well. Other info might become available here in the future, but currently it supports anything available as strings in metadata. (Note that arrays like this are only supported in TOML configuration.)

[[tool.scikit-build.generate]]
path = "package/_version.py"
template = '''
version = "${version}"
'''

template or template-path is required; this uses string.Template formatting. There are three options for output location; location = "install" (the default) will go to the wheel, location = "build" will go to the CMake build directory, and location = "source" will write out to the source directory (be sure to .gitignore this file. It will automatically be added to your SDist includes. It will overwrite existing files).

The path is generally relative to the base of the wheel / build dir / source dir, depending on which location you pick.