hook-tensorflow.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. # ------------------------------------------------------------------
  2. # Copyright (c) 2020 PyInstaller Development Team.
  3. #
  4. # This file is distributed under the terms of the GNU General Public
  5. # License (version 2.0 or later).
  6. #
  7. # The full license is available in LICENSE, distributed with
  8. # this software.
  9. #
  10. # SPDX-License-Identifier: GPL-2.0-or-later
  11. # ------------------------------------------------------------------
  12. from _pyinstaller_hooks_contrib.compat import importlib_metadata
  13. from packaging.version import Version
  14. from PyInstaller.compat import is_linux
  15. from PyInstaller.utils.hooks import (
  16. collect_data_files,
  17. collect_dynamic_libs,
  18. collect_submodules,
  19. get_module_attribute,
  20. is_module_satisfies,
  21. logger,
  22. )
  23. # Determine the name of `tensorflow` dist; this is available under different names (releases vs. nightly, plus build
  24. # variants). We need to determine the dist that we are dealing with, so we can query its version and metadata.
  25. _CANDIDATE_DIST_NAMES = (
  26. "tensorflow",
  27. "tensorflow-cpu",
  28. "tensorflow-gpu",
  29. "tensorflow-intel",
  30. "tensorflow-rocm",
  31. "tensorflow-macos",
  32. "tensorflow-aarch64",
  33. "tensorflow-cpu-aws",
  34. "tf-nightly",
  35. "tf-nightly-cpu",
  36. "tf-nightly-gpu",
  37. "tf-nightly-rocm",
  38. "intel-tensorflow",
  39. "intel-tensorflow-avx512",
  40. )
  41. dist = None
  42. for candidate_dist_name in _CANDIDATE_DIST_NAMES:
  43. try:
  44. dist = importlib_metadata.distribution(candidate_dist_name)
  45. break
  46. except importlib_metadata.PackageNotFoundError:
  47. continue
  48. version = None
  49. if dist is None:
  50. logger.warning(
  51. "hook-tensorflow: failed to determine tensorflow dist name! Reading version from tensorflow.__version__!"
  52. )
  53. try:
  54. version = get_module_attribute("tensorflow", "__version__")
  55. except Exception as e:
  56. raise Exception("Failed to read tensorflow.__version__") from e
  57. else:
  58. logger.info("hook-tensorflow: tensorflow dist name: %s", dist.name)
  59. version = dist.version
  60. # Parse version
  61. logger.info("hook-tensorflow: tensorflow version: %s", version)
  62. try:
  63. version = Version(version)
  64. except Exception as e:
  65. raise Exception("Failed to parse tensorflow version!") from e
  66. # Exclude from data collection:
  67. # - development headers in include subdirectory
  68. # - XLA AOT runtime sources
  69. # - libtensorflow_framework and libtensorflow_cc (since TF 2.12) shared libraries (to avoid duplication)
  70. # - import library (.lib) files (Windows-only)
  71. data_excludes = [
  72. "include",
  73. "xla_aot_runtime_src",
  74. "libtensorflow_framework.*",
  75. "libtensorflow_cc.*",
  76. "**/*.lib",
  77. ]
  78. # Under tensorflow 2.3.0 (the most recent version at the time of writing), _pywrap_tensorflow_internal extension module
  79. # ends up duplicated; once as an extension, and once as a shared library. In addition to increasing program size, this
  80. # also causes problems on macOS, so we try to prevent the extension module "variant" from being picked up.
  81. #
  82. # See pyinstaller/pyinstaller-hooks-contrib#49 for details.
  83. #
  84. # With PyInstaller >= 6.0, this issue is alleviated, because the binary dependency analysis (which picks up the
  85. # extension in question as a shared library that other extensions are linked against) now preserves the parent directory
  86. # layout, and creates a symbolic link to the top-level application directory.
  87. if is_module_satisfies('PyInstaller >= 6.0'):
  88. excluded_submodules = []
  89. else:
  90. excluded_submodules = ['tensorflow.python._pywrap_tensorflow_internal']
  91. def _submodules_filter(x):
  92. return x not in excluded_submodules
  93. if version < Version("1.15.0a0"):
  94. # 1.14.x and earlier: collect everything from tensorflow
  95. hiddenimports = collect_submodules('tensorflow', filter=_submodules_filter)
  96. datas = collect_data_files('tensorflow', excludes=data_excludes)
  97. elif version >= Version("1.15.0a0") and version < Version("2.2.0a0"):
  98. # 1.15.x - 2.1.x: collect everything from tensorflow_core
  99. hiddenimports = collect_submodules('tensorflow_core', filter=_submodules_filter)
  100. datas = collect_data_files('tensorflow_core', excludes=data_excludes)
  101. # Under 1.15.x, we seem to fail collecting a specific submodule, and need to add it manually...
  102. if version < Version("2.0.0a0"):
  103. hiddenimports += ['tensorflow_core._api.v1.compat.v2.summary.experimental']
  104. else:
  105. # 2.2.0 and newer: collect everything from tensorflow again
  106. hiddenimports = collect_submodules('tensorflow', filter=_submodules_filter)
  107. datas = collect_data_files('tensorflow', excludes=data_excludes)
  108. # From 2.6.0 on, we also need to explicitly collect keras (due to lazy mapping of tensorflow.keras.xyz -> keras.xyz)
  109. if version >= Version("2.6.0a0"):
  110. hiddenimports += collect_submodules('keras')
  111. # Starting with 2.14.0, we need `ml_dtypes` among hidden imports.
  112. if version >= Version("2.14.0"):
  113. hiddenimports += ['ml_dtypes']
  114. binaries = []
  115. excludedimports = excluded_submodules
  116. # Suppress warnings for missing hidden imports generated by this hook.
  117. # Requires PyInstaller > 5.1 (with pyinstaller/pyinstaller#6914 merged); no-op otherwise.
  118. warn_on_missing_hiddenimports = False
  119. # Collect the AutoGraph part of `tensorflow` code, to avoid a run-time warning about AutoGraph being unavailable:
  120. # `WARNING:tensorflow:AutoGraph is not available in this environment: functions lack code information. ...`
  121. # The warning is emitted if source for `log` function from `tensorflow.python.autograph.utils.ag_logging` cannot be
  122. # looked up. Not sure if we need sources for other parts of `tesnorflow`, though.
  123. # Requires PyInstaller >= 5.3, no-op in older versions.
  124. module_collection_mode = {
  125. 'tensorflow.python.autograph': 'py+pyz',
  126. }
  127. # Linux builds of tensorflow can optionally use CUDA from nvidia-* packages. If we managed to obtain dist, query the
  128. # requirements from metadata (the `and-cuda` extra marker), and convert them to module names.
  129. #
  130. # NOTE: while the installation of nvidia-* packages via `and-cuda` extra marker is not gated by the OS version check,
  131. # it is effectively available only on Linux (last Windows-native build that supported GPU is v2.10.0, and assumed that
  132. # CUDA is externally available).
  133. if is_linux and dist is not None:
  134. def _infer_nvidia_hiddenimports():
  135. import packaging.requirements
  136. from _pyinstaller_hooks_contrib.utils import nvidia_cuda as cudautils
  137. requirements = [packaging.requirements.Requirement(req) for req in dist.requires or []]
  138. env = {'extra': 'and-cuda'}
  139. requirements = [req.name for req in requirements if req.marker is None or req.marker.evaluate(env)]
  140. return cudautils.infer_hiddenimports_from_requirements(requirements)
  141. try:
  142. nvidia_hiddenimports = _infer_nvidia_hiddenimports()
  143. except Exception:
  144. # Log the exception, but make it non-fatal
  145. logger.warning("hook-tensorflow: failed to infer NVIDIA CUDA hidden imports!", exc_info=True)
  146. nvidia_hiddenimports = []
  147. logger.info("hook-tensorflow: inferred hidden imports for CUDA libraries: %r", nvidia_hiddenimports)
  148. hiddenimports += nvidia_hiddenimports
  149. # Collect the tensorflow-plugins (pluggable device plugins)
  150. hiddenimports += ['tensorflow-plugins']
  151. binaries += collect_dynamic_libs('tensorflow-plugins')
  152. # On Linux, prevent binary dependency analysis from generating symbolic links for libtensorflow_cc.so.2,
  153. # libtensorflow_framework.so.2, and _pywrap_tensorflow_internal.so to the top-level application directory. These
  154. # symbolic links seem to confuse tensorflow about its location (likely because code in one of the libraries looks up the
  155. # library file's location, but does not fully resolve it), which in turn prevents it from finding the collected CUDA
  156. # libraries in the nvidia/cu* package directories.
  157. #
  158. # The `bindepend_symlink_suppression` hook attribute requires PyInstaller >= 6.11, and is no-op in earlier versions.
  159. if is_linux:
  160. bindepend_symlink_suppression = [
  161. '**/libtensorflow_cc.so*',
  162. '**/libtensorflow_framework.so*',
  163. '**/_pywrap_tensorflow_internal.so',
  164. ]