diff --git a/easybuild/easyblocks/generic/pythonbundle.py b/easybuild/easyblocks/generic/pythonbundle.py index 5ed19b42ccb..344201c95de 100644 --- a/easybuild/easyblocks/generic/pythonbundle.py +++ b/easybuild/easyblocks/generic/pythonbundle.py @@ -30,12 +30,11 @@ import os from easybuild.easyblocks.generic.bundle import Bundle -from easybuild.easyblocks.generic.pythonpackage import EXTS_FILTER_PYTHON_PACKAGES, run_pip_check +from easybuild.easyblocks.generic.pythonpackage import EXTS_FILTER_PYTHON_PACKAGES, run_pip_check, set_py_env_vars from easybuild.easyblocks.generic.pythonpackage import PythonPackage, get_pylibdirs, find_python_cmd_from_ec from easybuild.tools.build_log import EasyBuildError from easybuild.tools.config import build_option, PYTHONPATH, EBPYTHONPREFIXES from easybuild.tools.modules import get_software_root -import easybuild.tools.environment as env class PythonBundle(Bundle): @@ -101,8 +100,7 @@ def prepare_step(self, *args, **kwargs): def extensions_step(self, *args, **kwargs): """Install extensions (usually PythonPackages)""" - # don't add user site directory to sys.path (equivalent to python -s) - env.setvar('PYTHONNOUSERSITE', '1', verbose=False) + set_py_env_vars(self.log) super().extensions_step(*args, **kwargs) def test_step(self): @@ -149,16 +147,16 @@ def make_module_extra(self, *args, **kwargs): return txt def load_module(self, *args, **kwargs): + """(Re)set environment variables after loading module file. + + Required here to ensure the variables are also defined for stand-alone installations, + because the environment is reset to the initial environment right before loading the module. """ - Make sure that $PYTHONNOUSERSITE is defined after loading module file for this software.""" super().load_module(*args, **kwargs) - - # Don't add user site directory to sys.path (equivalent to python -s), - # to avoid that any Python packages installed in $HOME/.local/lib affect the sanity check. # Required here to ensure that it is defined for sanity check commands of the bundle # because the environment is reset to the initial environment right before loading the module - env.setvar('PYTHONNOUSERSITE', '1', verbose=False) + set_py_env_vars(self.log) def sanity_check_step(self, *args, **kwargs): """Custom sanity check for bundle of Python package.""" diff --git a/easybuild/easyblocks/generic/pythonpackage.py b/easybuild/easyblocks/generic/pythonpackage.py index ab41b7505fc..08f21af381d 100644 --- a/easybuild/easyblocks/generic/pythonpackage.py +++ b/easybuild/easyblocks/generic/pythonpackage.py @@ -41,7 +41,7 @@ import easybuild.tools.environment as env from easybuild.base import fancylogger -from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES +from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES, set_py_env_vars from easybuild.easyblocks.python import det_installed_python_packages, det_pip_version, run_pip_check from easybuild.framework.easyconfig import CUSTOM from easybuild.framework.easyconfig.default import DEFAULT_CONFIG @@ -489,15 +489,7 @@ def __init__(self, *args, **kwargs): self.determine_install_command() - # avoid that pip (ab)uses $HOME/.cache/pip - # cfr. https://pip.pypa.io/en/stable/reference/pip_install/#caching - env.setvar('XDG_CACHE_HOME', os.path.join(self.builddir, 'xdg-cache-home')) - self.log.info("Using %s as pip cache directory", os.environ['XDG_CACHE_HOME']) - # Users or sites may require using a virtualenv for user installations - # We need to disable this to be able to install into the modules - env.setvar('PIP_REQUIRE_VIRTUALENV', 'false') - # Don't let pip connect to PYPI to check for a new version - env.setvar('PIP_DISABLE_PIP_VERSION_CHECK', 'true') + set_py_env_vars(self.log) # avoid that lib subdirs are appended to $*LIBRARY_PATH if they don't provide libraries # typically, only lib/pythonX.Y/site-packages should be added to $PYTHONPATH (see make_module_extra) @@ -727,9 +719,7 @@ def prepare_step(self, *args, **kwargs): def configure_step(self): """Configure Python package build/install.""" - # don't add user site directory to sys.path (equivalent to python -s) - # see https://www.python.org/dev/peps/pep-0370/ - env.setvar('PYTHONNOUSERSITE', '1', verbose=False) + set_py_env_vars(self.log) if self.python_cmd is None: self.prepare_python() @@ -964,16 +954,14 @@ def install_extension(self, *args, **kwargs): step_method(self)() def load_module(self, *args, **kwargs): + """(Re)set environment variables after loading module file for this software. + + Required here to ensure the variables are also defined for stand-alone installations, + because the environment is reset to the initial environment right before loading the module. """ - Make sure that $PYTHONNOUSERSITE is defined after loading module file for this software.""" super().load_module(*args, **kwargs) - - # don't add user site directory to sys.path (equivalent to python -s), - # to avoid that any Python packages installed in $HOME/.local/lib affect the sanity check; - # required here to ensure that it is defined for stand-alone installations, - # because the environment is reset to the initial environment right before loading the module - env.setvar('PYTHONNOUSERSITE', '1', verbose=False) + set_py_env_vars(self.log) def sanity_check_step(self, *args, **kwargs): """ @@ -990,13 +978,9 @@ def sanity_check_step(self, *args, **kwargs): extra_modules = kwargs.get('extra_modules', None) self.sanity_check_load_module(extension=extension, extra_modules=extra_modules) - # don't add user site directory to sys.path (equivalent to python -s) - # see https://www.python.org/dev/peps/pep-0370/; - # must be set here to ensure that it is defined when running sanity check for extensions, - # since load_module is not called for every extension, - # to avoid that any Python packages installed in $HOME/.local/lib affect the sanity check; + # Must be called here since load_module is not called for every extension, # see also https://github.com/easybuilders/easybuild-easyblocks/issues/1877 - env.setvar('PYTHONNOUSERSITE', '1', verbose=False) + set_py_env_vars(self.log) if self.cfg.get('download_dep_fail', True): self.log.info("Detection of downloaded depdenencies enabled, checking output of installation command...") diff --git a/easybuild/easyblocks/p/python.py b/easybuild/easyblocks/p/python.py index be766f0262e..012db01403b 100644 --- a/easybuild/easyblocks/p/python.py +++ b/easybuild/easyblocks/p/python.py @@ -62,6 +62,17 @@ # magic value for unlimited stack size UNLIMITED = 'unlimited' +# Environment variables and values to avoid common issues during Python package installations and usage in EasyBuild +PY_ENV_VARS = { + # don't add user site directory to sys.path (equivalent to python -s), see https://www.python.org/dev/peps/pep-0370 + 'PYTHONNOUSERSITE': '1', + # Users or sites may require using a virtualenv for user installations + # We need to disable this to be able to install into modules + 'PIP_REQUIRE_VIRTUALENV': 'false', + # Don't let pip connect to PYPI to check for a new version + 'PIP_DISABLE_PIP_VERSION_CHECK': 'true', +} + # We want the following import order: # 1. Packages installed into VirtualEnv # 2. Packages installed into $EBPYTHONPREFIXES (e.g. our modules) @@ -237,6 +248,22 @@ def run_pip_check(python_cmd=None, unversioned_packages=None): raise EasyBuildError('\n'.join(pip_check_errors)) +def set_py_env_vars(log, verbose=False): + """Set environment variables required/useful for installing or using Python packages""" + + py_vars = PY_ENV_VARS.copy() + # avoid that pip (ab)uses $HOME/.cache/pip + # cfr. https://pip.pypa.io/en/stable/reference/pip_install/#caching + py_vars['XDG_CACHE_HOME'] = os.path.join(tempfile.gettempdir(), 'xdg-cache-home') + # Only set (all) environment variables if any has a different value to + # avoid (non)changes (and log messages) for each package in a bundle + set_required = any(os.environ.get(name, None) != value for name, value in py_vars.items()) + if set_required: + for name, value in py_vars.items(): + env.setvar(name, value, verbose=verbose) + log.info("Using %s as pip cache directory", os.environ['XDG_CACHE_HOME']) + + class EB_Python(ConfigureMake): """Support for building/installing Python - default configure/build_step/make install works fine @@ -423,8 +450,7 @@ def prepare_for_extensions(self): self.cfg['exts_defaultclass'] = "PythonPackage" self.cfg['exts_filter'] = EXTS_FILTER_PYTHON_PACKAGES - # don't add user site directory to sys.path (equivalent to python -s) - env.setvar('PYTHONNOUSERSITE', '1') + set_py_env_vars(self.log) # don't pass down any build/install options that may have been specified # 'make' options do not make sense for when building/installing Python libraries (usually via 'python setup.py') @@ -560,9 +586,8 @@ def configure_step(self): env.setvar('TCLTK_CFLAGS', '-I%s/include -I%s/include' % (tcl, tk)) env.setvar('TCLTK_LIBS', tcltk_libs) - # don't add user site directory to sys.path (equivalent to python -s) # This matters e.g. when python installs the bundled pip & setuptools (for >= 3.4) - env.setvar('PYTHONNOUSERSITE', '1') + set_py_env_vars(self.log) super().configure_step() diff --git a/easybuild/easyblocks/t/tensorflow.py b/easybuild/easyblocks/t/tensorflow.py index ef942264244..433d72aba1f 100644 --- a/easybuild/easyblocks/t/tensorflow.py +++ b/easybuild/easyblocks/t/tensorflow.py @@ -41,7 +41,7 @@ import easybuild.tools.environment as env import easybuild.tools.toolchain as toolchain from easybuild.easyblocks.generic.pythonpackage import PythonPackage, det_python_version -from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES +from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES, PY_ENV_VARS from easybuild.framework.easyconfig import CUSTOM from easybuild.tools import LooseVersion from easybuild.tools.build_log import EasyBuildError, print_warning @@ -907,7 +907,7 @@ def build_step(self): action_env['EBPYTHONPREFIXES'] = INHERIT # Ignore user environment for Python - action_env['PYTHONNOUSERSITE'] = '1' + action_env.update(PY_ENV_VARS) # TF 2 (final) sets this in configure if (LooseVersion(self.version) < LooseVersion('2.0')) and self._with_cuda: diff --git a/easybuild/easyblocks/t/tensorflow_compression.py b/easybuild/easyblocks/t/tensorflow_compression.py index 0f580ec6f73..29ce799464a 100644 --- a/easybuild/easyblocks/t/tensorflow_compression.py +++ b/easybuild/easyblocks/t/tensorflow_compression.py @@ -32,6 +32,7 @@ import easybuild.tools.environment as env from easybuild.easyblocks.generic.pythonpackage import PythonPackage +from easybuild.easyblocks.python import PY_ENV_VARS from easybuild.tools import LooseVersion from easybuild.tools.modules import get_software_version from easybuild.tools.run import run_shell_cmd @@ -104,7 +105,7 @@ def build_step(self): action_env['EBPYTHONPREFIXES'] = INHERIT # Ignore user environment for Python - action_env['PYTHONNOUSERSITE'] = '1' + action_env.update(PY_ENV_VARS) # Use the same configuration (i.e. environment) for compiling and using host tools # This means that our action_envs are (almost) always passed