# ------------------------------------------------------------------
|
# Copyright (c) 2021 PyInstaller Development Team.
|
#
|
# This file is distributed under the terms of the GNU General Public
|
# License (version 2.0 or later).
|
#
|
# The full license is available in LICENSE, distributed with
|
# this software.
|
#
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
# ------------------------------------------------------------------
|
import os
|
import pathlib
|
import shutil
|
|
from PyInstaller import compat
|
from PyInstaller.depend import bindepend
|
from PyInstaller.utils.hooks import logger
|
|
|
def _collect_graphviz_files():
|
binaries = []
|
datas = []
|
|
# A working `pygraphviz` installation requires graphviz programs in PATH. Attempt to resolve the `dot` executable to
|
# see if this is the case.
|
dot_binary = shutil.which('dot')
|
if not dot_binary:
|
logger.warning(
|
"hook-pygraphviz: 'dot' program not found in PATH!"
|
)
|
return binaries, datas
|
logger.info("hook-pygraphviz: found 'dot' program: %r", dot_binary)
|
bin_dir = pathlib.Path(dot_binary).parent
|
|
# Collect graphviz programs that might be called from `pygaphviz.agraph.AGraph`:
|
# https://github.com/pygraphviz/pygraphviz/blob/pygraphviz-1.14/pygraphviz/agraph.py#L1330-L1348
|
# On macOS and on Linux, several of these are symbolic links to a single executable.
|
progs = (
|
"neato",
|
"dot",
|
"twopi",
|
"circo",
|
"fdp",
|
"nop",
|
"osage",
|
"patchwork",
|
"gc",
|
"acyclic",
|
"gvpr",
|
"gvcolor",
|
"ccomps",
|
"sccmap",
|
"tred",
|
"sfdp",
|
"unflatten",
|
)
|
|
logger.debug("hook-pygraphviz: collecting graphviz program executables...")
|
for program_name in progs:
|
program_binary = shutil.which(program_name)
|
if not program_binary:
|
logger.debug("hook-pygaphviz: graphviz program %r not found!", program_name)
|
continue
|
|
# Ensure that the program executable was found in the same directory as the `dot` executable. This should
|
# prevent us from falling back to other graphviz installations that happen to be in PATH.
|
if pathlib.Path(program_binary).parent != bin_dir:
|
logger.debug(
|
"hook-pygraphviz: found program %r (%r) outside of directory %r - ignoring!",
|
program_name, program_binary, str(bin_dir)
|
)
|
continue
|
|
logger.debug("hook-pygraphviz: collecting graphviz program %r: %r", program_name, program_binary)
|
binaries += [(program_binary, '.')]
|
|
# Graphviz shared libraries should be automatically collected when PyInstaller performs binary dependency
|
# analysis of the collected program executables as part of the main build process. However, we need to manually
|
# collect plugins and their accompanying config file.
|
logger.debug("hook-pygraphviz: looking for graphviz plugin directory...")
|
if compat.is_win:
|
# Under Windows, we have several installation variants:
|
# - official installers and builds from https://gitlab.com/graphviz/graphviz/-/releases
|
# - chocolatey
|
# - msys2
|
# - Anaconda
|
# In all variants, the plugins and the config file are located in the `bin` directory, next to the program
|
# executables.
|
plugin_dir = bin_dir
|
plugin_dest_dir = '.' # Collect into top-level application directory.
|
# Official builds and Anaconda use unversioned `gvplugin-{name}.dll` plugin names, while msys2 uses
|
# versioned `libgvplugin-{name}-{version}.dll` plugin names (with "lib" prefix).
|
plugin_pattern = '*gvplugin*.dll'
|
else:
|
# Perform binary dependency analysis on the `dot` executable to obtain the path to graphiz shared libraries.
|
# These need to be in the library search path for the programs to work, or discoverable via run-paths
|
# (e.g., Anaconda on Linux and macOS, Homebrew on macOS).
|
graphviz_lib_candidates = ['cdt', 'gvc', 'cgraph']
|
|
if hasattr(bindepend, 'get_imports'):
|
# PyInstaller >= 6.0
|
dot_imports = [path for name, path in bindepend.get_imports(dot_binary) if path is not None]
|
else:
|
# PyInstaller < 6.0
|
dot_imports = bindepend.getImports(dot_binary)
|
|
graphviz_lib_paths = [
|
path for path in dot_imports
|
if any(candidate in os.path.basename(path) for candidate in graphviz_lib_candidates)
|
]
|
|
if not graphviz_lib_paths:
|
logger.warning("hook-pygraphviz: could not determine location of graphviz shared libraries!")
|
return binaries, datas
|
|
graphviz_lib_dir = pathlib.Path(graphviz_lib_paths[0]).parent
|
logger.debug("hook-pygraphviz: location of graphviz shared libraries: %r", str(graphviz_lib_dir))
|
|
# Plugins should be located in `graphviz` directory next to shared libraries.
|
plugin_dir = graphviz_lib_dir / 'graphviz'
|
plugin_dest_dir = 'graphviz' # Collect into graphviz sub-directory.
|
|
if compat.is_darwin:
|
plugin_pattern = '*gvplugin*.dylib'
|
else:
|
# Collect only versioned .so library files (for example, `/lib64/graphviz/libgvplugin_core.so.6` and
|
# `/lib64/graphviz/libgvplugin_core.so.6.0.0`; the former usually being a symbolic link to the latter).
|
# The unversioned .so library files (such as `lib64/graphviz/libgvplugin_core.so`), if available, are
|
# meant for linking (and are usually installed as part of development package).
|
plugin_pattern = '*gvplugin*.so.*'
|
|
if not plugin_dir.is_dir():
|
logger.warning("hook-pygraphviz: could not determine location of graphviz plugins!")
|
return binaries, datas
|
|
logger.info("hook-pygraphviz: collecting graphviz plugins from directory: %r", str(plugin_dir))
|
|
binaries += [(str(file), plugin_dest_dir) for file in plugin_dir.glob(plugin_pattern)]
|
datas += [(str(file), plugin_dest_dir) for file in plugin_dir.glob("config*")] # e.g., `config6`
|
|
return binaries, datas
|
|
|
binaries, datas = _collect_graphviz_files()
|