Beispiel #1
0
    def uninstall(self, apps):
        """ Uninstall apps """
        for name in apps:
            if name == 'autopip' and len(list(self.apps)) > 1:
                if apps[-1] == 'autopip':
                    error(
                        '! autopip can not be uninstalled until other apps are uninstalled: %s',
                        ' '.join(a.name for a in self.apps
                                 if a.name != 'autopip'))
                else:  # Try again after uninstall the other apps
                    apps.append('autopip')

                continue

            app = App(name, self.paths)
            if app.is_installed:
                group_specs = app.group_specs(name_only=True)
                app.uninstall()

                if group_specs:
                    info(
                        'This app has defined "autopip" entry points to uninstall: %s',
                        ' '.join(group_specs))
                    apps.extend(group_specs)

            else:
                info(f'{name} is not installed')

        if not list(self.apps):
            try:
                crontab.remove('autopip')
            except Exception as e:
                debug('Could not remove crontab for autopip: %s', e)
Beispiel #2
0
    def uninstall(self):
        """ Uninstall app """
        info('Uninstalling %s', self.name)

        try:
            crontab.remove(self._crontab_id)
        except exceptions.MissingError as e:
            debug('Could not remove crontab for %s: %s', self._crontab_id, e)

        for script in self.scripts():
            script_symlink = self.paths.symlink_root / script
            if ((script_symlink.exists() or script_symlink.is_symlink()) and
                    str(script_symlink.resolve()).startswith(str(self.path))):
                script_symlink.unlink()

        shutil.rmtree(self.path)
Beispiel #3
0
    def update(self, apps=None, wait=False):
        """
        Update installed apps

        :param list apps: List of apps to update. Defaults to all.
        :param bool wait: Wait for a new version to be published and then install it.
        """
        app_instances = list([a for a in self.apps
                              if a.name in apps] if apps else self.apps)

        if app_instances:
            app_specs = []
            for app in app_instances:
                settings = app.settings()
                if settings.get('update'):
                    app_specs.append(
                        (settings['app_spec'], settings['update']))
                elif sys.stdout.isatty() or wait:
                    app_specs.append((settings.get('app_spec',
                                                   app.name), None))

            if app_specs:
                self.install(app_specs, wait=wait)

            elif not apps:
                try:
                    crontab.remove('autopip')
                except Exception as e:
                    debug('Could not remove crontab for autopip: %s', e)

        elif list(self.apps):
            info('No apps found matching: %s', ', '.join(apps))
            info('Available apps: %s', ', '.join([a.name for a in self.apps]))

        else:
            info('No apps installed yet.')
