diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli/stasis/stasis_main.c | 32 | ||||
| -rw-r--r-- | src/lib/core/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/lib/core/include/strlist.h | 6 | ||||
| -rw-r--r-- | src/lib/core/include/version_compare.h | 20 | ||||
| -rw-r--r-- | src/lib/core/strlist.c | 59 | ||||
| -rw-r--r-- | src/lib/core/utils.c | 39 | ||||
| -rw-r--r-- | src/lib/core/version_compare.c | 185 | ||||
| -rw-r--r-- | src/lib/delivery/delivery_install.c | 136 | ||||
| -rw-r--r-- | src/lib/delivery/include/delivery.h | 11 |
9 files changed, 448 insertions, 41 deletions
diff --git a/src/cli/stasis/stasis_main.c b/src/cli/stasis/stasis_main.c index 78aae0c..95cb7c5 100644 --- a/src/cli/stasis/stasis_main.c +++ b/src/cli/stasis/stasis_main.c @@ -323,6 +323,37 @@ static void install_packaging_tools() { } } +static void force_conda_package_reinstallation_on_mismatch(struct Delivery *ctx, const char *env_name) { + const size_t conda_package_count = strlist_count(ctx->conda.conda_packages); + if (conda_package_count) { + msg(STASIS_MSG_L1, "Enforcing Conda package versions: %s\n", env_name); + for (size_t i = 0; i < conda_package_count; i++) { + const char *item = strlist_item(ctx->conda.conda_packages, i); + if (!item) { + msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "NULL record in conda package list\n"); + exit(1); + } + char *pkg_name = strdup(item); + if (!pkg_name) { + msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "unable to allocate memory for package name\n"); + exit(1); + } + const char *spec = find_version_spec(pkg_name); + if (spec) { + pkg_name[spec - pkg_name] = '\0'; + } + + msg(STASIS_MSG_L2, "%s\n", pkg_name); + if (delivery_conda_enforce_package_version(ctx, env_name, pkg_name)) { + msg(STASIS_MSG_L3 | STASIS_MSG_ERROR, "Failed to determine conda package version: %s\n", pkg_name); + guard_free(pkg_name); + exit(1); + } + guard_free(pkg_name); + } + } +} + static void configure_package_overlay(struct Delivery *ctx, const char *env_name) { if (!isempty(ctx->meta.based_on)) { msg(STASIS_MSG_L1, "Generating package overlay from environment: %s\n", env_name); @@ -398,6 +429,7 @@ static void release_install_conda_packages(struct Delivery *ctx, char *env_name) if (delivery_install_packages(ctx, ctx->storage.conda_install_prefix, env_name, INSTALL_PKG_CONDA, (struct StrList *[]) {ctx->conda.conda_packages, NULL})) { exit(1); } + force_conda_package_reinstallation_on_mismatch(ctx, env_name); } if (strlist_count(ctx->conda.conda_packages_defer)) { msg(STASIS_MSG_L3, "Installing deferred conda packages\n"); diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt index 0fb273c..462d7d8 100644 --- a/src/lib/core/CMakeLists.txt +++ b/src/lib/core/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(stasis_core STATIC envctl.c multiprocessing.c semaphore.c + version_compare.c ) target_include_directories(stasis_core PRIVATE ${core_INCLUDE} diff --git a/src/lib/core/include/strlist.h b/src/lib/core/include/strlist.h index b2d7da7..f44025c 100644 --- a/src/lib/core/include/strlist.h +++ b/src/lib/core/include/strlist.h @@ -45,8 +45,10 @@ int strlist_append_file(struct StrList *pStrList, char *path, ReaderFn *readerFn void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2); void strlist_append(struct StrList **pStrList, char *str); void strlist_append_array(struct StrList *pStrList, char **arr); -void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim); -void strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim); + +int strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim); + +int strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim); int strlist_appendf(struct StrList **pStrList, const char *fmt, ...); struct StrList *strlist_copy(struct StrList *pStrList); int strlist_cmp(struct StrList *a, struct StrList *b); diff --git a/src/lib/core/include/version_compare.h b/src/lib/core/include/version_compare.h new file mode 100644 index 0000000..857de47 --- /dev/null +++ b/src/lib/core/include/version_compare.h @@ -0,0 +1,20 @@ +#ifndef STASIS_VERSION_COMPARE_H +#define STASIS_VERSION_COMPARE_H + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include "str.h" + +#define GT 1 << 1 +#define LT 1 << 2 +#define EQ 1 << 3 +#define NOT 1 << 4 +#define EPOCH_MOD 10000 + +int version_sum(const char *str); +int version_parse_operator(const char *str); +int version_compare(int flags, const char *aa, const char *bb); + +#endif //STASIS_VERSION_COMPARE_H
\ No newline at end of file diff --git a/src/lib/core/strlist.c b/src/lib/core/strlist.c index 42d5b85..0d25a66 100644 --- a/src/lib/core/strlist.c +++ b/src/lib/core/strlist.c @@ -218,22 +218,30 @@ void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2 * @param str * @param delim */ - void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim) { - if (!str || !delim) { - return; - } +int strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim) { + if (!str || !delim) { + return -1; + } - char *tmp = strdup(str); - char **token = split(tmp, delim, 0); - if (token) { - for (size_t i = 0; token[i] != NULL; i++) { - lstrip(token[i]); - strlist_append(&pStrList, token[i]); - } - guard_array_free(token); - } + char *tmp = strdup(str); + if (!tmp) { + return -1; + } + + char **token = split(tmp, delim, 0); + if (!token) { + guard_free(tmp); + return -1; + } + for (size_t i = 0; token[i] != NULL; i++) { + lstrip(token[i]); + strlist_append(&pStrList, token[i]); + } + guard_array_free(token); guard_free(tmp); - } + + return 0; +} /** * Append the contents of a newline delimited string without @@ -242,20 +250,29 @@ void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2 * @param str * @param delim */ -void strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim) { +int strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim) { if (!str || !delim) { - return; + return -1; } char *tmp = strdup(str); + if (!tmp) { + return -1; + } + char **token = split(tmp, delim, 0); - if (token) { - for (size_t i = 0; token[i] != NULL; i++) { - strlist_append(&pStrList, token[i]); - } - guard_array_free(token); + if (!token) { + guard_free(tmp); + return -1; } + + for (size_t i = 0; token[i] != NULL; i++) { + strlist_append(&pStrList, token[i]); + } + + guard_array_free(token); guard_free(tmp); + return 0; } /** diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c index 90dac1d..e6a8315 100644 --- a/src/lib/core/utils.c +++ b/src/lib/core/utils.c @@ -742,29 +742,32 @@ int fix_tox_conf(const char *filename, char **result, size_t maxlen) { return 0; } -static size_t count_blanks(char *s) { - // return the number of leading blanks (tab/space) in a string - size_t blank = 0; - for (size_t i = 0; i < strlen(s); i++) { - if (isblank(s[i])) { - blank++; +/** + * Collapse all whitespace in a string (to single spaces) + * @param s address of string to modify + * @return + */ +char *collapse_whitespace(char **s) { + char *dest = NULL; + char *src = NULL; + int in_ws = 1; + + for (src = dest = *s; *src != '\0'; ++src) { + if (isspace(*src)) { + if (!in_ws) { + *dest++ = ' '; + in_ws = 1; + } } else { - break; + *dest++ = *src; + in_ws = 0; } } - return blank; -} -char *collapse_whitespace(char **s) { - char *x = (*s); - size_t len = strlen(x); - for (size_t i = 0; i < len; i++) { - size_t blank = count_blanks(&x[i]); - if (blank > 1) { - memmove(&x[i], &x[i] + blank, strlen(&x[i])); - } + if (dest > *s && *(dest - 1) == ' ') { + --dest; } - + *dest = '\0'; return *s; } diff --git a/src/lib/core/version_compare.c b/src/lib/core/version_compare.c new file mode 100644 index 0000000..4939c8f --- /dev/null +++ b/src/lib/core/version_compare.c @@ -0,0 +1,185 @@ +#include "version_compare.h" + +const struct { + const char *key; + int value; +} WEIGHT[] = { + {.key = "post", 1000}, + {.key = "rc", -1000}, + {.key = "dev", -2000}, +}; + +/** + * Sum each part of a '.'-delimited version string + * @param str version string + * @return sum of each part + * @return -1 on error + */ +int version_sum(const char *str) { + char *end; + + if (!str || isempty((char *) str)) { + return -1; + } + + int result = 0; + int epoch = 0; + char *s = strdup(str); + if (!s) { + return -1; + } + char *ptr = s; + end = ptr; + + // Parsing stops at the first non-alpha, non-'.' character + // Digits are processed until the first invalid character + // I'm torn whether this should be considered an error + int i = 0; + while (end != NULL) { + int tmp_result = 0; + + tmp_result = (int) strtoul(ptr, &end, 10); + + // Circumvent a bug which allows a smaller version to be greater + // than a larger version + // Bug: + // 1.0.3 == 1 + 0 + 3 = 4 + // 2.0.0 == 2 + 0 + 0 = 2 + // Correction: + // ((1 * EPOCH_MOD) + 1).0.3 = 104 + // ((2 * EPOCH_MOD) + 2).0.0 = 202 + if (!i && tmp_result && *end != ':') { + result += tmp_result * EPOCH_MOD; + i++; + } + + ptr = end; + if (*ptr == '.' || *ptr == '-') { + ptr++; + } + else if (!epoch && *ptr == ':') { + epoch = 1; + result += EPOCH_MOD; + ptr++; + } + else if (isalpha(*ptr)) { + int adjusted = 0; + for (size_t w = 0; w < sizeof(WEIGHT) / sizeof(WEIGHT[0]); w++) { + const int has_suffix = strncasecmp(ptr, WEIGHT[w].key, strlen(WEIGHT[w].key)) == 0; + if (has_suffix) { + // skip the suffix + ptr += strlen(WEIGHT[w].key); + // adjust result based on suffix weight + result += WEIGHT[w].value; + adjusted = 1; + break; + } + } + + if (!adjusted) { + result += *ptr - ('a' - 1); + ptr++; + } + } + else { + end = NULL; + } + + if (tmp_result) { + result += tmp_result; + } + } + + free(s); + return result; +} + +/** + * Convert version operator(s) to flags + * @param str input string + * @return operator flags + */ +int version_parse_operator(const char *str) { + const char *valid = "><=!"; + + const char *pos = str; + int result = 0; + + if (isempty((char *) str)) { + return -1; + } + while ((pos = strpbrk(pos, valid)) != NULL) { + switch (*pos) { + case '>': + result |= GT; + break; + case '<': + result |= LT; + break; + case '=': + result |= EQ; + break; + case '!': + result |= NOT; + break; + default: + return -1; + } + pos++; + } + + return result; +} + +static int version_has_epoch(const char *str) { + const char *result = strchr(str, ':'); + return result ? 1 : 0; +} + +/** + * Compare version strings based on flag(s) + * @param flags verison operators + * @param aa version1 + * @param bb version2 + * @return 1 flag operation is true + * @return 0 flag operation is false + */ +int version_compare(const int flags, const char *aa, const char *bb) { + if (!flags || flags < 0) { + return -1; + } + + int result_a = version_sum(aa); + if (result_a < 0) { + return -1; + } + + int result_b = version_sum(bb); + if (result_b < 0) { + return -1; + } + + if (version_has_epoch(aa) && !version_has_epoch(bb)) { + result_a -= EPOCH_MOD; + } + if (!version_has_epoch(aa) && version_has_epoch(bb)) { + result_b -= EPOCH_MOD; + } + + int result = 0; + if (flags & GT && flags & EQ) { + result |= result_a >= result_b; + } else if (flags & LT && flags & EQ) { + result |= result_a <= result_b; + } else if (flags & NOT && flags & EQ) { + result |= result_a != result_b; + } else if (flags & GT) { + result |= result_a > result_b; + } else if (flags & LT) { + result |= result_a < result_b; + } else if (flags & EQ) { + result |= result_a == result_b; + } + + return result; +}
\ No newline at end of file diff --git a/src/lib/delivery/delivery_install.c b/src/lib/delivery/delivery_install.c index 3d54eaa..6ad9407 100644 --- a/src/lib/delivery/delivery_install.c +++ b/src/lib/delivery/delivery_install.c @@ -134,6 +134,142 @@ int delivery_overlay_packages_from_env(struct Delivery *ctx, const char *env_nam return 0; } +int delivery_conda_enforce_package_version(struct Delivery *ctx, const char *env_name, const char *name) { + char *spec_installed = NULL; + char *spec_request = NULL; + int status = 0; + + if (isempty((char *) env_name)) { + SYSERROR("%s", "environment name cannot be NULL or empty"); + return -1; + } + if (isempty((char *) name)) { + SYSERROR("%s", "name cannot be NULL or empty"); + return -1; + } + + int proc_status = 0; + char cmd[PATH_MAX] = {0}; + snprintf(cmd, PATH_MAX, "conda list --name %s", env_name); + + char *output = shell_output(cmd, &proc_status); + if (!output || proc_status) { + SYSERROR("unable to retreive list of installed packages (exit: %d)", proc_status); + guard_free(output); + return -1; + } + + struct StrList *lines = strlist_init(); + if (!lines) { + SYSERROR("%s", "unable to allocate memory for installed package list"); + guard_free(output); + status = -1; + goto cleanup; + } + + if (strlist_append_tokenize(lines, output, LINE_SEP)) { + SYSERROR("%s", "unable to tokenize installed package list"); + guard_free(output); + strlist_free(&lines); + status = -1; + goto cleanup; + } + + for (size_t i = 0; i < strlist_count(lines); i++) { + char *line = strlist_item(lines, i); + if (!line) { + SYSERROR("%s", "line is NULL"); + status = -1; + goto cleanup; + } + if (startswith(line, "#") || isempty(line)) { + continue; + } + collapse_whitespace(&line); + strip(line); + + struct StrList *tokens = strlist_init(); + if (!tokens) { + SYSERROR("%s", "unable to allocate memory for tokenized installed package list"); + status = -1; + goto cleanup; + } + + if (strlist_append_tokenize(tokens, line, " ")) { + SYSERROR("%s", "unable to tokenize installed package list"); + status = -1; + goto cleanup; + } + + const char *installed_version = strlist_item(tokens, 1); + if (!installed_version) { + SYSERROR("%s", "not enough data in line (name and version not found)"); + guard_strlist_free(&tokens); + status = -1; + goto cleanup; + } + + if (strstr(line, name)) { + spec_installed = strdup(installed_version); + if (!spec_installed) { + SYSERROR("%s", "unable to allocated memory for installed package version"); + guard_strlist_free(&tokens); + status = -1; + goto cleanup; + } + guard_strlist_free(&tokens); + break; + } + + guard_strlist_free(&tokens); + } + + for (size_t i = 0; i < strlist_count(ctx->conda.conda_packages); i++) { + const char *item = strlist_item(ctx->conda.conda_packages, i); + if (!item) { + SYSERROR("conda_packages list record %zu is NULL", i); + status = -1; + goto cleanup; + } + if (strstr(item, name)) { + char *spec_tmp = find_version_spec((char *) item); + while (!isalnum(*spec_tmp)) { + spec_tmp++; + } + + spec_request = strdup(spec_tmp); + if (!spec_request) { + SYSERROR("%s", "unable to allocate memory for conda package spec request"); + status = -1; + goto cleanup; + } + break; + } + } + + const int stop = version_compare(NOT | EQ, spec_request, spec_installed); + if (stop < 0) { + SYSERROR("version comparison failed (spec_request: %s, spec_installed: %s)", spec_request, spec_installed); + status = -1; + goto cleanup; + } + if (stop == 0) { + goto cleanup; + } + + snprintf(cmd, PATH_MAX, "remove --name %s %s", env_name, name); + conda_exec(cmd); + snprintf(cmd, PATH_MAX, "install --name %s %s=%s", env_name, name, spec_request); + conda_exec(cmd); + + cleanup: + guard_free(spec_request); + guard_free(spec_installed); + strlist_free(&lines); + guard_free(output); + return status; +} + static int fn_nop(const char *command) { (void) command; return 1; diff --git a/src/lib/delivery/include/delivery.h b/src/lib/delivery/include/delivery.h index e524f4d..c091182 100644 --- a/src/lib/delivery/include/delivery.h +++ b/src/lib/delivery/include/delivery.h @@ -21,6 +21,7 @@ #include "wheel.h" #include "wheelinfo.h" #include "environment.h" +#include "version_compare.h" #define DELIVERY_PLATFORM_MAX 4 #define DELIVERY_PLATFORM_MAXLEN 65 @@ -451,6 +452,16 @@ int delivery_exists(struct Delivery *ctx); int delivery_overlay_packages_from_env(struct Delivery *ctx, const char *env_name); /** + * Conda does not handle version suffixes well, if at all. For example, if pkg-1.2.3rc1 is installed Conda will + * silently ignore a request to install pkg-1.2.3. This function serves as a workaround by comparing the version + * on-disk, and the requested version from the package list, and if the versions are not equal the on-disk package + * is replaced by the one in the package list. + * + * When a package is present in the list without a pinned version it will be reinstalled with whatever is available + */ +int delivery_conda_enforce_package_version(struct Delivery *ctx, const char *env_name, const char *name); + +/** * Retrieve remote deliveries associated with the current version series * @param ctx Delivery context * @return -1 on error |
