pyi_rth_inspect.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2021-2023, PyInstaller Development Team.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: Apache-2.0
  10. #-----------------------------------------------------------------------------
  11. def _pyi_rthook():
  12. import inspect
  13. import os
  14. import sys
  15. import zipfile
  16. # Use sys._MEIPASS with normalized path component separator. This is necessary on some platforms (i.e., msys2/mingw
  17. # python on Windows), because we use string comparisons on the paths.
  18. SYS_PREFIX = os.path.normpath(sys._MEIPASS)
  19. BASE_LIBRARY = os.path.join(SYS_PREFIX, "base_library.zip")
  20. # Obtain the list of modules in base_library.zip, so we can use it in our `_pyi_getsourcefile` implementation.
  21. def _get_base_library_files(filename):
  22. # base_library.zip might not exit
  23. if not os.path.isfile(filename):
  24. return set()
  25. with zipfile.ZipFile(filename, 'r') as zf:
  26. namelist = zf.namelist()
  27. return set(os.path.normpath(entry) for entry in namelist)
  28. base_library_files = _get_base_library_files(BASE_LIBRARY)
  29. # Provide custom implementation of inspect.getsourcefile() for frozen applications that properly resolves relative
  30. # filenames obtained from object (e.g., inspect stack-frames). See #5963.
  31. #
  32. # Although we are overriding `inspect.getsourcefile` function, we are NOT trying to resolve source file here!
  33. # The main purpose of this implementation is to properly resolve relative file names obtained from `co_filename`
  34. # attribute of code objects (which are, in turn, obtained from in turn are obtained from `frame` and `traceback`
  35. # objects). PyInstaller strips absolute paths from `co_filename` when collecting modules, as the original absolute
  36. # paths are not portable/relocatable anyway. The `inspect` module tries to look up the module that corresponds to
  37. # the code object by comparing modules' `__file__` attribute to the value of `co_filename`. Therefore, our override
  38. # needs to resolve the relative file names (usually having a .py suffix) into absolute module names (which, in the
  39. # frozen application, usually have .pyc suffix).
  40. #
  41. # The `inspect` module retrieves the actual source code using `linecache.getlines()`. If the passed source filename
  42. # does not exist, the underlying implementation end up resolving the module, and obtains the source via loader's
  43. # `get_source` method. So for modules in the PYZ archive, it ends up calling `get_source` implementation on our
  44. # `PyiFrozenLoader`. For modules in `base_library.zip`, it ends up calling `get_source` on python's own
  45. # `zipimport.zipimporter`; to properly handle out-of-zip source files, we therefore need to monkey-patch
  46. # `get_source` with our own override that translates the in-zip .pyc filename into out-of-zip .py file location
  47. # and loads the source (this override is done in `pyimod02_importers` module).
  48. #
  49. # The above-described fallback takes place if the .pyc file does not exist on filesystem - if this ever becomes
  50. # a problem, we could consider monkey-patching `linecache.updatecache` (and possibly `checkcache`) to translate
  51. # .pyc paths in `sys._MEIPASS` and `base_library.zip` into .py paths in `sys._MEIPASS` before calling the original
  52. # implementation.
  53. _orig_inspect_getsourcefile = inspect.getsourcefile
  54. def _pyi_getsourcefile(object):
  55. filename = inspect.getfile(object)
  56. filename = os.path.normpath(filename) # Ensure path component separators are normalized.
  57. if not os.path.isabs(filename):
  58. # Check if given filename matches the basename of __main__'s __file__.
  59. main_file = getattr(sys.modules['__main__'], '__file__', None)
  60. if main_file and filename == os.path.basename(main_file):
  61. return main_file
  62. # If the relative filename does not correspond to the frozen entry-point script, convert it to the absolute
  63. # path in either `sys._MEIPASS/base_library.zip` or `sys._MEIPASS`, whichever applicable.
  64. #
  65. # The modules in `sys._MEIPASS/base_library.zip` are handled by python's `zipimport.zipimporter`, and have
  66. # their __file__ attribute point to the .pyc file in the archive. So we match the behavior, in order to
  67. # facilitate matching via __file__ attribute and use of loader's `get_source`, as per the earlier comment
  68. # block.
  69. #
  70. # The modules in PYZ archive are handled by our `PyFrozenLoader`, which now sets the module's __file__
  71. # attribute to point to where .py files would be. Therefore, we can directly merge SYS_PREFIX and filename
  72. # (and if the source .py file exists, it will be loaded directly from filename, without the intermediate
  73. # loader look-up).
  74. pyc_filename = filename + 'c'
  75. if pyc_filename in base_library_files:
  76. return os.path.normpath(os.path.join(BASE_LIBRARY, pyc_filename))
  77. return os.path.normpath(os.path.join(SYS_PREFIX, filename))
  78. elif filename.startswith(SYS_PREFIX):
  79. # If filename is already an absolute file path pointing into application's top-level directory, return it
  80. # as-is and prevent any further processing.
  81. return filename
  82. # Use original implementation as a fallback.
  83. return _orig_inspect_getsourcefile(object)
  84. inspect.getsourcefile = _pyi_getsourcefile
  85. _pyi_rthook()
  86. del _pyi_rthook