From eb27b5ba27e468c657223d90227aa64be6a21c02 Mon Sep 17 00:00:00 2001 From: Rok Mandeljc Date: Thu, 12 Sep 2024 16:13:02 +0200 Subject: [PATCH] hooks: add run-time hook for findlibs Add run-time hook for `findlibs` that overrides `findlibs.find()` function with custom implementation that gives precedence to searching `sys._MEIPASS` and then immediately falling back to `ctypes.util.find_library`. The original implementation searches a set of hard-coded paths before falling back to `ctypes.util.find_library`; those path include Homebrew directory on macOS, so if that happens to be installed on the run-time system, the original `findlibs.find()` implementation ends up returning system-wide copy of the library (if it is available) instead of the bundled one. Which, especially on macOS, can lead to problems due to (lack of proper) signature on the system-wide copy of the library when application itself is signed with developer identity. that searches a set of hard-coded paths before --- _pyinstaller_hooks_contrib/rthooks.dat | 1 + .../rthooks/pyi_rth_findlibs.py | 52 +++++++++++++++++++ news/799.new.rst | 7 +++ tests/test_libraries.py | 3 ++ 4 files changed, 63 insertions(+) create mode 100644 _pyinstaller_hooks_contrib/rthooks/pyi_rth_findlibs.py create mode 100644 news/799.new.rst 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!"