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)
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)
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.')
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
def test_remove(mock_run): crontab.remove('hello') mock_run.assert_called_with( '( crontab -l | grep -vi "hello" ) | crontab -', shell=True, stderr=-2)