scikit-build-core 0.10

Scikit-build-core 0.10 is out, and it is one of the largest releases we’ve produced in terms of new configuration options. It automatically reads your minimum CMake version, you can tell it to read your scikit-build-core minimum-version, and there are settings for many of the advanced things users used to do with scikit-build (classic), like rebuilds or pure Python fallbacks on build failures.

Auto CMake version

Scikit-build-core 0.10 has a change to the default behavior; if you don’t specify the minimum version of CMake you require, we will now read it from your CMakeLists.txt file. This provides better behaviour out of the box, and you can still manually specify it if you want.

To use it, just write your CMakeLists.txt as you would normally, with something like this at the top:

cmake_minimum_requires(VERSION 3.15...3.30)

Scikit-build-core will warn if your minimum is less than our supported minimum (3.15); set it manually if you support building your package without scikit-build-core and want a lower version.

If we can’t find a minimum, then the default (3.15) will be used; you can force the new auto-search with the special string:

[tool.scikit-build]
cmake.version = "CMakeLists.txt"

Implementing the feature reliably was interesting; we run a custom streaming tokenizer and AST generator for CMake, and this process will stop when it finds this expression. We’ve tested this on every CMakeLists.txt on PyPI (32,000 files) to ensure it is reliable. Because of this, we are not tripped up by multiline comments, if blocks, etc, and will only detect the minimum version if it is at the top level.

Auto scikit-build minimum-version

One of scikit-build-core’s most powerful features is the minimum-version setting. Much like cmake_minimum_requires for CMake, this allows us to change things without breaking existing packages. The above feature, for example, will only apply if your minimum-version is either unspecified or set to 0.10 or greater.

However, there’s an issue: you often specify exactly the same information in your build-system.requires table, and these to values should be updated together. Now you can opt-into single sourcing this! You can do it like this:

[build-system]
requires = ["scikit-build-core>=0.10"]
build-backend = "scikit_build_core.build"

[tool.scikit-build]
minimum-version = "build-system.requires"

This is opt-in only, since it could be surprising if changing the value in this table affects the build. If you specify this, then there must be a lower bound on the scikit-build-core requirement.

Environment markers are supported, so if you have different minimum versions for different versions of Python, for example, that is also supported.

Many new overrides

There are over half a dozen new override options, including several that are much more powerful than anything we’ve had here before. You can look over each one in the changelog, but I want to instead highlight what you can do with them here.

Control what happens on build failure

You can now use a new override, if.failed, to specify a new configuration if the build fails. This can be used to make a pure-Python “fallback” version, for example:

[tool.scikit-build]
wheel.packages = ["compiled/mypkg"]

[[tool.scikit-build.overrides]]
if.failed = true
wheel.cmake = false
wheel.packages = ["pure-python/mypkg"]

If the build fails, then the configuration will be re-evaluated once; if there’s a matching override with if.failed = true, it will return once with the new settings. Above, cmake is no longer used and a different Python package is copied in as “mypkg”.

You could also use this to rebuild with different CMake defines, etc.

Customize what happens if cmake isn’t available

The cmake wheel is great on systems that provide wheels, but has a tendency to struggle to build on systems that don’t have a modern CMake already available and aren’t supported by PyPI wheels. Two new if selectors, if.system-cmake and if.cmake-wheel, have been added to allow you to fully control what happens in every possible case. For example, if you want to have a pure Python fallback if there’s no system CMake and no pre-built wheel available, you could do this:

[tool.scikit-build]
wheel.cmake = false

[[tool.scikit-build.overrides]]
if.any.system-cmake = ">=3.15"
if.any.cmake-wheel = true
wheel.cmake = true

If there’s a system CMake (3.15+) or a known wheel for the platform, this will build binary wheels. Other systems will get pure Python wheels. You can combine this with the build failure above, or with environment variables (if.any.env.<NAME>) as well.

Support a range of scikit-build-core versions

In preparation for removing Python 3.7 support, we are making sure packages can support a range of scikit-build-core versions. You’ve already seen that the auto minimum-version feature supports environment markers; another aspect of this is the new if.scikit-build-version selector. This is special selector: if it is present, unknown selectors are ignored. Combined with the fact that overrides are not processed if the selectors don’t match, you can specify future features in protected overrides!

