Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TOML lists in cmake.define #921

Merged
merged 12 commits into from
Oct 2, 2024
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]]
LecrisUT marked this conversation as resolved.
Show resolved Hide resolved

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