Skip to content
Merged
31 changes: 29 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,17 +413,44 @@ You can select a different build type, such as `Debug`:

```

You can specify CMake defines:
You can specify CMake defines as strings or bools:

````{tab} pyproject.toml

```toml
[tool.scikit-build.cmake.define]
SOME_DEFINE = "ON"
SOME_DEFINE = "Foo"
SOME_OPTION = true
```

````

You can even specify a CMake define as a list of strings:

````{tab} pyproject.toml

```toml
[tool.scikit-build.cmake.define]
FOOD_GROUPS = [
"Apple",
"Lemon;Lime",
"Banana",
"Pineapple;Mango",
]
```

````

Semicolons inside the list elements will be escaped with a backslash (`\`) and
the resulting list elements will be joined together with semicolons (`;`) before
being converted to command-line arguments.

:::{versionchanged} 0.11

Support for list of strings.

:::

`````{tab} config-settings


Expand Down
7 changes: 1 addition & 6 deletions src/scikit_build_core/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,7 @@ def configure(
cmake_defines["CMAKE_OSX_ARCHITECTURES"] = ";".join(archs)

# Add the pre-defined or passed CMake defines
cmake_defines.update(
{
k: ("TRUE" if v else "FALSE") if isinstance(v, bool) else v
for k, v in self.settings.cmake.define.items()
}
)
cmake_defines.update(self.settings.cmake.define)

self.config.configure(
defines=cmake_defines,
Expand Down
12 changes: 12 additions & 0 deletions src/scikit_build_core/resources/scikit-build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
},
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
Expand All @@ -58,6 +64,12 @@
},
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
Expand Down
2 changes: 2 additions & 0 deletions src/scikit_build_core/settings/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ def convert_type(t: Any, *, normalize_keys: bool) -> dict[str, Any]:
}
if origin is Literal:
return {"enum": list(args)}
if hasattr(t, "json_schema"):
return convert_type(t.json_schema, normalize_keys=normalize_keys)

msg = f"Cannot convert type {t} to JSON Schema"
raise FailedConversionError(msg)
25 changes: 24 additions & 1 deletion src/scikit_build_core/settings/skbuild_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"BackportSettings",
"BuildSettings",
"CMakeSettings",
"CMakeSettingsDefine",
"EditableSettings",
"GenerateSettings",
"InstallSettings",
Expand All @@ -27,6 +28,28 @@ def __dir__() -> List[str]:
return __all__


class CMakeSettingsDefine(str):
"""
A str subtype for automatically normalizing bool and list values
to the CMake representation in the `cmake.define` settings key.
"""

json_schema = Union[str, bool, List[str]]

def __new__(cls, raw: Union[str, bool, List[str]]) -> "CMakeSettingsDefine":
def escape_semicolons(item: str) -> str:
return item.replace(";", r"\;")

if isinstance(raw, bool):
value = "TRUE" if raw else "FALSE"
elif isinstance(raw, list):
value = ";".join(map(escape_semicolons, raw))
else:
value = raw

return super().__new__(cls, value)


@dataclasses.dataclass
class CMakeSettings:
minimum_version: Optional[Version] = None
Expand All @@ -49,7 +72,7 @@ class CMakeSettings:
in config or envvar will override toml. See also ``cmake.define``.
"""

define: Annotated[Dict[str, Union[str, bool]], "EnvVar"] = dataclasses.field(
define: Annotated[Dict[str, CMakeSettingsDefine], "EnvVar"] = dataclasses.field(
default_factory=dict
)
"""
Expand Down
20 changes: 20 additions & 0 deletions tests/packages/cmake_defines/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.15)
project(cmake_defines LANGUAGES NONE)

set(ONE_LEVEL_LIST
""
CACHE STRING "")
set(NESTED_LIST
""
CACHE STRING "")

set(out_file "${CMAKE_CURRENT_BINARY_DIR}/log.txt")
file(WRITE "${out_file}" "")

