File: cmaker.py

package info (click to toggle)
scikit-build 0.18.1-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,768 kB
  • sloc: python: 5,259; cpp: 284; makefile: 171; f90: 12; sh: 7
file content (771 lines) | stat: -rw-r--r-- 31,420 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
"""
This module provides an interface for invoking CMake executable.
"""

from __future__ import annotations

import argparse
import configparser
import contextlib
import glob
import itertools
import os
import os.path
import platform
import re
import shlex
import subprocess
import sys
import sysconfig
import textwrap
from pathlib import Path
from shlex import quote
from typing import Mapping, Sequence, overload

import distutils.sysconfig as du_sysconfig

from .constants import (
    CMAKE_BUILD_DIR,
    CMAKE_DEFAULT_EXECUTABLE,
    CMAKE_INSTALL_DIR,
    SETUPTOOLS_INSTALL_DIR,
)
from .exceptions import SKBuildError
from .platform_specifics import get_platform

RE_FILE_INSTALL = re.compile(r"""[ \t]*file\(INSTALL DESTINATION "([^"]+)".*"([^"]+)"\).*""")


@overload
def pop_arg(arg: str, args: Sequence[str], default: None = None) -> tuple[list[str], str | None]: ...


@overload
def pop_arg(arg: str, args: Sequence[str], default: str) -> tuple[list[str], str]: ...


