diff --git a/_pyinstaller_hooks_contrib/rthooks.dat b/_pyinstaller_hooks_contrib/rthooks.dat index 538e94909..f868f56f7 100644 --- a/_pyinstaller_hooks_contrib/rthooks.dat +++ b/_pyinstaller_hooks_contrib/rthooks.dat @@ -1,6 +1,7 @@ { 'cryptography': ['pyi_rth_cryptography_openssl.py'], 'enchant': ['pyi_rth_enchant.py'], + 'findlibs': ['pyi_rth_findlibs.py'], 'ffpyplayer': ['pyi_rth_ffpyplayer.py'], 'osgeo': ['pyi_rth_osgeo.py'], 'traitlets': ['pyi_rth_traitlets.py'], diff --git a/_pyinstaller_hooks_contrib/rthooks/pyi_rth_findlibs.py b/_pyinstaller_hooks_contrib/rthooks/pyi_rth_findlibs.py new file mode 100644 index 000000000..43d3db11d --- /dev/null +++ b/_pyinstaller_hooks_contrib/rthooks/pyi_rth_findlibs.py @@ -0,0 +1,52 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2024, PyInstaller Development Team. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# The full license is in the file COPYING.txt, distributed with this software. +# +# SPDX-License-Identifier: Apache-2.0 +#----------------------------------------------------------------------------- + +# Override the findlibs.find() function to give precedence to sys._MEIPASS, followed by `ctypes.util.find_library`, +# and only then the hard-coded paths from the original implementation. The main aim here is to avoid loading libraries +# from Homebrew environment on macOS when it happens to be present at run-time and we have a bundled copy collected from +# the build system. This happens because we (try not to) modify `DYLD_LIBRARY_PATH`, and the original `findlibs.find()` +# implementation gives precedence to environment variables and several fixed/hard-coded locations, and uses +# `ctypes.util.find_library` as the final fallback... +def _pyi_rthook(): + import sys + import os + import ctypes.util + + import findlibs + + _orig_find = getattr(findlibs, 'find', None) + + def _pyi_find(lib_name, pkg_name=None): + pkg_name = pkg_name or lib_name + extension = findlibs.EXTENSIONS.get(sys.platform, ".so") + + # First check sys._MEIPASS + fullname = os.path.join(sys._MEIPASS, "lib{}{}".format(lib_name, extension)) + if os.path.isfile(fullname): + return fullname + + # Fall back to `ctypes.util.find_library` (to give it precedence over hard-coded paths from original + # implementation). + lib = ctypes.util.find_library(lib_name) + if lib is not None: + return lib + + # Finally, fall back to original implementation + if _orig_find is not None: + return _orig_find(lib_name, pkg_name) + + return None + + findlibs.find = _pyi_find + + +_pyi_rthook() +del _pyi_rthook diff --git a/news/799.new.rst b/news/799.new.rst new file mode 100644 index 000000000..73fd3b0df --- /dev/null +++ b/news/799.new.rst @@ -0,0 +1,7 @@ +Add run-time hook for ``findlibs`` that overrides the ``findlibs.find`` +function with custom implementation in order to ensure that the top-level +application directory is searched first. This prevents a system-wide +copy of the library being found and loaded instead of the bundled copy +when the system-wide copy happens to be available in one of fixed +locations that is scanned by the original implementation of ``findlibs.find`` +(for example, Homebrew directory on macOS). diff --git a/tests/test_libraries.py b/tests/test_libraries.py index c3270a49f..3233a986e 100644 --- a/tests/test_libraries.py +++ b/tests/test_libraries.py @@ -2068,6 +2068,9 @@ def test_eccodes_gribapi(pyi_builder): # - sys._MEIPASS/eccodes # - sys._MEIPASS/eccodes.libs # - sys._MEIPASS/eccodes/.dylibs + # as well as sys._MEIPASS itself (in case system-wide copy was collected into top-level application + # directory but is reported with full path instead of just basename due to our override of `findlibs.find()` + # via run-time hook). if pathlib.PurePath(sys._MEIPASS) not in pathlib.PurePath(library_path).parents: raise RuntimeError( f"Shared library path {library_path} is not rooted in top-level application directory!"