summaryrefslogtreecommitdiff
path: root/distutils2/install.py
diff options
context:
space:
mode:
Diffstat (limited to 'distutils2/install.py')
-rw-r--r--distutils2/install.py357
1 files changed, 286 insertions, 71 deletions
diff --git a/distutils2/install.py b/distutils2/install.py
index 21510e8..053787f 100644
--- a/distutils2/install.py
+++ b/distutils2/install.py
@@ -1,14 +1,3 @@
-from tempfile import mkdtemp
-import logging
-import shutil
-import os
-import errno
-
-from distutils2._backport.pkgutil import get_distributions
-from distutils2.depgraph import generate_graph
-from distutils2.index import wrapper
-from distutils2.index.errors import ProjectNotFound, ReleaseNotFound
-
"""Provides installations scripts.
The goal of this script is to install a release from the indexes (eg.
@@ -17,26 +6,32 @@ PyPI), including the dependencies of the releases if needed.
It uses the work made in pkgutil and by the index crawlers to browse the
installed distributions, and rely on the instalation commands to install.
"""
+import shutil
+import os
+import sys
+import stat
+import errno
+import itertools
+import logging
+import tempfile
-
-class InstallationException(Exception):
- """Base exception for installation scripts"""
+from distutils2 import logger
+from distutils2._backport.pkgutil import get_distributions
+from distutils2._backport.pkgutil import get_distribution
+from distutils2._backport.sysconfig import get_config_var
+from distutils2.depgraph import generate_graph
+from distutils2.index import wrapper
+from distutils2.index.errors import ProjectNotFound, ReleaseNotFound
+from distutils2.errors import (DistutilsError, InstallationException,
+ InstallationConflict)
+from distutils2.version import get_version_predicate
-class InstallationConflict(InstallationException):
- """Raised when a conflict is detected"""
+__all__ = ['install_dists', 'install_from_infos', 'get_infos', 'remove',
+ 'install']
-def _update_infos(infos, new_infos):
- """extends the lists contained in the `info` dict with those contained
- in the `new_info` one
- """
- for key, value in infos.items():
- if key in new_infos:
- infos[key].extend(new_infos[key])
-
-
-def move_files(files, destination=None):
+def _move_files(files, destination):
"""Move the list of files in the destination folder, keeping the same
structure.
@@ -44,13 +39,11 @@ def move_files(files, destination=None):
:param files: a list of files to move.
:param destination: the destination directory to put on the files.
- if not defined, create a new one, using mkdtemp
"""
- if not destination:
- destination = mkdtemp()
-
for old in files:
- new = '%s%s' % (destination, old)
+ # not using os.path.join() because basename() might not be
+ # unique in destination
+ new = "%s%s" % (destination, old)
# try to make the paths.
try:
@@ -61,10 +54,66 @@ def move_files(files, destination=None):
else:
raise e
os.rename(old, new)
- yield(old, new)
+ yield old, new
+
+
+def _run_d1_install(archive_dir, path):
+ # backward compat: using setuptools or plain-distutils
+ cmd = '%s setup.py install --root=%s --record=%s'
+ setup_py = os.path.join(archive_dir, 'setup.py')
+ if 'setuptools' in open(setup_py).read():
+ cmd += ' --single-version-externally-managed'
+
+ # how to place this file in the egg-info dir
+ # for non-distutils2 projects ?
+ record_file = os.path.join(archive_dir, 'RECORD')
+ os.system(cmd % (sys.executable, path, record_file))
+ if not os.path.exists(record_file):
+ raise ValueError('failed to install')
+ return open(record_file).read().split('\n')
+
+def _run_d2_install(archive_dir, path):
+ # using our own install command
+ raise NotImplementedError()
-def install_dists(dists, path=None):
+
+def _install_dist(dist, path):
+ """Install a distribution into a path.
+
+ This:
+
+ * unpack the distribution
+ * copy the files in "path"
+ * determine if the distribution is distutils2 or distutils1.
+ """
+ where = dist.unpack(path)
+
+ # get into the dir
+ archive_dir = None
+ for item in os.listdir(where):
+ fullpath = os.path.join(where, item)
+ if os.path.isdir(fullpath):
+ archive_dir = fullpath
+ break
+
+ if archive_dir is None:
+ raise ValueError('Cannot locate the unpacked archive')
+
+ # install
+ old_dir = os.getcwd()
+ os.chdir(archive_dir)
+ try:
+ # distutils2 or distutils1 ?
+ if 'setup.py' in os.listdir(archive_dir):
+ return _run_d1_install(archive_dir, path)
+ else:
+ return _run_d2_install(archive_dir, path)
+ finally:
+ os.chdir(old_dir)
+
+
+def install_dists(dists, path, paths=sys.path):
"""Install all distributions provided in dists, with the given prefix.
If an error occurs while installing one of the distributions, uninstall all
@@ -73,24 +122,29 @@ def install_dists(dists, path=None):
Return a list of installed files.
:param dists: distributions to install
- :param path: base path to install distribution on
+ :param path: base path to install distribution in
+ :param paths: list of paths (defaults to sys.path) to look for info
"""
- if not path:
- path = mkdtemp()
installed_dists, installed_files = [], []
- for d in dists:
+ for dist in dists:
+ logger.info('installing %s %s', dist.name, dist.version)
try:
- installed_files.extend(d.install(path))
- installed_dists.append(d)
- except Exception, e :
- for d in installed_dists:
- d.uninstall()
+ installed_files.extend(_install_dist(dist, path))
+ installed_dists.append(dist)
+ except Exception, e:
+ logger.info('failed: %s', e)
+
+ # reverting
+ for installed_dist in installed_dists:
+ _remove_dist(installed_dist, paths)
raise e
+
return installed_files
-def install_from_infos(install=[], remove=[], conflicts=[], install_path=None):
+def install_from_infos(install_path=None, install=[], remove=[], conflicts=[],
+ paths=sys.path):
"""Install and remove the given distributions.
The function signature is made to be compatible with the one of get_infos.
@@ -109,37 +163,57 @@ def install_from_infos(install=[], remove=[], conflicts=[], install_path=None):
4. Else, move the distributions to the right locations, and remove for
real the distributions thats need to be removed.
- :param install: list of distributions that will be installed.
+ :param install_path: the installation path where we want to install the
+ distributions.
+ :param install: list of distributions that will be installed; install_path
+ must be provided if this list is not empty.
:param remove: list of distributions that will be removed.
:param conflicts: list of conflicting distributions, eg. that will be in
conflict once the install and remove distribution will be
processed.
- :param install_path: the installation path where we want to install the
- distributions.
+ :param paths: list of paths (defaults to sys.path) to look for info
"""
# first of all, if we have conflicts, stop here.
if conflicts:
raise InstallationConflict(conflicts)
+ if install and not install_path:
+ raise ValueError("Distributions are to be installed but `install_path`"
+ " is not provided.")
+
# before removing the files, we will start by moving them away
# then, if any error occurs, we could replace them in the good place.
temp_files = {} # contains lists of {dist: (old, new)} paths
+ temp_dir = None
if remove:
+ temp_dir = tempfile.mkdtemp()
for dist in remove:
files = dist.get_installed_files()
- temp_files[dist] = move_files(files)
+ temp_files[dist] = _move_files(files, temp_dir)
try:
if install:
- installed_files = install_dists(install, install_path) # install to tmp first
- for files in temp_files.values():
- for old, new in files:
- os.remove(new)
-
- except Exception,e:
- # if an error occurs, put back the files in the good place.
+ install_dists(install, install_path, paths)
+ except:
+ # if an error occurs, put back the files in the right place.
for files in temp_files.values():
for old, new in files:
shutil.move(new, old)
+ if temp_dir:
+ shutil.rmtree(temp_dir)
+ # now re-raising
+ raise
+
+ # we can remove them for good
+ for files in temp_files.values():
+ for old, new in files:
+ os.remove(new)
+ if temp_dir:
+ shutil.rmtree(temp_dir)
+
+
+def _get_setuptools_deps(release):
+ # NotImplementedError
+ pass
def get_infos(requirements, index=None, installed=None, prefer_final=True):
@@ -162,47 +236,188 @@ def get_infos(requirements, index=None, installed=None, prefer_final=True):
Conflict contains all the conflicting distributions, if there is a
conflict.
"""
+ if not installed:
+ logger.info('reading installed distributions')
+ installed = get_distributions(use_egg_info=True)
+
+ infos = {'install': [], 'remove': [], 'conflict': []}
+ # Is a compatible version of the project is already installed ?
+ predicate = get_version_predicate(requirements)
+ found = False
+ installed = list(installed)
+
+ # check that the project isnt already installed
+ for installed_project in installed:
+ # is it a compatible project ?
+ if predicate.name.lower() != installed_project.name.lower():
+ continue
+ found = True
+ logger.info('found %s %s', installed_project.name,
+ installed_project.version)
+
+ # if we already have something installed, check it matches the
+ # requirements
+ if predicate.match(installed_project.version):
+ return infos
+ break
+
+ if not found:
+ logger.info('project not installed')
if not index:
index = wrapper.ClientWrapper()
- if not installed:
- installed = get_distributions()
-
# Get all the releases that match the requirements
try:
releases = index.get_releases(requirements)
- except (ReleaseNotFound, ProjectNotFound), e:
+ except (ReleaseNotFound, ProjectNotFound):
raise InstallationException('Release not found: "%s"' % requirements)
# Pick up a release, and try to get the dependency tree
release = releases.get_last(requirements, prefer_final=prefer_final)
- # Iter since we found something without conflicts
+ if release is None:
+ logger.info('could not find a matching project')
+ return infos
+
+ # this works for Metadata 1.2
metadata = release.fetch_metadata()
- # Get the distributions already_installed on the system
- # and add the one we want to install
+ # for earlier, we need to build setuptools deps if any
+ if 'requires_dist' not in metadata:
+ deps = _get_setuptools_deps(release)
+ else:
+ deps = metadata['requires_dist']
+
+ # XXX deps not used
- distributions = installed + [release]
+ distributions = itertools.chain(installed, [release])
depgraph = generate_graph(distributions)
# Store all the already_installed packages in a list, in case of rollback.
- infos = {'install': [], 'remove': [], 'conflict': []}
-
# Get what the missing deps are
- for dists in depgraph.missing.values():
- if dists:
- logging.info("missing dependencies found, installing them")
- # we have missing deps
- for dist in dists:
- _update_infos(infos,
- get_infos(dist, index, installed))
+ dists = depgraph.missing[release]
+ if dists:
+ logger.info("missing dependencies found, retrieving metadata")
+ # we have missing deps
+ for dist in dists:
+ _update_infos(infos, get_infos(dist, index, installed))
# Fill in the infos
existing = [d for d in installed if d.name == release.name]
+
if existing:
infos['remove'].append(existing[0])
infos['conflict'].extend(depgraph.reverse_list[existing[0]])
infos['install'].append(release)
return infos
+
+
+def _update_infos(infos, new_infos):
+ """extends the lists contained in the `info` dict with those contained
+ in the `new_info` one
+ """
+ for key, value in infos.items():
+ if key in new_infos:
+ infos[key].extend(new_infos[key])
+
+
+def _remove_dist(dist, paths=sys.path):
+ remove(dist.name, paths)
+
+
+def remove(project_name, paths=sys.path):
+ """Removes a single project from the installation"""
+ dist = get_distribution(project_name, use_egg_info=True, paths=paths)
+ if dist is None:
+ raise DistutilsError('Distribution "%s" not found' % project_name)
+ files = dist.get_installed_files(local=True)
+ rmdirs = []
+ rmfiles = []
+ tmp = tempfile.mkdtemp(prefix=project_name + '-uninstall')
+ try:
+ for file_, md5, size in files:
+ if os.path.isfile(file_):
+ dirname, filename = os.path.split(file_)
+ tmpfile = os.path.join(tmp, filename)
+ try:
+ os.rename(file_, tmpfile)
+ finally:
+ if not os.path.isfile(file_):
+ os.rename(tmpfile, file_)
+ if file_ not in rmfiles:
+ rmfiles.append(file_)
+ if dirname not in rmdirs:
+ rmdirs.append(dirname)
+ finally:
+ shutil.rmtree(tmp)
+
+ logger.info('removing %r...', project_name)
+
+ file_count = 0
+ for file_ in rmfiles:
+ os.remove(file_)
+ file_count += 1
+
+ dir_count = 0
+ for dirname in rmdirs:
+ if not os.path.exists(dirname):
+ # could
+ continue
+
+ files_count = 0
+ for root, dir, files in os.walk(dirname):
+ files_count += len(files)
+
+ if files_count > 0:
+ # XXX Warning
+ continue
+
+ # empty dirs with only empty dirs
+ if bool(os.stat(dirname).st_mode & stat.S_IWUSR):
+ # XXX Add a callable in shutil.rmtree to count
+ # the number of deleted elements
+ shutil.rmtree(dirname)
+ dir_count += 1
+
+ # removing the top path
+ # XXX count it ?
+ if os.path.exists(dist.path):
+ shutil.rmtree(dist.path)
+
+ logger.info('success: removed %d files and %d dirs',
+ file_count, dir_count)
+
+
+def install(project):
+ logger.info('getting information about %r', project)
+ try:
+ info = get_infos(project)
+ except InstallationException:
+ logger.info('cound not find %r', project)
+ return
+
+ if info['install'] == []:
+ logger.info('nothing to install')
+ return
+
+ install_path = get_config_var('base')
+ try:
+ install_from_infos(install_path,
+ info['install'], info['remove'], info['conflict'])
+
+ except InstallationConflict, e:
+ if logger.isEnabledFor(logging.INFO):
+ projects = ['%s %s' % (p.name, p.version) for p in e.args[0]]
+ logger.info('%r conflicts with %s', project, ','.join(projects))
+
+
+def _main(**attrs):
+ if 'script_args' not in attrs:
+ import sys
+ attrs['requirements'] = sys.argv[1]
+ get_infos(**attrs)
+
+
+if __name__ == '__main__':
+ _main()