Shipping a library for ctypes

If your package is a thin ctypes (or cffi) wrapper around a CMake-built shared library, rather than a compiled Python extension, the goal is to install the library alongside your Python code and then locate it at runtime without hard-coding a path. This has the nice property that a single wheel works on every Python version, since the library does not touch the Python ABI. The tradeoff is that you have to find and load the library yourself.

Install the library next to your Python code

Point the install destination at your package directory so the library lands in site-packages/mypackage/ next to __init__.py:

install(TARGETS mylib DESTINATION mypackage)

The destination is relative to the platlib (${SKBUILD_PLATLIB_DIR}) by default; you can name any of the install trees explicitly if you need to. If you set wheel.install-dir = "mypackage", then the destination is relative to that instead, and a bare DESTINATION . works.

Find it at runtime with importlib.resources

Do not compute the path relative to __file__ — that assumes the package lives on a real filesystem, which is not guaranteed (it could be in a zip, and in an editable install the Python source and the compiled library live in different directories). Use importlib.resources instead, which scikit-build-core’s editable installs fully support:

import ctypes
import sys
from importlib.resources import files

# Pick the right suffix for the platform.
_suffix = {"win32": ".dll", "darwin": ".dylib"}.get(sys.platform, ".so")
_lib = files("mypackage") / f"libmylib{_suffix}"

lib = ctypes.CDLL(str(_lib))

For the general (zip-safe) case, wrap the traversable in importlib.resources.as_file, which extracts the resource to a real path if necessary. Because ctypes needs the file to remain on disk for the lifetime of the process, keep the context manager open — for example with an contextlib.ExitStack closed at interpreter exit:

import atexit, ctypes
from contextlib import ExitStack
from importlib.resources import files, as_file

_files = ExitStack()
atexit.register(_files.close)
_lib = _files.enter_context(as_file(files("mypackage") / f"libmylib{_suffix}"))
lib = ctypes.CDLL(str(_lib))

Editable installs and rebuilds

In redirect-mode editable installs (the default), importlib.resources finds the compiled library through the redirecting finder, so the code above works unchanged. Note that accessing a resource does not trigger a rebuild — plain libraries are not importable modules, so the automatic editable.rebuild on-import hook does not fire for them. To pick up C/C++ changes, either request a rebuild explicitly (this works whenever a persistent build-dir is set, with or without editable.rebuild)…

import mypackage

mypackage.__loader__.rebuild()

…or import a real extension module from the same project first, which does fire the on-import hook when editable.rebuild is enabled. See Triggering a rebuild manually for the details and the build-dir requirement.

Runtime search paths

If your shipped library links against other shared libraries, you still need to make those discoverable at load time (RPATH on Linux/macOS, os.add_dll_directory on Windows). See Differences Between Unix and Windows for the full set of options, including wheel-repair tools.