| Index: package_management.py
|
| diff --git a/package_management.py b/package_management.py
|
| deleted file mode 100644
|
| index 4180a3ffb7298de8a243394a06181fb04aad86df..0000000000000000000000000000000000000000
|
| --- a/package_management.py
|
| +++ /dev/null
|
| @@ -1,602 +0,0 @@
|
| -#!/usr/bin/env python
|
| -# Copyright (c) 2012 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.
|
| -
|
| -"""Package Management
|
| -
|
| -This module is used to bring in external Python dependencies via eggs from the
|
| -PyPi repository.
|
| -
|
| -The approach is to create a site directory in depot_tools root which will
|
| -contain those packages that are listed as dependencies in the module variable
|
| -PACKAGES. Since we can't guarantee that setuptools is available in all
|
| -distributions this module also contains the ability to bootstrap the site
|
| -directory by manually downloading and installing setuptools. Once setuptools is
|
| -available it uses that to install the other packages in the traditional
|
| -manner.
|
| -
|
| -Use is simple:
|
| -
|
| - import package_management
|
| -
|
| - # Before any imports from the site directory, call this. This only needs
|
| - # to be called in one place near the beginning of the program.
|
| - package_management.SetupSiteDirectory()
|
| -
|
| - # If 'SetupSiteDirectory' fails it will complain with an error message but
|
| - # continue happily. Expect ImportErrors when trying to import any third
|
| - # party modules from the site directory.
|
| -
|
| - import some_third_party_module
|
| -
|
| - ... etc ...
|
| -"""
|
| -
|
| -import cStringIO
|
| -import os
|
| -import re
|
| -import shutil
|
| -import site
|
| -import subprocess
|
| -import sys
|
| -import tempfile
|
| -import urllib2
|
| -
|
| -
|
| -# This is the version of setuptools that we will download if the local
|
| -# python distribution does not include one.
|
| -SETUPTOOLS = ('setuptools', '0.6c11')
|
| -
|
| -# These are the packages that are to be installed in the site directory.
|
| -# easy_install makes it so that the most recently installed version of a
|
| -# package is the one that takes precedence, even if a newer version exists
|
| -# in the site directory. This allows us to blindly install these one on top
|
| -# of the other without worrying about whats already installed.
|
| -#
|
| -# NOTE: If we are often rolling these dependencies then users' site
|
| -# directories will grow monotonically. We could be purging any orphaned
|
| -# packages using the tools provided by pkg_resources.
|
| -PACKAGES = (('logilab-common', '0.57.1'),
|
| - ('logilab-astng', '0.23.1'),
|
| - ('pylint', '0.25.1'))
|
| -
|
| -
|
| -# The Python version suffix used in generating the site directory and in
|
| -# requesting packages from PyPi.
|
| -VERSION_SUFFIX = "%d.%d" % sys.version_info[0:2]
|
| -
|
| -# This is the root directory of the depot_tools installation.
|
| -ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
|
| -
|
| -# This is the path of the site directory we will use. We make this
|
| -# python version specific so that this will continue to work even if the
|
| -# python version is rolled.
|
| -SITE_DIR = os.path.join(ROOT_DIR, 'site-packages-py%s' % VERSION_SUFFIX)
|
| -
|
| -# This status file is created the last time PACKAGES were rolled successfully.
|
| -# It is used to determine if packages need to be rolled by comparing against
|
| -# the age of __file__.
|
| -LAST_ROLLED = os.path.join(SITE_DIR, 'last_rolled.txt')
|
| -
|
| -
|
| -class Error(Exception):
|
| - """The base class for all module errors."""
|
| - pass
|
| -
|
| -
|
| -class InstallError(Error):
|
| - """Thrown if an installation is unable to complete."""
|
| - pass
|
| -
|
| -
|
| -class Package(object):
|
| - """A package represents a release of a project.
|
| -
|
| - We use this as a lightweight version of pkg_resources, allowing us to
|
| - perform an end-run around setuptools for the purpose of bootstrapping. Its
|
| - functionality is very limited.
|
| -
|
| - Attributes:
|
| - name: the name of the package.
|
| - version: the version of the package.
|
| - safe_name: the safe name of the package.
|
| - safe_version: the safe version string of the package.
|
| - file_name: the filename-safe name of the package.
|
| - file_version: the filename-safe version string of the package.
|
| - """
|
| -
|
| - def __init__(self, name, version):
|
| - """Initialize this package.
|
| -
|
| - Args:
|
| - name: the name of the package.
|
| - version: the version of the package.
|
| - """
|
| - self.name = name
|
| - self.version = version
|
| - self.safe_name = Package._MakeSafeName(self.name)
|
| - self.safe_version = Package._MakeSafeVersion(self.version)
|
| - self.file_name = Package._MakeSafeForFilename(self.safe_name)
|
| - self.file_version = Package._MakeSafeForFilename(self.safe_version)
|
| -
|
| - @staticmethod
|
| - def _MakeSafeName(name):
|
| - """Makes a safe package name, as per pkg_resources."""
|
| - return re.sub('[^A-Za-z0-9]+', '-', name)
|
| -
|
| - @staticmethod
|
| - def _MakeSafeVersion(version):
|
| - """Makes a safe package version string, as per pkg_resources."""
|
| - version = re.sub('\s+', '.', version)
|
| - return re.sub('[^A-Za-z0-9\.]+', '-', version)
|
| -
|
| - @staticmethod
|
| - def _MakeSafeForFilename(safe_name_or_version):
|
| - """Makes a safe name or safe version safe to use in a file name.
|
| - |safe_name_or_version| must be a safe name or version string as returned
|
| - by GetSafeName or GetSafeVersion.
|
| - """
|
| - return re.sub('-', '_', safe_name_or_version)
|
| -
|
| - def GetAsRequirementString(self):
|
| - """Builds an easy_install requirements string representing this package."""
|
| - return '%s==%s' % (self.name, self.version)
|
| -
|
| - def GetFilename(self, extension=None):
|
| - """Builds a filename for this package using the setuptools convention.
|
| - If |extensions| is provided it will be appended to the generated filename,
|
| - otherwise only the basename is returned.
|
| -
|
| - The following url discusses the filename format:
|
| -
|
| - http://svn.python.org/projects/sandbox/trunk/setuptools/doc/formats.txt
|
| - """
|
| - filename = '%s-%s-py%s' % (self.file_name, self.file_version,
|
| - VERSION_SUFFIX)
|
| -
|
| - if extension:
|
| - if not extension.startswith('.'):
|
| - filename += '.'
|
| - filename += extension
|
| -
|
| - return filename
|
| -
|
| - def GetPyPiUrl(self, extension):
|
| - """Returns the URL where this package is hosted on PyPi."""
|
| - return 'http://pypi.python.org/packages/%s/%c/%s/%s' % (VERSION_SUFFIX,
|
| - self.file_name[0], self.file_name, self.GetFilename(extension))
|
| -
|
| - def DownloadEgg(self, dest_dir, overwrite=False):
|
| - """Downloads the EGG for this URL.
|
| -
|
| - Args:
|
| - dest_dir: The directory where the EGG should be written. If the EGG
|
| - has already been downloaded and cached the returned path may not
|
| - be in this directory.
|
| - overwite: If True the destination path will be overwritten even if
|
| - it already exists. Defaults to False.
|
| -
|
| - Returns:
|
| - The path to the written EGG.
|
| -
|
| - Raises:
|
| - Error: if dest_dir doesn't exist, the EGG is unable to be written,
|
| - the URL doesn't exist, or the server returned an error, or the
|
| - transmission was interrupted.
|
| - """
|
| - if not os.path.exists(dest_dir):
|
| - raise Error('Path does not exist: %s' % dest_dir)
|
| -
|
| - if not os.path.isdir(dest_dir):
|
| - raise Error('Path is not a directory: %s' % dest_dir)
|
| -
|
| - filename = os.path.abspath(os.path.join(dest_dir, self.GetFilename('egg')))
|
| - if os.path.exists(filename):
|
| - if os.path.isdir(filename):
|
| - raise Error('Path is a directory: %s' % filename)
|
| - if not overwrite:
|
| - return filename
|
| -
|
| - url = self.GetPyPiUrl('egg')
|
| -
|
| - try:
|
| - url_stream = urllib2.urlopen(url)
|
| - local_file = open(filename, 'wb')
|
| - local_file.write(url_stream.read())
|
| - local_file.close()
|
| - return filename
|
| - except (IOError, urllib2.HTTPError, urllib2.URLError):
|
| - # Reraise with a new error type, keeping the original message and
|
| - # traceback.
|
| - raise Error, sys.exc_info()[1], sys.exc_info()[2]
|
| -
|
| -
|
| -def AddToPythonPath(path):
|
| - """Adds the provided path to the head of PYTHONPATH and sys.path."""
|
| - path = os.path.abspath(path)
|
| - if path not in sys.path:
|
| - sys.path.insert(0, path)
|
| -
|
| - paths = os.environ.get('PYTHONPATH', '').split(os.pathsep)
|
| - if path not in paths:
|
| - paths.insert(0, path)
|
| - os.environ['PYTHONPATH'] = os.pathsep.join(paths)
|
| -
|
| -
|
| -def AddSiteDirectory(path):
|
| - """Adds the provided path to the runtime as a site directory.
|
| -
|
| - Any modules that are in the site directory will be available for importing
|
| - after this returns. If modules are added or deleted this must be called
|
| - again for the changes to be reflected in the runtime.
|
| -
|
| - This calls both AddToPythonPath and site.addsitedir. Both are needed to
|
| - convince easy_install to treat |path| as a site directory.
|
| - """
|
| - AddToPythonPath(path)
|
| - site.addsitedir(path) # pylint: disable=E1101
|
| -
|
| -def EnsureSiteDirectory(path):
|
| - """Creates and/or adds the provided path to the runtime as a site directory.
|
| -
|
| - This works like AddSiteDirectory but it will create the directory if it
|
| - does not yet exist.
|
| -
|
| - Raise:
|
| - Error: if the site directory is unable to be created, or if it exists and
|
| - is not a directory.
|
| - """
|
| - if os.path.exists(path):
|
| - if not os.path.isdir(path):
|
| - raise Error('Path is not a directory: %s' % path)
|
| - else:
|
| - try:
|
| - os.mkdir(path)
|
| - except IOError:
|
| - raise Error('Unable to create directory: %s' % path)
|
| -
|
| - AddSiteDirectory(path)
|
| -
|
| -
|
| -def ModuleIsFromPackage(module, package_path):
|
| - """Determines if a module has been imported from a given package.
|
| -
|
| - Args:
|
| - module: the module to test.
|
| - package_path: the path to the package to test.
|
| -
|
| - Returns:
|
| - True if |module| has been imported from |package_path|, False otherwise.
|
| - """
|
| - try:
|
| - m = os.path.abspath(module.__file__)
|
| - p = os.path.abspath(package_path)
|
| - if len(m) <= len(p):
|
| - return False
|
| - if m[0:len(p)] != p:
|
| - return False
|
| - return m[len(p)] == os.sep
|
| - except AttributeError:
|
| - return False
|
| -
|
| -
|
| -def _CaptureStdStreams(function, *args, **kwargs):
|
| - """Captures stdout and stderr while running the provided function.
|
| -
|
| - This only works if |function| only accesses sys.stdout and sys.stderr. If
|
| - we need more than this we'll have to use subprocess.Popen.
|
| -
|
| - Args:
|
| - function: the function to be called.
|
| - args: the arguments to pass to |function|.
|
| - kwargs: the keyword arguments to pass to |function|.
|
| - """
|
| - orig_stdout = sys.stdout
|
| - orig_stderr = sys.stderr
|
| - sys.stdout = cStringIO.StringIO()
|
| - sys.stderr = cStringIO.StringIO()
|
| - try:
|
| - return function(*args, **kwargs)
|
| - finally:
|
| - sys.stdout = orig_stdout
|
| - sys.stderr = orig_stderr
|
| -
|
| -
|
| -def InstallPackage(url_or_req, site_dir):
|
| - """Installs a package to a site directory.
|
| -
|
| - |site_dir| must exist and already be an active site directory. setuptools
|
| - must in the path. Uses easy_install which may involve a download from
|
| - pypi.python.org, so this also requires network access.
|
| -
|
| - Args:
|
| - url_or_req: the package to install, expressed as an URL (may be local),
|
| - or a requirement string.
|
| - site_dir: the site directory in which to install it.
|
| -
|
| - Raises:
|
| - InstallError: if installation fails for any reason.
|
| - """
|
| - args = ['--quiet', '--install-dir', site_dir, '--exclude-scripts',
|
| - '--always-unzip', '--no-deps', url_or_req]
|
| -
|
| - # The easy_install script only calls SystemExit if something goes wrong.
|
| - # Otherwise, it falls through returning None.
|
| - try:
|
| - import setuptools.command.easy_install
|
| - _CaptureStdStreams(setuptools.command.easy_install.main, args)
|
| - except (ImportError, SystemExit):
|
| - # Re-raise the error, preserving the stack trace and message.
|
| - raise InstallError, sys.exc_info()[1], sys.exc_info()[2]
|
| -
|
| -
|
| -def _RunInSubprocess(pycode):
|
| - """Launches a python subprocess with the provided code.
|
| -
|
| - The subprocess will be launched with the same stdout and stderr. The
|
| - subprocess will use the same instance of python as is currently running,
|
| - passing |pycode| as arguments to this script. |pycode| will be interpreted
|
| - as python code in the context of this module.
|
| -
|
| - Returns:
|
| - True if the subprocess returned 0, False if it returned an error.
|
| - """
|
| - return not subprocess.call([sys.executable, __file__, pycode])
|
| -
|
| -
|
| -def _LoadSetupToolsFromEggAndInstall(egg_path):
|
| - """Loads setuptools from the provided egg |egg_path|, and installs it to
|
| - SITE_DIR.
|
| -
|
| - This is intended to be run from a subprocess as it pollutes the running
|
| - instance of Python by importing a module and then forcibly deleting its
|
| - source.
|
| -
|
| - Returns:
|
| - True on success, False on failure.
|
| - """
|
| - AddToPythonPath(egg_path)
|
| -
|
| - try:
|
| - # Import setuptools and ensure it comes from the EGG.
|
| - import setuptools
|
| - if not ModuleIsFromPackage(setuptools, egg_path):
|
| - raise ImportError()
|
| - except ImportError:
|
| - print ' Unable to import downloaded package!'
|
| - return False
|
| -
|
| - try:
|
| - print ' Using setuptools to install itself ...'
|
| - InstallPackage(egg_path, SITE_DIR)
|
| - except InstallError:
|
| - print ' Unable to install setuptools!'
|
| - return False
|
| -
|
| - return True
|
| -
|
| -
|
| -def BootstrapSetupTools():
|
| - """Bootstraps the runtime with setuptools.
|
| -
|
| - Will try to import setuptools directly. If not found it will attempt to
|
| - download it and load it from there. If the download is successful it will
|
| - then use setuptools to install itself in the site directory.
|
| -
|
| - This is meant to be run from a child process as it modifies the running
|
| - instance of Python by importing modules and then physically deleting them
|
| - from disk.
|
| -
|
| - Returns:
|
| - Returns True if 'import setuptools' will succeed, False otherwise.
|
| - """
|
| - AddSiteDirectory(SITE_DIR)
|
| -
|
| - # Check if setuptools is already available. If so, we're done.
|
| - try:
|
| - import setuptools # pylint: disable=W0612
|
| - return True
|
| - except ImportError:
|
| - pass
|
| -
|
| - print 'Bootstrapping setuptools ...'
|
| -
|
| - EnsureSiteDirectory(SITE_DIR)
|
| -
|
| - # Download the egg to a temp directory.
|
| - dest_dir = tempfile.mkdtemp('depot_tools')
|
| - path = None
|
| - try:
|
| - package = Package(*SETUPTOOLS)
|
| - print ' Downloading %s ...' % package.GetFilename()
|
| - path = package.DownloadEgg(dest_dir)
|
| - except Error:
|
| - print ' Download failed!'
|
| - shutil.rmtree(dest_dir)
|
| - return False
|
| -
|
| - try:
|
| - # Load the downloaded egg, and install it to the site directory. Do this
|
| - # in a subprocess so as not to pollute this runtime.
|
| - pycode = '_LoadSetupToolsFromEggAndInstall(%s)' % repr(path)
|
| - if not _RunInSubprocess(pycode):
|
| - raise Error()
|
| -
|
| - # Reload our site directory, which should now contain setuptools.
|
| - AddSiteDirectory(SITE_DIR)
|
| -
|
| - # Try to import setuptools
|
| - import setuptools
|
| - except ImportError:
|
| - print ' Unable to import setuptools!'
|
| - return False
|
| - except Error:
|
| - # This happens if RunInSubProcess fails, and the appropriate error has
|
| - # already been written to stdout.
|
| - return False
|
| - finally:
|
| - # Delete the temp directory.
|
| - shutil.rmtree(dest_dir)
|
| -
|
| - return True
|
| -
|
| -
|
| -def _GetModTime(path):
|
| - """Gets the last modification time associated with |path| in seconds since
|
| - epoch, returning 0 if |path| does not exist.
|
| - """
|
| - try:
|
| - return os.stat(path).st_mtime
|
| - except: # pylint: disable=W0702
|
| - # This error is different depending on the OS, hence no specified type.
|
| - return 0
|
| -
|
| -
|
| -def _SiteDirectoryIsUpToDate():
|
| - return _GetModTime(LAST_ROLLED) > _GetModTime(__file__)
|
| -
|
| -
|
| -def UpdateSiteDirectory():
|
| - """Installs the packages from PACKAGES if they are not already installed.
|
| - At this point we must have setuptools in the site directory.
|
| -
|
| - This is intended to be run in a subprocess *prior* to the site directory
|
| - having been added to the parent process as it may cause packages to be
|
| - added and/or removed.
|
| -
|
| - Returns:
|
| - True on success, False otherwise.
|
| - """
|
| - if _SiteDirectoryIsUpToDate():
|
| - return True
|
| -
|
| - try:
|
| - EnsureSiteDirectory(SITE_DIR)
|
| - import pkg_resources
|
| -
|
| - # Determine if any packages actually need installing.
|
| - missing_packages = []
|
| - for package in [SETUPTOOLS] + list(PACKAGES):
|
| - pkg = Package(*package)
|
| - req = pkg.GetAsRequirementString()
|
| -
|
| - # It may be that this package is already available in the site
|
| - # directory. If so, we can skip past it without trying to install it.
|
| - pkg_req = pkg_resources.Requirement.parse(req)
|
| - try:
|
| - dist = pkg_resources.working_set.find(pkg_req)
|
| - if dist:
|
| - continue
|
| - except pkg_resources.VersionConflict:
|
| - # This happens if another version of the package is already
|
| - # installed in another site directory (ie: the system site directory).
|
| - pass
|
| -
|
| - missing_packages.append(pkg)
|
| -
|
| - # Install the missing packages.
|
| - if missing_packages:
|
| - print 'Updating python packages ...'
|
| - for pkg in missing_packages:
|
| - print ' Installing %s ...' % pkg.GetFilename()
|
| - InstallPackage(pkg.GetAsRequirementString(), SITE_DIR)
|
| -
|
| - # Touch the status file so we know that we're up to date next time.
|
| - open(LAST_ROLLED, 'wb')
|
| - except InstallError, e:
|
| - print ' Installation failed: %s' % str(e)
|
| - return False
|
| -
|
| - return True
|
| -
|
| -
|
| -def SetupSiteDirectory():
|
| - """Sets up the site directory, bootstrapping setuptools if necessary.
|
| -
|
| - If this finishes successfully then SITE_DIR will exist and will contain
|
| - the appropriate version of setuptools and all of the packages listed in
|
| - PACKAGES.
|
| -
|
| - This is the main workhorse of this module. Calling this will do everything
|
| - necessary to ensure that you have the desired packages installed in the
|
| - site directory, and the site directory enabled in this process.
|
| -
|
| - Returns:
|
| - True on success, False on failure.
|
| - """
|
| - if _SiteDirectoryIsUpToDate():
|
| - AddSiteDirectory(SITE_DIR)
|
| - return True
|
| -
|
| - if not _RunInSubprocess('BootstrapSetupTools()'):
|
| - return False
|
| -
|
| - if not _RunInSubprocess('UpdateSiteDirectory()'):
|
| - return False
|
| -
|
| - # Process the site directory so that the packages within it are available
|
| - # for import.
|
| - AddSiteDirectory(SITE_DIR)
|
| -
|
| - return True
|
| -
|
| -
|
| -def CanImportFromSiteDirectory(package_name):
|
| - """Determines if the given package can be imported from the site directory.
|
| -
|
| - Args:
|
| - package_name: the name of the package to import.
|
| -
|
| - Returns:
|
| - True if 'import package_name' will succeed and return a module from the
|
| - site directory, False otherwise.
|
| - """
|
| - try:
|
| - return ModuleIsFromPackage(__import__(package_name), SITE_DIR)
|
| - except ImportError:
|
| - return False
|
| -
|
| -
|
| -def Test():
|
| - """Runs SetupSiteDirectory and then tries to load pylint, ensuring that it
|
| - comes from the site directory just created. This is an end-to-end unittest
|
| - and allows for simple testing from the command-line by running
|
| -
|
| - ./package_management.py 'Test()'
|
| - """
|
| - print 'Testing package_management.'
|
| - if not SetupSiteDirectory():
|
| - print 'SetupSiteDirectory failed.'
|
| - return False
|
| - if not CanImportFromSiteDirectory('pylint'):
|
| - print 'CanImportFromSiteDirectory failed.'
|
| - return False
|
| - print 'Success!'
|
| - return True
|
| -
|
| -
|
| -def Main():
|
| - """The main entry for the package management script.
|
| -
|
| - If no arguments are provided simply runs SetupSiteDirectory. If arguments
|
| - have been passed we execute the first argument as python code in the
|
| - context of this module. This mechanism is used during the bootstrap
|
| - process so that the main instance of Python does not have its runtime
|
| - polluted by various intermediate packages and imports.
|
| -
|
| - Returns:
|
| - 0 on success, 1 otherwise.
|
| - """
|
| - if len(sys.argv) == 2:
|
| - result = False
|
| - exec('result = %s' % sys.argv[1])
|
| -
|
| - # Translate the success state to a return code.
|
| - return not result
|
| - else:
|
| - return not SetupSiteDirectory()
|
| -
|
| -
|
| -if __name__ == '__main__':
|
| - sys.exit(Main())
|
|
|