def pop_arg(arg: str, args: Sequence[str], default: str | None = None) -> tuple[list[str], str | None]:
    """Pops an argument ``arg`` from an argument list ``args`` and returns the
    new list and the value of the argument if present and a default otherwise.
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(arg)
    namespace_names, args = parser.parse_known_args(args)
    namespace = tuple(vars(namespace_names).items())
    val = namespace[0][1] if namespace and namespace[0][1] is not None else default
    return args, val


def _remove_cwd_prefix(path: str) -> str:
    cwd = os.getcwd()

    result = path.replace("/", os.sep)
    if result.startswith(cwd):
        result = os.path.relpath(result, cwd)

    if platform.system() == "Windows":
        result = result.replace("\\\\", os.sep)

    return result.replace("\n", "")


def has_cmake_cache_arg(cmake_args: list[str], arg_name: str, arg_value: str | None = None) -> bool:
    """Return True if ``-D<arg_name>:TYPE=<arg_value>`` is found
    in ``cmake_args``. If ``arg_value`` is None, return True only if
    ``-D<arg_name>:`` is found in the list."""
    for arg in reversed(cmake_args):
        if arg.startswith(f"-D{arg_name}:"):
            if arg_value is None:
                return True
            if "=" in arg:
                return arg.split("=")[1] == arg_value
    return False


def get_cmake_version(cmake_executable: str = CMAKE_DEFAULT_EXECUTABLE) -> str:
    """
    Runs CMake and extracts associated version information.
    Raises :class:`skbuild.exceptions.SKBuildError` if it failed to execute CMake.

    Example:
        >>> # xdoc: IGNORE_WANT
        >>> from skbuild.cmaker import get_cmake_version
        >>> print(get_cmake_version())
        3.14.4
    """
    try:
        version_string_bytes = subprocess.run(
            [cmake_executable, "--version"], check=True, stdout=subprocess.PIPE
        ).stdout
    except (OSError, subprocess.CalledProcessError) as err:
        msg = f"Problem with the CMake installation, aborting build. CMake executable is {cmake_executable}"
        raise SKBuildError(msg) from err

    version_string = version_string_bytes.decode()

    return version_string.splitlines()[0].split(" ")[-1]


class CMaker:
    r"""Interface to CMake executable.

    Example:
        >>> # Setup dummy repo
        >>> from skbuild.cmaker import CMaker
        >>> import ubelt as ub
        >>> from os.path import join
        >>> repo_dpath = ub.ensure_app_cache_dir('skbuild', 'test_cmaker')
        >>> ub.delete(repo_dpath)
        >>> src_dpath = ub.ensuredir(join(repo_dpath, 'SRC'))
        >>> cmake_fpath = join(src_dpath, 'CMakeLists.txt')
        >>> open(cmake_fpath, 'w').write(ub.codeblock(
                '''
                cmake_minimum_required(VERSION 3.5.0)
                project(foobar NONE)
                file(WRITE "${CMAKE_BINARY_DIR}/foo.txt" "# foo")
                install(FILES "${CMAKE_BINARY_DIR}/foo.txt" DESTINATION ".")
                install(CODE "message(STATUS \\"Project has been installed\\")")
                message(STATUS "CMAKE_SOURCE_DIR:${CMAKE_SOURCE_DIR}")
                message(STATUS "CMAKE_BINARY_DIR:${CMAKE_BINARY_DIR}")
                '''
        >>> ))
        >>> # create a cmaker instance in the dummy repo, configure, and make.
        >>> from skbuild.utils import push_dir
        >>> with push_dir(repo_dpath):
        >>>     cmkr = CMaker()
        >>>     config_kwargs = {'cmake_source_dir': str(src_dpath)}
        >>>     print('--- test cmaker configure ---')
        >>>     env = cmkr.configure(**config_kwargs)
        >>>     print('--- test cmaker make ---')
        >>>     cmkr.make(env=env)
    """

    def __init__(self, cmake_executable: str = CMAKE_DEFAULT_EXECUTABLE) -> None:
        self.cmake_executable = cmake_executable
        self.cmake_version = get_cmake_version(self.cmake_executable)
        self.platform = get_platform()

    @staticmethod
    def get_cached(variable_name: str) -> str | None:
        """If set, returns the variable cached value from the :func:`skbuild.constants.CMAKE_BUILD_DIR()`, otherwise returns None"""
        variable_name = f"{variable_name}:"
        cmake_cache = Path(CMAKE_BUILD_DIR()) / "CMakeCache.txt"

        with contextlib.suppress(OSError):
            for line in cmake_cache.read_text("utf8").splitlines():
                if line.startswith(variable_name):
                    return line.split("=", 1)[-1].strip()

        return None

    @classmethod
    def get_cached_generator_name(cls) -> str | None:
        """Reads and returns the cached generator from the :func:`skbuild.constants.CMAKE_BUILD_DIR()`:.
        Returns None if not found.
        """
        return cls.get_cached("CMAKE_GENERATOR")

    def get_cached_generator_env(self) -> dict[str, str] | None:
        """If any, return a mapping of environment associated with the cached generator."""
        generator_name = self.get_cached_generator_name()
        if generator_name is not None:
            return self.platform.get_generator(generator_name).env

        return None

    def configure(
        self,
        clargs: Sequence[str] = (),
        generator_name: str | None = None,
        skip_generator_test: bool = False,
        cmake_source_dir: str = ".",
        cmake_install_dir: str = "",
        languages: Sequence[str] = ("C", "CXX"),
        cleanup: bool = True,
    ) -> dict[str, str]:
        """Calls cmake to generate the Makefile/VS Solution/XCode project.

        clargs: tuple
            List of command line arguments to pass to cmake executable.

        generator_name: string
            The string representing the CMake generator to use.
            If None, uses defaults for your platform.

        skip_generator_test: bool
            If set to True and if a generator name is specified (either as a keyword
            argument or as `clargs` using `-G <generator_name>`), the generator test
            is skipped.

        cmake_source_dir: string
            Path to source tree containing a ``CMakeLists.txt``

        cmake_install_dir: string
            Relative directory to append
            to :func:`skbuild.constants.CMAKE_INSTALL_DIR()`.

        languages: tuple
            List of languages required to configure the project and expected to
            be supported by the compiler. The language identifier that can be specified
            in the list corresponds to the one recognized by CMake.

        cleanup: bool
            If True, cleans up temporary folder used to test
            generators. Set to False for debugging to see CMake's
            output files.

        Return a mapping of the environment associated with the
        selected :class:`skbuild.platform_specifics.abstract.CMakeGenerator`.

        Mapping of the environment can also be later retrieved using :meth:`.get_cached_generator_env`.
        """

        # if no provided default generator_name, check environment
        if generator_name is None:
            generator_name = os.environ.get("CMAKE_GENERATOR")

        # if generator_name is provided on command line, use it
        clargs, cli_generator_name = pop_arg("-G", clargs)
        if cli_generator_name is not None:
            generator_name = cli_generator_name

        # if arch is provided on command line, use it
        clargs, cli_arch = pop_arg("-A", clargs)

        generator = self.platform.get_best_generator(
            generator_name,
            skip_generator_test=skip_generator_test,
            cmake_executable=self.cmake_executable,
            cmake_args=clargs,
            languages=languages,
            cleanup=cleanup,
            architecture=cli_arch,
        )

        ninja_executable_path = None
        if generator.name == "Ninja":
            with contextlib.suppress(ImportError):
                import ninja  # pylint: disable=import-outside-toplevel

                ninja_executable_path = os.path.join(ninja.BIN_DIR, "ninja")

        if not os.path.exists(CMAKE_BUILD_DIR()):
            os.makedirs(CMAKE_BUILD_DIR())

        if not os.path.exists(CMAKE_INSTALL_DIR()):
            os.makedirs(CMAKE_INSTALL_DIR())

        if not os.path.exists(SETUPTOOLS_INSTALL_DIR()):
            os.makedirs(SETUPTOOLS_INSTALL_DIR())

        python_version = CMaker.get_python_version()
        python_include_dir = CMaker.get_python_include_dir(python_version)
        python_library = CMaker.get_python_library(python_version)

        cmake_source_dir = os.path.abspath(cmake_source_dir)
        cmake_resource_dir = os.path.join(os.path.dirname(__file__), "resources", "cmake")
        cmake_install_prefix = os.path.abspath(os.path.join(CMAKE_INSTALL_DIR(), cmake_install_dir))
        python_version_string = sys.version.split(" ", maxsplit=1)[0]

        cmd = [
            self.cmake_executable,
            cmake_source_dir,
            "-G",
            generator.name,
            *generator.args,
            "--no-warn-unused-cli",
            f"-DCMAKE_INSTALL_PREFIX:PATH={cmake_install_prefix}",
            f"-DPYTHON_VERSION_STRING:STRING={python_version_string}",
            "-DSKBUILD:INTERNAL=TRUE",
            f"-DCMAKE_MODULE_PATH:PATH={cmake_resource_dir}",
            f"-DPYTHON_EXECUTABLE:PATH={sys.executable}",
        ]
        if python_include_dir:
            cmd.append(f"-DPYTHON_INCLUDE_DIR:PATH={python_include_dir}")
        if python_library:
            cmd.append(f"-DPYTHON_LIBRARY:PATH={python_library}")

        for prefix in ["-DPython", "-DPython3"]:
            cmd.extend(
                [
                    f"{prefix}_EXECUTABLE:PATH={sys.executable}",
                    f"{prefix}_ROOT_DIR:PATH={sys.prefix}",
                    f"{prefix}_FIND_REGISTRY:STRING=NEVER",
                ]
            )
            if python_include_dir:
                cmd.append(f"{prefix}_INCLUDE_DIR:PATH={python_include_dir}")
            if python_library and sysconfig.get_platform().startswith("win"):
                cmd.append(f"{prefix}_LIBRARY:PATH={python_library}")
            if sys.implementation.name == "pypy":
                cmd.append(f"{prefix}_FIND_IMPLEMENTATIONS:STRING=PyPy")

            with contextlib.suppress(ImportError):
                import numpy as np  # pylint: disable=import-outside-toplevel

                cmd.append(f"{prefix}_NumPy_INCLUDE_DIRS:PATH=" + np.get_include())

        if generator.toolset:
            cmd.extend(["-T", generator.toolset])
        if generator.architecture and "Visual Studio" in generator.name:
            cmd.extend(["-A", generator.architecture])
        if ninja_executable_path is not None:
            cmd.append(f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}")

        cmd.extend(clargs)

        # Parse CMAKE_ARGS only if SKBUILD_CONFIGURE_OPTIONS is not present
        if "SKBUILD_CONFIGURE_OPTIONS" in os.environ:
            env_cmake_args = list(filter(None, shlex.split(os.environ["SKBUILD_CONFIGURE_OPTIONS"])))
            if any("CMAKE_INSTALL_PREFIX" in arg for arg in env_cmake_args):
                msg = "CMAKE_INSTALL_PREFIX may not be passed via SKBUILD_CONFIGURE_OPTIONS."
                raise ValueError(msg)
        else:
            env_cmake_args_filtered = filter(None, shlex.split(os.environ.get("CMAKE_ARGS", "")))
            env_cmake_args = [s for s in env_cmake_args_filtered if "CMAKE_INSTALL_PREFIX" not in s]

        cmd.extend(env_cmake_args)

        # changes dir to cmake_build and calls cmake's configure step
        # to generate makefile
        print(
            "Configuring Project\n"
            "  Working directory:\n"
            f"    {os.path.abspath(CMAKE_BUILD_DIR())}\n"
            "  Command:\n"
            f"    {self._formatArgsForDisplay(cmd)}\n",
            flush=True,
        )
        rtn = subprocess.run(cmd, cwd=CMAKE_BUILD_DIR(), env=generator.env, check=False).returncode
        if rtn != 0:
            msg = textwrap.dedent(
                f"""\
                An error occurred while configuring with CMake.
                  Command:
                    {self._formatArgsForDisplay(cmd)}
                  Source directory:
                    {os.path.abspath(cmake_source_dir)}
                  Working directory:
                    {os.path.abspath(CMAKE_BUILD_DIR())}
                Please see CMake's output for more information.
                """
            )

            raise SKBuildError(msg)

        CMaker.check_for_bad_installs()

        return generator.env

    @staticmethod
    def get_python_version() -> str:
        """Get version associated with the current python interpreter.

        Returns:
            str: python version string

        Example:
            >>> # xdoc: +IGNORE_WANT
            >>> from skbuild.cmaker import CMaker
            >>> python_version = CMaker.get_python_version()
            >>> print('python_version = {!r}'.format(python_version))
            python_version = '3.7'
        """
        python_version = sysconfig.get_config_var("VERSION")

        if not python_version:
            python_version = sysconfig.get_config_var("py_version_short")

        if not python_version:
            python_version = ".".join(map(str, sys.version_info[:2]))

        assert isinstance(python_version, str)

        return python_version

    # NOTE(opadron): The try-excepts raise the cyclomatic complexity, but we
    # need them for this function.
    @staticmethod
    def get_python_include_dir(python_version: str) -> str | None:
        """Get include directory associated with the current python
        interpreter.

        Args:
            python_version (str): python version, may be partial.

        Returns:
            PathLike: python include dir

        Example:
            >>> # xdoc: +IGNORE_WANT
            >>> from skbuild.cmaker import CMaker
            >>> python_version = CMaker.get_python_version()
            >>> python_include_dir = CMaker.get_python_include_dir(python_version)
            >>> print('python_include_dir = {!r}'.format(python_include_dir))
            python_include_dir = '.../conda/envs/py37/include/python3.7m'
        """
        # determine python include dir
        python_include_dir: str | None = sysconfig.get_config_var("INCLUDEPY")

        # if Python.h not found (or python_include_dir is None), try to find a
        # suitable include dir
        found_python_h = python_include_dir is not None and os.path.exists(os.path.join(python_include_dir, "Python.h"))

        if not found_python_h:
            # NOTE(opadron): these possible prefixes must be guarded against
            # AttributeErrors and KeyErrors because they each can throw on
            # different platforms or even different builds on the same platform.
            include_py: str | None = sysconfig.get_config_var("INCLUDEPY")
            include_dir: str | None = sysconfig.get_config_var("INCLUDEDIR")
            include: str | None = None
            plat_include: str | None = None
            python_inc: str | None = None
            python_inc2: str | None = None

            with contextlib.suppress(AttributeError, KeyError):
                include = sysconfig.get_path("include")

            with contextlib.suppress(AttributeError, KeyError):
                plat_include = sysconfig.get_path("platinclude")

            with contextlib.suppress(AttributeError):
                python_inc = sysconfig.get_python_inc()  # type: ignore[attr-defined]

            if include_py is not None:
                include_py = os.path.dirname(include_py)
            if include is not None:
                include = os.path.dirname(include)
            if plat_include is not None:
                plat_include = os.path.dirname(plat_include)
            if python_inc is not None:
                python_inc2 = os.path.join(python_inc, ".".join(map(str, sys.version_info[:2])))

            all_candidate_prefixes = [include_py, include_dir, include, plat_include, python_inc, python_inc2]
            candidate_prefixes: list[str] = [pre for pre in all_candidate_prefixes if pre]

            candidate_versions: tuple[str, ...] = (python_version,)
            if python_version:
                candidate_versions += ("",)

                pymalloc = None
                with contextlib.suppress(AttributeError):
                    pymalloc = bool(sysconfig.get_config_var("WITH_PYMALLOC"))

                if pymalloc:
                    candidate_versions += (python_version + "m",)

            candidates = (
                os.path.join(prefix, "".join(("python", ver)))
                for (prefix, ver) in itertools.product(candidate_prefixes, candidate_versions)
            )

            for candidate in candidates:
                if os.path.exists(os.path.join(candidate, "Python.h")):
                    # we found an include directory
                    python_include_dir = candidate
                    break

        # TODO(opadron): what happens if we don't find an include directory?
        #                Throw SKBuildError?

        return python_include_dir

    @staticmethod
    def get_python_library(python_version: str) -> str | None:
        """Get path to the python library associated with the current python
        interpreter.

        Args:
            python_version (str): python version, may be partial.

        Returns:
            PathLike: python_library : python shared library

        Example:
            >>> # xdoc: +IGNORE_WANT
            >>> from skbuild.cmaker import CMaker
            >>> python_version = CMaker.get_python_version()
            >>> python_library = CMaker.get_python_include_dir(python_version)
            >>> print('python_library = {!r}'.format(python_library))
            python_library = '.../conda/envs/py37/include/python3.7m'
        """
        # On Windows, support cross-compiling in the same way as setuptools
        # When cross-compiling, check DIST_EXTRA_CONFIG first
        config_file = os.environ.get("DIST_EXTRA_CONFIG", None)
        if config_file and Path(config_file).is_file():
            cp = configparser.ConfigParser()
            cp.read(config_file)
            result = cp.get("build_ext", "library_dirs", fallback="")
            if result:
                minor = sys.version_info[1]
                return str(Path(result) / f"python3{minor}.lib")

        # This seems to be the simplest way to detect the library path with
        # modern python versions that avoids the complicated construct below.
        # It avoids guessing the library name. Tested with cpython 3.8 and
        # pypy 3.8 on Ubuntu.
        libdir: str | None = sysconfig.get_config_var("LIBDIR")
        ldlibrary: str | None = sysconfig.get_config_var("LDLIBRARY")
        if libdir and ldlibrary and os.path.exists(libdir):
            if sysconfig.get_config_var("MULTIARCH"):
                masd = sysconfig.get_config_var("multiarchsubdir")
                if masd:
                    if masd.startswith(os.sep):
                        masd = masd[len(os.sep) :]
                    libdir_masd = os.path.join(libdir, masd)
                    if os.path.exists(libdir_masd):
                        libdir = libdir_masd
            libpath = Path(libdir) / ldlibrary
            if sys.platform.startswith("win") and libpath.suffix == ".dll":
                libpath = libpath.with_suffix(".lib")
            if libpath.is_file():
                return str(libpath)

        return CMaker._guess_python_library(python_version)

    @staticmethod
    def _guess_python_library(python_version: str) -> str | None:
        # determine direct path to libpython
        python_library: str | None = sysconfig.get_config_var("LIBRARY")

        # if static (or nonexistent), try to find a suitable dynamic libpython
        if not python_library or os.path.splitext(python_library)[1][-2:] == ".a":
            candidate_lib_prefixes = ["", "lib"]

            candidate_suffixes = [""]
            candidate_implementations = ["python"]
            if sys.implementation.name == "pypy":
                candidate_implementations[:0] = ["pypy-c", "pypy3-c", "pypy"]
                candidate_suffixes.append("-c")

            candidate_extensions = [".lib", ".so", ".a"]
            # On pypy + MacOS, the variable WITH_DYLD is not set. It would
            # actually be possible to determine the python library there using
            # LDLIBRARY + LIBDIR. As a simple fix, we check if the LDLIBRARY
            # ends with .dylib and add it to the candidate matrix in this case.
            with_ld = sysconfig.get_config_var("WITH_DYLD")
            ld_lib = sysconfig.get_config_var("LDLIBRARY")
            if with_ld or (ld_lib and ld_lib.endswith(".dylib")):
                candidate_extensions.insert(0, ".dylib")

            candidate_versions = [python_version]
            if python_version:
                candidate_versions.append("")
                candidate_versions.insert(0, "".join(python_version.split(".")[:2]))

            abiflags = getattr(sys, "abiflags", "")
            candidate_abiflags = [abiflags]
            if abiflags:
                candidate_abiflags.append("")

            # Ensure the value injected by virtualenv is
            # returned on windows.
            # Because calling `sysconfig.get_config_var('multiarchsubdir')`
            # returns an empty string on Linux, `du_sysconfig` is only used to
            # get the value of `LIBDIR`.
            candidate_libdirs = []
            libdir_a = du_sysconfig.get_config_var("LIBDIR")
            assert not isinstance(libdir_a, int)
            if libdir_a is None:
                libdest = sysconfig.get_config_var("LIBDEST")
                candidate_libdirs.append(os.path.abspath(os.path.join(libdest, "..", "libs") if libdest else "libs"))
            libdir_b = sysconfig.get_config_var("LIBDIR")
            for libdir in (libdir_a, libdir_b):
                if libdir is None:
                    continue
                if sysconfig.get_config_var("MULTIARCH"):
                    masd = sysconfig.get_config_var("multiarchsubdir")
                    if masd:
                        if masd.startswith(os.sep):
                            masd = masd[len(os.sep) :]
                        candidate_libdirs.append(os.path.join(libdir, masd))
                candidate_libdirs.append(libdir)

            candidates = (
                os.path.join(libdir, "".join((pre, impl, ver, abi, suf, ext)))
                for (libdir, pre, impl, ext, ver, abi, suf) in itertools.product(
                    candidate_libdirs,
                    candidate_lib_prefixes,
                    candidate_implementations,
                    candidate_extensions,
                    candidate_versions,
                    candidate_abiflags,
                    candidate_suffixes,
                )
            )

            for candidate in candidates:
                if os.path.exists(candidate):
                    # we found a (likely alternate) libpython
                    python_library = candidate
                    break

        # Temporary workaround for some libraries (opencv) processing the
        # string output.  Will return None instead of empty string in future
        # versions if the library does not exist.
        if python_library is None:
            return None
        return python_library if python_library and os.path.exists(python_library) else ""

    @staticmethod
    def check_for_bad_installs() -> None:
        """This function tries to catch files that are meant to be installed
        outside the project root before they are actually installed.

        Indeed, we can not wait for the manifest, so we try to extract the
        information (install destination) from the CMake build files
        ``*.cmake`` found in :func:`skbuild.constants.CMAKE_BUILD_DIR()`.

        It raises :class:`skbuild.exceptions.SKBuildError` if it found install destination outside of
        :func:`skbuild.constants.CMAKE_INSTALL_DIR()`.
        """

        bad_installs = []
        install_dir = os.path.join(os.getcwd(), CMAKE_INSTALL_DIR())

        for root, _, file_list in os.walk(CMAKE_BUILD_DIR()):
            for filename in file_list:
                if os.path.splitext(filename)[1] != ".cmake":
                    continue

                with open(os.path.join(root, filename), encoding="utf-8") as fp:
                    lines = fp.readlines()

                for line in lines:
                    match = RE_FILE_INSTALL.match(line)
                    if match is None:
                        continue

                    destination = os.path.normpath(match.group(1).replace("${CMAKE_INSTALL_PREFIX}", install_dir))

                    if not destination.startswith(install_dir):
                        bad_installs.append(os.path.join(destination, os.path.basename(match.group(2))))

        if bad_installs:
            msg = "\n".join(
                (
                    "  CMake-installed files must be within the project root.",
                    "    Project Root:",
                    f"      {install_dir}",
                    "    Violating Files:",
                    "\n".join(f"      {_install}" for _install in bad_installs),
                )
            )
            raise SKBuildError(msg)

    def make(
        self,
        clargs: Sequence[str] = (),
        config: str = "Release",
        source_dir: str = ".",
        install_target: str = "install",
        env: Mapping[str, str] | None = None,
    ) -> None:
        """Calls the system-specific make program to compile code.

        install_target: string
             Name of the target responsible to install the project.
             Default is "install".

             .. note::

                To workaround CMake issue #8438.
                See https://gitlab.kitware.com/cmake/cmake/-/issues/8438
                Due to a limitation of CMake preventing from adding a dependency
                on the "build-all" built-in target, we explicitly build the project first when
                the install target is different from the default on.
        """
        clargs, config = pop_arg("--config", clargs, config)
        clargs, install_target = pop_arg("--install-target", clargs, install_target)
        if not os.path.exists(CMAKE_BUILD_DIR()):
            msg = (
                f"CMake build folder ({CMAKE_BUILD_DIR()}) does not exist. "
                "Did you forget to run configure before make?"
            )
            raise SKBuildError(msg)

        # Workaround CMake issue #8438
        # See https://gitlab.kitware.com/cmake/cmake/-/issues/8438
        # Due to a limitation of CMake preventing from adding a dependency
        # on the "build-all" built-in target, we explicitly build
        # the project first when
        # the install target is different from the default on.
        if install_target != "install":
            self.make_impl(clargs=clargs, config=config, source_dir=source_dir, install_target=None, env=env)

        self.make_impl(clargs=clargs, config=config, source_dir=source_dir, install_target=install_target, env=env)

    def make_impl(
        self,
        clargs: list[str],
        config: str,
        source_dir: str,
        install_target: str | None,
        env: Mapping[str, str] | None = None,
    ) -> None:
        """
        Precondition: clargs does not have --config nor --install-target options.
        These command line arguments are extracted in the caller function
        `make` with `clargs, config = pop_arg('--config', clargs, config)`

        This is a refactor effort for calling the function `make` twice in
        case the install_target is different than the default `install`.
        """
        if not install_target:
            cmd = [self.cmake_executable, "--build", source_dir, "--config", config, "--"]
        else:
            cmd = [self.cmake_executable, "--build", source_dir, "--target", install_target, "--config", config, "--"]
        cmd.extend(clargs)
        cmd.extend(filter(bool, shlex.split(os.environ.get("SKBUILD_BUILD_OPTIONS", ""))))

        rtn = subprocess.run(cmd, cwd=CMAKE_BUILD_DIR(), env=env, check=False).returncode
        # For reporting errors (if any)
        if not install_target:
            install_target = "internal build step [valid]"

        if rtn != 0:
            msg = textwrap.dedent(
                f"""\
                An error occurred while building with CMake.
                  Command:
                    {self._formatArgsForDisplay(cmd)}
                  Install target:
                    {install_target}
                  Source directory:
                    {os.path.abspath(source_dir)}
                  Working directory:
                    {os.path.abspath(CMAKE_BUILD_DIR())}
                Please check the install target is valid and see CMake's output for more information.
                """
            )
            raise SKBuildError(msg)

    def install(self) -> list[str]:
        """Returns a list of file paths to install via setuptools that is
        compatible with the data_files keyword argument.
        """
        return self._parse_manifests()

    def _parse_manifests(self) -> list[str]:
        paths = glob.glob(os.path.join(CMAKE_BUILD_DIR(), "install_manifest*.txt"))
        try:
            return next(self._parse_manifest(path) for path in paths)
        except StopIteration:
            return []

    @staticmethod
    def _parse_manifest(install_manifest_path: str) -> list[str]:
        with open(install_manifest_path, encoding="utf-8") as manifest:
            return [_remove_cwd_prefix(path) for path in manifest]

    @staticmethod
    def _formatArgsForDisplay(args: Sequence[str]) -> str:
        """Format a list of arguments appropriately for display. When formatting
        a command and its arguments, the user should be able to execute the
        command by copying and pasting the output directly into a shell.

        Currently, the only formatting is naively surrounding each argument with
        quotation marks.
        """

        return " ".join(quote(arg) for arg in args)