Repository: scikit-build/scikit-build-core
Pull request #646 (2024-02-20 22:21:03)

doc: be explicit about what it means to "use SKBUILD_SOABI"
I'm not super experienced with building Python modules with cmake, so it wasn't clear to me until I found it in a test what exactly "use SKBUILD_SOABI" meant until I found it in one of the tests, so I thought others might like to see the `set(Python_SOABI ${SKBUILD_SOABI})`.

LecrisUT: What about `USE_SABI` in `Python_add_library`. Is that meant to be used with this?
henryiii: `USE_SABI` is for the stable ABI, and not related to this. This is changing the SOABI value that `USE_SOABI` will use to make the extension. I'm a little reluctant to recommend this, because it's not supported upstream, even though it does work in all versions. I've even done it this way because it's simple and easy. The "correct" alternative is to set the suffix using this after making the target.

I'll take a look later and see if I can add the "correct" way, with this mentioned as an easy but slightly upstream unsupported shortcut.
minrk: I'm certainly not tied to this one, but I think it will be helpful to point to what it looks like to override the SOABI value, _especially_ if this isn't the right way.

Patch:
diff --git a/docs/cmakelists.md b/docs/cmakelists.md
index 0681186..3bc3efc 100644
--- a/docs/cmakelists.md
+++ b/docs/cmakelists.md
@@ -42,7 +42,26 @@ If you are making a Limited ABI / Stable API package, you'll need the
 
 If you want to cross-compile to Windows ARM, you'll need to use
 `${SKBUILD_SOABI}`, which is always correct, instead of trusting FindPython's
-`Python_SOABI` value.
+`Python_SOABI` value. You can manually set the extension suffix after making a
+target:
+
+```cmake
+if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    set_property (TARGET ${name} PROPERTY SUFFIX "$.{SKBUILD_SOSABI}.pyd")
+else()
+    set_property (TARGET ${name} PROPERTY SUFFIX ".${SKBUILD_SOSABI}${CMAKE_SHARED_MODULE_SUFFIX}")
+endif()
+```
+
+A quicker way to do this would be to instead override `Python_SOABI` after
+`find_package(Python)`:
+
+```cmake
+set(Python_SOABI ${SKBUILD_SOABI})
+```
+
+However, this isn't officially supported upstream, and only works due to the way
+this variable is used when creating targets.
 
 :::
 
diff --git a/docs/configuration.md b/docs/configuration.md
index 878e0f9..a207379 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -595,8 +595,9 @@ artifacts, etc. Also, to make your binaries importable, you should set
 all possible `*_<CONFIG>` variations) to make sure they are placed inside your
 source directory inside the Python packages; this will be run from the build
 directory, rather than installed. This will also not support automatic rebuilds.
-The build directory setting must be empty/unset to use this. You can detect this
-mode by checking for an in-place build and checking `SKBUILD` being set.
+The build directory setting will be ignored if you use this and perform an
+editable install. You can detect this mode by checking for an in-place build and
+checking `SKBUILD` being set.
 
 With all the caveats, this is very logically simple (one directory) and a near
 identical replacement for `python setup.py build_ext --inplace`. Some third
@@ -604,8 +605,7 @@ party tooling might work better with this mode. Scikit-build-core will simply
 install a `.pth` file that points at your source package(s) and do an inplace
 CMake build.
 
-On the command line, you can pass `-Ceditable.mode=inplace -Cbuild-dir=""` to
-enable this mode.
+On the command line, you can pass `-Ceditable.mode=inplace` to enable this mode.
 
 :::
 
diff --git a/pyproject.toml b/pyproject.toml
index 1319a8e..4e244da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -170,6 +170,7 @@ py-version = "3.7"
 jobs = "0"
 reports.output-format = "colorized"
 good-names = ["f"]