foreach(list IN ITEMS ONE_LEVEL_LIST NESTED_LIST)
list(LENGTH ${list} length)
file(APPEND "${out_file}" "${list}.LENGTH = ${length}\n")
foreach(item IN LISTS ${list})
file(APPEND "${out_file}" "${item}\n")
endforeach()
endforeach()
11 changes: 11 additions & 0 deletions tests/packages/cmake_defines/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[tool.scikit-build]
cmake.version = '>=3.15'

[tool.scikit-build.cmake.define]
ONE_LEVEL_LIST = [
"Foo",
"Bar",
"ExceptionallyLargeListEntryThatWouldOverflowTheLine",
"Baz",
]
NESTED_LIST = [ "Apple", "Lemon;Lime", "Banana" ]
38 changes: 38 additions & 0 deletions tests/test_cmake_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
import shutil
import sysconfig
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING

import pytest
from packaging.specifiers import SpecifierSet
from packaging.version import Version

from scikit_build_core.builder.builder import Builder
from scikit_build_core.cmake import CMake, CMaker
from scikit_build_core.errors import CMakeNotFoundError
from scikit_build_core.settings.skbuild_read_settings import SettingsReader

if TYPE_CHECKING:
from collections.abc import Generator
Expand Down Expand Up @@ -201,6 +204,41 @@ def test_cmake_paths(
assert len(fp.calls) == 2


@pytest.mark.configure
def test_cmake_defines(
tmp_path: Path,
):
source_dir = DIR / "packages" / "cmake_defines"
binary_dir = tmp_path / "build"

config = CMaker(
CMake.default_search(),
source_dir=source_dir,
build_dir=binary_dir,
build_type="Release",
)

reader = SettingsReader.from_file(source_dir / "pyproject.toml")

builder = Builder(reader.settings, config)
builder.configure(defines={})

configure_log = Path.read_text(binary_dir / "log.txt")
assert configure_log == dedent(
"""\
ONE_LEVEL_LIST.LENGTH = 4
Foo
Bar
ExceptionallyLargeListEntryThatWouldOverflowTheLine
Baz
NESTED_LIST.LENGTH = 3
Apple
Lemon;Lime
Banana
"""
)


def test_get_cmake_via_envvar(monkeypatch: pytest.MonkeyPatch, fp):
monkeypatch.setattr("shutil.which", lambda x: x)
cmake_path = Path("some-prog")
Expand Down
24 changes: 20 additions & 4 deletions tests/test_skbuild_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,8 @@ def test_skbuild_settings_pyproject_toml_envvar_defines(
"a": "1",
"b": "2",
"c": "empty",
"d": False,
"e": False,
"d": "FALSE",
"e": "FALSE",
}

monkeypatch.setenv("DEFAULT", "3")
Expand All @@ -552,8 +552,8 @@ def test_skbuild_settings_pyproject_toml_envvar_defines(
"a": "1",
"b": "2",
"c": "3",
"d": False,
"e": True,
"d": "FALSE",
"e": "TRUE",
}


Expand Down Expand Up @@ -754,3 +754,19 @@ def test_skbuild_settings_auto_cmake_warning(
Report this or (and) set manually to avoid this warning. Using 3.15 as a fall-back.
""".split()
)


def test_skbuild_settings_cmake_define_list():
pyproject_toml = (
Path(__file__).parent / "packages" / "cmake_defines" / "pyproject.toml"
)

config_settings: dict[str, list[str] | str] = {}

settings_reader = SettingsReader.from_file(pyproject_toml, config_settings)
settings = settings_reader.settings

assert settings.cmake.define == {
"NESTED_LIST": r"Apple;Lemon\;Lime;Banana",
"ONE_LEVEL_LIST": "Foo;Bar;ExceptionallyLargeListEntryThatWouldOverflowTheLine;Baz",
}
Loading