Beispiel #1
0
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)
Beispiel #2
0
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.")
Beispiel #4
0
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)
Beispiel #5
0
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))
Beispiel #9
0
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)
Beispiel #10
0
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
Beispiel #11
0
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
Beispiel #12
0
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
Beispiel #13
0
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
Beispiel #14
0
 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))
Beispiel #15
0
 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))
Beispiel #16
0
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
Beispiel #17
0
    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])))
Beispiel #18
0
    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])))
Beispiel #19
0
 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))
Beispiel #20
0
 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))
Beispiel #21
0
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)
Beispiel #22
0
    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)))
Beispiel #23
0
    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)))
Beispiel #24
0
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