+ignore-patterns = ['.*\.pyi']
 messages_control.disable = [
     "design",
     "fixme",
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index d562ecf..974d71c 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -173,23 +173,27 @@ def _build_wheel_impl(
             archs_to_tags(get_archs(os.environ)),
             settings.wheel.py_api,
             expand_macos=settings.wheel.expand_macos_universal_tags,
+            root_is_purelib=targetlib == "purelib",
             build_tag=settings.wheel.build_tag,
         )
 
         # A build dir can be specified, otherwise use a temporary directory
-        build_dir = (
-            Path(
-                settings.build_dir.format(
-                    cache_tag=sys.implementation.cache_tag,
-                    wheel_tag=str(tags),
-                    build_type=settings.cmake.build_type,
-                    state=state,
+        if cmake is not None and editable and settings.editable.mode == "inplace":
+            build_dir = settings.cmake.source_dir
+        else:
+            build_dir = (
+                Path(
+                    settings.build_dir.format(
+                        cache_tag=sys.implementation.cache_tag,
+                        wheel_tag=str(tags),
+                        build_type=settings.cmake.build_type,
+                        state=state,
+                    )
                 )
+                if settings.build_dir
+                else build_tmp_folder / "build"
             )
-            if settings.build_dir
-            else build_tmp_folder / "build"
-        )
-        logger.info("Build directory: {}", build_dir.resolve())
+            logger.info("Build directory: {}", build_dir.resolve())
 
         wheel_dirs = {
             targetlib: wheel_dir / targetlib,
@@ -269,7 +273,7 @@ def _build_wheel_impl(
             if gen.location == "build":
                 path = build_dir / gen.path
             elif gen.location == "install":
-                path = install_dir / gen.path
+                path = wheel_dirs[targetlib] / gen.path
             else:
                 assert_never(gen.location)
             path.parent.mkdir(parents=True, exist_ok=True)
@@ -279,8 +283,6 @@ def _build_wheel_impl(
         install_options = []
 
         if cmake is not None:
-            if editable and settings.editable.mode == "inplace":
-                build_dir = settings.cmake.source_dir
             config = CMaker(
                 cmake,
                 source_dir=settings.cmake.source_dir,
diff --git a/src/scikit_build_core/builder/wheel_tag.py b/src/scikit_build_core/builder/wheel_tag.py
index 3e6a006..09c5506 100644
--- a/src/scikit_build_core/builder/wheel_tag.py
+++ b/src/scikit_build_core/builder/wheel_tag.py
@@ -37,6 +37,7 @@ class WheelTag:
         py_api: str = "",
         *,
         expand_macos: bool = False,
+        root_is_purelib: bool = False,
         build_tag: str = "",
     ) -> Self:
         if build_tag:
@@ -77,12 +78,20 @@ class WheelTag:
             # Remove duplicates (e.g. universal2 if macOS > 11.0 and expanded)
             plats = list(dict.fromkeys(plats))
 
+        if root_is_purelib:
+            plats = ["any"]
+            abi = "none"
+
         if py_api:
             pyvers_new = py_api.split(".")
             if all(x.startswith("cp3") and x[3:].isdecimal() for x in pyvers_new):
                 if len(pyvers_new) != 1:
                     msg = "Unexpected py-api, must be a single cp version (e.g. cp39), not {py_api}"
                     raise AssertionError(msg)
+                if root_is_purelib:
+                    msg = f"Unexpected py-api, since platlib is set to false, must be Pythonless (e.g. py2.py3), not {py_api}"
+                    raise AssertionError(msg)
+
                 minor = int(pyvers_new[0][3:])
                 if (
                     sys.implementation.name == "cpython"
diff --git a/src/scikit_build_core/settings/skbuild_read_settings.py b/src/scikit_build_core/settings/skbuild_read_settings.py
index 10f603b..be4bb72 100644
--- a/src/scikit_build_core/settings/skbuild_read_settings.py
+++ b/src/scikit_build_core/settings/skbuild_read_settings.py
@@ -262,25 +262,19 @@ class SettingsReader:
                 )
                 raise CMakeConfigError(msg)
 
-        if self.settings.editable.mode == "inplace":
-            if self.settings.editable.rebuild:
+        if self.settings.editable.rebuild:
+            if self.settings.editable.mode == "inplace":
                 rich_print(
                     "[red][bold]ERROR:[/bold] editable rebuild is incompatible with inplace mode"
                 )
                 raise SystemExit(7)
 
-            if self.settings.build_dir:
+            if not self.settings.build_dir:
                 rich_print(
-                    "[red][bold]ERROR:[/bold] build-dir must be empty for editable inplace mode"
+                    "[red][bold]ERROR:[/bold] editable mode with rebuild requires build-dir"
                 )
                 raise SystemExit(7)
 
-        if self.settings.editable.rebuild and not self.settings.build_dir:
-            rich_print(
-                "[red][bold]ERROR:[/bold] editable mode with rebuild requires build-dir"
-            )
-            raise SystemExit(7)
-
         install_policy = (
             self.settings.minimum_version is None
             or self.settings.minimum_version >= Version("0.5")


Test patch:
diff --git a/tests/test_pyproject_purelib.py b/tests/test_pyproject_purelib.py
index 5c1dc53..e726215 100644
--- a/tests/test_pyproject_purelib.py
+++ b/tests/test_pyproject_purelib.py
@@ -11,7 +11,7 @@ from scikit_build_core.build import build_wheel
 def test_pep517_wheel(virtualenv):
     dist = Path("dist")
     out = build_wheel("dist", {})
-    (wheel,) = dist.glob("purelib_example-0.0.1-*.whl")
+    (wheel,) = dist.glob("purelib_example-0.0.1-*-none-any.whl")
     assert wheel == dist / out
 
     virtualenv.install(wheel)