Beispiel #4
0
    def install(self, version, app_spec, update=None, python_version=None):
        """
        Install the version of the app if it is not already installed

        :param str version: Version of the app to install
        :param pkg_resources.Requirement app_spec: App version requirement from user
        :param UpdateFreq|None update: How often to update. Choose from hourly, daily, weekly, monthly
        :param str python_version: Python version to run app
        :return: True if install or update happened, otherwise False when nothing happened (already installed / non-tty)
        """
        version_path = self.path / version
        prev_version_path = self.current_path and self.current_path.resolve()
        important_paths = [
            version_path, prev_version_path, self._current_symlink
        ]

        if self.settings():
            if not python_version:
                python_version = self.settings().get('python_version')

            if not update:
                update = self.settings().get(
                    'update') and UpdateFreq.from_name(
                        self.settings()['update'])

        if not python_version:
            python_version = PYTHON_VERSION

        if version_path.exists():
            if self.current_version == version:
                # Skip printing / ensuring symlinks / cronjob when running from cron
                if not sys.stdout.isatty():
                    return False

                pinned = str(app_spec).lstrip(self.name)
                pin_info = f' [per spec: {pinned}]' if pinned else ''

                info(f'{self.name} is up-to-date{pin_info}')

            else:
                info(
                    f'{self.name} {version} was previously installed and will be set as the current version'
                )

        else:
            if not shutil.which('python' + python_version):
                error(
                    f'! python{python_version} does not exist. '
                    'Please install it first, or ensure its path is in PATH.')
                sys.exit(1)

            if python_version.startswith('2'):
                venv = f'virtualenv --python=python{python_version}'
            else:
                venv = f'python{python_version} -m venv'

            old_venv_dir = None
            old_path = None
            no_compile = '--no-compile ' if os.getuid() else ''

            info(f'Installing {self.name} to {version_path}')

            os.environ.pop('PYTHONPATH', None)
            if 'VIRTUAL_ENV' in os.environ:
                old_venv_dir = os.environ.pop('VIRTUAL_ENV')
                old_path = os.environ['PATH']
                os.environ['PATH'] = os.pathsep.join([
                    p for p in os.environ['PATH'].split(os.pathsep)
                    if os.path.exists(p) and not p.startswith(old_venv_dir)
                ])

            try:
                run(f"""set -e
                    {venv} {version_path}
                    source {version_path / 'bin' / 'activate'}
                    pip install --upgrade pip wheel
                    pip install {no_compile}{self.name}=={version}
                    """,
                    executable='/bin/bash',
                    stderr=STDOUT,
                    shell=True)

            except BaseException as e:
                shutil.rmtree(version_path, ignore_errors=True)

                if isinstance(e, CalledProcessError):
                    if e.output:
                        output = e.output.decode('utf-8')

                        if not python_version.startswith('2'):
                            python2 = shutil.which('python2') or shutil.which(
                                'python2.7')
                            if python2:
                                py2_version = Path(python2).name.lstrip(
                                    'python')
                                if 'is a builtin module since Python 3' in output:
                                    info(
                                        f'Failed to install using Python {python_version} venv, '
                                        f'let\'s try using Python {py2_version} virtualenv.'
                                    )
                                    return self.install(
                                        version,
                                        app_spec,
                                        update=update,
                                        python_version=py2_version)

                        info(
                            re.sub(r'(https?://)[^/]+:[^/]+@',
                                   r'\1<xxx>:<xxx>@', output))

                    error(
                        f'! Failed to install using Python {python_version}.'
                        ' If this app requires a different Python version, please specify it using --python option.'
                    )

                raise

            finally:
                if old_venv_dir:
                    os.environ['VIRTUAL_ENV'] = old_venv_dir
                    os.environ['PATH'] = old_path

            try:
                shutil.rmtree(version_path / 'share' / 'python-wheels',
                              ignore_errors=True)
                run(f"""set -e
                    source {version_path / 'bin' / 'activate'}
                    pip uninstall --yes pip
                    """,
                    executable='/bin/bash',
                    stderr=STDOUT,
                    shell=True)

            except Exception as e:
                debug('Could not remove unnecessary packages/files: %s', e)

        # Update current symlink
        if not self.current_path or self.current_path.resolve(
        ) != version_path:
            atomic_symlink = self.path / f'atomic_symlink_for_{self.name}'
            atomic_symlink.symlink_to(version_path)
            atomic_symlink.replace(self._current_symlink)

            # Remove older versions
            for path in [
                    p for p in self.path.iterdir() if p not in important_paths
            ]:
                shutil.rmtree(path, ignore_errors=True)

        current_scripts = self.scripts()

        if not (current_scripts or self.group_specs()):
            self.uninstall()
            raise exceptions.InvalidAction(
                'Odd, there are no scripts included in the app, so there is no point installing it.\n'
                '  autopip is for installing apps with scripts. To install libraries, please use pip.\n'
                '  If you are the app owner, make sure to setup entry_points in setup.py.\n'
                '  See http://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation'
            )

        self.settings(app_spec=str(app_spec), python_version=python_version)

        # Install cronjobs
        if 'update' not in sys.argv:
            pinning = '==' in str(app_spec) and not str(app_spec).endswith('*')
            if pinning and (self.settings().get('update') or update):
                info(
                    'Auto-update will be disabled since we are pinning to a specific version.'
                )
                info(
                    'To enable, re-run without pinning to specific version with --update option'
                )

                if self.settings().get('update'):
                    self.settings(update=None)
                    try:
                        crontab.remove(self._crontab_id)
                    except exceptions.MissingError as e:
                        debug('Could not remove crontab for %s: %s',
                              self._crontab_id, e)

            elif update:
                try:
                    autopip_path = shutil.which('autopip')
                    if not autopip_path:
                        raise exceptions.MissingError(
                            'autopip is not available. Please make sure its bin folder is in PATH env var'
                        )

                    # Migrate old crontabs
                    try:
                        old_crons = [
                            c for c in crontab.list().split('\n')
                            if c and 'autopip update' not in c
                        ]
                        if old_crons:
                            cron_re = re.compile('autopip install "(.+)"')
                            for cron in old_crons:
                                match = cron_re.search(cron)
                                if match:
                                    old_app_spec = next(
                                        iter(
                                            pkg_resources.parse_requirements(
                                                match.group(1))))
                                    old_app = App(old_app_spec.name,
                                                  self.paths)
                                    if old_app.is_installed:
                                        old_app.settings(
                                            app_spec=str(old_app_spec))
                            crontab.remove('autopip')

                    except Exception as e:
                        debug('Could not migrate old crontabs: %s', e)

                    crontab.add(
                        f'{autopip_path} update '
                        f'2>&1 >> {self.paths.log_root / "cron.log"}',
                        cmd_id='autopip update')
                    info(update.name.title() +
                         ' auto-update enabled via cron service')

                    self.settings(update=update.name.lower())

                except Exception as e:
                    error('! Auto-update was not enabled because: %s',
                          e,
                          exc_info=self.debug)

        # Install script symlinks
        prev_scripts = self.scripts(
            prev_version_path) if prev_version_path else set()
        old_scripts = prev_scripts - current_scripts

        printed_updating = False

        for script in sorted(current_scripts):
            script_symlink = self.paths.symlink_root / script
            script_path = self.current_path / 'bin' / script

            if script_symlink.resolve() == script_path.resolve():
                continue

            if not printed_updating:
                info('Updating script symlinks in {}'.format(
                    self.paths.symlink_root))
                printed_updating = True

            if script_symlink.exists():
                if self.paths.covers(script_symlink) or self.name == 'autopip':
                    atomic_symlink = self.paths.symlink_root / f'atomic_symlink_for_{self.name}'
                    atomic_symlink.symlink_to(script_path)
                    atomic_symlink.replace(script_symlink)
                    info('* {} (updated)'.format(script_symlink.name))

                else:
                    info('! {} (can not change / not managed by autopip)'.
                         format(script_symlink.name))

            else:
                script_symlink.symlink_to(script_path)
                info('+ ' + str(script_symlink.name))

        for script in sorted(old_scripts):
            script_symlink = self.paths.symlink_root / script
            if script_symlink.exists():
                script_symlink.unlink()
                info('- '.format(script_symlink.name))

        if not printed_updating and sys.stdout.isatty(
        ) and current_scripts and 'update' not in sys.argv:
            info('Scripts are in {}: {}'.format(
                self.paths.symlink_root, ', '.join(sorted(current_scripts))))

        # Remove pyc for non-root installs for all versions, not just current.
        if os.getuid():
            try:
                run(f'find {self.path} -name *.pyc | xargs rm',
                    executable='/bin/bash',
                    stderr=STDOUT,
                    shell=True)
            except Exception as e:
                debug('Could not remove *.pyc files: %s', e)

        return True
Beispiel #5
0
def test_remove(mock_run):
    crontab.remove('hello')
    mock_run.assert_called_with(
        '( crontab -l | grep -vi "hello" ) | crontab -', shell=True, stderr=-2)