Skip to content

Commit

Permalink
feat: if failed (retry)
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii committed Jul 19, 2024
1 parent 2b3e4d2 commit f014420
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 17 deletions.
13 changes: 13 additions & 0 deletions docs/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ and `metadata_editable`. Takes a regex.
Note that you can build directly to wheel; you don't have to go through an
SDist.

### `failed` (bool)

This override is a bit special. If a build fails, scikit-build-core will check
to see if there'a a matching `failed = true` override. If there is, the the build will
be retried once with the new settings. This can be used to build a pure-Python fallback
if a build fails, for example:

```toml
[[tool.scikit-build.overrides]]
if.failed = true
wheel.cmake = false
```

## Any matching condition

If you use `if.any` instead of `if`, then the override is true if any one of the
Expand Down
2 changes: 1 addition & 1 deletion src/scikit_build_core/build/_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ def __dir__() -> list[str]:
@functools.lru_cache(1)
def setup_logging(log_level: str) -> None:
level_value = LEVEL_VALUE[log_level]
logger.setLevel(level_value)

ch = logging.StreamHandler()
ch.setLevel(level_value)
# create formatter and add it to the handlers
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
Expand Down
63 changes: 58 additions & 5 deletions src/scikit_build_core/build/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@
import sysconfig
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from packaging.requirements import Requirement
from packaging.utils import canonicalize_name

