diff --git a/pywheel/shim/find_libpython.py b/pywheel/shim/find_libpython.py deleted file mode 100644 index abab278ae3..0000000000 --- a/pywheel/shim/find_libpython.py +++ /dev/null @@ -1,379 +0,0 @@ -#!/usr/bin/env python - -""" -Locate libpython associated with this Python executable. -""" - -# License -# -# Copyright 2018, Takafumi Arakaki -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from __future__ import print_function, absolute_import - -from logging import getLogger -import ctypes.util -import functools -import os -import sys -import sysconfig - -logger = getLogger("find_libpython") - -is_windows = os.name == "nt" -is_apple = sys.platform == "darwin" - -SHLIB_SUFFIX = sysconfig.get_config_var("SHLIB_SUFFIX") -if SHLIB_SUFFIX is None: - if is_windows: - SHLIB_SUFFIX = ".dll" - else: - SHLIB_SUFFIX = ".so" -if is_apple: - # sysconfig.get_config_var("SHLIB_SUFFIX") can be ".so" in macOS. - # Let's not use the value from sysconfig. - SHLIB_SUFFIX = ".dylib" - - -def linked_libpython(): - """ - Find the linked libpython using dladdr (in *nix). - Calling this in Windows always return `None` at the moment. - Returns - ------- - path : str or None - A path to linked libpython. Return `None` if statically linked. - """ - if is_windows: - return None - return _linked_libpython_unix() - - -class Dl_info(ctypes.Structure): - _fields_ = [ - ("dli_fname", ctypes.c_char_p), - ("dli_fbase", ctypes.c_void_p), - ("dli_sname", ctypes.c_char_p), - ("dli_saddr", ctypes.c_void_p), - ] - - -def _linked_libpython_unix(): - libdl = ctypes.CDLL(ctypes.util.find_library("dl")) - libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(Dl_info)] - libdl.dladdr.restype = ctypes.c_int - - dlinfo = Dl_info() - retcode = libdl.dladdr( - ctypes.cast(ctypes.pythonapi.Py_GetVersion, ctypes.c_void_p), - ctypes.pointer(dlinfo), - ) - if retcode == 0: # means error - return None - path = os.path.realpath(dlinfo.dli_fname.decode()) - if path == os.path.realpath(sys.executable): - return None - return path - - -def library_name(name, suffix=SHLIB_SUFFIX, is_windows=is_windows): - """ - Convert a file basename `name` to a library name (no "lib" and ".so" etc.) - >>> library_name("libpython3.7m.so") # doctest: +SKIP - 'python3.7m' - >>> library_name("libpython3.7m.so", suffix=".so", is_windows=False) - 'python3.7m' - >>> library_name("libpython3.7m.dylib", suffix=".dylib", is_windows=False) - 'python3.7m' - >>> library_name("python37.dll", suffix=".dll", is_windows=True) - 'python37' - """ - if not is_windows and name.startswith("lib"): - name = name[len("lib") :] - if suffix and name.endswith(suffix): - name = name[: -len(suffix)] - return name - - -def append_truthy(list, item): - if item: - list.append(item) - - -def uniquifying(items): - """ - Yield items while excluding the duplicates and preserving the order. - >>> list(uniquifying([1, 2, 1, 2, 3])) - [1, 2, 3] - """ - seen = set() - for x in items: - if x not in seen: - yield x - seen.add(x) - - -def uniquified(func): - """ Wrap iterator returned from `func` by `uniquifying`. """ - - @functools.wraps(func) - def wrapper(*args, **kwds): - return uniquifying(func(*args, **kwds)) - - return wrapper - - -@uniquified -def candidate_names(suffix=SHLIB_SUFFIX): - """ - Iterate over candidate file names of libpython. - Yields - ------ - name : str - Candidate name libpython. - """ - LDLIBRARY = sysconfig.get_config_var("LDLIBRARY") - if LDLIBRARY: - yield LDLIBRARY - - LIBRARY = sysconfig.get_config_var("LIBRARY") - if LIBRARY: - yield os.path.splitext(LIBRARY)[0] + suffix - - dlprefix = "" if is_windows else "lib" - sysdata = dict( - v=sys.version_info, - # VERSION is X.Y in Linux/macOS and XY in Windows: - VERSION=( - sysconfig.get_config_var("VERSION") - or "{v.major}.{v.minor}".format(v=sys.version_info) - ), - ABIFLAGS=( - sysconfig.get_config_var("ABIFLAGS") - or sysconfig.get_config_var("abiflags") - or "" - ), - ) - - for stem in [ - "python{VERSION}{ABIFLAGS}".format(**sysdata), - "python{VERSION}".format(**sysdata), - "python{v.major}".format(**sysdata), - "python", - ]: - yield dlprefix + stem + suffix - - -@uniquified -def candidate_paths(suffix=SHLIB_SUFFIX): - """ - Iterate over candidate paths of libpython. - Yields - ------ - path : str or None - Candidate path to libpython. The path may not be a fullpath - and may not exist. - """ - - yield linked_libpython() - - # List candidates for directories in which libpython may exist - lib_dirs = [] - append_truthy(lib_dirs, sysconfig.get_config_var("LIBPL")) - append_truthy(lib_dirs, sysconfig.get_config_var("srcdir")) - append_truthy(lib_dirs, sysconfig.get_config_var("LIBDIR")) - - # LIBPL seems to be the right config_var to use. It is the one - # used in python-config when shared library is not enabled: - # https://github.com/python/cpython/blob/v3.7.0/Misc/python-config.in#L55-L57 - # - # But we try other places just in case. - - if is_windows: - lib_dirs.append(os.path.join(os.path.dirname(sys.executable))) - else: - lib_dirs.append( - os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "lib") - ) - - # For macOS: - append_truthy(lib_dirs, sysconfig.get_config_var("PYTHONFRAMEWORKPREFIX")) - - lib_dirs.append(sys.exec_prefix) - lib_dirs.append(os.path.join(sys.exec_prefix, "lib")) - - lib_basenames = list(candidate_names(suffix=suffix)) - - for directory in lib_dirs: - for basename in lib_basenames: - yield os.path.join(directory, basename) - - # In macOS and Windows, ctypes.util.find_library returns a full path: - for basename in lib_basenames: - yield ctypes.util.find_library(library_name(basename)) - - -# Possibly useful links: -# * https://packages.ubuntu.com/bionic/amd64/libpython3.6/filelist -# * https://github.com/Valloric/ycmd/issues/518 -# * https://github.com/Valloric/ycmd/pull/519 - - -def normalize_path(path, suffix=SHLIB_SUFFIX, is_apple=is_apple): - """ - Normalize shared library `path` to a real path. - If `path` is not a full path, `None` is returned. If `path` does - not exists, append `SHLIB_SUFFIX` and check if it exists. - Finally, the path is canonicalized by following the symlinks. - Parameters - ---------- - path : str ot None - A candidate path to a shared library. - """ - if not path: - return None - if not os.path.isabs(path): - return None - if os.path.exists(path): - return os.path.realpath(path) - if os.path.exists(path + suffix): - return os.path.realpath(path + suffix) - if is_apple: - return normalize_path(_remove_suffix_apple(path), suffix=".so", is_apple=False) - return None - - -def _remove_suffix_apple(path): - """ - Strip off .so or .dylib. - >>> _remove_suffix_apple("libpython.so") - 'libpython' - >>> _remove_suffix_apple("libpython.dylib") - 'libpython' - >>> _remove_suffix_apple("libpython3.7") - 'libpython3.7' - """ - if path.endswith(".dylib"): - return path[: -len(".dylib")] - if path.endswith(".so"): - return path[: -len(".so")] - return path - - -@uniquified -def finding_libpython(): - """ - Iterate over existing libpython paths. - The first item is likely to be the best one. - Yields - ------ - path : str - Existing path to a libpython. - """ - logger.debug("is_windows = %s", is_windows) - logger.debug("is_apple = %s", is_apple) - for path in candidate_paths(): - logger.debug("Candidate: %s", path) - normalized = normalize_path(path) - if normalized: - logger.debug("Found: %s", normalized) - yield normalized - else: - logger.debug("Not found.") - - -def find_libpython(): - """ - Return a path (`str`) to libpython or `None` if not found. - Parameters - ---------- - path : str or None - Existing path to the (supposedly) correct libpython. - """ - for path in finding_libpython(): - return os.path.realpath(path) - - -def print_all(items): - for x in items: - print(x) - - -def cli_find_libpython(cli_op, verbose): - import logging - - # Importing `logging` module here so that using `logging.debug` - # instead of `logger.debug` outside of this function becomes an - # error. - - if verbose: - logging.basicConfig(format="%(levelname)s %(message)s", level=logging.DEBUG) - - if cli_op == "list-all": - print_all(finding_libpython()) - elif cli_op == "candidate-names": - print_all(candidate_names()) - elif cli_op == "candidate-paths": - print_all(p for p in candidate_paths() if p and os.path.isabs(p)) - else: - path = find_libpython() - if path is None: - return 1 - print(path, end="") - - -def main(args=None): - import argparse - - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--verbose", "-v", action="store_true", help="Print debugging information." - ) - - group = parser.add_mutually_exclusive_group() - group.add_argument( - "--list-all", - action="store_const", - dest="cli_op", - const="list-all", - help="Print list of all paths found.", - ) - group.add_argument( - "--candidate-names", - action="store_const", - dest="cli_op", - const="candidate-names", - help="Print list of candidate names of libpython.", - ) - group.add_argument( - "--candidate-paths", - action="store_const", - dest="cli_op", - const="candidate-paths", - help="Print list of candidate paths of libpython.", - ) - - ns = parser.parse_args(args) - parser.exit(cli_find_libpython(**vars(ns))) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt index 7cbc9f8099..d593716d0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pytest pytest-cov sympy numpy +find_pythonlib diff --git a/setup.py b/setup.py index e0a6fa59e5..4e064a6cab 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ """ import stat from pkg_resources import working_set -from pywheel.shim.find_libpython import find_libpython +from find_libpython import find_libpython # Main source of the version. Dont rename, used by Cmake @@ -78,6 +78,7 @@ def run(self, *args, **kwargs): install_requirements = [ "PyYAML>=3.13", "sympy>=1.3", + "find_libpython" ] @@ -103,7 +104,7 @@ def run(self, *args, **kwargs): long_description=long_description, long_description_content_type='text/markdown', packages=["nmodl"], - scripts=["pywheel/shim/nmodl", "pywheel/shim/find_libpython.py"], + scripts=["pywheel/shim/nmodl"], include_package_data=True, cmake_minimum_required_version="3.15.0", cmake_args=cmake_args, diff --git a/src/pybind/pyembed.cpp b/src/pybind/pyembed.cpp index ba17acf7ff..81fa641b22 100644 --- a/src/pybind/pyembed.cpp +++ b/src/pybind/pyembed.cpp @@ -45,13 +45,16 @@ void EmbeddedPythonLoader::load_libraries() { logger->critical(errstr); throw std::runtime_error("Failed to dlopen"); } + if (std::getenv("NMODLHOME") == nullptr) { + logger->critical("NMODLHOME environment variable must be set to load embedded python"); + throw std::runtime_error("NMODLHOME not set"); + } auto pybind_wraplib_env = fs::path(std::getenv("NMODLHOME")) / "lib" / "libpywrapper"; pybind_wraplib_env.concat(CMakeInfo::SHARED_LIBRARY_SUFFIX); if (!fs::exists(pybind_wraplib_env)) { - logger->critical( - "NMODLHOME environment variable must be set to load the pybind " - "wrapper library"); - throw std::runtime_error("NMODLHOME not set"); + logger->critical("NMODLHOME doesn't contain libpywrapper{} library", + CMakeInfo::SHARED_LIBRARY_SUFFIX); + throw std::runtime_error("NMODLHOME doesn't have lib/libpywrapper library"); } pybind_wrapper_handle = dlopen(pybind_wraplib_env.c_str(), dlopen_opts); if (!pybind_wrapper_handle) {