Repository: scikit-build/scikit-build-core
Pull request #867 (2024-08-20 02:48:53)

Editable installs now respect the value of wheel.install-dir
Attempting to fix: https://github.com/scikit-build/scikit-build-core/issues/866

I'm passing down wheel.install-dir to the editable redirect script and appending it to DIR so that rebuilds happen in the correct path. This worked for me in a local testing environment, but I'm not familiar with this codebase, so I don't know if this is an optimal solution.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/867?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.84%. Comparing base [(`8cbd92f`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/8cbd92f430c20ad5e0d9725198a1397ab599437d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`e6c87a2`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/e6c87a27193c0b610ff32790a47b6d4c05017a3b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #867      +/-   ##
==========================================
- Coverage   83.91%   83.84%   -0.07%     
==========================================
  Files          74       74              
  Lines        4363     4364       +1     
==========================================
- Hits         3661     3659       -2     
- Misses        702      705       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/867?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

henryiii: FYI, https://github.com/scikit-build/scikit-build-core/blob/8cbd92f430c20ad5e0d9725198a1397ab599437d/src/scikit_build_core/settings/skbuild_model.py#L170 can't be None, I think you are introducing a None with the `or`.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1be5cf7..fa954db 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,7 +49,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.7", "pypy-3.9", "3.12"]
+        python-version: ["3.7", "pypy-3.9", "3.12", "3.13"]
         runs-on: [ubuntu-latest, macos-13]
         cmake-version: ["3.15.x"]
 
@@ -93,6 +93,9 @@ jobs:
           - python-version: "3.12"
             runs-on: windows-latest
             cmake-version: "3.26.x"
+          - python-version: "3.13"
+            runs-on: windows-latest
+            cmake-version: "3.26.x"
 
     steps:
       - uses: actions/checkout@v4
@@ -320,7 +323,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        runs-on: [ubuntu-latest, macos-14, windows-latest]
+        runs-on: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.runs-on }}
     timeout-minutes: 15
 
@@ -331,7 +334,7 @@ jobs:
 
       - uses: wntrblm/nox@2024.04.15
         with:
-          python-versions: "3.11"
+          python-versions: "3.12"
 
       - name: Linkcheck
         run: nox -s docs -- -b linkcheck
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4f886a1..268fcf7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -88,7 +88,7 @@ repos:
           - rich
           - setuptools-scm
           - tomli
-          - types-setuptools>=69.2
+          - types-setuptools~=70.0
 
   - repo: https://github.com/henryiii/check-sdist
     rev: "v1.0.0rc2"
diff --git a/src/scikit_build_core/build/_editable.py b/src/scikit_build_core/build/_editable.py
index 5cb1ae4..9a1cca1 100644
--- a/src/scikit_build_core/build/_editable.py
+++ b/src/scikit_build_core/build/_editable.py
@@ -30,6 +30,7 @@ def editable_redirect(
     verbose: bool,
     build_options: Sequence[str],
     install_options: Sequence[str],
+    install_dir: str,
 ) -> str:
     """
     Prepare the contents of the _editable_redirect.py file.
@@ -46,6 +47,7 @@ def editable_redirect(
         verbose,
         build_options,
         install_options,
+        install_dir,
     )
     arguments_str = ", ".join(repr(x) for x in arguments)
     editable_txt += f"\n\ninstall({arguments_str})\n"
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index 2610693..a389df0 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -58,7 +58,9 @@ def _make_editable(
 ) -> None:
     modules = mapping_to_modules(mapping, libdir)
     installed = libdir_to_installed(libdir)
-
+    if settings.wheel.install_dir.startswith("/"):
+        msg = "Editable installs cannot rebuild an absolute wheel.install-dir. Use an override to change if needed."
+        raise AssertionError(msg)
     editable_txt = editable_redirect(
         modules=modules,
         installed=installed,
@@ -67,6 +69,7 @@ def _make_editable(
         verbose=settings.editable.verbose,
         build_options=build_options,
         install_options=install_options,
+        install_dir=settings.wheel.install_dir,
     )
 
     wheel.writestr(
diff --git a/src/scikit_build_core/builder/sysconfig.py b/src/scikit_build_core/builder/sysconfig.py
index 6ac3b68..47aa644 100644
--- a/src/scikit_build_core/builder/sysconfig.py
+++ b/src/scikit_build_core/builder/sysconfig.py
@@ -62,7 +62,15 @@ def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path |
 
     libdirstr = sysconfig.get_config_var("LIBDIR")
     ldlibrarystr = sysconfig.get_config_var("LDLIBRARY")
-    librarystr = sysconfig.get_config_var("LDLIBRARY")
+    librarystr = sysconfig.get_config_var("LIBRARY")
+    if abi3:
+        if ldlibrarystr is not None:
+            ldlibrarystr = ldlibrarystr.replace(
+                f"python3{sys.version_info[1]}", "python3"
+            )
+        if librarystr is not None:
+            librarystr = librarystr.replace(f"python3{sys.version_info[1]}", "python3")
+
     libdir: Path | None = libdirstr and Path(libdirstr)
     ldlibrary: Path | None = ldlibrarystr and Path(ldlibrarystr)
     library: Path | None = librarystr and Path(librarystr)
diff --git a/src/scikit_build_core/resources/_editable_redirect.py b/src/scikit_build_core/resources/_editable_redirect.py
index af75519..0c25c41 100644
--- a/src/scikit_build_core/resources/_editable_redirect.py
+++ b/src/scikit_build_core/resources/_editable_redirect.py
@@ -32,7 +32,8 @@ class ScikitBuildRedirectingFinder(importlib.abc.MetaPathFinder):
         verbose: bool,
         build_options: list[str],
         install_options: list[str],
-        dir: str = DIR,
+        dir: str,
+        install_dir: str,
     ) -> None:
         self.known_source_files = known_source_files
         self.known_wheel_files = known_wheel_files
@@ -42,6 +43,8 @@ class ScikitBuildRedirectingFinder(importlib.abc.MetaPathFinder):
         self.build_options = build_options
         self.install_options = install_options
         self.dir = dir
+        self.install_dir = os.path.join(DIR, install_dir)
+
         # Construct the __path__ of all resource files
         # I.e. the paths of all package-like objects
         submodule_search_locations: dict[str, set[str]] = {}
@@ -136,7 +139,14 @@ class ScikitBuildRedirectingFinder(importlib.abc.MetaPathFinder):
         result.check_returncode()
 
         result = subprocess.run(
-            ["cmake", "--install", ".", "--prefix", DIR, *self.install_options],
+            [
+                "cmake",
+                "--install",
+                ".",
+                "--prefix",
+                self.install_dir,
+                *self.install_options,
+            ],
             cwd=self.path,
             stdout=sys.stderr if verbose else subprocess.PIPE,
             env=env,
@@ -159,6 +169,7 @@ def install(
     verbose: bool = False,
     build_options: list[str] | None = None,
     install_options: list[str] | None = None,
+    install_dir: str = "",
 ) -> None:
     """
     Install a meta path finder that redirects imports to the source files, and