# WARNING: imaginary example, 0.11 doesn't exist yet!
[build-system]
requires = [
  "scikit-build-core>=0.10; python_version<3.8",
  "scikit-build-core>=0.11; python_version>=3.8",
]

[tool.scikit-build]
minimum-version = "build-system.requires"

[[tool.scikit-build.overrides]]
if.scikit-build-version = ">=0.11"
if.new-selector = true
not-in-010 = "I'm okay"

The above example will work on Python 3.7, because it is going to get a minimum-version of 0.10, and the extra selector will be ignored.

A few other overrides

The other new overrides not mentioned above are if.abi-flags, for detecting free-threaded Python (3.13+) andif.from-sdist, which lets you distinguish source -> wheel vs. sdist -> wheel.

Custom output messages for users

You can now add custom messages for your users with messages.after-failure and messages.after-success. These support .format style keyword arguments; we provide several like sys, platform, and several style options; more can be added by request.

[tool.scikit-build]
messages.after-sucesss = "{green}Wheel successfully built"
messages.after-failure = """
{bold.red}Sorry{normal}, build failed. Your platform is {platform.platform}.
"""

You can provide debugging information for failed builds, for example. These are very useful when mixed with overrides to tell users if a pure or compiled wheel was built, etc.

Color system

The messages.* options expose a redesigned color system that scikit-build-core uses to colorize its messages. These include the main eight terminal colors and the most common terminal styles like bold, italic, and underline. You can chain these together, like bold.red to produce optimized escape codes, as well. default is the default color, normal is the default style, and reset brings everything back to normal.

On Windows or when using pip, you need FORCE_COLOR to get colors.

Fail a build based on Python version

If you know you don’t support a platform or Python version, you can set the new fail option to immediately fail the build. Combined with the messages.after-failure, you can provide helpful information as to why it is failing. For example:

[[tool.scikit-build.overrides]]
if.python-version = ">=3.13"
fail = true
messages.after-failure = """
This package doesn't support Python 3.13 yet.
"""

This is much better than setting project.requires-python with an upper cap, which doesn’t work as you probably would expect, either causing an older version to be selected (its purpose is to allow back-solving for older versions, after all), or producing a “no package found” message.

As a reminder, only use this if you know for certain the next version of Python won’t be supported! This is only true for a very small number of packages.

You can use fail = true with any other selector - or even in the main table (though if you don’t then override it to be false, you will have made a very boring package!)

Regex plugin improvements

The regex plugin has been expanded with two new features to enable a much wider range of possible versions read in: result=, which gives you complete control over how the regex is processed, and remove=, which can filter out unused bits of the generated version.

Here’s how nanobind could use this, for example:

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

There are four parts which are then combined into a result, and if the result ends in dev0, that is stripped from the version number.

Other changes

cmake.targets and cmake.verbose have been moved to build.targets and build.verbose. As usual, warnings and errors are handled by your minimum-version setting.

We now warn if you put "cmake" or "ninja" in your build-system.requires. You should not do this; we add the dependencies for you only if they are needed.

We now support nested gitignores, as well as local git ignores.

Be sure to check out our docs again if you’ve not looked at them in a while, they’ve been improved a bit with more pages (including a dedicated overrides page and a projects page).

There aren’t a huge number of fixes in this release, as we’ve been backporting most of them to the 0.9.x series. We do now detect manual generator selections in CMake args and respect that when producing the dynamic build requirements.

In the future

We’ve completed most of the core set of features we expect to be present for scikit-build-core 1.0. There are just a few things left. We plan to work on the plugins (setuptools and hatching), and pull them out into separate packages. The setuptools one will be used as the backend for scikit-build (classic), removing the need to maintain two build systems and is planned to be the “1.0” point for both packages.

We also will be removing Python 3.7 support in the near future, probably just before 1.0.

We are working on cython-cmake and f2py-cmake, modern replacements for the helpers in scikit-build (classic). Feel free to try them out, and you can embed them into your cmake folder to allow direct CMake runs as well as protect you from changes as we finalize the interfaces to these packages.

And we are working on standardizing dynamic-metadata; feel free to chime in on that package if you have design considerations!

Conclusion

We are excited to release the highly-capable scikit-build-core 0.10. Several packages have been waiting on this release to transition from older build systems. We’ve got new documentation pages, including on highlighting some of our main users. If you have questions, feel free to visit our monthly community meeting; it takes place at noon EST/EDT every third Friday on Google Meet.