| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- #-----------------------------------------------------------------------------
- # Copyright (c) 2013-2023, PyInstaller Development Team.
- #
- # Distributed under the terms of the GNU General Public License (version 2
- # or later) with exception for distributing the bootloader.
- #
- # The full license is in the file COPYING.txt, distributed with this software.
- #
- # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
- #-----------------------------------------------------------------------------
- from PyInstaller.compat import is_darwin
- from PyInstaller.utils.hooks import logger, get_hook_config
- from PyInstaller import isolated
- @isolated.decorate
- def _get_configured_default_backend():
- """
- Return the configured default matplotlib backend name, if available as matplotlib.rcParams['backend'] (or overridden
- by MPLBACKEND environment variable. If the value of matplotlib.rcParams['backend'] corresponds to the auto-sentinel
- object, returns None
- """
- import matplotlib
- # matplotlib.rcParams overrides the __getitem__ implementation and attempts to determine and load the default
- # backend using pyplot.switch_backend(). Therefore, use dict.__getitem__().
- val = dict.__getitem__(matplotlib.rcParams, 'backend')
- if isinstance(val, str):
- return val
- return None
- @isolated.decorate
- def _list_available_mpl_backends():
- """
- Returns the names of all available matplotlib backends.
- """
- import matplotlib
- return matplotlib.rcsetup.all_backends
- @isolated.decorate
- def _check_mpl_backend_importable(module_name):
- """
- Attempts to import the given module name (matplotlib backend module).
- Exceptions are propagated to caller.
- """
- __import__(module_name)
- # Bytecode scanning
- def _recursive_scan_code_objects_for_mpl_use(co):
- """
- Recursively scan the bytecode for occurrences of matplotlib.use() or mpl.use() calls with const arguments, and
- collect those arguments into list of used matplotlib backend names.
- """
- from PyInstaller.depend.bytecode import any_alias, recursive_function_calls
- mpl_use_names = {
- *any_alias("matplotlib.use"),
- *any_alias("mpl.use"), # matplotlib is commonly aliased as mpl
- }
- backends = []
- for calls in recursive_function_calls(co).values():
- for name, args in calls:
- # matplotlib.use(backend) or matplotlib.use(backend, force)
- # We support only literal arguments. Similarly, kwargs are
- # not supported.
- if len(args) not in {1, 2} or not isinstance(args[0], str):
- continue
- if name in mpl_use_names:
- backends.append(args[0])
- return backends
- def _backend_module_name(name):
- """
- Converts matplotlib backend name to its corresponding module name.
- Equivalent to matplotlib.cbook._backend_module_name().
- """
- if name.startswith("module://"):
- return name[9:]
- return f"matplotlib.backends.backend_{name.lower()}"
- def _autodetect_used_backends(hook_api):
- """
- Returns a list of automatically-discovered matplotlib backends in use, or the name of the default matplotlib
- backend. Implements the 'auto' backend selection method.
- """
- # Scan the code for matplotlib.use()
- modulegraph = hook_api.analysis.graph
- mpl_code_objs = modulegraph.get_code_using("matplotlib")
- used_backends = []
- for name, co in mpl_code_objs.items():
- co_backends = _recursive_scan_code_objects_for_mpl_use(co)
- if co_backends:
- logger.info(
- "Discovered Matplotlib backend(s) via `matplotlib.use()` call in module %r: %r", name, co_backends
- )
- used_backends += co_backends
- # Deduplicate and sort the list of used backends before displaying it.
- used_backends = sorted(set(used_backends))
- if used_backends:
- HOOK_CONFIG_DOCS = 'https://pyinstaller.org/en/stable/hooks-config.html#matplotlib-hooks'
- logger.info(
- "The following Matplotlib backends were discovered by scanning for `matplotlib.use()` calls: %r. If your "
- "backend of choice is not in this list, either add a `matplotlib.use()` call to your code, or configure "
- "the backend collection via hook options (see: %s).", used_backends, HOOK_CONFIG_DOCS
- )
- return used_backends
- # Determine the default matplotlib backend.
- #
- # Ideally, this would be done by calling ``matplotlib.get_backend()``. However, that function tries to switch to the
- # default backend (calling ``matplotlib.pyplot.switch_backend()``), which seems to occasionally fail on our linux CI
- # with an error and, on other occasions, returns the headless Agg backend instead of the GUI one (even with display
- # server running). Furthermore, using ``matplotlib.get_backend()`` returns headless 'Agg' when display server is
- # unavailable, which is not ideal for automated builds.
- #
- # Therefore, we try to emulate ``matplotlib.get_backend()`` ourselves. First, we try to obtain the configured
- # default backend from settings (rcparams and/or MPLBACKEND environment variable). If that is unavailable, we try to
- # find the first importable GUI-based backend, using the same list as matplotlib.pyplot.switch_backend() uses for
- # automatic backend selection. The difference is that we only test whether the backend module is importable, without
- # trying to switch to it.
- default_backend = _get_configured_default_backend() # isolated sub-process
- if default_backend:
- logger.info("Found configured default matplotlib backend: %s", default_backend)
- return [default_backend]
- # `QtAgg` supersedes `Qt5Agg`; however, we keep `Qt5Agg` in the candidate list to support older versions of
- # matplotlib that do not have `QtAgg`.
- candidates = ["QtAgg", "Qt5Agg", "Gtk4Agg", "Gtk3Agg", "TkAgg", "WxAgg"]
- if is_darwin:
- candidates = ["MacOSX"] + candidates
- logger.info("Trying determine the default backend as first importable candidate from the list: %r", candidates)
- for candidate in candidates:
- try:
- module_name = _backend_module_name(candidate)
- _check_mpl_backend_importable(module_name) # NOTE: uses an isolated sub-process.
- except Exception:
- continue
- return [candidate]
- # Fall back to headless Agg backend
- logger.info("None of the backend candidates could be imported; falling back to headless Agg!")
- return ['Agg']
- def _collect_all_importable_backends(hook_api):
- """
- Returns a list of all importable matplotlib backends. Implements the 'all' backend selection method.
- """
- # List of the human-readable names of all available backends.
- backend_names = _list_available_mpl_backends() # NOTE: retrieved in an isolated sub-process.
- logger.info("All available matplotlib backends: %r", backend_names)
- # Try to import the module(s).
- importable_backends = []
- # List of backends to exclude; Qt4 is not supported by PyInstaller anymore.
- exclude_backends = {'Qt4Agg', 'Qt4Cairo'}
- # Ignore "CocoaAgg" on OSes other than macOS; attempting to import it on other OSes halts the current
- # (sub)process without printing output or raising exceptions, preventing reliable detection. Apply the
- # same logic for the (newer) "MacOSX" backend.
- if not is_darwin:
- exclude_backends |= {'CocoaAgg', 'MacOSX'}
- # For safety, attempt to import each backend in an isolated sub-process.
- for backend_name in backend_names:
- if backend_name in exclude_backends:
- logger.info(' Matplotlib backend %r: excluded', backend_name)
- continue
- try:
- module_name = _backend_module_name(backend_name)
- _check_mpl_backend_importable(module_name) # NOTE: uses an isolated sub-process.
- except Exception:
- # Backend is not importable, for whatever reason.
- logger.info(' Matplotlib backend %r: ignored due to import error', backend_name)
- continue
- logger.info(' Matplotlib backend %r: added', backend_name)
- importable_backends.append(backend_name)
- return importable_backends
- def hook(hook_api):
- # Backend collection setting
- backends_method = get_hook_config(hook_api, 'matplotlib', 'backends')
- if backends_method is None:
- backends_method = 'auto' # default method
- # Select backend(s)
- if backends_method == 'auto':
- logger.info("Matplotlib backend selection method: automatic discovery of used backends")
- backend_names = _autodetect_used_backends(hook_api)
- elif backends_method == 'all':
- logger.info("Matplotlib backend selection method: collection of all importable backends")
- backend_names = _collect_all_importable_backends(hook_api)
- else:
- logger.info("Matplotlib backend selection method: user-provided name(s)")
- if isinstance(backends_method, str):
- backend_names = [backends_method]
- else:
- assert isinstance(backends_method, list), "User-provided backend name(s) must be either a string or a list!"
- backend_names = backends_method
- # Deduplicate and sort the list of selected backends before displaying it.
- backend_names = sorted(set(backend_names))
- logger.info("Selected matplotlib backends: %r", backend_names)
- # Set module names as hiddenimports
- module_names = [_backend_module_name(backend) for backend in backend_names] # backend name -> module name
- hook_api.add_imports(*module_names)
|