def main(args=None): args = command_handle_args(args, definition) config = get_config() try: ic = InstallerContext(config=config) default_installer_name = ic.get_default_installer_name() results, errors = resolve(args.xylem_key, all_keys=args.all, config=config, installer_context=ic) if errors: error("\n".join(indent(exc_to_str(e), 2, exclude_first=True) for _, e in errors)) for key, (installer_name, resolutions) in results: if installer_name != default_installer_name or \ args.show_default_installer: installer_string = "{0}: ".format(installer_name) else: installer_string = "" resolution_string = ', '.join(map(to_str, resolutions)) info("{0} --> {1}{2}". format(ansi("cyanf") + key + ansi("reset"), ansi("bluef") + installer_string, ansi("yellowf") + resolution_string)) if errors: sys.exit(1) except (KeyboardInterrupt, EOFError): info('') sys.exit(1)
def main(sysargs=None): parser = argparse.ArgumentParser( description="xylem is a package manager abstraction tool.", add_help=False, formatter_class=ConfigHelpFormatter ) add_config_arguments(parser) add_global_arguments(parser) cmds = list_commands() create_subparsers(parser, cmds) args = parser.parse_args(sysargs) handle_global_arguments(args) handle_config_arguments(args) # FIXME: the following logic and error handling try: args.func result = args.func(args) except XylemError as e: if is_debug() or isinstance(e, XylemInternalError): print_exc(exc_to_str(e, tb=True, chain=True)) else: error(exc_to_str(e, tb=False, chain=True)) sys.exit(1) except (KeyboardInterrupt, EOFError): info('') sys.exit(1) sys.stdout.write(ansi('reset')) sys.exit(result or 0)
def check_package_manager_updated(self, os_tuple, fix_unsatisfied=False, interactive=True): info_v("Checking if package manager '{}' is updated.". format(self.name)) try: updated = self.is_package_manager_updated() except NotImplementedError: info_v("Check if package manager '{}' is updated not implemented; " "skipping check.".format(self.name)) return if updated: info_v("Package manager '{}' is updated.".format(self.name)) return info_v("Package manager '{}' not updated.".format(self.name)) if fix_unsatisfied: info_v("Trying to update '{}'.".format(self.name)) try: self.update_package_manager(interactive) if self.is_package_manager_updated(): return except NotImplementedError: error("Installer plugin '{}' does not support updating " "itself.".format(self.name)) else: error("Installer plugin '{}' failed to update itself.". format(self.name)) warning("Package manager '{}' seems to be out-of-date. Please " "consider updating for best results.")
def main(args=None): args = command_handle_args(args, definition) config = get_config() try: resolve_errors, install_errors = install( args.xylem_key, all_keys=args.all, config=config, reinstall=args.reinstall, simulate=args.dry_run, continue_on_error=args.continue_on_error, fix_prerequisites=args.fix_prerequisites) if resolve_errors: # error("The following errors occurred during resolution:") error("\n".join( indent(exc_to_str(e), 2, exclude_first=True) for _, e in resolve_errors)) if install_errors: # error("The following errors occurred during installation:") error("\n".join( indent(exc_to_str(e), 2, exclude_first=True) for e in install_errors)) if resolve_errors or install_errors: sys.exit(1) except (KeyboardInterrupt, EOFError): info('') sys.exit(1)
def parse_source_descriptions(data, file_path='<string>'): """Parse a YAML string as source descriptions. If parsing failes an error message is printed to console and an empty list is returned. :param str data: string containing YAML representation of source descriptions :param str file_path: name of the file whose contents ``data`` contains :returns: tuple of ``file_path`` and parsed source descriptions :rtype: `tuple(str, list)` """ try: descriptions = load_yaml(data) verify_source_description_list(descriptions) except yaml.YAMLError as exc: if hasattr(exc, 'problem_mark'): mark = exc.problem_mark.line col = exc.problem_mark.column error("Invalid YAML in source list file '{0}' at '{1}:{2}':\n" .format(file_path, mark + 1, col + 1) + to_str(exc)) else: error("Invalid YAML in source list file '{0}':\n" .format(file_path) + to_str(exc)) descriptions = [] return (file_path, descriptions)
def check_package_manager_installed(self, os_tuple, fix_unsatisfied=False, interactive=True): info_v("Checking if package manager '{}' is installed.". format(self.name)) if self.is_package_manager_installed(): info_v("Package manager '{}' is installed.".format(self.name)) return info_v("Package manager '{}' not installed ('{}' not found).". format(self.name, self.executable_name)) if fix_unsatisfied: info_v("Trying to install '{}'.".format(self.name)) try: self.install_package_manager(os_tuple, interactive) if self.is_package_manager_installed(): return except NotImplementedError: error("Installer plugin '{}' does not support installing " "itself.".format(self.name)) else: error("Installer plugin '{}' failed to install itself.". format(self.name)) raise InstallerPrerequisiteError("Package manager '{}' not installed". format(self.name))
def check_package_manager_updated(self, os_tuple, fix_unsatisfied=False, interactive=True): info_v("Checking if package manager '{}' is updated.".format( self.name)) try: updated = self.is_package_manager_updated() except NotImplementedError: info_v("Check if package manager '{}' is updated not implemented; " "skipping check.".format(self.name)) return if updated: info_v("Package manager '{}' is updated.".format(self.name)) return info_v("Package manager '{}' not updated.".format(self.name)) if fix_unsatisfied: info_v("Trying to update '{}'.".format(self.name)) try: self.update_package_manager(interactive) if self.is_package_manager_updated(): return except NotImplementedError: error("Installer plugin '{}' does not support updating " "itself.".format(self.name)) else: error("Installer plugin '{}' failed to update itself.".format( self.name)) warning("Package manager '{}' seems to be out-of-date. Please " "consider updating for best results.")
def check_package_manager_installed(self, os_tuple, fix_unsatisfied=False, interactive=True): info_v("Checking if package manager '{}' is installed.".format( self.name)) if self.is_package_manager_installed(): info_v("Package manager '{}' is installed.".format(self.name)) return info_v("Package manager '{}' not installed ('{}' not found).".format( self.name, self.executable_name)) if fix_unsatisfied: info_v("Trying to install '{}'.".format(self.name)) try: self.install_package_manager(os_tuple, interactive) if self.is_package_manager_installed(): return except NotImplementedError: error("Installer plugin '{}' does not support installing " "itself.".format(self.name)) else: error("Installer plugin '{}' failed to install itself.".format( self.name)) raise InstallerPrerequisiteError( "Package manager '{}' not installed".format(self.name))
def main(args=None): args = command_handle_args(args, definition) config = get_config() try: resolve_errors, install_errors = install( args.xylem_key, all_keys=args.all, config=config, reinstall=args.reinstall, simulate=args.dry_run, continue_on_error=args.continue_on_error, fix_prerequisites=args.fix_prerequisites) if resolve_errors: # error("The following errors occurred during resolution:") error("\n".join(indent(exc_to_str(e), 2, exclude_first=True) for _, e in resolve_errors)) if install_errors: # error("The following errors occurred during installation:") error("\n".join(indent(exc_to_str(e), 2, exclude_first=True) for e in install_errors)) if resolve_errors or install_errors: sys.exit(1) except (KeyboardInterrupt, EOFError): info('') sys.exit(1)
def install(xylem_keys, all_keys=False, interactive=True, reinstall=False, simulate=False, continue_on_error=False, fix_prerequisites=False, config=None, database=None, sources_context=None, installer_context=None): # 1. Prepare config and contexts and load database config = ensure_config(config) installer_context = ensure_installer_context(installer_context, config) # 2. Resolve keys results, resolve_errors = resolve(xylem_keys, all_keys=all_keys, config=config, database=database, sources_context=sources_context, installer_context=installer_context) if resolve_errors and not continue_on_error: return resolve_errors, [] resolved = _squash_resolutions(res_tuple for _, res_tuple in results) # 3. check general prerequisites for all installers check_general_prerequisites(_installer_names(resolved), installer_context, fix_unsatisfied=fix_prerequisites, interactive=interactive) # 4. Determine uninstalled resolutions if not reinstall: resolved, uninstalled_errors = filter_uninstalled( resolved, installer_context) # TODO: figure out what exactly to do with these errors... For # now print them here for err in uninstalled_errors: error(exc_to_str(uninstalled_errors)) if uninstalled_errors and not continue_on_error: return resolve_errors, [] # 5. check prerequisites all resolutions to be installed check_install_prerequisites(_map_resolutions(resolved), installer_context, fix_unsatisfied=fix_prerequisites, interactive=interactive) # 6. Install resolved items install_errors = install_resolved(resolved, installer_context, interactive=interactive, reinstall=reinstall, simulate=simulate, continue_on_error=continue_on_error) return resolve_errors, install_errors
def load_plugins(kind, base_class, group, disabled=[]): """Load plugins form entry points. Load the plugins of given ``kind`` from entry points ``group``, instantiating objects and ignoring duplicates. The entry points must be valid plugin definitions (see :func:`verify_plugin_definition`). The list of plugins is free of duplicates by plugin class name (not plugin name), whereas the list of ``disabled`` plugins refer to the plugin names instead. :param str kind: kind of plugin (e.g. "installer") :param base_class: (abstract) base class plugins (must implement PluginBase) :param group: entry point group to load plugins from :param disabled: list of plugins to ignore; these are plugin names, not plugin object names :type disabled: `list` of `str` :return: list of the loaded and instantiated plugin classes :rtype: `list` """ plugin_list = [] name_set = set() for entry_point in pkg_resources.iter_entry_points(group=group): definition = entry_point.load() try: verify_plugin_definition(definition, kind, base_class) except InvalidPluginError as e: # TODO: somehow decide when to reraise and when to display error error("Skipping {} plugin. Failed to load from '{}' entry point " "with name '{}':\n{}".format(kind, group, entry_point.name, e)) continue plugin_name = definition["plugin_name"] if plugin_name in disabled: info_v("Skipping disabled {} plugin '{}'.".format( kind, plugin_name)) continue plugin_class = definition[kind] plugin_obj = plugin_class() obj_name = plugin_obj.name try: plugin_class.verify_plugin(plugin_obj) except InvalidPluginError as e: # TODO: somehow decide when to reraise and when to display error error("Skipping {} plugin '{}'. Plugin is invalid:\n{}".format( kind, plugin_name, exc_to_str(e))) continue if obj_name in name_set: # TODO: somehow decide when to reraise and when to display error warning( "Ignoring {0} plugin '{1}' with duplicate name '{1}'".format( kind, definition['plugin_name'], obj_name)) continue info_v("Loaded {0} plugin '{1}' with {0} name '{2}'.".format( kind, plugin_name, obj_name)) name_set.add(obj_name) plugin_list.append(plugin_obj) return plugin_list
def load_plugins(kind, base_class, group, disabled=[]): """Load plugins form entry points. Load the plugins of given ``kind`` from entry points ``group``, instantiating objects and ignoring duplicates. The entry points must be valid plugin definitions (see :func:`verify_plugin_definition`). The list of plugins is free of duplicates by plugin class name (not plugin name), whereas the list of ``disabled`` plugins refer to the plugin names instead. :param str kind: kind of plugin (e.g. "installer") :param base_class: (abstract) base class plugins (must implement PluginBase) :param group: entry point group to load plugins from :param disabled: list of plugins to ignore; these are plugin names, not plugin object names :type disabled: `list` of `str` :return: list of the loaded and instantiated plugin classes :rtype: `list` """ plugin_list = [] name_set = set() for entry_point in pkg_resources.iter_entry_points(group=group): definition = entry_point.load() try: verify_plugin_definition(definition, kind, base_class) except InvalidPluginError as e: # TODO: somehow decide when to reraise and when to display error error("Skipping {} plugin. Failed to load from '{}' entry point " "with name '{}':\n{}".format( kind, group, entry_point.name, e)) continue plugin_name = definition["plugin_name"] if plugin_name in disabled: info_v("Skipping disabled {} plugin '{}'.". format(kind, plugin_name)) continue plugin_class = definition[kind] plugin_obj = plugin_class() obj_name = plugin_obj.name try: plugin_class.verify_plugin(plugin_obj) except InvalidPluginError as e: # TODO: somehow decide when to reraise and when to display error error("Skipping {} plugin '{}'. Plugin is invalid:\n{}".format( kind, plugin_name, exc_to_str(e))) continue if obj_name in name_set: # TODO: somehow decide when to reraise and when to display error warning("Ignoring {0} plugin '{1}' with duplicate name '{1}'". format(kind, definition['plugin_name'], obj_name)) continue info_v("Loaded {0} plugin '{1}' with {0} name '{2}'.". format(kind, plugin_name, obj_name)) name_set.add(obj_name) plugin_list.append(plugin_obj) return plugin_list
def load_command_definition(command_name): for entry_point in pkg_resources.iter_entry_points(group=XYLEM_CMDS_GROUP): if entry_point.name == command_name: defi = entry_point.load() if not isinstance(defi, dict): error("Invalid entry point: '{0}', expected dict got '{1}'" .format(entry_point, type_name(defi))) return None return defi
def save_to_cache(self): for source in self.sources: try: source.save_to_cache() except Exception as e: # TODO: be more specific about which exceptions to catch here if self.raise_on_error: raise else: error("Failed to save source '{0}' to cache:\n{1}".format( source.unique_id(), e))
def save_to_cache(self): for source in self.sources: try: source.save_to_cache() except Exception as e: # TODO: be more specific about which exceptions to catch here if self.raise_on_error: raise else: error("Failed to save source '{0}' to cache:\n{1}". format(source.unique_id(), e))
def compact_os_dict(os_dict, default_installers): for os_name, version_dict in os_dict.items(): try: if os_name == 'any_os': os_dict['any_os'] = compact_installer_dict( version_dict['any_version'], None) else: os_dict[os_name] = compact_version_dict( version_dict, default_installers.get(os_name, None)) except Exception as e: error("Failed to expand version dict for os {0} with error: " "{1}\n{2}".format(os_name, to_str(e), version_dict)) raise return os_dict
def setup_installers(self): """For current os, setup configured installers. Installers are set based on the current os, user config and installer plugins. """ os = self.get_os() os_tuple = os.get_tuple() _, os_version = os_tuple self.core_installers = [] self.additional_installers = [] # 1. Go through all installers and set options from config for inst in self.installer_plugins: inst.options = self.config.installer_options.get(inst.name, {}) # 2. setup core installers from config or OS plugin if self.config.core_installers is not None: core_installer_names = self.config.core_installers info_v("setting up core installers from config: '{}'".format( ", ".join(core_installer_names))) else: core_installer_names = os.get_core_installers( os_version, os.options) info_v("setting up core installers from os plugin: '{}'".format( ", ".join(core_installer_names))) for name in core_installer_names: inst = self.lookup_installer(name) if inst is None: error("ignoring core installer '{}'; according plugin was not " "found".format(name)) else: self.core_installers.append(inst) # 3. Go through all installers and check if they should be used # as additional installers for the current OS. if self.config.use_additional_installers: for inst in self.installer_plugins: if inst not in self.core_installers and \ inst.use_as_additional_installer(os_tuple): self.additional_installers.append(inst) info_v("Using additional installers: '{}'".format(", ".join( [i.name for i in self.additional_installers])))
def setup_installers(self): """For current os, setup configured installers. Installers are set based on the current os, user config and installer plugins. """ os = self.get_os() os_tuple = os.get_tuple() _, os_version = os_tuple self.core_installers = [] self.additional_installers = [] # 1. Go through all installers and set options from config for inst in self.installer_plugins: inst.options = self.config.installer_options.get(inst.name, {}) # 2. setup core installers from config or OS plugin if self.config.core_installers is not None: core_installer_names = self.config.core_installers info_v("setting up core installers from config: '{}'". format(", ".join(core_installer_names))) else: core_installer_names = os.get_core_installers(os_version, os.options) info_v("setting up core installers from os plugin: '{}'". format(", ".join(core_installer_names))) for name in core_installer_names: inst = self.lookup_installer(name) if inst is None: error("ignoring core installer '{}'; according plugin was not " "found".format(name)) else: self.core_installers.append(inst) # 3. Go through all installers and check if they should be used # as additional installers for the current OS. if self.config.use_additional_installers: for inst in self.installer_plugins: if inst not in self.core_installers and \ inst.use_as_additional_installer(os_tuple): self.additional_installers.append(inst) info_v("Using additional installers: '{}'".format( ", ".join([i.name for i in self.additional_installers])))
def load_from_source(self): origins = set() for source in self.sources: if source.origin not in origins: origins.add(source.origin) if self.print_info: info("Processing '{0}'...".format(source.origin)) if self.print_info: info("Loading: {0} : {1}". format(source.spec.name, source.arguments)) try: source.load_from_source() except Exception as e: # TODO: be more specific about which exceptions to catch here if self.raise_on_error: raise else: error("Failed to load source '{0}':\n{1}". format(source.unique_id(), e))
def load_from_source(self): origins = set() for source in self.sources: if source.origin not in origins: origins.add(source.origin) if self.print_info: info("Processing '{0}'...".format(source.origin)) if self.print_info: info("Loading: {0} : {1}".format(source.spec.name, source.arguments)) try: source.load_from_source() except Exception as e: # TODO: be more specific about which exceptions to catch here if self.raise_on_error: raise else: error("Failed to load source '{0}':\n{1}".format( source.unique_id(), e))
def create_subparsers(parser, cmds): defis = [] for cmd in list(cmds): defi = load_command_definition(cmd) if defi is None: error("skipping invalid command '{0}'".format(cmd)) del cmds[cmds.index(cmd)] continue defis.append(defi) if not defis or not cmds: return public_cmds = [c for c in cmds if not c.startswith("_")] metavar = '[' + ' | '.join(public_cmds) + ']' subparser = parser.add_subparsers( title='commands', metavar=metavar, description="""Call `xylem <command> -h` for help on a specific command.""", dest='cmd' ) for defi in defis: create_command_parser(defi, subparser.add_parser, parser_title=True)
def update(self): # TODO: save exceptions if they are not raised and then for the # cli command recognize permission errors and suggest to use # 'sudo' # We don't just call `load_from_source` and `save_to_cache` here # since we want errors with saving for each source to happen # directly after loading, not at the end. origins = set() for source in self.sources: if source.origin not in origins: origins.add(source.origin) if self.print_info: info("Processing '{0}'...".format(source.origin)) if self.print_info: info("Loading: {0} : {1}". format(source.spec.name, source.arguments)) try: source.load_from_source() except Exception as e: # TODO: be more specific about which exceptions to catch here if self.raise_on_error: raise else: error("Failed to load source '{0}':\n{1}". format(source.unique_id(), e)) else: try: source.save_to_cache() except Exception as e: # TODO: be more specific about which exceptions to catch if self.raise_on_error: raise else: error("Failed to save source '{0}' to cache:\n{1}". format(source.unique_id(), to_str(e)))
def update(self): # TODO: save exceptions if they are not raised and then for the # cli command recognize permission errors and suggest to use # 'sudo' # We don't just call `load_from_source` and `save_to_cache` here # since we want errors with saving for each source to happen # directly after loading, not at the end. origins = set() for source in self.sources: if source.origin not in origins: origins.add(source.origin) if self.print_info: info("Processing '{0}'...".format(source.origin)) if self.print_info: info("Loading: {0} : {1}".format(source.spec.name, source.arguments)) try: source.load_from_source() except Exception as e: # TODO: be more specific about which exceptions to catch here if self.raise_on_error: raise else: error("Failed to load source '{0}':\n{1}".format( source.unique_id(), e)) else: try: source.save_to_cache() except Exception as e: # TODO: be more specific about which exceptions to catch if self.raise_on_error: raise else: error("Failed to save source '{0}' to cache:\n{1}". format(source.unique_id(), to_str(e)))
def resolve(xylem_keys, all_keys=False, config=None, database=None, sources_context=None, installer_context=None): """TODO""" # 1. Prepare config and contexts and load database config = ensure_config(config) ic = ensure_installer_context(installer_context, config) del installer_context # don't use further down, use `ic` only if not database: sources_context = ensure_sources_context(sources_context, config) database = RulesDatabase(sources_context) database.load_from_cache() del sources_context # don't use further down, use `database` only # 2. Prepare set of keys to look up if all_keys: lookup_keys = remove_duplicates(xylem_keys + sorted(database.keys(ic))) else: lookup_keys = remove_duplicates(xylem_keys) result = [] errors = [] # 3. Create an inverse install-from mapping # TODO: maybe allow pattern matching here like # `install-from "pip: python-*"` install_from_map = dict() for inst, keys in six.iteritems(config.install_from): for k in keys: if k in install_from_map: error("ignoring 'install from {}' for key '{}'; " "already configured to install from '{}'". format(inst, k, install_from_map[k])) else: install_from_map[k] = inst # 4. Resolve each key for key in lookup_keys: # 4.1. Lookup key in the database try: installer_dict = database.lookup(key, ic) if not installer_dict: errors.append((key, ResolutionError( "could not find rule for xylem key '{}' on '{}'.". format(key, ic.get_os_string())))) continue except LookupError as e: errors.append((key, chain_exception( ResolutionError, "lookup for key '{}' failed".format(key), e))) continue # 4.2. Decide which installer to use if key in install_from_map: inst_name = install_from_map[key] if not ic.lookup_installer(inst_name): errors.append((key, ResolutionError( "explicitly requested to install '{}' from '{}', but that " "installer is not loaded".format(key, inst_name)))) continue if inst_name not in installer_dict: errors.append((key, ResolutionError( "explicitly requested to install '{}' from '{}', but no " "rule for that installer was found; found rules for " "installers: `{}`". format(key, inst_name, to_str(installer_dict.keys()))))) continue info_v("found rule for key '{}' for explicitly requested " "installer '{}'".format(key, inst_name)) rule = installer_dict[inst_name] installer = ic.lookup_installer(inst_name) else: installer = None for inst in ic.core_installers: if inst.name in installer_dict: info_v("found rule for key '{}' for core installer '{}'". format(key, inst.name)) rule = installer_dict[inst.name] installer = inst break if installer is None: for inst in ic.additional_installers: if inst.name in installer_dict: info_v("found rule for key '{}' for additional " "installer '{}'".format(key, inst.name)) rule = installer_dict[inst.name] installer = inst if installer is None: errors.append((key, ResolutionError( "did not find rule for key '{}' for neither core " "installers '{}' nor additional installers '{}'; rules " "found for installers: '{}'". format(key, to_str(ic.core_installer_names), to_str(ic.additional_installer_names), to_str(installer_dict.keys()))))) continue # 4.3. Resolve with determined installer try: resolutions = installer.resolve(rule) except InstallerError as e: errors.append((key, chain_exception( ResolutionError, "failed to resolve with installer '{}'".format(installer.name), e))) else: result.append((key, (installer.name, resolutions))) return result, errors