/* solver.c - Alpine Package Keeper (APK) * A backtracking, forward checking dependency graph solver. * * Copyright (C) 2011 Timo Teräs * All rights reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. See http://www.gnu.org/ for details. */ /* FIXME: install-if not supported yet */ #include #include "apk_defines.h" #include "apk_database.h" #include "apk_package.h" #include "apk_solver.h" #if 0 #include #define dbg_printf(args...) fprintf(stderr, args) #else #define dbg_printf(args...) #endif #define APK_PKGSTF_NOINSTALL 0 #define APK_PKGSTF_INSTALL 1 #define APK_PKGSTF_BRANCH 2 #define APK_PKGSTF_ALT_BRANCH 4 struct apk_package_state { struct apk_package *backtrack; unsigned short flags; unsigned short conflicts; }; struct apk_name_state { struct list_head unsolved_list; struct apk_package *chosen; unsigned short requirers; }; struct apk_solver_state { struct apk_database *db; struct apk_package_state *pkg_state; struct apk_name_state *name_state; struct list_head unsolved_list_head; struct apk_package *latest_decision; unsigned int topology_position; unsigned int assigned_names; struct apk_package_array *best_solution; unsigned int best_cost; }; static int apply_constraint(struct apk_solver_state *ss, struct apk_dependency *dep); static int undo_constraint(struct apk_solver_state *ss, struct apk_dependency *dep); static int push_decision(struct apk_solver_state *ss, struct apk_package *pkg, int flags); static inline int pkg_available(struct apk_database *db, struct apk_package *pkg) { if (pkg->ipkg != NULL) return TRUE; if (pkg->installed_size == 0) return TRUE; if (pkg->filename != NULL) return TRUE; if (apk_db_select_repo(db, pkg) != NULL) return TRUE; return FALSE; } static int foreach_dependency(struct apk_solver_state *ss, struct apk_dependency_array *deps, int (*func)(struct apk_solver_state *ss, struct apk_dependency *dep)) { int i, r = 0; for (i = 0; i < deps->num; i++) r += func(ss, &deps->item[i]); return r; } static int inline can_consider_package(struct apk_solver_state *ss, struct apk_package *pkg) { struct apk_package_state *ps = &ss->pkg_state[pkg->topology_sort]; if (pkg->topology_sort >= ss->topology_position) return FALSE; if (ps->conflicts) return FALSE; return TRUE; } static int is_pkg_preferred(struct apk_solver_state *ss, struct apk_package *pkg) { struct apk_name *name = pkg->name; int i; if (!(apk_flags & APK_UPGRADE)) { /* not upgrading, prefer the installed package; unless we * need additional availability checks */ if (pkg->ipkg != NULL) { if (pkg->repos != 0 || !(apk_flags & APK_PREFER_AVAILABLE)) return TRUE; } } /* check if the suggested package is the most preferred one of * available packages for the name */ for (i = 0; i < name->pkgs->num; i++) { struct apk_package *pkg0 = name->pkgs->item[i]; if (pkg0 == pkg || !can_consider_package(ss, pkg0)) continue; if (apk_flags & APK_PREFER_AVAILABLE) { /* pkg available, pkg0 not */ if (pkg->repos != 0 && pkg0->repos == 0) continue; /* pkg0 available, pkg not */ if (pkg0->repos != 0 && pkg->repos == 0) return FALSE; } if (!(apk_flags & APK_UPGRADE)) { /* not upgrading, prefer the installed package */ if (pkg0->ipkg != NULL) return FALSE; } /* upgrading, or neither of the package is installed, so * we just fall back comparing to versions */ if (apk_pkg_version_compare(pkg0, pkg) == APK_VERSION_GREATER) return FALSE; } /* no package greater than the selected */ return TRUE; } static int update_name_state(struct apk_solver_state *ss, struct apk_name *name, struct apk_name_state *ns) { struct apk_package *pkg_best = NULL; int i, options = 0; for (i = 0; i < name->pkgs->num; i++) { struct apk_package *pkg0 = name->pkgs->item[i]; if (!can_consider_package(ss, pkg0)) continue; options++; if (pkg_best == NULL || pkg0->topology_sort > pkg_best->topology_sort) pkg_best = pkg0; } ns->chosen = pkg_best; dbg_printf("%s: adjusted preference %d -> %d (options left %d)\n", name->name, ss->topology_position, ns->chosen->topology_sort, options); return options; } static int apply_decision(struct apk_solver_state *ss, struct apk_package *pkg, struct apk_package_state *ps) { struct apk_name_state *ns = &ss->name_state[pkg->name->id]; dbg_printf("apply_decision: " PKG_VER_FMT " %s\n", PKG_VER_PRINTF(pkg), (ps->flags & APK_PKGSTF_INSTALL) ? "INSTALL" : "NO_INSTALL"); if (ps->flags & APK_PKGSTF_INSTALL) { ss->assigned_names++; ns->chosen = pkg; if (list_hashed(&ns->unsolved_list)) { list_del(&ns->unsolved_list); list_init(&ns->unsolved_list); dbg_printf("%s: deleting from unsolved list\n", pkg->name->name); } return foreach_dependency(ss, pkg->depends, apply_constraint); } else { if (!list_hashed(&ns->unsolved_list)) { ns->chosen = NULL; return 0; } if (update_name_state(ss, pkg->name, ns) != 1) return 0; /* the name is required and we are left with only one candidate * after deciding to not install pkg; autoselect the last option */ return push_decision(ss, ns->chosen, APK_PKGSTF_INSTALL); } } static void undo_decision(struct apk_solver_state *ss, struct apk_package *pkg, struct apk_package_state *ps) { struct apk_name_state *ns = &ss->name_state[pkg->name->id]; dbg_printf("undo_decision: " PKG_VER_FMT " %s\n", PKG_VER_PRINTF(pkg), (ps->flags & APK_PKGSTF_INSTALL) ? "INSTALL" : "NO_INSTALL"); if (ps->flags & APK_PKGSTF_INSTALL) { ss->assigned_names--; foreach_dependency(ss, pkg->depends, undo_constraint); if (ns->requirers) { list_add(&ns->unsolved_list, &ss->unsolved_list_head); dbg_printf("%s: adding back to unsolved list (requirers: %d)\n", pkg->name->name, ns->requirers); } else { ns->chosen = NULL; } } } static int push_decision(struct apk_solver_state *ss, struct apk_package *pkg, int flags) { struct apk_package_state *ps = &ss->pkg_state[pkg->topology_sort]; ps->backtrack = ss->latest_decision; ps->flags = flags; ss->latest_decision = pkg; if (flags & APK_PKGSTF_BRANCH) { ss->topology_position = pkg->topology_sort; dbg_printf("push_decision: adding new BRANCH at topology_position %d\n", ss->topology_position); } else ps->flags |= APK_PKGSTF_ALT_BRANCH; return apply_decision(ss, pkg, ps); } static int next_branch(struct apk_solver_state *ss) { struct apk_package *pkg; struct apk_package_state *ps; int r; while (1) { pkg = ss->latest_decision; ps = &ss->pkg_state[pkg->topology_sort]; undo_decision(ss, pkg, ps); if (ps->flags & APK_PKGSTF_ALT_BRANCH) { pkg = ps->backtrack; ss->latest_decision = pkg; if (pkg == NULL) /* at root, can't back track */ return 1; ss->topology_position = pkg->topology_sort; dbg_printf("next_branch: undo decision at topology_position %d\n", ss->topology_position); } else { dbg_printf("next_branch: swapping BRANCH at topology_position %d\n", ss->topology_position); ps->flags |= APK_PKGSTF_ALT_BRANCH; ps->flags ^= APK_PKGSTF_INSTALL; r = apply_decision(ss, pkg, ps); if (r == 0 /*|| report_errors */) return r; } } } static int apply_constraint(struct apk_solver_state *ss, struct apk_dependency *dep) { struct apk_name *name = dep->name; struct apk_name_state *ns = &ss->name_state[name->id]; struct apk_package *pkg_best = NULL; int i, options = 0; for (i = 0; i < name->pkgs->num; i++) { struct apk_package *pkg0 = name->pkgs->item[i]; struct apk_package_state *ps0 = &ss->pkg_state[pkg0->topology_sort]; if (pkg0->topology_sort >= ss->topology_position) continue; if (!apk_dep_is_satisfied(dep, pkg0)) { ps0->conflicts++; dbg_printf(PKG_VER_FMT ": conflicts++ -> %d\n", PKG_VER_PRINTF(pkg0), ps0->conflicts); } if (ps0->conflicts == 0) { options++; if (pkg_best == NULL || pkg0->topology_sort > pkg_best->topology_sort) pkg_best = pkg0; } } ns->requirers++; if (!list_hashed(&ns->unsolved_list) && ns->chosen != NULL) { dbg_printf(PKG_VER_FMT " selected already for %s\n", PKG_VER_PRINTF(ns->chosen), dep->name->name); return !apk_dep_is_satisfied(dep, ns->chosen); } ns->chosen = pkg_best; if (options == 0) { /* we conflicted with all possible options */ if (list_hashed(&ns->unsolved_list)) { dbg_printf("%s: deleting unsolved (unable to satisfy)\n", name->name); list_del(&ns->unsolved_list); list_init(&ns->unsolved_list); } return 1; } if (options == 1) { /* we can short circuit to select the only option * possible */ return push_decision(ss, pkg_best, APK_PKGSTF_INSTALL); } /* multiple ways to satisfy the requirement */ if (ns->requirers == 1) { list_init(&ns->unsolved_list); list_add(&ns->unsolved_list, &ss->unsolved_list_head); dbg_printf("%s: adding to unsolved list (%d options)\n", name->name, options); } return 0; } static int undo_constraint(struct apk_solver_state *ss, struct apk_dependency *dep) { struct apk_name *name = dep->name; struct apk_name_state *ns = &ss->name_state[name->id]; struct apk_package *pkg_best = NULL; int i, had_options = 0, options = 0; for (i = 0; i < name->pkgs->num; i++) { struct apk_package *pkg0 = name->pkgs->item[i]; struct apk_package_state *ps0 = &ss->pkg_state[pkg0->topology_sort]; if (pkg0->topology_sort >= ss->topology_position) continue; if (ps0->conflicts == 0) had_options++; if (!apk_dep_is_satisfied(dep, pkg0)) { ps0->conflicts--; dbg_printf(PKG_VER_FMT ": conflicts-- -> %d\n", PKG_VER_PRINTF(pkg0), ps0->conflicts); } if (ps0->conflicts == 0) { options++; if (pkg_best == NULL || pkg0->topology_sort > pkg_best->topology_sort) pkg_best = pkg0; } } ns->requirers--; if (ns->requirers == 0) { if (list_hashed(&ns->unsolved_list)) { list_del(&ns->unsolved_list); list_init(&ns->unsolved_list); ns->chosen = NULL; } } else { ns->chosen = pkg_best; if (had_options == 0 && options != 0) { if (!list_hashed(&ns->unsolved_list)) { list_add(&ns->unsolved_list, &ss->unsolved_list_head); dbg_printf("%s: adding back to unsolved list (with %d options, %d requirers)\n", name->name, options, ns->requirers); } else { ns->chosen = NULL; } return 0; } } return 0; } static int expand_branch(struct apk_solver_state *ss) { int r; while (1) { struct apk_name_state *ns; struct apk_package *pkg0 = NULL; /* FIXME: change unsolved_list to a priority queue */ list_for_each_entry(ns, &ss->unsolved_list_head, unsolved_list) { if (pkg0 == NULL || ns->chosen->topology_sort > pkg0->topology_sort) pkg0 = ns->chosen; } if (pkg0 == NULL) { dbg_printf("expand_branch: list is empty\n"); return 0; } /* someone needs to provide this name -- find next eligible * provider candidate */ ns = &ss->name_state[pkg0->name->id]; dbg_printf("expand_branch: %s %d\n", pkg0->name->name, pkg0->topology_sort); r = push_decision(ss, pkg0, is_pkg_preferred(ss, pkg0) ? (APK_PKGSTF_INSTALL | APK_PKGSTF_BRANCH) : (APK_PKGSTF_NOINSTALL | APK_PKGSTF_BRANCH)); if (/*no_error_reporting &&*/ r) return r; } return 0; } static void record_solution(struct apk_solver_state *ss) { struct apk_package *pkg; struct apk_package_state *ps; int i; apk_package_array_resize(&ss->best_solution, ss->assigned_names); i = 0; pkg = ss->latest_decision; while (pkg != NULL) { ps = &ss->pkg_state[pkg->topology_sort]; if (ps->flags & APK_PKGSTF_INSTALL) ss->best_solution->item[i++] = pkg; dbg_printf("record_solution: " PKG_VER_FMT ": %sINSTALL\n", PKG_VER_PRINTF(pkg), (ps->flags & APK_PKGSTF_INSTALL) ? "" : "NO_"); pkg = ps->backtrack; } } int apk_solver_solve(struct apk_database *db, struct apk_dependency_array *world, struct apk_package_array **solution) { struct apk_solver_state *ss; int r; ss = calloc(1, sizeof(struct apk_solver_state)); ss->db = db; ss->topology_position = -1; list_init(&ss->unsolved_list_head); ss->pkg_state = calloc(db->available.packages.num_items+1, sizeof(struct apk_package_state)); ss->name_state = calloc(db->available.names.num_items+1, sizeof(struct apk_name_state)); r = foreach_dependency(ss, world, apply_constraint); while (r == 0) { if (expand_branch(ss) == 0) { /* found solution - it is optimal because we permutate * each preferred local option first, and permutations * happen in topologally sorted order. */ break; } /* conflicting constraints -- backtrack */ r = next_branch(ss); } /* collect packages */ if (r == 0) { record_solution(ss); *solution = ss->best_solution; } free(ss->name_state); free(ss->pkg_state); free(ss); return r; } static int compare_change(const void *p1, const void *p2) { const struct apk_change *c1 = (const struct apk_change *) p1; const struct apk_change *c2 = (const struct apk_change *) p2; if (c1->newpkg == NULL) { if (c2->newpkg == NULL) /* both deleted - reverse topology order */ return c1->oldpkg->topology_sort - c2->oldpkg->topology_sort; /* c1 deleted, c2 installed -> c2 first*/ return -1; } if (c2->newpkg == NULL) /* c1 installed, c2 deleted -> c1 first*/ return 1; return c2->newpkg->topology_sort - c1->newpkg->topology_sort; } int apk_solver_generate_changeset(struct apk_database *db, struct apk_package_array *solution, struct apk_changeset *changeset) { struct apk_name *name; struct apk_package *pkg, *pkg0; struct apk_installed_package *ipkg; int num_installs = 0, num_removed = 0, ci = 0; int i, j; /* calculate change set size */ for (i = 0; i < solution->num; i++) { pkg = solution->item[i]; name = pkg->name; if (pkg->ipkg == NULL || (name->flags & APK_NAME_REINSTALL)) num_installs++; name->flags |= APK_NAME_VISITED; } list_for_each_entry(ipkg, &db->installed.packages, installed_pkgs_list) { if (!(ipkg->pkg->name->flags & APK_NAME_VISITED)) num_removed++; } /* construct changeset */ apk_change_array_resize(&changeset->changes, num_installs + num_removed); list_for_each_entry(ipkg, &db->installed.packages, installed_pkgs_list) { if (ipkg->pkg->name->flags & APK_NAME_VISITED) continue; changeset->changes->item[ci].oldpkg = ipkg->pkg; ci++; } for (i = 0; i < solution->num; i++) { pkg = solution->item[i]; name = pkg->name; if (pkg->ipkg == NULL || (name->flags & APK_NAME_REINSTALL)) { for (j = 0; j < name->pkgs->num; j++) { pkg0 = name->pkgs->item[j]; if (pkg0->ipkg == NULL) continue; changeset->changes->item[ci].oldpkg = pkg0; break; } changeset->changes->item[ci].newpkg = pkg; ci++; } name->flags &= ~APK_NAME_VISITED; } /* sort changeset to topology order */ qsort(changeset->changes->item, changeset->changes->num, sizeof(struct apk_change), compare_change); return 0; }