diff --git a/src/scikit_build_core/_shutil.py b/src/scikit_build_core/_shutil.py index 04c08c87f..ba600af10 100644 --- a/src/scikit_build_core/_shutil.py +++ b/src/scikit_build_core/_shutil.py @@ -1,9 +1,12 @@ from __future__ import annotations +import contextlib import dataclasses import os +import stat import subprocess -from collections.abc import Iterable +import sys +from collections.abc import Generator, Iterable from typing import ClassVar from ._logging import logger @@ -78,3 +81,26 @@ def _key_diff(self, k: str) -> str: if k in self._prev_env and k not in self.env: return "-" return " " + + +def _fix_all_permissions(directory: str) -> None: + """ + Makes sure the write permission is set. Only run this on Windows. + """ + with os.scandir(directory) as it: + for entry in it: + if entry.is_dir(): + _fix_all_permissions(entry.path) + continue + mode = stat.S_IMODE(entry.stat().st_mode) + if not mode & stat.S_IWRITE: + os.chmod(entry.path, mode | stat.S_IWRITE) # noqa: PTH101 + + +@contextlib.contextmanager +def fix_win_37_all_permissions(tmpdir: str) -> Generator[None, None, None]: + try: + yield + finally: + if sys.version_info < (3, 8) and sys.platform.startswith("win"): + _fix_all_permissions(tmpdir) diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py index 3dc914ef5..d0bd6f8b5 100644 --- a/src/scikit_build_core/build/wheel.py +++ b/src/scikit_build_core/build/wheel.py @@ -14,6 +14,7 @@ from .. import __version__ from .._compat import tomllib from .._logging import 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 @@ -102,7 +103,7 @@ def _build_wheel_impl( f"[red]({action})[/red]", ) - with tempfile.TemporaryDirectory() as tmpdir: + with tempfile.TemporaryDirectory() as tmpdir, fix_win_37_all_permissions(tmpdir): build_tmp_folder = Path(tmpdir) wheel_dir = build_tmp_folder / "wheel" diff --git a/tests/test_shutil.py b/tests/test_shutil.py new file mode 100644 index 000000000..1cfd2c383 --- /dev/null +++ b/tests/test_shutil.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import shutil +import stat +import sys +import tempfile +from pathlib import Path + +import pytest + +from scikit_build_core._shutil import _fix_all_permissions, fix_win_37_all_permissions + + +def _make_dir_with_ro(tmp_path: Path) -> Path: + base = tmp_path / "fix_all_perm" + base.mkdir() + base.joinpath("normal_file.txt").touch() + ro = base / "read_only.txt" + ro.touch() + ro.chmod(stat.S_IREAD) + nested = base / "nested" + nested.mkdir() + ro2 = nested / "read_only_2.txt" + ro2.touch() + ro2.chmod(stat.S_IREAD) + + # Validity check + assert not stat.S_IMODE(ro2.stat().st_mode) & stat.S_IWRITE + + return base + + +@pytest.fixture() +def make_dir_with_ro(tmp_path: Path) -> Path: + return _make_dir_with_ro(tmp_path) + + +def test_broken_all_permissions(make_dir_with_ro: Path) -> None: + if sys.platform.startswith("win"): + with pytest.raises(PermissionError): + shutil.rmtree(make_dir_with_ro) + else: + shutil.rmtree(make_dir_with_ro) + + +def test_fix_all_permissions(make_dir_with_ro: Path) -> None: + _fix_all_permissions(str(make_dir_with_ro)) + shutil.rmtree(make_dir_with_ro) + + +def test_tmpdir(): + with tempfile.TemporaryDirectory() as tmp, fix_win_37_all_permissions(tmp): + _make_dir_with_ro(Path(tmp))