diff options
Diffstat (limited to 'Lib/packaging/install.py')
-rw-r--r-- | Lib/packaging/install.py | 529 |
1 files changed, 0 insertions, 529 deletions
diff --git a/Lib/packaging/install.py b/Lib/packaging/install.py deleted file mode 100644 index 776ba40..0000000 --- a/Lib/packaging/install.py +++ /dev/null @@ -1,529 +0,0 @@ -"""Building blocks for installers. - -When used as a script, this module installs a release thanks to info -obtained from an index (e.g. PyPI), with dependencies. - -This is a higher-level module built on packaging.database and -packaging.pypi. -""" -import os -import sys -import stat -import errno -import shutil -import logging -import tempfile -from sysconfig import get_config_var, get_path, is_python_build - -from packaging import logger -from packaging.dist import Distribution -from packaging.util import (_is_archive_file, ask, get_install_method, - egginfo_to_distinfo) -from packaging.pypi import wrapper -from packaging.version import get_version_predicate -from packaging.database import get_distributions, get_distribution -from packaging.depgraph import generate_graph - -from packaging.errors import (PackagingError, InstallationException, - InstallationConflict, CCompilerError) -from packaging.pypi.errors import ProjectNotFound, ReleaseNotFound -from packaging import database - - -__all__ = ['install_dists', 'install_from_infos', 'get_infos', 'remove', - 'install', 'install_local_project'] - - -def _move_files(files, destination): - """Move the list of files in the destination folder, keeping the same - structure. - - Return a list of tuple (old, new) emplacement of files - - :param files: a list of files to move. - :param destination: the destination directory to put on the files. - """ - - for old in files: - filename = os.path.split(old)[-1] - new = os.path.join(destination, filename) - # try to make the paths. - try: - os.makedirs(os.path.dirname(new)) - except OSError as e: - if e.errno != errno.EEXIST: - raise - os.rename(old, new) - yield old, new - - -def _run_distutils_install(path): - # backward compat: using setuptools or plain-distutils - cmd = '%s setup.py install --record=%s' - record_file = os.path.join(path, 'RECORD') - os.system(cmd % (sys.executable, record_file)) - if not os.path.exists(record_file): - raise ValueError('failed to install') - else: - egginfo_to_distinfo(record_file, remove_egginfo=True) - - -def _run_setuptools_install(path): - cmd = '%s setup.py install --record=%s --single-version-externally-managed' - record_file = os.path.join(path, 'RECORD') - - os.system(cmd % (sys.executable, record_file)) - if not os.path.exists(record_file): - raise ValueError('failed to install') - else: - egginfo_to_distinfo(record_file, remove_egginfo=True) - - -def _run_packaging_install(path): - # XXX check for a valid setup.cfg? - dist = Distribution() - dist.parse_config_files() - try: - dist.run_command('install_dist') - name = dist.metadata['Name'] - return database.get_distribution(name) is not None - except (IOError, os.error, PackagingError, CCompilerError) as msg: - raise ValueError("Failed to install, " + str(msg)) - - -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 packaging or distutils1. - """ - where = dist.unpack() - - if where is None: - raise ValueError('Cannot locate the unpacked archive') - - return _run_install_from_archive(where) - - -def install_local_project(path): - """Install a distribution from a source directory. - - If the source directory contains a setup.py install using distutils1. - If a setup.cfg is found, install using the install_dist command. - - Returns True on success, False on Failure. - """ - path = os.path.abspath(path) - if os.path.isdir(path): - logger.info('Installing from source directory: %r', path) - return _run_install_from_dir(path) - elif _is_archive_file(path): - logger.info('Installing from archive: %r', path) - _unpacked_dir = tempfile.mkdtemp() - try: - shutil.unpack_archive(path, _unpacked_dir) - return _run_install_from_archive(_unpacked_dir) - finally: - shutil.rmtree(_unpacked_dir) - else: - logger.warning('No project to install.') - return False - - -def _run_install_from_archive(source_dir): - # XXX need a better way - for item in os.listdir(source_dir): - fullpath = os.path.join(source_dir, item) - if os.path.isdir(fullpath): - source_dir = fullpath - break - return _run_install_from_dir(source_dir) - - -install_methods = { - 'packaging': _run_packaging_install, - 'setuptools': _run_setuptools_install, - 'distutils': _run_distutils_install} - - -def _run_install_from_dir(source_dir): - old_dir = os.getcwd() - os.chdir(source_dir) - install_method = get_install_method(source_dir) - func = install_methods[install_method] - try: - func = install_methods[install_method] - try: - func(source_dir) - return True - except ValueError as err: - # failed to install - logger.info(str(err)) - return False - finally: - os.chdir(old_dir) - - -def install_dists(dists, path, paths=None): - """Install all distributions provided in dists, with the given prefix. - - If an error occurs while installing one of the distributions, uninstall all - the installed distribution (in the context if this function). - - Return a list of installed dists. - - :param dists: distributions to install - :param path: base path to install distribution in - :param paths: list of paths (defaults to sys.path) to look for info - """ - - installed_dists = [] - for dist in dists: - logger.info('Installing %r %s...', dist.name, dist.version) - try: - _install_dist(dist, path) - installed_dists.append(dist) - except Exception as e: - logger.info('Failed: %s', e) - - # reverting - for installed_dist in installed_dists: - logger.info('Reverting %r', installed_dist) - remove(installed_dist.name, paths) - raise e - return installed_dists - - -def install_from_infos(install_path=None, install=[], remove=[], conflicts=[], - paths=None): - """Install and remove the given distributions. - - The function signature is made to be compatible with the one of get_infos. - The aim of this script is to povide a way to install/remove what's asked, - and to rollback if needed. - - So, it's not possible to be in an inconsistant state, it could be either - installed, either uninstalled, not half-installed. - - The process follow those steps: - - 1. Move all distributions that will be removed in a temporary location - 2. Install all the distributions that will be installed in a temp. loc. - 3. If the installation fails, rollback (eg. move back) those - distributions, or remove what have been installed. - 4. Else, move the distributions to the right locations, and remove for - real the distributions thats need to be removed. - - :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 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.list_installed_files() - temp_files[dist] = _move_files(files, temp_dir) - try: - if install: - 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): - """Return the informations on what's going to be installed and upgraded. - - :param requirements: is a *string* containing the requirements for this - project (for instance "FooBar 1.1" or "BarBaz (<1.2)") - :param index: If an index is specified, use this one, otherwise, use - :class index.ClientWrapper: to get project metadatas. - :param installed: a list of already installed distributions. - :param prefer_final: when picking up the releases, prefer a "final" one - over a beta/alpha/etc one. - - The results are returned in a dict, containing all the operations - needed to install the given requirements:: - - >>> get_install_info("FooBar (<=1.2)") - {'install': [<FooBar 1.1>], 'remove': [], 'conflict': []} - - Conflict contains all the conflicting distributions, if there is a - conflict. - """ - # this function does several things: - # 1. get a release specified by the requirements - # 2. gather its metadata, using setuptools compatibility if needed - # 3. compare this tree with what is currently installed on the system, - # return the requirements of what is missing - # 4. do that recursively and merge back the results - # 5. return a dict containing information about what is needed to install - # or remove - - if not installed: - logger.debug('Reading installed distributions') - installed = list(get_distributions(use_egg_info=True)) - - infos = {'install': [], 'remove': [], 'conflict': []} - # Is a compatible version of the project already installed ? - predicate = get_version_predicate(requirements) - found = False - - # check that the project isn't 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 %r %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.debug('Project not installed') - - if not index: - index = wrapper.ClientWrapper() - - if not installed: - installed = get_distributions(use_egg_info=True) - - # Get all the releases that match the requirements - try: - release = index.get_release(requirements) - except (ReleaseNotFound, ProjectNotFound): - raise InstallationException('Release not found: %r' % requirements) - - if release is None: - logger.info('Could not find a matching project') - return infos - - metadata = release.fetch_metadata() - - # we need to build setuptools deps if any - if 'requires_dist' not in metadata: - metadata['requires_dist'] = _get_setuptools_deps(release) - - # build the dependency graph with local and required dependencies - dists = list(installed) - dists.append(release) - depgraph = generate_graph(dists) - - # Get what the missing deps are - 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(project_name, paths=None, auto_confirm=True): - """Removes a single project from the installation. - - Returns True on success - """ - dist = get_distribution(project_name, use_egg_info=True, paths=paths) - if dist is None: - raise PackagingError('Distribution %r not found' % project_name) - files = dist.list_installed_files(local=True) - rmdirs = [] - rmfiles = [] - tmp = tempfile.mkdtemp(prefix=project_name + '-uninstall') - - def _move_file(source, target): - try: - os.rename(source, target) - except OSError as err: - return err - return None - - success = True - error = None - 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: - error = _move_file(file_, tmpfile) - if error is not None: - success = False - break - 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) - - if not success: - logger.info('%r cannot be removed.', project_name) - logger.info('Error: %s', error) - return False - - logger.info('Removing %r: ', project_name) - - for file_ in rmfiles: - logger.info(' %s', file_) - - # Taken from the pip project - if auto_confirm: - response = 'y' - else: - response = ask('Proceed (y/n)? ', ('y', 'n')) - - if response == 'y': - 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 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) - - return True - - -def install(project): - """Installs a project. - - Returns True on success, False on failure - """ - if is_python_build(): - # Python would try to install into the site-packages directory under - # $PREFIX, but when running from an uninstalled code checkout we don't - # want to create directories under the installation root - message = ('installing third-party projects from an uninstalled ' - 'Python is not supported') - logger.error(message) - return False - - logger.info('Checking the installation location...') - purelib_path = get_path('purelib') - - # trying to write a file there - try: - with tempfile.NamedTemporaryFile(suffix=project, - dir=purelib_path) as testfile: - testfile.write(b'test') - except OSError: - # FIXME this should check the errno, or be removed altogether (race - # condition: the directory permissions could be changed between here - # and the actual install) - logger.info('Unable to write in "%s". Do you have the permissions ?' - % purelib_path) - return False - - logger.info('Getting information about %r...', project) - try: - info = get_infos(project) - except InstallationException: - logger.info('Cound not find %r', project) - return False - - if info['install'] == []: - logger.info('Nothing to install') - return False - - install_path = get_config_var('base') - try: - install_from_infos(install_path, - info['install'], info['remove'], info['conflict']) - - except InstallationConflict as e: - if logger.isEnabledFor(logging.INFO): - projects = ('%r %s' % (p.name, p.version) for p in e.args[0]) - logger.info('%r conflicts with %s', project, ','.join(projects)) - - return True |