@@ -169,6 +180,8 @@ def install(
     :param path: The path to the build directory, or None
     :param verbose: Whether to print the cmake commands (also controlled by the
                     SKBUILD_EDITABLE_VERBOSE environment variable)
+    :param install_dir: The wheel install directory override, if one was
+                        specified
     """
     sys.meta_path.insert(
         0,
@@ -180,5 +193,7 @@ def install(
             verbose,
             build_options or [],
             install_options or [],
+            DIR,
+            install_dir,
         ),
     )


Test patch:
diff --git a/tests/test_editable.py b/tests/test_editable.py
index 6dcf3ff..507ba12 100644
--- a/tests/test_editable.py
+++ b/tests/test_editable.py
@@ -1,4 +1,5 @@
 import sys
+import textwrap
 from pathlib import Path
 
 import pytest
@@ -105,3 +106,53 @@ def test_cython_pxd(monkeypatch, tmp_path, editable, editable_mode, isolated):
         *editable_flag,
         ".",
     )
+
+
+@pytest.mark.compile()
+@pytest.mark.configure()
+@pytest.mark.integration()
+@pytest.mark.usefixtures("package_simplest_c")
+def test_install_dir(isolated):
+    isolated.install("pip>=23")
+    isolated.install("scikit-build-core")
+
+    settings_overrides = {
+        "build-dir": "build/{wheel_tag}",
+        "wheel.install-dir": "other_pkg",
+        "editable.rebuild": "true",
+    }
+    # Create a dummy other_pkg package to satisfy the import
+    other_pkg_src = Path("./src/other_pkg")
+    other_pkg_src.joinpath("simplest").mkdir(parents=True)
+    other_pkg_src.joinpath("__init__.py").write_text(
+        textwrap.dedent(
+            """
+            from .simplest._module import square
+            """
+        )
+    )
+    other_pkg_src.joinpath("simplest/__init__.py").touch()
+
+    isolated.install(
+        "-v",
+        *[f"--config-settings={k}={v}" for k, v in settings_overrides.items()],
+        "--no-build-isolation",
+        "-e",
+        ".",
+    )
+
+    # Make sure the package is correctly installed in the subdirectory
+    other_pkg_path = isolated.platlib / "other_pkg"
+    c_module_glob = list(other_pkg_path.glob("simplest/_module*"))
+    assert len(c_module_glob) == 1
+    c_module = c_module_glob[0]
+    assert c_module.exists()
+    # If `install-dir` was not taken into account it would install here
+    failed_c_module = other_pkg_path / "../simplest" / c_module.name
+    assert not failed_c_module.exists()
+
+    # Run an import in order to re-trigger the rebuild and check paths again
+    out = isolated.execute("import other_pkg.simplest")
+    assert "Running cmake" in out
+    assert c_module.exists()
+    assert not failed_c_module.exists()
diff --git a/tests/test_editable_redirect.py b/tests/test_editable_redirect.py
index b646730..6c67201 100644
--- a/tests/test_editable_redirect.py
+++ b/tests/test_editable_redirect.py
@@ -44,6 +44,7 @@ def test_editable_redirect():
         build_options=[],
         install_options=[],
         dir=str(Path("/sitepackages")),
+        install_dir="",
     )
 
     assert finder.submodule_search_locations == process_dict_set(
diff --git a/tests/test_editable_unit.py b/tests/test_editable_unit.py
index ab3e24b..4858436 100644
--- a/tests/test_editable_unit.py
+++ b/tests/test_editable_unit.py
@@ -181,6 +181,7 @@ def test_navigate_editable_pkg(editable_package: EditablePackage, virtualenv: VE
         verbose=False,
         build_options=[],
         install_options=[],
+        install_dir="",
     )
 
     site_packages.joinpath("_pkg_editable.py").write_text(editable_txt)