-
Notifications
You must be signed in to change notification settings - Fork 312
verify installation of a dummy Python package before actually installing a Python package #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,8 @@ | |
| @author: Jens Timmerman (Ghent University) | ||
| """ | ||
| import os | ||
| import re | ||
| import shutil | ||
| import tempfile | ||
| from os.path import expanduser | ||
| from vsc import fancylogger | ||
|
|
@@ -40,10 +42,37 @@ | |
| from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES | ||
| from easybuild.framework.easyconfig import CUSTOM | ||
| from easybuild.framework.extensioneasyblock import ExtensionEasyBlock | ||
| from easybuild.tools.filetools import mkdir, rmtree2, run_cmd | ||
| from easybuild.tools.filetools import mkdir, rmtree2, run_cmd, write_file | ||
| from easybuild.tools.modules import get_software_version | ||
|
|
||
|
|
||
| # test setup.py script for PythonPackage.python_safe_install | ||
| TEST_SETUP_PY = """#!/usr/bin/env python | ||
| try: | ||
| from setuptools import setup | ||
| except ImportError: | ||
| from distutils.core import setup | ||
|
|
||
| setup( | ||
| name='%(pkg)s', | ||
| version='1.0', | ||
| scripts=['%(pkg)s.py'], | ||
| packages=['%(pkg)s'], | ||
| data_files=['%(pkg)s.txt'], | ||
| provides=['%(pkg)s.py', '%(pkg)s'], | ||
| zip_safe=False, | ||
| ) | ||
| """ | ||
| TEST_SCRIPT_PY = """#!/usr/bin/env python | ||
| import os, sys | ||
| sys.stdout.write(os.path.dirname(os.path.abspath(__file__))) | ||
| """ | ||
| TEST_INIT_PY = """import os, sys | ||
| def where(): | ||
| sys.stdout.write(os.path.dirname(os.path.abspath(__file__))) | ||
| """ | ||
|
|
||
|
|
||
| def det_pylibdir(): | ||
| """Determine Python library directory.""" | ||
| log = fancylogger.getLogger('det_pylibdir', fname=False) | ||
|
|
@@ -153,28 +182,142 @@ def build_step(self): | |
| cmd = "python setup.py build %s" % self.cfg['buildopts'] | ||
| run_cmd(cmd, log_all=True, simple=True) | ||
|
|
||
| def python_install(self, prefix=None, preinstallopts=None, installopts=None): | ||
| """Install using 'python setup.py install --prefix'.""" | ||
| if prefix is None: | ||
| prefix = self.installdir | ||
| if preinstallopts is None: | ||
| preinstallopts = self.cfg['preinstallopts'] | ||
| if installopts is None: | ||
| installopts = self.cfg['installopts'] | ||
|
|
||
| if not self.pylibdir: | ||
| self.pylibdir = det_pylibdir() | ||
|
|
||
| # create expected directories | ||
| abs_pylibdir = os.path.join(prefix, self.pylibdir) | ||
| mkdir(abs_pylibdir, parents=True) | ||
|
|
||
| # set PYTHONPATH as expected | ||
| pythonpath = os.getenv('PYTHONPATH') | ||
| env.setvar('PYTHONPATH', ":".join([x for x in [abs_pylibdir, pythonpath] if x is not None])) | ||
|
|
||
| # install using setup.py | ||
| install_cmd_template = "%(preinstallopts)s python setup.py install --prefix=%(prefix)s %(installopts)s" | ||
| cmd = install_cmd_template % { | ||
| 'preinstallopts': preinstallopts, | ||
| 'prefix': prefix, | ||
| 'installopts': installopts, | ||
| } | ||
| run_cmd(cmd, log_all=True, simple=True) | ||
|
|
||
| # setuptools stubbornly replaces the shebang line in scripts with | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. patch setuptools? (the one used is one EB installed right?)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also, move in separate method |
||
| # the full path to the Python interpreter used to install; | ||
| # we change it (back) to '#!/usr/bin/env python' here | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you need to handle the case where the shebang is followed with options, because this search and repalce trickery will break the script otehrwise |
||
| shebang_re = re.compile("^#!/.*python") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. define in the |
||
| bindir = os.path.join(prefix, 'bin') | ||
| if os.path.exists(bindir): | ||
| for script in os.listdir(bindir): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be better to use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and loop over |
||
| script = os.path.join(bindir, script) | ||
| if os.path.isfile(script): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe a |
||
| try: | ||
| txt = open(script, 'r').read() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use filehandle, so you'll open it only once (and close it explicitly) |
||
| if shebang_re.search(txt): | ||
| new_shebang = "#!/usr/bin/env python" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. define outside loop? |
||
| self.log.debug("Patching shebang header line in %s to '%s'" % (script, new_shebang)) | ||
| txt = shebang_re.sub(new_shebang, txt) | ||
| open(script, 'w').write(txt) | ||
| except IOError, err: | ||
| self.log.error("Failed to patch shebang header line in %s: %s" % (script, err)) | ||
|
|
||
| # restore PYTHONPATH if it was set | ||
| if pythonpath is not None: | ||
| env.setvar('PYTHONPATH', pythonpath) | ||
|
|
||
| def python_safe_install(self, **kwargs): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this actually sound like a good test to run after EB installs setupstools
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
| """Install using 'python setup.py install --prefix', after verifying it does the right thing.""" | ||
| cwd = os.getcwd() | ||
|
|
||
| # create dummy Python package to verify whether 'python setup.py install --prefix' does the right thing | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why don't you ship these files as part of the easyblocks release. is there something defined at runtime that i miss? |
||
| tmpdir = tempfile.mkdtemp() | ||
| pkg = 'easybuild_pyinstalltest' | ||
| mkdir(os.path.join(tmpdir, pkg)) | ||
| write_file(os.path.join(tmpdir, 'setup.py'), TEST_SETUP_PY % {'pkg': pkg}) | ||
| test_py_script = '%s.py' % pkg | ||
| write_file(os.path.join(tmpdir, test_py_script), TEST_SCRIPT_PY) | ||
| test_data_file = '%s.txt' % pkg | ||
| write_file(os.path.join(tmpdir, test_data_file), 'data') | ||
| write_file(os.path.join(tmpdir, pkg, '__init__.py'), TEST_INIT_PY) | ||
|
|
||
| # install dummy Python package | ||
| try: | ||
| os.chdir(tmpdir) | ||
| testinstalldir = tempfile.mkdtemp() | ||
| self.python_install(prefix=testinstalldir) | ||
| os.chdir(cwd) | ||
| except OSError, err: | ||
| self.log.error("Failed to move to %s: %s" % (tmpdir, err)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the OSError only comes from the initial
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or the latter |
||
|
|
||
| # verify installation of dummy Python package | ||
| verified = True | ||
| full_pylibdir = os.path.join(testinstalldir, self.pylibdir) | ||
| cmds = [ | ||
| ("python -c 'from %s import where; where()'" % pkg, full_pylibdir), | ||
| (test_py_script, testinstalldir), | ||
| ] | ||
| for cmd, out_prefix in cmds: | ||
| precmd = "PYTHONPATH=%s:$PYTHONPATH PATH=%s:$PATH" % (full_pylibdir, os.path.join(testinstalldir, 'bin')) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. define outside for loop |
||
| fullcmd = ' '.join([precmd, cmd]) | ||
| (out, ec) = run_cmd(fullcmd, simple=False) | ||
| tup = (out_prefix, fullcmd) | ||
| if out.startswith(out_prefix): | ||
| self.log.debug("Found %s in output of '%s' during verification of dummy Python installation" % tup) | ||
| else: | ||
| tup = (tup[0], tup[1], ec, out) | ||
| self.log.warning("%s not found in output of '%s' (exit code: %s, output: %s)" % tup) | ||
| verified = False | ||
| pyver = get_software_version('Python') | ||
| if not pyver: | ||
| self.log.error("Python module not loaded.") | ||
| pyver = '.'.join(pyver.split('.')[:2]) | ||
| datainstalldir = os.path.join(full_pylibdir, '%s-1.0-py%s.egg' % (pkg, pyver)) | ||
| tup = (test_data_file, datainstalldir) | ||
| if os.path.exists(os.path.join(datainstalldir, test_data_file)): | ||
| self.log.debug("Found file %s in %s during verification of dummy Python installation" % tup) | ||
| else: | ||
| self.log.warning("Failed to find file %s in %s during verification of dummy Python installation" % tup) | ||
| verified = False | ||
|
|
||
| if verified: | ||
| self.log.debug("Verification of dummy Python installation OK.") | ||
| else: | ||
| self.log.error("Verification of dummy Python installation failed, setuptools not honoring --prefix?") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so, actually you don't know if it'ssetuptools...
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, setuptools may (apparently) blatently ignore this seems to happen when setuptools itself was installed using |
||
|
|
||
| # cleanup | ||
| shutil.rmtree(testinstalldir) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cleanup the case where it was not installed in the prefix? |
||
| shutil.rmtree(tmpdir) | ||
|
|
||
| # actually run install command | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please don't. keep the tes function separate
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i meant it's ok to have a safe install, but the test code should be a separate method
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually, this should be the |
||
| self.python_install(**kwargs) | ||
|
|
||
| def test_step(self): | ||
| """Test the built Python package.""" | ||
|
|
||
| if isinstance(self.cfg['runtest'], basestring): | ||
| self.testcmd = self.cfg['runtest'] | ||
|
|
||
| if self.cfg['runtest'] and not self.testcmd is None: | ||
| extrapath = "" | ||
| extrapath = '' | ||
| testinstalldir = None | ||
|
|
||
| if self.testinstall: | ||
| # install in test directory and export PYTHONPATH | ||
|
|
||
| # install in test directory and export PYTHONPATH for running tests | ||
| try: | ||
| testinstalldir = tempfile.mkdtemp() | ||
| mkdir(os.path.join(testinstalldir, self.pylibdir), parents=True) | ||
| except OSError, err: | ||
| self.log.error("Failed to create test install dir: %s" % err) | ||
|
|
||
| tup = (self.cfg['preinstallopts'], testinstalldir, self.cfg['installopts']) | ||
| cmd = "%s python setup.py install --prefix=%s %s" % tup | ||
| run_cmd(cmd, log_all=True, simple=True) | ||
| self.python_safe_install(prefix=testinstalldir) | ||
|
|
||
| run_cmd("python -c 'import sys; print(sys.path)'") # print Python search path (debug) | ||
| extrapath = "export PYTHONPATH=%s:$PYTHONPATH && " % os.path.join(testinstalldir, self.pylibdir) | ||
|
|
@@ -191,23 +334,7 @@ def test_step(self): | |
|
|
||
| def install_step(self): | ||
| """Install Python package to a custom path using setup.py""" | ||
|
|
||
| # create expected directories | ||
| abs_pylibdir = os.path.join(self.installdir, self.pylibdir) | ||
| mkdir(abs_pylibdir, parents=True) | ||
|
|
||
| # set PYTHONPATH as expected | ||
| pythonpath = os.getenv('PYTHONPATH') | ||
| env.setvar('PYTHONPATH', ":".join([x for x in [abs_pylibdir, pythonpath] if x is not None])) | ||
|
|
||
| # actually install Python package | ||
| tup = (self.cfg['preinstallopts'], self.installdir, self.cfg['installopts']) | ||
| cmd = "%s python setup.py install --prefix=%s %s" % tup | ||
| run_cmd(cmd, log_all=True, simple=True) | ||
|
|
||
| # restore PYTHONPATH if it was set | ||
| if pythonpath is not None: | ||
| env.setvar('PYTHONPATH', pythonpath) | ||
| self.python_safe_install() | ||
|
|
||
| def run(self): | ||
| """Perform the actual Python package build/installation procedure""" | ||
|
|
@@ -233,6 +360,5 @@ def sanity_check_step(self, *args, **kwargs): | |
|
|
||
| def make_module_extra(self): | ||
| """Add install path to PYTHONPATH""" | ||
|
|
||
| txt = self.moduleGenerator.prepend_paths("PYTHONPATH", [self.pylibdir]) | ||
| return super(PythonPackage, self).make_module_extra(txt) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why would you do this? now you don't even know what you are verifying.
and we are dealing with EB installing python packages with an EB installed python and setuptools, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessarily, e.g. when installing EasyBuild with EasyBuild, we're typically using the system Python
and even if we think we're using a EB-installed Python with an EB-installed setuptools, another setuptools may 'win' (e.g. one installed with
--user)defining
$PYTHONNOUSERSITEwould already help there, though...