from .. import __version__
from .._compat import tomllib
from .._compat.typing import Literal, assert_never
from .._logging import logger, rich_print
from .._logging import LEVEL_VALUE, logger, rich_print
from .._shutil import fix_win_37_all_permissions
from ..builder.builder import Builder, archs_to_tags, get_archs
from ..builder.wheel_tag import WheelTag
from ..cmake import CMake, CMaker
from ..errors import FailedLiveProcessError
from ..settings.skbuild_read_settings import SettingsReader
from ._editable import editable_redirect, libdir_to_installed, mapping_to_modules
from ._init import setup_logging
Expand Down Expand Up @@ -123,6 +124,7 @@ def _build_wheel_impl(
) -> WheelImplReturn:
"""
Build a wheel or just prepare metadata (if wheel dir is None). Can be editable.
Handles one retry attempt if "failed" override present.
"""
state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"]
if exit_after_config:
Expand All @@ -136,9 +138,10 @@ def _build_wheel_impl(
with pyproject_path.open("rb") as ft:
pyproject = tomllib.load(ft)

settings_reader = SettingsReader(pyproject, config_settings or {}, state=state)
settings = settings_reader.settings
setup_logging(settings.logging.level)
settings_reader = SettingsReader(
pyproject, config_settings or {}, state=state, retry=False
)
setup_logging(settings_reader.settings.logging.level)

settings_reader.validate_may_exit()

Expand All @@ -156,6 +159,56 @@ def _build_wheel_impl(
"ninja should not be in build-system.requires - scikit-build-core will inject it as needed"
)

try:
return _build_wheel_impl_impl(
wheel_directory,
metadata_directory,
exit_after_config=exit_after_config,
editable=editable,
state=state,
settings=settings_reader.settings,
pyproject=pyproject,
)
except FailedLiveProcessError as err:
settings_reader = SettingsReader(
pyproject, config_settings or {}, state=state, retry=True
)
if "failed" not in settings_reader.overrides:
raise

Check warning on line 177 in src/scikit_build_core/build/wheel.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/build/wheel.py#L177

Added line #L177 was not covered by tests

rich_print(
f"\n[yellow bold]*** {' '.join(err.args)} - retrying due to override..."
)

logger.setLevel(LEVEL_VALUE[settings_reader.settings.logging.level])

settings_reader.validate_may_exit()

return _build_wheel_impl_impl(
wheel_directory,
metadata_directory,
exit_after_config=exit_after_config,
editable=editable,
state=state,
settings=settings_reader.settings,
pyproject=pyproject,
)


def _build_wheel_impl_impl(
wheel_directory: str | None,
metadata_directory: str | None,
*,
exit_after_config: bool = False,
editable: bool,
state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"],
settings: ScikitBuildSettings,
pyproject: dict[str, Any],
) -> WheelImplReturn:
"""
Build a wheel or just prepare metadata (if wheel dir is None). Can be editable.
"""

metadata = get_standard_metadata(pyproject, settings)

if metadata.version is None:
Expand Down
4 changes: 4 additions & 0 deletions src/scikit_build_core/resources/scikit-build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,10 @@
"type": "string",
"description": "The state of the build, one of `sdist`, `wheel`, `editable`, `metadata_wheel`, and `metadata_editable`. Takes a regex."
},
"failed": {
"type": "boolean",
"description": "Matches if the build fails. A build will be retried if there is at least one matching override with this set to true."
},
"env": {
"type": "object",
"patternProperties": {
Expand Down
86 changes: 75 additions & 11 deletions src/scikit_build_core/settings/skbuild_read_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def override_match(
current_state: Literal[
"sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"
],
retry: bool,
python_version: str | None = None,
implementation_name: str | None = None,
implementation_version: str | None = None,
Expand All @@ -79,20 +80,32 @@ def override_match(
platform_node: str | None = None,
env: dict[str, str] | None = None,
state: str | None = None,
) -> bool:
failed: bool | None = None,
) -> set[str]:
"""
Check if the current environment matches the overrides. Returns the set of matched keys.
"""

matches = []

passed_keys = set()

if current_env is None:
current_env = os.environ

if python_version is not None:
current_python_version = ".".join(str(x) for x in sys.version_info[:2])
match_msg = version_match(current_python_version, python_version, "Python")
matches.append(match_msg)
if match_msg:
passed_keys.add("python-version")

if implementation_name is not None:
current_impementation_name = sys.implementation.name
match_msg = regex_match(current_impementation_name, implementation_name)
matches.append(match_msg)
if match_msg:
passed_keys.add("implementation-name")

if implementation_version is not None:
info = sys.implementation.version
Expand All @@ -104,40 +117,64 @@ def override_match(
version, implementation_version, "Python implementation"
)
matches.append(match_msg)
if match_msg:
passed_keys.add("implementation-version")

Check warning on line 121 in src/scikit_build_core/settings/skbuild_read_settings.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/settings/skbuild_read_settings.py#L120-L121

Added lines #L120 - L121 were not covered by tests

if platform_system is not None:
current_platform_system = sys.platform
match_msg = regex_match(current_platform_system, platform_system)
matches.append(match_msg)
if match_msg:
passed_keys.add("platform-system")

if platform_machine is not None:
current_platform_machine = platform.machine()
match_msg = regex_match(current_platform_machine, platform_machine)
matches.append(match_msg)
if match_msg:
passed_keys.add("platform-machine")

if platform_node is not None:
current_platform_node = platform.node()
match_msg = regex_match(current_platform_node, platform_node)
matches.append(match_msg)
if match_msg:
passed_keys.add("platform-node")

if state is not None:
match_msg = regex_match(current_state, state)
matches.append(match_msg)
if match_msg:
passed_keys.add("state")

if failed is not None:
if failed and retry:
matches.append("Previous run failed")
passed_keys.add("failed")
elif not failed and not retry:
matches.append("Running on a fresh run")
passed_keys.add("failed")

Check warning on line 156 in src/scikit_build_core/settings/skbuild_read_settings.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/settings/skbuild_read_settings.py#L155-L156

Added lines #L155 - L156 were not covered by tests
else:
matches.append("")

if env:
for key, value in env.items():
if isinstance(value, bool):
matches.append(
match_msg = (
f"env {key} is {value}"
if strtobool(current_env.get(key, "")) == value
else ""
)
matches.append(match_msg)
passed_keys.add(f"env.{key}")
elif key not in current_env:
matches.append("")
else:
current_value = current_env.get(key, "")
match_msg = regex_match(current_value, value)
matches.append(match_msg and f"env {key}: {match_msg}")
if match_msg:
passed_keys.add(f"env.{key}")

if not matches:
msg = "At least one override must be provided"
Expand All @@ -151,7 +188,8 @@ def override_match(
matched = any(matches)
if matched:
logger.info("Overrides {}", " or ".join([m for m in matches if m]))
return matched

return passed_keys if matched else set()


def _handle_minimum_version(
Expand Down Expand Up @@ -260,15 +298,17 @@ def inherit_join(

def process_overides(
tool_skb: dict[str, Any],
*,
state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"],
retry: bool,
env: Mapping[str, str] | None = None,
) -> None:
) -> set[str]:
"""
Process overrides into the main dictionary if they match. Modifies the input dictionary.
"""

global_matched: set[str] = set()
for override in tool_skb.pop("overrides", []):
matched = True
matched: set[str] | None = None
if_override = override.pop("if", None)
if not if_override:
msg = "At least one 'if' override must be provided"
Expand All @@ -280,7 +320,11 @@ def process_overides(
any_override = if_override.pop("any")
select = {k.replace("-", "_"): v for k, v in any_override.items()}
matched = override_match(
match_all=False, current_env=env, current_state=state, **select
match_all=False,
current_env=env,
current_state=state,
retry=retry,
**select,
)

inherit_override = override.pop("inherit", {})
Expand All @@ -290,9 +334,22 @@ def process_overides(

select = {k.replace("-", "_"): v for k, v in if_override.items()}
if select:
matched = matched and override_match(
match_all=True, current_env=env, current_state=state, **select
each_matched = override_match(
match_all=True,
current_env=env,
current_state=state,
retry=retry,
**select,
)
if matched is None:
matched = each_matched
elif matched and each_matched:
matched |= each_matched
else:
matched = set()
if matched is None:
matched = set()

Check warning on line 351 in src/scikit_build_core/settings/skbuild_read_settings.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/settings/skbuild_read_settings.py#L351

Added line #L351 was not covered by tests
global_matched |= matched
if matched:
for key, value in override.items():
inherit1 = inherit_override.get(key, {})
Expand All @@ -311,6 +368,7 @@ def process_overides(
tool_skb[key] = inherit_join(
value, tool_skb.get(key), inherit_override_tmp
)
return global_matched


class SettingsReader:
Expand All @@ -325,12 +383,18 @@ def __init__(
extra_settings: Mapping[str, Any] | None = None,
verify_conf: bool = True,
env: Mapping[str, str] | None = None,
retry: bool = False,
) -> None:
self.state = state

# Handle overrides
pyproject = copy.deepcopy(pyproject)
process_overides(pyproject.get("tool", {}).get("scikit-build", {}), state, env)
self.overrides = process_overides(
pyproject.get("tool", {}).get("scikit-build", {}),
state=state,
env=env,
retry=retry,
)

# Support for minimum-version='build-system.requires'
tmp_min_v = (
Expand Down Expand Up @@ -362,7 +426,7 @@ def __init__(

if extra_settings is not None:
extra_skb = copy.deepcopy(dict(extra_settings))
process_overides(extra_skb, state, env)
process_overides(extra_skb, state=state, env=env, retry=retry)

Check warning on line 429 in src/scikit_build_core/settings/skbuild_read_settings.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/settings/skbuild_read_settings.py#L429

Added line #L429 was not covered by tests
toml_srcs.insert(0, TOMLSource(settings=extra_skb))

prefixed = {
Expand Down
4 changes: 4 additions & 0 deletions src/scikit_build_core/settings/skbuild_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
"type": "string",
"description": "The state of the build, one of `sdist`, `wheel`, `editable`, `metadata_wheel`, and `metadata_editable`. Takes a regex.",
},
"failed": {
"type": "boolean",
"description": "Matches if the build fails. A build will be retried if there is at least one matching override with this set to true.",
},
"env": {
"type": "object",
"patternProperties": {
Expand Down
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,15 @@ def navigate_editable(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Packag
return package


@pytest.fixture()
def broken_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> PackageInfo:
package = PackageInfo(
"broken_fallback",
)
process_package(package, tmp_path, monkeypatch)
return package


@pytest.fixture()
def package_sdist_config(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
Expand Down
Loading

0 comments on commit f014420

Please sign in to comment.