| # Copyright 2015 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import contextlib |
| import os |
| import platform |
| import re |
| import tempfile |
| import shutil |
| import subprocess |
| import sys |
| import urllib |
| import urlparse |
| |
| |
| # Copied from pip.wheel.Wheel.wheel_file_re to avoid requiring pip here. |
| WHEEL_FILE_RE = re.compile( |
| r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?)) |
| ((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?) |
| \.whl)$""", |
| re.VERBOSE |
| ) |
| |
| |
| class GlycoError(Exception): |
| """Base class for Glyco errors""" |
| |
| |
| class GlycoSetupError(GlycoError): |
| """Issue outside the reach of Glyco that prevents execution.""" |
| |
| |
| class InvalidWheelFile(GlycoError): |
| """The file passed is not a valid wheel file. |
| |
| This includes errors on the file name. |
| """ |
| |
| |
| def setup_virtualenv(env_path, relocatable=False): |
| """Create a virtualenv in specified location. |
| |
| The virtualenv contains a standard Python installation, plus setuptools, pip |
| and wheel. |
| |
| Args: |
| env_path (str): where to create the virtual environment. |
| |
| """ |
| if os.path.exists(os.path.join(os.path.expanduser('~'), '.pydistutils.cfg')): |
| raise GlycoSetupError('\n'.join([ |
| '', |
| 'You have a ~/.pydistutils.cfg file, which interferes with the ', |
| 'infra virtualenv environment. Please move it to the side and bootstrap ', |
| 'again. Once infra has bootstrapped, you may move it back.', |
| '', |
| 'Upstream bug: https://github.com/pypa/virtualenv/issues/88/', |
| '' |
| ])) |
| |
| print 'Creating environment: %r' % env_path |
| |
| if os.path.exists(env_path): |
| print ' Removing existing one...' |
| shutil.rmtree(env_path, ignore_errors=True) |
| |
| print ' Building new environment...' |
| # Import bundled virtualenv lib |
| import virtualenv # pylint: disable=F0401 |
| virtualenv.create_environment( |
| env_path, search_dirs=virtualenv.file_search_dirs()) |
| |
| if relocatable: |
| print ' Make environment relocatable' |
| virtualenv.make_environment_relocatable(env_path) |
| |
| print 'Done creating environment' |
| |
| |
| def platform_tag(): |
| if sys.platform.startswith('linux'): |
| return '_{0}_{1}'.format(*platform.linux_distribution()) |
| return '' |
| |
| |
| class Virtualenv(object): |
| def __init__(self, prefix='glyco-', keep_directory=False): |
| """Helper class to run commands from virtual environments. |
| |
| Keyword Args: |
| prefix (str): prefix to the temporary directory used to create the |
| virtualenv. |
| keep_directory (boolean): if True the temporary virtualenv directory is |
| kept around instead of being deleted. Useful mainly for debugging. |
| |
| Returns: self. Only the check_call and check_output methods are meant to be |
| used inside the with block. |
| """ |
| self._prefix = prefix |
| self._keep_directory = keep_directory |
| |
| # Where the virtualenv is |
| self._venvdir = None |
| self._bin_dir = 'Scripts' if sys.platform.startswith('win') else 'bin' |
| |
| def check_call(self, args, **kwargs): |
| """Run a command from inside the virtualenv using check_call. |
| |
| Args: |
| cmd (str): name of the command. Must be found in the 'bin' directory of |
| the virtualenv. |
| args (list of strings): arguments passed to the command. |
| Keyword Args: |
| kwargs: keyword arguments passed to subprocess.check_output |
| """ |
| subprocess.check_call( |
| (os.path.join(self._venvdir, self._bin_dir, args[0]),) + tuple(args[1:]), |
| **kwargs) |
| |
| def check_output(self, args, **kwargs): |
| """Run a command from inside the virtualenv using check_output. |
| |
| Args: |
| cmd (str): name of the command. Must be found in the 'bin' directory of |
| the virtualenv. |
| args (list of strings): arguments passed to the command. |
| Keyword Args: |
| kwargs: keyword arguments passed to subprocess.check_output |
| """ |
| return subprocess.check_output( |
| (os.path.join(self._venvdir, self._bin_dir, args[0]),) + tuple(args[1:]), |
| **kwargs) |
| |
| def __cleanup_venv(self): |
| """Remove the virtualenv directory""" |
| try: |
| # TODO(pgervais,496347) Make this work reliably on Windows. |
| shutil.rmtree(self._venvdir, ignore_errors=True) |
| except OSError as ex: |
| print >> sys.stderr, ( |
| "ERROR: {!r} while cleaning up {!r}".format(ex, self._venvdir)) |
| self._venvdir = None |
| |
| |
| def __enter__(self): |
| self._venvdir = tempfile.mkdtemp('', self._prefix, None) |
| try: |
| setup_virtualenv(self._venvdir) |
| except Exception: |
| self.__cleanup_venv() |
| raise |
| return self |
| |
| def __exit__(self, err_type, value, tb): |
| if self._venvdir and not self._keep_directory: |
| self.__cleanup_venv() |
| |
| |
| # dir is a built-in. We're matching the Python 3 function signature here. |
| # pylint: disable=redefined-builtin |
| @contextlib.contextmanager |
| def temporary_directory(suffix="", prefix="tmp", dir=None, |
| keep_directory=False): |
| """Create and return a temporary directory. This has the same |
| behavior as mkdtemp but can be used as a context manager. For |
| example: |
| |
| with temporary_directory() as tmpdir: |
| ... |
| |
| Upon exiting the context, the directory and everything contained |
| in it are removed. |
| |
| Args: |
| suffix, prefix, dir: same arguments as for tempfile.mkdtemp. |
| keep_directory (bool): if True, do not delete the temporary directory |
| when exiting. Useful for debugging. |
| |
| Returns: |
| tempdir (str): full path to the temporary directory. |
| """ |
| tempdir = None # Handle mkdtemp raising an exception |
| try: |
| tempdir = tempfile.mkdtemp(suffix, prefix, dir) |
| yield tempdir |
| |
| finally: |
| if tempdir and not keep_directory: |
| try: |
| # TODO(pgervais,496347) Make this work reliably on Windows. |
| shutil.rmtree(tempdir, ignore_errors=True) |
| except OSError as ex: |
| print >> sys.stderr, ( |
| "ERROR: {!r} while cleaning up {!r}".format(ex, tempdir)) |
| |
| |
| def path2fileurl(path): |
| """Convert a local absolute path to a file:/// URL |
| |
| There is no way to provide a relative path in a file:// URI, because there |
| is no notion of 'working directory'. |
| |
| Output conforms to https://tools.ietf.org/html/rfc1630 |
| """ |
| if not os.path.isabs(path): |
| raise ValueError('Only absolute paths can be turned into a file url. ' |
| 'Got: %s' % path) |
| path_comp = urllib.pathname2url(path) |
| return 'file:///' + path_comp.lstrip('/') |
| |
| |
| def fileurl2path(url): |
| """Convert a file:// URL to a local path. |
| |
| Note that per https://tools.ietf.org/html/rfc1630 page 18 a host name |
| should be provided. So |
| file://localhost/file/name points to /file/name on localhost |
| file:///file/name points to /file/name ('localhost' is optional) |
| file://file/name points to /name on machine 'file'. |
| |
| """ |
| if not url.startswith('file://'): |
| raise ValueError('URL must start with "file://". Got %s' % url) |
| parts = urlparse.urlparse(url) |
| return urllib.url2pathname(parts.path) |