├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── noxfile.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── find_libpython │ ├── __init__.py │ ├── __main__.py │ └── _version.py └── tests └── test_find_libpython.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | ignore: 8 | - dependency-name: "codecov/codecov-actions" 9 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | schedule: 5 | - cron: "10 16 * * 3" 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | branches: [ master ] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | ubuntu-system-python: 15 | name: ${{ matrix.os }}-system-python 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-22.04, ubuntu-24.04] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install System Python 24 | run: | 25 | sudo apt install python3-dev python3-pip python3-venv 26 | - name: Build venv 27 | run: | 28 | python -m venv .venv 29 | . .venv/bin/activate 30 | echo PATH=$PATH >> $GITHUB_ENV 31 | - name: Install Testing Requirements 32 | run: python3 -m pip install nox 33 | - name: Run Tests 34 | run: python3 -m nox -e tests 35 | - name: Upload to codecov 36 | uses: codecov/codecov-action@v5 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | fail_ci_if_error: true 40 | 41 | setup-python: 42 | name: ${{ matrix.os }}-setup-python 43 | runs-on: ${{ matrix.os }} 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | os: [windows-latest, macos-latest, ubuntu-latest] 48 | steps: 49 | - name: Checkout project 50 | uses: actions/checkout@v4 51 | - name: Install Python 52 | uses: actions/setup-python@v5 53 | with: 54 | python-version: '3.x' 55 | - name: Install Testing Requirements 56 | run: python -m pip install nox 57 | - name: Run tests 58 | run: python -m nox -e tests 59 | - name: Upload to codecov 60 | uses: codecov/codecov-action@v5 61 | with: 62 | token: ${{ secrets.CODECOV_TOKEN }} 63 | fail_ci_if_error: true 64 | 65 | anaconda: 66 | name: ${{ matrix.os }}-anaconda 67 | runs-on: ${{ matrix.os }} 68 | defaults: 69 | run: 70 | shell: bash -l {0} # setup-miniconda requires using login bash shells to activate env 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | os: [ubuntu-latest, windows-latest, macos-latest] 75 | steps: 76 | - uses: actions/checkout@v4 77 | - uses: conda-incubator/setup-miniconda@v3 78 | with: 79 | auto-update-conda: true 80 | auto-activate-base: true 81 | activate-environment: '' 82 | - name: Install Testing Requirements 83 | run: python -m pip install nox 84 | - name: Run Tests 85 | run: python -m nox -e tests 86 | - name: Upload to codecov 87 | uses: codecov/codecov-action@v5 88 | with: 89 | token: ${{ secrets.CODECOV_TOKEN }} 90 | fail_ci_if_error: true 91 | 92 | rhel8-system-python: 93 | name: rhel8-system-python 94 | runs-on: ubuntu-latest 95 | container: "almalinux:8" 96 | steps: 97 | - name: Install System Python and Git 98 | run: yum install -y python3-devel python3-pip python3 git 99 | - uses: actions/checkout@v4 100 | - name: Install Testing Requirements 101 | run: python3 -m pip install nox 102 | - name: Run Tests 103 | run: python3 -m nox -e tests 104 | # - name: Upload to codecov 105 | # uses: codecov/codecov-action@v5 106 | # with: 107 | # token: ${{ secrets.CODECOV_TOKEN }} 108 | # fail_ci_if_error: true 109 | 110 | rhel8-appstream-py38: 111 | name: rhel8-appstream-py38 112 | runs-on: ubuntu-latest 113 | container: "almalinux:8" 114 | steps: 115 | - name: Install Python 3.8 and Git from AppStream 116 | run: yum install -y python38-devel python38-pip python38-pip-wheel python38 git 117 | - uses: actions/checkout@v4 118 | - name: Install Testing Requirements 119 | run: python3.8 -m pip install nox 120 | - name: Run Tests 121 | run: python3.8 -m nox -e tests 122 | # - name: Upload to codecov 123 | # uses: codecov/codecov-action@v5 124 | # with: 125 | # token: ${{ secrets.CODECOV_TOKEN }} 126 | # fail_ci_if_error: true 127 | 128 | rhel8-appstream-py39: 129 | name: rhel8-appstream-py39 130 | runs-on: ubuntu-latest 131 | container: "almalinux:8" 132 | steps: 133 | - name: Install Python 3.9 and Git from AppStream 134 | run: yum install -y python39-devel python39-pip python39-pip-wheel python39 git 135 | - uses: actions/checkout@v4 136 | - name: Install Testing Requirements 137 | run: python3.9 -m pip install nox 138 | - name: Run Tests 139 | run: python3.9 -m nox -e tests 140 | # - name: Upload to codecov 141 | # uses: codecov/codecov-action@v5 142 | # with: 143 | # token: ${{ secrets.CODECOV_TOKEN }} 144 | # fail_ci_if_error: true 145 | 146 | rhel9-system-python: 147 | name: rhel9-system-python 148 | runs-on: ubuntu-latest 149 | container: "almalinux:9" 150 | steps: 151 | - name: Install System Python and Git 152 | run: yum install -y python3-devel python3-pip python3 git 153 | - uses: actions/checkout@v4 154 | - name: Install Testing Requirements 155 | run: python3 -m pip install nox 156 | - name: Run Tests 157 | run: python3 -m nox -e tests 158 | - name: Upload to codecov 159 | uses: codecov/codecov-action@v5 160 | with: 161 | token: ${{ secrets.CODECOV_TOKEN }} 162 | fail_ci_if_error: true 163 | 164 | msys: 165 | name: ${{ matrix.msystem }}-system-python 166 | runs-on: windows-latest 167 | defaults: 168 | run: 169 | shell: msys2 {0} 170 | strategy: 171 | fail-fast: false 172 | matrix: 173 | msystem: 174 | - MSYS 175 | - MINGW32 176 | - MINGW64 177 | - UCRT64 178 | steps: 179 | - name: Install msys2 180 | uses: msys2/setup-msys2@v2 181 | with: 182 | msystem: ${{ matrix.msystem }} 183 | pacboy: >- 184 | gnupg 185 | python:p 186 | python-pip:p 187 | update: true 188 | - uses: actions/checkout@v4 189 | - name: Run tests 190 | run: | 191 | python -m venv .venv 192 | . .venv/bin/activate 193 | pip install nox 194 | nox -e tests 195 | - name: Upload to codecov 196 | uses: codecov/codecov-action@v5 197 | with: 198 | token: ${{ secrets.CODECOV_TOKEN }} 199 | fail_ci_if_error: true 200 | 201 | alpine: 202 | name: alpine-system-python 203 | runs-on: ubuntu-latest 204 | container: "alpine:latest" 205 | steps: 206 | - name: Update Packages 207 | run: | 208 | apk update 209 | apk upgrade 210 | - name: Install Testing and Coverage Upload Dependencies 211 | run: | 212 | apk add python3 python3-dev py3-pip py3-nox py3-attrs git 213 | apk add bash gpg curl 214 | - name: Download Source 215 | uses: actions/checkout@v4 216 | - name: Run Tests 217 | run: | 218 | nox -e tests 219 | - name: Upload to codecov 220 | uses: codecov/codecov-action@v5 221 | with: 222 | token: ${{ secrets.CODECOV_TOKEN }} 223 | fail_ci_if_error: true 224 | 225 | homebrew: 226 | name: homebrew-system-python 227 | runs-on: macos-latest 228 | steps: 229 | - name: Install System Python and Git 230 | run: | 231 | brew install python git 232 | - name: Download Source 233 | uses: actions/checkout@v4 234 | - name: Build venv 235 | run: | 236 | python3 -m venv .venv 237 | . .venv/bin/activate 238 | echo PATH=$PATH >> $GITHUB_ENV 239 | - name: Install Testing Requirements 240 | run: | 241 | python3 -m pip install nox 242 | - name: Run Tests 243 | run: | 244 | python3 -m nox -e tests 245 | - name: Upload to codecov 246 | uses: codecov/codecov-action@v5 247 | with: 248 | token: ${{ secrets.CODECOV_TOKEN }} 249 | fail_ci_if_error: true 250 | 251 | archlinux: 252 | name: archlinux-system-python 253 | runs-on: ubuntu-latest 254 | container: 255 | image: archlinux 256 | options: --privileged 257 | steps: 258 | - name: Install System Python and Git 259 | run: | 260 | pacman --noconfirm -Sy python python-pip git 261 | - name: Download Source 262 | uses: actions/checkout@v4 263 | - name: Create and activate environment 264 | run: | 265 | python -m venv .venv 266 | . .venv/bin/activate 267 | echo PATH=$PATH >> $GITHUB_ENV 268 | - name: Install Testing Requirements 269 | run: | 270 | pip install nox 271 | - name: Run Tests 272 | run: | 273 | nox -e tests 274 | - name: Upload to codecov 275 | uses: codecov/codecov-action@v5 276 | with: 277 | token: ${{ secrets.CODECOV_TOKEN }} 278 | fail_ci_if_error: true 279 | 280 | concurrency: 281 | group: ${{ github.workflow }}-${{ github.ref }} 282 | cancel-in-progress: ${{ contains(github.ref, 'master') }} 283 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .coverage 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: "https://github.com/pre-commit/pre-commit-hooks" 4 | rev: v5.0.0 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: end-of-file-fixer 8 | - id: mixed-line-ending 9 | args: 10 | - --fix=lf 11 | - id: fix-byte-order-marker 12 | - id: check-merge-conflict 13 | 14 | - repo: https://github.com/henryiii/validate-pyproject-schema-store 15 | rev: "2025.05.12" 16 | hooks: 17 | - id: validate-pyproject 18 | 19 | - repo: https://github.com/codespell-project/codespell 20 | rev: v2.4.1 21 | hooks: 22 | - id: codespell 23 | additional_dependencies: 24 | - tomli 25 | 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: v0.11.12 28 | hooks: 29 | # Run the linter. 30 | - id: ruff-check 31 | args: 32 | - --fix 33 | - --exit-non-zero-on-fix 34 | # Run the formatter. 35 | - id: ruff-format 36 | 37 | ci: 38 | autofix_prs: false 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2018, Takafumi Arakaki 3 | Copyright Kaleb Barrett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # find_libpython 2 | 3 | A pypi project version of [this](https://gist.github.com/tkf/d980eee120611604c0b9b5fef5b8dae6) gist, which also appears 4 | within the [PyCall](https://github.com/JuliaPy/PyCall.jl/blob/master/deps/find_libpython.py) library. 5 | 6 | The library is designed to find the path to the libpython dynamic library for the current Python environment. 7 | It should work with many types of installations, whether it be conda-managed, system-managed, or otherwise. 8 | And it should function on Windows, Mac OS/OS X, and any Linux distribution. 9 | 10 | This code is useful in several contexts, including projects that embed a Python interpreter into another process, 11 | or Python library build systems. 12 | 13 | ## Usage 14 | 15 | `find_libpython` is both a script and a Python package. 16 | Usage as a script is useful in contexts like obtaining the path to libpython for linking in makefile-based build systems. 17 | It could also be used to determine the path to libpython for embedding a Python interpreter in a process written in another language. 18 | In that case the recommended usage is to simply call the script in a subprocess with no arguments and parse the output. 19 | 20 | ``` 21 | > find_libpython 22 | /home/kaleb/miniconda3/envs/test/lib/libpython3.8.so.1.0 23 | ``` 24 | 25 | The full help message: 26 | ``` 27 | > find_libpython --help 28 | usage: find_libpython [-h] [-v] [--list-all | --candidate-names | --candidate-paths | --platform-info | --version] 29 | 30 | Locate libpython associated with this Python executable. 31 | 32 | options: 33 | -h, --help show this help message and exit 34 | -v, --verbose Print debugging information. 35 | --list-all Print list of all paths found. 36 | --candidate-names Print list of candidate names of libpython. 37 | --candidate-paths Print list of candidate paths of libpython. 38 | --platform-info Print information about the platform and exit. 39 | --version show program's version number and exit 40 | ``` 41 | 42 | Usage as a library might occur when you need to obtain the path to the library in a Python-based build system like distutils. 43 | It is recommended to use the `find_libpython` method which will return the path to libpython as a string, or `None` if it cannot be found. 44 | 45 | ```python 46 | >>> from find_libpython import find_libpython 47 | >>> find_libpython() 48 | '/home/kaleb/miniconda3/envs/test/lib/libpython3.8.so.1.0' 49 | ``` 50 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import nox 4 | 5 | test_reqs = ["pytest", "pytest-cov", "coverage"] 6 | pytest_cov_args = ["--cov=find_libpython", "--cov-branch"] 7 | coverage_file = "coverage.xml" 8 | 9 | 10 | @nox.session 11 | def tests(session): 12 | # install current module and runtime dependencies 13 | session.install("-e", ".") 14 | 15 | # install testing dependencies 16 | session.install(*test_reqs) 17 | 18 | # print info 19 | session.run( 20 | "python", 21 | "-m", 22 | "coverage", 23 | "run", 24 | "-m", 25 | "find_libpython", 26 | "-v", 27 | "--platform-info", 28 | ) 29 | session.run( 30 | "python", 31 | "-m", 32 | "coverage", 33 | "run", 34 | "-m", 35 | "find_libpython", 36 | "-v", 37 | "--candidate-names", 38 | ) 39 | session.run( 40 | "python", 41 | "-m", 42 | "coverage", 43 | "run", 44 | "-m", 45 | "find_libpython", 46 | "-v", 47 | "--candidate-paths", 48 | ) 49 | session.run("python", "-m", "coverage", "run", "-m", "find_libpython", "-v") 50 | 51 | # run pytest 52 | install_loc = session.run( 53 | "python", 54 | "-c", 55 | "import find_libpython; print(find_libpython.__file__)", 56 | silent=True, 57 | ) 58 | install_loc = os.path.dirname(install_loc.strip()) 59 | session.run("pytest", *pytest_cov_args, "tests/") 60 | session.run( 61 | "pytest", *pytest_cov_args, "--cov-append", "--doctest-modules", install_loc 62 | ) 63 | 64 | # create coverage report for upload 65 | session.run("coverage", "xml", "-o", coverage_file) 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=43", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.ruff] 6 | target-version = "py37" 7 | 8 | [tool.ruff.lint] 9 | select = [ 10 | "E", # pycodestyle errors 11 | "F", # pyflakes 12 | "I", # isort 13 | "UP", # pyupgrade 14 | "PL", # pylint 15 | ] 16 | ignore = [ 17 | "E501", # Line too long 18 | "PLR0912", # Too many branches 19 | ] 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = find_libpython 3 | version = attr: find_libpython.__version__ 4 | url = https://github.com/ktbarrett/find_libpython 5 | author = Takafumi Arakaki 6 | maintainer = Kaleb Barrett 7 | maintainer_email = dev.ktbarrett@gmail.com 8 | license = MIT 9 | description = Finds the libpython associated with your environment, wherever it may be hiding 10 | long_description = file: README.md 11 | long_description_content_type = text/markdown 12 | keywords = 13 | libpython 14 | classifiers = 15 | Programming Language :: Python :: 3 16 | Topic :: Software Development :: Libraries 17 | 18 | [options] 19 | packages = 20 | find_libpython 21 | package_dir = 22 | = src 23 | 24 | [options.entry_points] 25 | console_scripts = 26 | find_libpython = find_libpython:main 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/find_libpython/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Locate libpython associated with this Python executable. 3 | """ 4 | 5 | # License 6 | # 7 | # Copyright 2018, Takafumi Arakaki 8 | # Copyright Kaleb Barrett 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining 11 | # a copy of this software and associated documentation files (the 12 | # "Software"), to deal in the Software without restriction, including 13 | # without limitation the rights to use, copy, modify, merge, publish, 14 | # distribute, sublicense, and/or sell copies of the Software, and to 15 | # permit persons to whom the Software is furnished to do so, subject to 16 | # the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be 19 | # included in all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | import ctypes 30 | import os 31 | import sys 32 | from ctypes.util import find_library as _find_library 33 | from logging import getLogger as _getLogger 34 | from sysconfig import get_config_var as _get_config_var 35 | 36 | from find_libpython._version import __version__ # noqa: F401 37 | 38 | _logger = _getLogger("find_libpython") 39 | 40 | _is_apple = sys.platform == "darwin" 41 | _is_cygwin = sys.platform in ("msys", "cygwin") 42 | _is_mingw = sys.platform == "mingw" 43 | _is_windows = os.name == "nt" and not _is_mingw and not _is_cygwin 44 | _is_posix = os.name == "posix" 45 | 46 | _SHLIB_SUFFIX = _get_config_var("_SHLIB_SUFFIX") 47 | if _SHLIB_SUFFIX is None: 48 | if _is_windows: 49 | _SHLIB_SUFFIX = ".dll" 50 | else: 51 | _SHLIB_SUFFIX = ".so" 52 | if _is_apple: 53 | # _get_config_var("_SHLIB_SUFFIX") can be ".so" in macOS. 54 | # Let's not use the value from sysconfig. 55 | _SHLIB_SUFFIX = ".dylib" 56 | 57 | 58 | def _linked_libpython_unix(libpython): 59 | if not hasattr(libpython, "Py_GetVersion"): 60 | return None 61 | 62 | class Dl_info(ctypes.Structure): 63 | _fields_ = [ 64 | ("dli_fname", ctypes.c_char_p), 65 | ("dli_fbase", ctypes.c_void_p), 66 | ("dli_sname", ctypes.c_char_p), 67 | ("dli_saddr", ctypes.c_void_p), 68 | ] 69 | 70 | libdl = ctypes.CDLL(_find_library("dl")) 71 | libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(Dl_info)] 72 | libdl.dladdr.restype = ctypes.c_int 73 | 74 | dlinfo = Dl_info() 75 | retcode = libdl.dladdr( 76 | ctypes.cast(libpython.Py_GetVersion, ctypes.c_void_p), 77 | ctypes.pointer(dlinfo), 78 | ) 79 | if retcode == 0: # means error 80 | return None 81 | return os.path.realpath(dlinfo.dli_fname.decode()) 82 | 83 | 84 | def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows): 85 | """ 86 | Convert a file basename `name` to a library name (no "lib" and ".so" etc.) 87 | 88 | >>> _library_name("libpython3.7m.so") # doctest: +SKIP 89 | 'python3.7m' 90 | >>> _library_name("libpython3.7m.so", suffix=".so", _is_windows=False) 91 | 'python3.7m' 92 | >>> _library_name("libpython3.7m.dylib", suffix=".dylib", _is_windows=False) 93 | 'python3.7m' 94 | >>> _library_name("python37.dll", suffix=".dll", _is_windows=True) 95 | 'python37' 96 | """ 97 | if not _is_windows and name.startswith("lib"): 98 | name = name[len("lib") :] 99 | if suffix and name.endswith(suffix): 100 | name = name[: -len(suffix)] 101 | return name 102 | 103 | 104 | def _append_truthy(list, item): 105 | if item: 106 | list.append(item) 107 | 108 | 109 | def _uniquifying(items): 110 | """ 111 | Yield items while excluding the duplicates and preserving the order. 112 | 113 | >>> list(_uniquifying([1, 2, 1, 2, 3])) 114 | [1, 2, 3] 115 | """ 116 | seen = set() 117 | for x in items: 118 | if x not in seen: 119 | yield x 120 | seen.add(x) 121 | 122 | 123 | def _uniquified(func): 124 | """Wrap iterator returned from `func` by `_uniquifying`.""" 125 | from functools import wraps 126 | 127 | @wraps(func) 128 | def wrapper(*args, **kwds): 129 | return _uniquifying(func(*args, **kwds)) 130 | 131 | return wrapper 132 | 133 | 134 | def _get_proc_library(): 135 | pid = os.getpid() 136 | path = f"/proc/{pid}/maps" 137 | lines = open(path).readlines() 138 | 139 | for line in lines: 140 | path = line.split(" ", 5)[5].strip() 141 | if "libpython" in os.path.basename(path): 142 | if not os.path.isfile(path): 143 | continue 144 | yield path 145 | 146 | 147 | @_uniquified 148 | def candidate_names(suffix=_SHLIB_SUFFIX): 149 | """ 150 | Iterate over candidate file names of libpython. 151 | 152 | Yields 153 | ------ 154 | name : str 155 | Candidate name libpython. 156 | """ 157 | 158 | # Quoting configure.ac in the cpython code base: 159 | # "INSTSONAME is the name of the shared library that will be use to install 160 | # on the system - some systems like version suffix, others don't."" 161 | # 162 | # A typical INSTSONAME is 'libpython3.8.so.1.0' on Linux, or 163 | # 'Python.framework/Versions/3.9/Python' on MacOS. Due to the possible 164 | # version suffix we have to find the suffix within the filename. 165 | INSTSONAME = _get_config_var("INSTSONAME") 166 | if INSTSONAME and suffix in INSTSONAME: 167 | yield INSTSONAME 168 | 169 | LDLIBRARY = _get_config_var("LDLIBRARY") 170 | if LDLIBRARY and os.path.splitext(LDLIBRARY)[1] == suffix: 171 | yield LDLIBRARY 172 | 173 | LIBRARY = _get_config_var("LIBRARY") 174 | if LIBRARY and os.path.splitext(LIBRARY)[1] == suffix: 175 | yield LIBRARY 176 | 177 | DLLLIBRARY = _get_config_var("DLLLIBRARY") 178 | if DLLLIBRARY: 179 | yield DLLLIBRARY 180 | 181 | if _is_mingw: 182 | dlprefix = "lib" 183 | elif _is_windows or _is_cygwin: 184 | dlprefix = "" 185 | else: 186 | dlprefix = "lib" 187 | 188 | sysdata = dict( 189 | v=sys.version_info, 190 | # VERSION is X.Y in Linux/macOS and XY in Windows: 191 | VERSION=( 192 | _get_config_var("VERSION") 193 | or f"{sys.version_info.major}.{sys.version_info.minor}" 194 | ), 195 | ABIFLAGS=(_get_config_var("ABIFLAGS") or _get_config_var("abiflags") or ""), 196 | ) 197 | 198 | for stem in [ 199 | "python{VERSION}{ABIFLAGS}".format(**sysdata), 200 | "python{VERSION}".format(**sysdata), 201 | ]: 202 | yield dlprefix + stem + suffix 203 | 204 | 205 | def _linked_pythondll() -> str: 206 | # On Windows there is the `sys.dllhandle` attribute which is the 207 | # DLL Handle ID for the associated python.dll for the installation. 208 | # We can use the GetModuleFileName function to get the path to the 209 | # python.dll this way. 210 | 211 | # sys.dllhandle is an module ID, which is just a void* cast to an integer, 212 | # we turn it back into a pointer for the ctypes call 213 | dll_hmodule = ctypes.cast(sys.dllhandle, ctypes.c_void_p) 214 | 215 | # create a buffer for the return path of the maximum length of filepaths in Windows unicode interfaces 216 | # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation 217 | path_return_buffer = ctypes.create_unicode_buffer(32768) 218 | 219 | # GetModuleFileName sets the return buffer to the value of the path used to load the module. 220 | # We expect it to always be a normalized absolute path to python.dll. 221 | r = ctypes.windll.kernel32.GetModuleFileNameW( 222 | dll_hmodule, path_return_buffer, len(path_return_buffer) 223 | ) 224 | 225 | # The return value is the length of the returned string in unicode characters 226 | # if the size of the buffer (argument 3) is returned, the buffer was too small. 227 | # Don't know what else to do here but give up. 228 | if r == len(path_return_buffer): 229 | return None 230 | 231 | return path_return_buffer.value 232 | 233 | 234 | @_uniquified 235 | def candidate_paths(suffix=_SHLIB_SUFFIX): 236 | """ 237 | Iterate over candidate paths of libpython. 238 | 239 | Yields 240 | ------ 241 | path : str or None 242 | Candidate path to libpython. The path may not be a fullpath 243 | and may not exist. 244 | """ 245 | 246 | if _is_windows: 247 | yield _linked_pythondll() 248 | 249 | # List candidates for directories in which libpython may exist 250 | lib_dirs = [] 251 | _append_truthy(lib_dirs, _get_config_var("LIBPL")) 252 | _append_truthy(lib_dirs, _get_config_var("srcdir")) 253 | _append_truthy(lib_dirs, _get_config_var("LIBDIR")) 254 | if _is_windows or _is_mingw or _is_cygwin: 255 | # On Windows DLLs go in bin/ while static libraries go in lib/ 256 | _append_truthy(lib_dirs, _get_config_var("BINDIR")) 257 | 258 | # LIBPL seems to be the right config_var to use. It is the one 259 | # used in python-config when shared library is not enabled: 260 | # https://github.com/python/cpython/blob/v3.7.0/Misc/python-config.in#L55-L57 261 | # 262 | # But we try other places just in case. 263 | 264 | if _is_windows or _is_cygwin or _is_mingw: 265 | lib_dirs.append(os.path.join(os.path.dirname(sys.executable))) 266 | else: 267 | lib_dirs.append( 268 | os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "lib") 269 | ) 270 | 271 | # For macOS: 272 | _append_truthy(lib_dirs, _get_config_var("PYTHONFRAMEWORKPREFIX")) 273 | 274 | lib_dirs.append(sys.exec_prefix) 275 | lib_dirs.append(os.path.join(sys.exec_prefix, "lib")) 276 | 277 | lib_basenames = list(candidate_names(suffix=suffix)) 278 | 279 | if _is_posix and not _is_cygwin: 280 | for basename in lib_basenames: 281 | try: 282 | libpython = ctypes.CDLL(basename) 283 | except OSError: 284 | pass 285 | else: 286 | yield _linked_libpython_unix(libpython) 287 | 288 | try: 289 | yield from _get_proc_library() 290 | except OSError: 291 | _logger.debug("Unable to check /proc filesystem for libpython") 292 | 293 | for directory in lib_dirs: 294 | for basename in lib_basenames: 295 | yield os.path.join(directory, basename) 296 | 297 | # In macOS and Windows, ctypes.util.find_library returns a full path: 298 | for basename in lib_basenames: 299 | yield _find_library(_library_name(basename)) 300 | 301 | 302 | # Possibly useful links: 303 | # * https://packages.ubuntu.com/bionic/amd64/libpython3.6/filelist 304 | # * https://github.com/Valloric/ycmd/issues/518 305 | # * https://github.com/Valloric/ycmd/pull/519 306 | 307 | 308 | def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple): 309 | """ 310 | Normalize shared library `path` to a real path. 311 | 312 | If `path` is not a full path, `None` is returned. If `path` does 313 | not exists, append `_SHLIB_SUFFIX` and check if it exists. 314 | Finally, the path is canonicalized by following the symlinks. 315 | 316 | Parameters 317 | ---------- 318 | path : str or None 319 | A candidate path to a shared library. 320 | """ 321 | if not path: 322 | return None 323 | if not os.path.isabs(path): 324 | return None 325 | if os.path.isfile(path): 326 | return os.path.realpath(path) 327 | if os.path.isfile(path + suffix): 328 | return os.path.realpath(path + suffix) 329 | if _is_apple: 330 | return _normalize_path( 331 | _remove_suffix_apple(path), suffix=".so", _is_apple=False 332 | ) 333 | return None 334 | 335 | 336 | def _remove_suffix_apple(path): 337 | """ 338 | Strip off .so or .dylib. 339 | 340 | >>> _remove_suffix_apple("libpython.so") 341 | 'libpython' 342 | >>> _remove_suffix_apple("libpython.dylib") 343 | 'libpython' 344 | >>> _remove_suffix_apple("libpython3.7") 345 | 'libpython3.7' 346 | """ 347 | if path.endswith(".dylib"): 348 | return path[: -len(".dylib")] 349 | if path.endswith(".so"): 350 | return path[: -len(".so")] 351 | return path 352 | 353 | 354 | @_uniquified 355 | def _finding_libpython(): 356 | """ 357 | Iterate over existing libpython paths. 358 | 359 | The first item is likely to be the best one. 360 | 361 | Yields 362 | ------ 363 | path : str 364 | Existing path to a libpython. 365 | """ 366 | for path in candidate_paths(): 367 | _logger.debug("Candidate: %s", path) 368 | normalized = _normalize_path(path) 369 | if normalized: 370 | _logger.debug("Found: %s", normalized) 371 | yield normalized 372 | else: 373 | _logger.debug("Not found.") 374 | 375 | 376 | def find_libpython(): 377 | """ 378 | Return a path (`str`) to libpython or `None` if not found. 379 | 380 | Parameters 381 | ---------- 382 | path : str or None 383 | Existing path to the (supposedly) correct libpython. 384 | """ 385 | for path in _finding_libpython(): 386 | return os.path.realpath(path) 387 | 388 | 389 | def _print_all(items): 390 | for x in items: 391 | print(x) 392 | 393 | 394 | def _cli_find_libpython(cli_op, verbose): 395 | import logging 396 | 397 | # Importing `logging` module here so that using `logging.debug` 398 | # instead of `_logger.debug` outside of this function becomes an 399 | # error. 400 | 401 | if verbose: 402 | logging.basicConfig(format="%(levelname)s %(message)s", level=logging.DEBUG) 403 | 404 | if cli_op == "list-all": 405 | _print_all(_finding_libpython()) 406 | elif cli_op == "candidate-names": 407 | _print_all(candidate_names()) 408 | elif cli_op == "candidate-paths": 409 | _print_all(p for p in candidate_paths() if p and os.path.isabs(p)) 410 | elif cli_op == "platform-info": 411 | _log_platform_info() 412 | else: 413 | path = find_libpython() 414 | if path is None: 415 | return 1 416 | print(path, end="") 417 | 418 | 419 | def _log_platform_info(): 420 | print(f"is_windows = {_is_windows}") 421 | print(f"is_apple = {_is_apple}") 422 | print(f"is_mingw = {_is_mingw}") 423 | print(f"is_msys = {_is_cygwin}") 424 | print(f"is_posix = {_is_posix}") 425 | 426 | 427 | def main(args=None): 428 | import argparse 429 | 430 | parser = argparse.ArgumentParser(description=__doc__) 431 | parser.add_argument( 432 | "-v", "--verbose", action="store_true", help="Print debugging information." 433 | ) 434 | 435 | group = parser.add_mutually_exclusive_group() 436 | group.add_argument( 437 | "--list-all", 438 | action="store_const", 439 | dest="cli_op", 440 | const="list-all", 441 | help="Print list of all paths found.", 442 | ) 443 | group.add_argument( 444 | "--candidate-names", 445 | action="store_const", 446 | dest="cli_op", 447 | const="candidate-names", 448 | help="Print list of candidate names of libpython.", 449 | ) 450 | group.add_argument( 451 | "--candidate-paths", 452 | action="store_const", 453 | dest="cli_op", 454 | const="candidate-paths", 455 | help="Print list of candidate paths of libpython.", 456 | ) 457 | group.add_argument( 458 | "--platform-info", 459 | action="store_const", 460 | dest="cli_op", 461 | const="platform-info", 462 | help="Print information about the platform and exit.", 463 | ) 464 | group.add_argument( 465 | "--version", action="version", version=f"find_libpython {__version__}" 466 | ) 467 | 468 | ns = parser.parse_args(args) 469 | parser.exit(_cli_find_libpython(**vars(ns))) 470 | -------------------------------------------------------------------------------- /src/find_libpython/__main__.py: -------------------------------------------------------------------------------- 1 | from find_libpython import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /src/find_libpython/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.4.1" 2 | -------------------------------------------------------------------------------- /tests/test_find_libpython.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import sys 3 | 4 | import pytest 5 | 6 | from find_libpython import ( 7 | _get_proc_library, 8 | _is_cygwin, 9 | _is_posix, 10 | _linked_libpython_unix, 11 | find_libpython, 12 | ) 13 | 14 | try: 15 | ctypes.CDLL("") 16 | can_get_handle_to_main = True 17 | except OSError: 18 | print("Platform does not support opening the current process library") 19 | can_get_handle_to_main = False 20 | 21 | 22 | def test_find_libpython(): 23 | # find path 24 | path = find_libpython() 25 | # ensure we have a path 26 | assert path is not None 27 | # check to ensure it is a libpython share object 28 | lib = ctypes.CDLL(path) 29 | assert hasattr(lib, "Py_Initialize") 30 | # ensure it's the right version... 31 | lib.Py_GetVersion.restype = ctypes.c_char_p 32 | lib_version = lib.Py_GetVersion().decode().split()[0] 33 | curr_version = sys.version.split()[0] 34 | assert lib_version == curr_version 35 | 36 | 37 | @pytest.mark.skipif( 38 | not _is_posix or _is_cygwin or not can_get_handle_to_main, 39 | reason="only linux support this", 40 | ) 41 | def test_get_proc_library(): 42 | # Get current library 43 | libpython = ctypes.CDLL("") 44 | 45 | # Get library for reference 46 | path_linked = _linked_libpython_unix(libpython) 47 | # Get library from /proc method 48 | path_from_proc = next(_get_proc_library()) 49 | 50 | # Only compare paths if both return a library (not /usr/bin/python3.x) 51 | if "libpython" in path_linked: 52 | assert path_linked == path_from_proc 53 | --------------------------------------------------------------------------------