hook-pygraphviz.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # ------------------------------------------------------------------
  2. # Copyright (c) 2021 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. import os
  13. import pathlib
  14. import shutil
  15. from PyInstaller import compat
  16. from PyInstaller.depend import bindepend
  17. from PyInstaller.utils.hooks import logger
  18. def _collect_graphviz_files():
  19. binaries = []
  20. datas = []
  21. # A working `pygraphviz` installation requires graphviz programs in PATH. Attempt to resolve the `dot` executable to
  22. # see if this is the case.
  23. dot_binary = shutil.which('dot')
  24. if not dot_binary:
  25. logger.warning(
  26. "hook-pygraphviz: 'dot' program not found in PATH!"
  27. )
  28. return binaries, datas
  29. logger.info("hook-pygraphviz: found 'dot' program: %r", dot_binary)
  30. bin_dir = pathlib.Path(dot_binary).parent
  31. # Collect graphviz programs that might be called from `pygaphviz.agraph.AGraph`:
  32. # https://github.com/pygraphviz/pygraphviz/blob/pygraphviz-1.14/pygraphviz/agraph.py#L1330-L1348
  33. # On macOS and on Linux, several of these are symbolic links to a single executable.
  34. progs = (
  35. "neato",
  36. "dot",
  37. "twopi",
  38. "circo",
  39. "fdp",
  40. "nop",
  41. "osage",
  42. "patchwork",
  43. "gc",
  44. "acyclic",
  45. "gvpr",
  46. "gvcolor",
  47. "ccomps",
  48. "sccmap",
  49. "tred",
  50. "sfdp",
  51. "unflatten",
  52. )
  53. logger.debug("hook-pygraphviz: collecting graphviz program executables...")
  54. for program_name in progs:
  55. program_binary = shutil.which(program_name)
  56. if not program_binary:
  57. logger.debug("hook-pygaphviz: graphviz program %r not found!", program_name)
  58. continue
  59. # Ensure that the program executable was found in the same directory as the `dot` executable. This should
  60. # prevent us from falling back to other graphviz installations that happen to be in PATH.
  61. if pathlib.Path(program_binary).parent != bin_dir:
  62. logger.debug(
  63. "hook-pygraphviz: found program %r (%r) outside of directory %r - ignoring!",
  64. program_name, program_binary, str(bin_dir)
  65. )
  66. continue
  67. logger.debug("hook-pygraphviz: collecting graphviz program %r: %r", program_name, program_binary)
  68. binaries += [(program_binary, '.')]
  69. # Graphviz shared libraries should be automatically collected when PyInstaller performs binary dependency
  70. # analysis of the collected program executables as part of the main build process. However, we need to manually
  71. # collect plugins and their accompanying config file.
  72. logger.debug("hook-pygraphviz: looking for graphviz plugin directory...")
  73. if compat.is_win:
  74. # Under Windows, we have several installation variants:
  75. # - official installers and builds from https://gitlab.com/graphviz/graphviz/-/releases
  76. # - chocolatey
  77. # - msys2
  78. # - Anaconda
  79. # In all variants, the plugins and the config file are located in the `bin` directory, next to the program
  80. # executables.
  81. plugin_dir = bin_dir
  82. plugin_dest_dir = '.' # Collect into top-level application directory.
  83. # Official builds and Anaconda use unversioned `gvplugin-{name}.dll` plugin names, while msys2 uses
  84. # versioned `libgvplugin-{name}-{version}.dll` plugin names (with "lib" prefix).
  85. plugin_pattern = '*gvplugin*.dll'
  86. else:
  87. # Perform binary dependency analysis on the `dot` executable to obtain the path to graphiz shared libraries.
  88. # These need to be in the library search path for the programs to work, or discoverable via run-paths
  89. # (e.g., Anaconda on Linux and macOS, Homebrew on macOS).
  90. graphviz_lib_candidates = ['cdt', 'gvc', 'cgraph']
  91. if hasattr(bindepend, 'get_imports'):
  92. # PyInstaller >= 6.0
  93. dot_imports = [path for name, path in bindepend.get_imports(dot_binary) if path is not None]
  94. else:
  95. # PyInstaller < 6.0
  96. dot_imports = bindepend.getImports(dot_binary)
  97. graphviz_lib_paths = [
  98. path for path in dot_imports
  99. if any(candidate in os.path.basename(path) for candidate in graphviz_lib_candidates)
  100. ]
  101. if not graphviz_lib_paths:
  102. logger.warning("hook-pygraphviz: could not determine location of graphviz shared libraries!")
  103. return binaries, datas
  104. graphviz_lib_dir = pathlib.Path(graphviz_lib_paths[0]).parent
  105. logger.debug("hook-pygraphviz: location of graphviz shared libraries: %r", str(graphviz_lib_dir))
  106. # Plugins should be located in `graphviz` directory next to shared libraries.
  107. plugin_dir = graphviz_lib_dir / 'graphviz'
  108. plugin_dest_dir = 'graphviz' # Collect into graphviz sub-directory.
  109. if compat.is_darwin:
  110. plugin_pattern = '*gvplugin*.dylib'
  111. else:
  112. # Collect only versioned .so library files (for example, `/lib64/graphviz/libgvplugin_core.so.6` and
  113. # `/lib64/graphviz/libgvplugin_core.so.6.0.0`; the former usually being a symbolic link to the latter).
  114. # The unversioned .so library files (such as `lib64/graphviz/libgvplugin_core.so`), if available, are
  115. # meant for linking (and are usually installed as part of development package).
  116. plugin_pattern = '*gvplugin*.so.*'
  117. if not plugin_dir.is_dir():
  118. logger.warning("hook-pygraphviz: could not determine location of graphviz plugins!")
  119. return binaries, datas
  120. logger.info("hook-pygraphviz: collecting graphviz plugins from directory: %r", str(plugin_dir))
  121. binaries += [(str(file), plugin_dest_dir) for file in plugin_dir.glob(plugin_pattern)]
  122. datas += [(str(file), plugin_dest_dir) for file in plugin_dir.glob("config*")] # e.g., `config6`
  123. return binaries, datas
  124. binaries, datas = _collect_graphviz_files()