From 184346129a4fd5976a4645c4007975b3eebeca13 Mon Sep 17 00:00:00 2001 From: Ed Smith Date: Tue, 16 Jan 2024 16:36:56 +0000 Subject: [PATCH] Add an alternative dependency resolution algorithm The existing dependency resolution, as implemented in resolvedep(), prioritises name matches for a dependency over packages that only provide that dependency. We would like the option to prioritise database ordering above a precise name match. To do this, we add a DependencyStrategy configuration parameter. The default is to behave as pacman currently does, with DependencyStrategy=Default. However, we also add a Strict strategy (to strictly honour the priorities of repositories) which matches our desired behaviour in some circumstances. For the sake of clarity, the default strategy will: - take the package with the exact name given from the highest priority repository, ignoring all other packages. - if this doesn't exist, take an installed package providing this name, ignoring all other packages. - otherwise, it will give a list of packages that provide this name, in repository priority order, accumulated from every repository. In contrast, the strict strategy will: - take an installed package providing the dependency name, ignoring all other packages. - otherwise, give a list of all packages that either have this name, or provide this name, accumulated from every repository. The ordering is by repository priority order, with packages having the exact name being first among the packages from their repository. When does this difference matter? Let's suppose we have two repositories, - customizations - base Let's say we have a list of instructions that involve installing the name foo, which is provided by customizations/foo-customized. One day, base/foo comes into existence. Under the default strategy, our instructions now no longer install customizations/foo-customized, meaning our instructions no longer install our customized version of foo. Under the strict strategy, our instructions will now either prompt for the correct package to install, or because the default will be correct, will simply install the correct package with --noconfirm. Signed-off-by: Ed Smith --- etc/pacman.conf.in | 1 + lib/libalpm/alpm.h | 45 ++++++++++- lib/libalpm/deps.c | 181 ++++++++++++++++++++++++++++++++++++++++++-- lib/libalpm/deps.h | 4 +- lib/libalpm/sync.c | 4 +- lib/libalpm/sync.h | 2 +- lib/libalpm/trans.c | 6 +- src/pacman/conf.c | 12 +++ src/pacman/conf.h | 1 + src/pacman/remove.c | 2 +- src/pacman/sync.c | 5 +- 11 files changed, 244 insertions(+), 19 deletions(-) diff --git a/etc/pacman.conf.in b/etc/pacman.conf.in index d1ab736e..78710275 100644 --- a/etc/pacman.conf.in +++ b/etc/pacman.conf.in @@ -24,6 +24,7 @@ Architecture = auto # Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup #IgnorePkg = #IgnoreGroup = +#DependencyStrategy = Default #NoUpgrade = #NoExtract = diff --git a/lib/libalpm/alpm.h b/lib/libalpm/alpm.h index e532f3de..8330926e 100644 --- a/lib/libalpm/alpm.h +++ b/lib/libalpm/alpm.h @@ -570,6 +570,22 @@ typedef enum _alpm_fileconflicttype_t { ALPM_FILECONFLICT_FILESYSTEM } alpm_fileconflicttype_t; +/** + * Dependency strategy type. + * The strategy changes the details of how dependency names are + * matched up against existing packages when requested. + */ +typedef enum _alpm_depstrategy_t { + /** Prefer packages with a name matching a dependency over + packages that provide that name, regardless of database + order. */ + ALPM_DEPSTRATEGY_DEFAULT = 1, + /** Prefer packages in higher priority databases over packages + in lower priority databases, regardless of whether their + names match the dependency or they just provide it. */ + ALPM_DEPSTRATEGY_STRICT +} alpm_depstrategy_t; + /** The basic dependency type. * * This type is used throughout libalpm, not just for dependencies @@ -644,7 +660,7 @@ alpm_list_t *alpm_checkdeps(alpm_handle_t *handle, alpm_list_t *pkglist, */ alpm_pkg_t *alpm_find_satisfier(alpm_list_t *pkgs, const char *depstring); -/** Find a package satisfying a specified dependency. +/** Find a package satisfying a specified dependency with the default strategy. * First look for a literal, going through each db one by one. Then look for * providers. The first satisfyer that belongs to an installed package is * returned. If no providers belong to an installed package then an @@ -659,6 +675,22 @@ alpm_pkg_t *alpm_find_satisfier(alpm_list_t *pkgs, const char *depstring); alpm_pkg_t *alpm_find_dbs_satisfier(alpm_handle_t *handle, alpm_list_t *dbs, const char *depstring); +/** Find a package satisfying a specified dependency. + * First look for a literal, going through each db one by one. Then look for + * providers. The first satisfyer that belongs to an installed package is + * returned. If no providers belong to an installed package then an + * alpm_question_select_provider_t is created to select the provider. + * The dependency can include versions with depmod operators. + * + * @param handle the context handle + * @param dbs an alpm_list_t* of alpm_db_t where the satisfyer will be searched + * @param depstrategy the dependency strategy to use + * @param depstring package or provision name, versioned or not + * @return a alpm_pkg_t* satisfying depstring + */ +alpm_pkg_t *alpm_find_dbs_satisfier_ex(alpm_handle_t *handle, + alpm_list_t *dbs, alpm_depstrategy_t depstrategy, const char *depstring); + /** Check the package conflicts in a database * * @param handle the context handle @@ -2811,7 +2843,7 @@ alpm_list_t *alpm_trans_get_remove(alpm_handle_t *handle); */ int alpm_trans_init(alpm_handle_t *handle, int flags); -/** Prepare a transaction. +/** Prepare a transaction with the default dependency strategy. * @param handle the context handle * @param data the address of an alpm_list where a list * of alpm_depmissing_t objects is dumped (conflicting packages) @@ -2819,6 +2851,15 @@ int alpm_trans_init(alpm_handle_t *handle, int flags); */ int alpm_trans_prepare(alpm_handle_t *handle, alpm_list_t **data); +/** Prepare a transaction specifying the dependency strategy. + * @param handle the context handle + * @param data the address of an alpm_list where a list + * of alpm_depmissing_t objects is dumped (conflicting packages) + * @param depstrategy the dependency resolution strategy to use. + * @return 0 on success, -1 on error (pm_errno is set accordingly) + */ +int alpm_trans_prepare_ex(alpm_handle_t *handle, alpm_depstrategy_t depstrategy, alpm_list_t **data); + /** Commit a transaction. * @param handle the context handle * @param data the address of an alpm_list where detailed description diff --git a/lib/libalpm/deps.c b/lib/libalpm/deps.c index 041e8988..7d3cc868 100644 --- a/lib/libalpm/deps.c +++ b/lib/libalpm/deps.c @@ -622,7 +622,9 @@ int _alpm_recursedeps(alpm_db_t *db, alpm_list_t **targs, int include_explicit) } /** - * helper function for resolvedeps: search for dep satisfier in dbs + * helper function for resolvedep: search for dep satisfier in dbs + * this version applies the "default" logic that puts name matching + * ahead of db ordering. * * @param handle the context handle * @param dep is the dependency to search for @@ -633,7 +635,7 @@ int _alpm_recursedeps(alpm_db_t *db, alpm_list_t **targs, int include_explicit) * packages. * @return the resolved package **/ -static alpm_pkg_t *resolvedep(alpm_handle_t *handle, alpm_depend_t *dep, +static alpm_pkg_t *resolvedep_default(alpm_handle_t *handle, alpm_depend_t *dep, alpm_list_t *dbs, alpm_list_t *excluding, int prompt) { alpm_list_t *i, *j; @@ -748,8 +750,169 @@ static alpm_pkg_t *resolvedep(alpm_handle_t *handle, alpm_depend_t *dep, return NULL; } +/** + * helper function for resolvedep: search for dep satisfier in dbs + * this version applies the "strict" logic that puts db order before + * exact matching. + * + * @param handle the context handle + * @param dep is the dependency to search for + * @param dbs are the databases to search + * @param excluding are the packages to exclude from the search + * @param prompt if true, ask an alpm_question_install_ignorepkg_t to decide + * if ignored packages should be installed; if false, skip ignored + * packages. + * @return the resolved package + **/ +static alpm_pkg_t *resolvedep_strict(alpm_handle_t *handle, alpm_depend_t *dep, + alpm_list_t *dbs, alpm_list_t *excluding, int prompt) +{ + alpm_list_t *i, *j; + int ignored = 0; + + alpm_list_t *providers = NULL; + int count; + + for(i = dbs; i; i = i->next) { + alpm_pkg_t *pkg; + alpm_db_t *db = i->data; + + if(!(db->usage & (ALPM_DB_USAGE_INSTALL|ALPM_DB_USAGE_UPGRADE))) { + continue; + } + + /* 1. literals */ + pkg = _alpm_db_get_pkgfromcache(db, dep->name); + if(pkg && _alpm_depcmp_literal(pkg, dep) + && !alpm_pkg_find(excluding, pkg->name)) { + if(alpm_pkg_should_ignore(handle, pkg)) { + alpm_question_install_ignorepkg_t question = { + .type = ALPM_QUESTION_INSTALL_IGNOREPKG, + .install = 0, + .pkg = pkg + }; + if(prompt) { + QUESTION(handle, &question); + } else { + _alpm_log(handle, ALPM_LOG_WARNING, _("ignoring package %s-%s\n"), + pkg->name, pkg->version); + } + if(!question.install) { + ignored = 1; + continue; + } + } + if(_alpm_db_get_pkgfromcache(handle->db_local, pkg->name)) { + alpm_list_free(providers); + return pkg; + } + providers = alpm_list_add(providers, pkg); + } + + /* 2. satisfiers (skip literals here) */ + for(j = _alpm_db_get_pkgcache(db); j; j = j->next) { + pkg = j->data; + if((pkg->name_hash != dep->name_hash || strcmp(pkg->name, dep->name) != 0) + && _alpm_depcmp_provides(dep, alpm_pkg_get_provides(pkg)) + && !alpm_pkg_find(excluding, pkg->name)) { + if(alpm_pkg_should_ignore(handle, pkg)) { + alpm_question_install_ignorepkg_t question = { + .type = ALPM_QUESTION_INSTALL_IGNOREPKG, + .install = 0, + .pkg = pkg + }; + if(prompt) { + QUESTION(handle, &question); + } else { + _alpm_log(handle, ALPM_LOG_WARNING, _("ignoring package %s-%s\n"), + pkg->name, pkg->version); + } + if(!question.install) { + ignored = 1; + continue; + } + } + _alpm_log(handle, ALPM_LOG_DEBUG, "provider found (%s provides %s)\n", + pkg->name, dep->name); + + /* provide is already installed so return early instead of prompting later */ + if(_alpm_db_get_pkgfromcache(handle->db_local, pkg->name)) { + alpm_list_free(providers); + return pkg; + } + + providers = alpm_list_add(providers, pkg); + /* keep looking for other providers in the all dbs */ + } + } + } + + count = alpm_list_count(providers); + if(count >= 1) { + alpm_question_select_provider_t question = { + .type = ALPM_QUESTION_SELECT_PROVIDER, + /* default to first provider if there is no QUESTION callback */ + .use_index = 0, + .providers = providers, + .depend = dep + }; + if(count > 1) { + /* if there is more than one provider, we ask the user */ + QUESTION(handle, &question); + } + if(question.use_index >= 0 && question.use_index < count) { + alpm_list_t *nth = alpm_list_nth(providers, question.use_index); + alpm_pkg_t *pkg = nth->data; + alpm_list_free(providers); + return pkg; + } + alpm_list_free(providers); + providers = NULL; + } + + if(ignored) { /* resolvedeps will override these */ + handle->pm_errno = ALPM_ERR_PKG_IGNORED; + } else { + handle->pm_errno = ALPM_ERR_PKG_NOT_FOUND; + } + return NULL; +} + +/** + * helper function for resolvedeps: search for dep satisfier in dbs + * + * @param handle the context handle + * @param dep is the dependency to search for + * @param depstrategy is the depstrategy to apply when searching + * @param dbs are the databases to search + * @param excluding are the packages to exclude from the search + * @param prompt if true, ask an alpm_question_install_ignorepkg_t to decide + * if ignored packages should be installed; if false, skip ignored + * packages. + * @return the resolved package + **/ +static alpm_pkg_t *resolvedep(alpm_handle_t *handle, alpm_depend_t *dep, + alpm_depstrategy_t depstrategy, alpm_list_t *dbs, alpm_list_t *excluding, int prompt) +{ + switch(depstrategy) { + case ALPM_DEPSTRATEGY_DEFAULT: + return resolvedep_default(handle, dep, dbs, excluding, prompt); + case ALPM_DEPSTRATEGY_STRICT: + return resolvedep_strict(handle, dep, dbs, excluding, prompt); + default: + /* We should never reach here */ + return NULL; + } +} + alpm_pkg_t SYMEXPORT *alpm_find_dbs_satisfier(alpm_handle_t *handle, alpm_list_t *dbs, const char *depstring) +{ + return alpm_find_dbs_satisfier_ex(handle, dbs, ALPM_DEPSTRATEGY_DEFAULT, depstring); +} + +alpm_pkg_t SYMEXPORT *alpm_find_dbs_satisfier_ex(alpm_handle_t *handle, + alpm_list_t *dbs, alpm_depstrategy_t depstrategy, const char *depstring) { alpm_depend_t *dep; alpm_pkg_t *pkg; @@ -759,7 +922,7 @@ alpm_pkg_t SYMEXPORT *alpm_find_dbs_satisfier(alpm_handle_t *handle, dep = alpm_dep_from_string(depstring); ASSERT(dep, return NULL); - pkg = resolvedep(handle, dep, dbs, NULL, 1); + pkg = resolvedep(handle, dep, depstrategy, dbs, NULL, 1); alpm_dep_free(dep); return pkg; } @@ -771,6 +934,8 @@ alpm_pkg_t SYMEXPORT *alpm_find_dbs_satisfier(alpm_handle_t *handle, * @param handle the context handle * @param localpkgs is the list of local packages * @param pkg is the package to resolve + * @param depstrategy is the strategy to follow (one of + ALPM_DEPSTRATEGY_DEFAULT, ALPM_DEPSTRATEGY_STRICT) * @param preferred packages to prefer when resolving * @param packages is a pointer to a list of packages which will be * searched first for any dependency packages needed to complete the @@ -786,8 +951,8 @@ alpm_pkg_t SYMEXPORT *alpm_find_dbs_satisfier(alpm_handle_t *handle, * unmodified by this function */ int _alpm_resolvedeps(alpm_handle_t *handle, alpm_list_t *localpkgs, - alpm_pkg_t *pkg, alpm_list_t *preferred, alpm_list_t **packages, - alpm_list_t *rem, alpm_list_t **data) + alpm_pkg_t *pkg, alpm_depstrategy_t depstrategy, alpm_list_t *preferred, + alpm_list_t **packages, alpm_list_t *rem, alpm_list_t **data) { int ret = 0; alpm_list_t *j; @@ -826,14 +991,14 @@ int _alpm_resolvedeps(alpm_handle_t *handle, alpm_list_t *localpkgs, alpm_pkg_t *spkg = find_dep_satisfier(preferred, missdep); if(!spkg) { /* find a satisfier package in the given repositories */ - spkg = resolvedep(handle, missdep, handle->dbs_sync, *packages, 0); + spkg = resolvedep(handle, missdep, depstrategy, handle->dbs_sync, *packages, 0); } - if(spkg && _alpm_resolvedeps(handle, localpkgs, spkg, preferred, packages, rem, data) == 0) { + if(spkg && _alpm_resolvedeps(handle, localpkgs, spkg, depstrategy, preferred, packages, rem, data) == 0) { _alpm_log(handle, ALPM_LOG_DEBUG, "pulling dependency %s (needed by %s)\n", spkg->name, pkg->name); alpm_depmissing_free(miss); - } else if(resolvedep(handle, missdep, (targ = alpm_list_add(NULL, handle->db_local)), rem, 0)) { + } else if(resolvedep(handle, missdep, depstrategy, (targ = alpm_list_add(NULL, handle->db_local)), rem, 0)) { alpm_depmissing_free(miss); } else { handle->pm_errno = ALPM_ERR_UNSATISFIED_DEPS; diff --git a/lib/libalpm/deps.h b/lib/libalpm/deps.h index 4bf6273f..34a8185f 100644 --- a/lib/libalpm/deps.h +++ b/lib/libalpm/deps.h @@ -32,8 +32,8 @@ alpm_list_t *_alpm_sortbydeps(alpm_handle_t *handle, alpm_list_t *targets, alpm_list_t *ignore, int reverse); int _alpm_recursedeps(alpm_db_t *db, alpm_list_t **targs, int include_explicit); int _alpm_resolvedeps(alpm_handle_t *handle, alpm_list_t *localpkgs, alpm_pkg_t *pkg, - alpm_list_t *preferred, alpm_list_t **packages, alpm_list_t *remove, - alpm_list_t **data); + alpm_depstrategy_t depstrategy, alpm_list_t *preferred, alpm_list_t **packages, + alpm_list_t *remove, alpm_list_t **data); int _alpm_depcmp_literal(alpm_pkg_t *pkg, alpm_depend_t *dep); int _alpm_depcmp_provides(alpm_depend_t *dep, alpm_list_t *provisions); int _alpm_depcmp(alpm_pkg_t *pkg, alpm_depend_t *dep); diff --git a/lib/libalpm/sync.c b/lib/libalpm/sync.c index cf436a84..fe89dbc4 100644 --- a/lib/libalpm/sync.c +++ b/lib/libalpm/sync.c @@ -362,7 +362,7 @@ finish: return ret; } -int _alpm_sync_prepare(alpm_handle_t *handle, alpm_list_t **data) +int _alpm_sync_prepare(alpm_handle_t *handle, alpm_depstrategy_t depstrategy, alpm_list_t **data) { alpm_list_t *i, *j; alpm_list_t *deps = NULL; @@ -424,7 +424,7 @@ int _alpm_sync_prepare(alpm_handle_t *handle, alpm_list_t **data) building up a list of packages which could not be resolved. */ for(i = trans->add; i; i = i->next) { alpm_pkg_t *pkg = i->data; - if(_alpm_resolvedeps(handle, localpkgs, pkg, trans->add, + if(_alpm_resolvedeps(handle, localpkgs, pkg, depstrategy, trans->add, &resolved, remove, data) == -1) { unresolvable = alpm_list_add(unresolvable, pkg); } diff --git a/lib/libalpm/sync.h b/lib/libalpm/sync.h index 26e57cdf..c0cefc6e 100644 --- a/lib/libalpm/sync.h +++ b/lib/libalpm/sync.h @@ -24,7 +24,7 @@ #include "alpm.h" -int _alpm_sync_prepare(alpm_handle_t *handle, alpm_list_t **data); +int _alpm_sync_prepare(alpm_handle_t *handle, alpm_depstrategy_t depstrategy, alpm_list_t **data); int _alpm_sync_load(alpm_handle_t *handle, alpm_list_t **data); int _alpm_sync_check(alpm_handle_t *handle, alpm_list_t **data); int _alpm_sync_commit(alpm_handle_t *handle); diff --git a/lib/libalpm/trans.c b/lib/libalpm/trans.c index 7048a059..d41115bf 100644 --- a/lib/libalpm/trans.c +++ b/lib/libalpm/trans.c @@ -108,6 +108,10 @@ static alpm_list_t *check_arch(alpm_handle_t *handle, alpm_list_t *pkgs) int SYMEXPORT alpm_trans_prepare(alpm_handle_t *handle, alpm_list_t **data) { + return alpm_trans_prepare_ex(handle, ALPM_DEPSTRATEGY_DEFAULT, data); +} + +int SYMEXPORT alpm_trans_prepare_ex(alpm_handle_t *handle, alpm_depstrategy_t depstrategy, alpm_list_t **data) { alpm_trans_t *trans; /* Sanity checks */ @@ -138,7 +142,7 @@ int SYMEXPORT alpm_trans_prepare(alpm_handle_t *handle, alpm_list_t **data) return -1; } } else { - if(_alpm_sync_prepare(handle, data) == -1) { + if(_alpm_sync_prepare(handle, depstrategy, data) == -1) { /* pm_errno is set by _alpm_sync_prepare() */ return -1; } diff --git a/src/pacman/conf.c b/src/pacman/conf.c index 8396d02d..8c519d84 100644 --- a/src/pacman/conf.c +++ b/src/pacman/conf.c @@ -115,6 +115,7 @@ config_t *config_new(void) newconfig->remotefilesiglevel = ALPM_SIG_USE_DEFAULT; } + newconfig->depstrategy = ALPM_DEPSTRATEGY_DEFAULT; /* by default use 1 download stream */ newconfig->parallel_downloads = 1; newconfig->colstr.colon = ":: "; @@ -745,6 +746,17 @@ static int _parse_options(const char *key, char *value, } config->parallel_downloads = number; + } else if(strcmp(key, "DependencyStrategy") == 0) { + if(strcmp(value, "Default") == 0) { + config->depstrategy = ALPM_DEPSTRATEGY_DEFAULT; + } else if(strcmp(value, "Strict") == 0) { + config->depstrategy = ALPM_DEPSTRATEGY_STRICT; + } else { + pm_printf(ALPM_LOG_ERROR, + _("config file %s, line %d: invalid value for '%s': '%s'\n"), + file, linenum, "DependencyStrategy", value); + return 1; + } } else { pm_printf(ALPM_LOG_WARNING, _("config file %s, line %d: directive '%s' in section '%s' not recognized.\n"), diff --git a/src/pacman/conf.h b/src/pacman/conf.h index 9b1beff0..c0fc884d 100644 --- a/src/pacman/conf.h +++ b/src/pacman/conf.h @@ -128,6 +128,7 @@ typedef struct __config_t { char *xfercommand; char **xfercommand_argv; size_t xfercommand_argc; + alpm_depstrategy_t depstrategy; /* our connection to libalpm */ alpm_handle_t *handle; diff --git a/src/pacman/remove.c b/src/pacman/remove.c index 98f8bcda..b584a4bc 100644 --- a/src/pacman/remove.c +++ b/src/pacman/remove.c @@ -107,7 +107,7 @@ int pacman_remove(alpm_list_t *targets) } /* Step 2: prepare the transaction based on its type, targets and flags */ - if(alpm_trans_prepare(config->handle, &data) == -1) { + if(alpm_trans_prepare_ex(config->handle, ALPM_DEPSTRATEGY_DEFAULT, &data) == -1) { alpm_errno_t err = alpm_errno(config->handle); pm_printf(ALPM_LOG_ERROR, _("failed to prepare transaction (%s)\n"), alpm_strerror(err)); diff --git a/src/pacman/sync.c b/src/pacman/sync.c index 4a01d402..ba5bb4c5 100644 --- a/src/pacman/sync.c +++ b/src/pacman/sync.c @@ -622,7 +622,8 @@ cleanup: static int process_targname(alpm_list_t *dblist, const char *targname, int error) { - alpm_pkg_t *pkg = alpm_find_dbs_satisfier(config->handle, dblist, targname); + alpm_pkg_t *pkg = alpm_find_dbs_satisfier_ex(config->handle, dblist, + config->depstrategy, targname); /* skip ignored packages when user says no */ if(alpm_errno(config->handle) == ALPM_ERR_PKG_IGNORED) { @@ -755,7 +756,7 @@ int sync_prepare_execute(void) int retval = 0; /* Step 2: "compute" the transaction based on targets and flags */ - if(alpm_trans_prepare(config->handle, &data) == -1) { + if(alpm_trans_prepare_ex(config->handle, config->depstrategy, &data) == -1) { alpm_errno_t err = alpm_errno(config->handle); pm_printf(ALPM_LOG_ERROR, _("failed to prepare transaction (%s)\n"), alpm_strerror(err));