diff options
-rw-r--r-- | README.md | 48 | ||||
-rw-r--r-- | include/conda.h | 13 | ||||
-rw-r--r-- | include/delivery.h | 2 | ||||
-rw-r--r-- | src/conda.c | 33 | ||||
-rw-r--r-- | src/delivery.c | 58 | ||||
-rw-r--r-- | src/delivery_install.c | 101 | ||||
-rw-r--r-- | src/stasis_main.c | 19 | ||||
-rw-r--r-- | stasis.ini | 2 | ||||
-rw-r--r-- | tests/data/generic.ini | 2 |
9 files changed, 195 insertions, 83 deletions
@@ -147,24 +147,26 @@ stasis mydelivery.ini ## Command Line Options -| Long Option | Short Option | Purpose | -|:---------------------|:------------:|:---------------------------------------------------------------| -| --help | -h | Display usage statement | -| --version | -V | Display program version | -| --continue-on-error | -C | Allow tests to fail | -| --config ARG | -c ARG | Read STASIS configuration file | -| --cpu-limit ARG | -l ARG | Number of processes to spawn concurrently (default: cpus - 1) | -| --python ARG | -p ARG | Override version of Python in configuration | -| --verbose | -v | Increase output verbosity | -| --unbuffered | -U | Disable line buffering | -| --update-base | n/a | Update conda installation prior to STATIS environment creation | -| --parallel-fail-fast | n/a | On test error, terminate all concurrent tasks | -| --overwrite | n/a | Overwrite an existing release | -| --no-docker | n/a | Do not build docker images | -| --no-artifactory | n/a | Do not upload artifacts to Artifactory | -| --no-testing | n/a | Do not execute test scripts | -| --no-rewrite | n/a | Do not rewrite paths and URLs in output files | -| DELIVERY_FILE | n/a | STASIS delivery file | +| Long Option | Short Option | Purpose | +|:---------------------------|:------------:|:---------------------------------------------------------------| +| --help | -h | Display usage statement | +| --version | -V | Display program version | +| --continue-on-error | -C | Allow tests to fail | +| --config ARG | -c ARG | Read STASIS configuration file | +| --cpu-limit ARG | -l ARG | Number of processes to spawn concurrently (default: cpus - 1) | +| --pool-status-interval ARG | n/a | Report task status every n seconds (default: 30) | +| --python ARG | -p ARG | Override version of Python in configuration | +| --verbose | -v | Increase output verbosity | +| --unbuffered | -U | Disable line buffering | +| --update-base | n/a | Update conda installation prior to STATIS environment creation | +| --fail-fast | n/a | On test error, terminate all tasks | +| --overwrite | n/a | Overwrite an existing release | +| --no-docker | n/a | Do not build docker images | +| --no-artifactory | n/a | Do not upload artifacts to Artifactory | +| --no-testing | n/a | Do not execute test scripts | +| --no-parallel | n/a | Do not execute tests in parallel | +| --no-rewrite | n/a | Do not rewrite paths and URLs in output files | +| DELIVERY_FILE | n/a | STASIS delivery file | ## Environment variables @@ -344,11 +346,11 @@ python = {{ env:MY_DYNAMIC_PYTHON_VERSION }} Template functions can be accessed using the `{{ func:NAME(ARG,...) }}` notation. -| Name | Purpose | -|-------------------------------|------------------------------------------------------------------| -| get_github_release_notes_auto | Generate release notes for all test contexts | -| basetemp_dir | Generate directory path to test block's temporary data directory | -| junitxml_file | Generate directory path and file name for test result file | +| Name | Arguments | Purpose | +|-------------------------------|-----------|------------------------------------------------------------------| +| get_github_release_notes_auto | n/a | Generate release notes for all test contexts | +| basetemp_dir | n/a | Generate directory path to test block's temporary data directory | +| junitxml_file | n/a | Generate directory path and file name for test result file | # Mission files diff --git a/include/conda.h b/include/conda.h index b5ea926..b26c7a3 100644 --- a/include/conda.h +++ b/include/conda.h @@ -186,15 +186,18 @@ int conda_index(const char *path); /** * Determine whether a simple index contains a package * @param index_url a file system path or url pointing to a simple index - * @param name package name (required) - * @param version package version (may be NULL) + * @param spec a pip package specification (e.g. `name==1.2.3`) * @return not found = 0, found = 1, error = -1 */ -int pip_index_provides(const char *index_url, const char *name, const char *version); - -char *conda_get_active_environment(); +int pip_index_provides(const char *index_url, const char *spec); +/** + * Determine whether conda can find a package in its channel list + * @param spec a conda package specification (e.g. `name=1.2.3`) + * @return not found = 0, found = 1, error = -1 + */ int conda_provides(const char *spec); +char *conda_get_active_environment(); #endif //STASIS_CONDA_H diff --git a/include/delivery.h b/include/delivery.h index 6da890a..15cde13 100644 --- a/include/delivery.h +++ b/include/delivery.h @@ -419,4 +419,6 @@ int filter_repo_tags(char *repo, struct StrList *patterns); */ int delivery_exists(struct Delivery *ctx); +int delivery_overlay_packages_from_env(struct Delivery *ctx, const char *env_name); + #endif //STASIS_DELIVERY_H diff --git a/src/conda.c b/src/conda.c index 43b9001..e60abc7 100644 --- a/src/conda.c +++ b/src/conda.c @@ -79,37 +79,26 @@ int pip_exec(const char *args) { return system(command); } -int pip_index_provides(const char *index_url, const char *name, const char *version) { +int pip_index_provides(const char *index_url, const char *spec) { char cmd[PATH_MAX] = {0}; - char name_local[255]; - char version_local[255] = {0}; - char spec[255] = {0}; + char spec_local[255] = {0}; - if (isempty((char *) name) < 0) { - // no package name means nothing to do. + if (isempty((char *) spec)) { + // NULL or zero-length; no package spec means there's nothing to do. return -1; } - // Fix up the package name - strncpy(name_local, name, sizeof(name_local) - 1); - tolower_s(name_local); - lstrip(name_local); - strip(name_local); - - if (version) { - // Fix up the package version - strncpy(version_local, version, sizeof(version_local) - 1); - tolower_s(version_local); - lstrip(version_local); - strip(version_local); - sprintf(spec, "==%s", version); - } + // Normalize the local spec string + strncpy(spec_local, spec, sizeof(spec_local) - 1); + tolower_s(spec_local); + lstrip(spec_local); + strip(spec_local); char logfile[] = "/tmp/STASIS-package_exists.XXXXXX"; int logfd = mkstemp(logfile); if (logfd < 0) { perror(logfile); - remove(logfile); // fail harmlessly if not present + remove(logfile); // fail harmlessly if not present return -1; } @@ -121,7 +110,7 @@ int pip_index_provides(const char *index_url, const char *name, const char *vers strcpy(proc.f_stdout, logfile); // Do an installation in dry-run mode to see if the package exists in the given index. - snprintf(cmd, sizeof(cmd) - 1, "python -m pip install --dry-run --no-deps --index-url=%s %s%s", index_url, name_local, spec); + snprintf(cmd, sizeof(cmd) - 1, "python -m pip install --dry-run --no-deps --index-url=%s %s", index_url, spec_local); status = shell(&proc, cmd); // Print errors only when shell() itself throws one diff --git a/src/delivery.c b/src/delivery.c index a689db2..07e04c8 100644 --- a/src/delivery.c +++ b/src/delivery.c @@ -178,38 +178,45 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { dataptr = ctx->conda.pip_packages; deferred = ctx->conda.pip_packages_defer; strcpy(mode, "pip"); + } else { + SYSERROR("BUG: type %d does not map to a supported package manager!\n", type); + exit(1); } msg(STASIS_MSG_L2, "Filtering %s packages by test definition...\n", mode); struct StrList *filtered = NULL; filtered = strlist_init(); for (size_t i = 0; i < strlist_count(dataptr); i++) { - int ignore_pkg = 0; + int build_for_host = 0; name = strlist_item(dataptr, i); if (!strlen(name) || isblank(*name) || isspace(*name)) { // no data continue; } - msg(STASIS_MSG_L3, "package '%s': ", name); // Compile a list of packages that are *also* to be tested. - char *version; char *spec_begin = strpbrk(name, "@~=<>!"); char *spec_end = spec_begin; + char package_name[255] = {0}; + if (spec_end) { // A version is present in the package name. Jump past operator(s). while (*spec_end != '\0' && !isalnum(*spec_end)) { spec_end++; } + strncpy(package_name, name, spec_begin - name); + } else { + strncpy(package_name, name, sizeof(package_name) - 1); } + msg(STASIS_MSG_L3, "package '%s': ", package_name); + // When spec is present in name, set tests->version to the version detected in the name for (size_t x = 0; x < sizeof(ctx->tests) / sizeof(ctx->tests[0]) && ctx->tests[x].name != NULL; x++) { struct Test *test = &ctx->tests[x]; - version = NULL; - char nametmp[1024] = {0}; + if (spec_end != NULL && spec_begin != NULL) { strncpy(nametmp, name, spec_begin - name); } else { @@ -220,18 +227,16 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { // Override test->version when a version is provided by the (pip|conda)_package list item guard_free(test->version); if (spec_begin && spec_end) { - *spec_begin = '\0'; test->version = strdup(spec_end); } else { // There are too many possible default branches nowadays: master, main, develop, xyz, etc. // HEAD is a safe bet. test->version = strdup("HEAD"); } - version = test->version; // Is the list item a git+schema:// URL? - if (strstr(name, "git+") && strstr(name, "://")) { - char *xrepo = strstr(name, "+"); + if (strstr(nametmp, "git+") && strstr(nametmp, "://")) { + char *xrepo = strstr(nametmp, "+"); if (xrepo) { xrepo++; guard_free(test->repository); @@ -239,7 +244,7 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { xrepo = NULL; } // Extract the name of the package - char *xbasename = path_basename(name); + char *xbasename = path_basename(nametmp); if (xbasename) { // Replace the git+schema:// URL with the package name strlist_set(&dataptr, i, xbasename); @@ -247,27 +252,36 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { } } - if (DEFER_PIP == type && pip_index_provides(PYPI_INDEX_DEFAULT, name, version)) { - fprintf(stderr, "(%s present on index %s): ", version, PYPI_INDEX_DEFAULT); - ignore_pkg = 0; + int upstream_exists = 0; + if (DEFER_PIP == type) { + upstream_exists = pip_index_provides(PYPI_INDEX_DEFAULT, name); + } else if (DEFER_CONDA == type) { + upstream_exists = conda_provides(name); } else { - ignore_pkg = 1; + fprintf(stderr, "\nUnknown package type: %d\n", type); + exit(1); } + + if (upstream_exists < 0) { + fprintf(stderr, "%s's existence command failed for '%s'\n" + "(This may be due to a network/firewall issue!)\n", mode, name); + exit(1); + } + if (!upstream_exists) { + build_for_host = 1; + } else { + build_for_host = 0; + } + break; } } - if (ignore_pkg) { - char build_at[PATH_MAX]; - if (DEFER_CONDA == type) { - sprintf(build_at, "%s=%s", name, version); - name = build_at; - } - + if (build_for_host) { printf("BUILD FOR HOST\n"); strlist_append(&deferred, name); } else { - printf("USE EXISTING\n"); + printf("USE EXTERNAL\n"); strlist_append(&filtered, name); } } diff --git a/src/delivery_install.c b/src/delivery_install.c index 1ecedc5..a7754e8 100644 --- a/src/delivery_install.c +++ b/src/delivery_install.c @@ -1,11 +1,9 @@ #include "delivery.h" static struct Test *requirement_from_test(struct Delivery *ctx, const char *name) { - struct Test *result; - - result = NULL; + struct Test *result = NULL; for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(ctx->tests[0]); i++) { - if (ctx->tests[i].name && strstr(name, ctx->tests[i].name)) { + if (ctx->tests[i].name && !strcmp(name, ctx->tests[i].name)) { result = &ctx->tests[i]; break; } @@ -13,6 +11,101 @@ static struct Test *requirement_from_test(struct Delivery *ctx, const char *name return result; } +static char *have_spec_in_config(struct Delivery *ctx, const char *name) { + for (size_t x = 0; x < strlist_count(ctx->conda.pip_packages); x++) { + char *config_spec = strlist_item(ctx->conda.pip_packages, x); + char *op = find_version_spec(config_spec); + char package[255] = {0}; + if (op) { + strncpy(package, config_spec, op - config_spec); + } else { + strncpy(package, config_spec, sizeof(package) - 1); + } + if (strncmp(package, name, strlen(package)) == 0) { + return config_spec; + } + } + return NULL; +} + +int delivery_overlay_packages_from_env(struct Delivery *ctx, const char *env_name) { + char *current_env = conda_get_active_environment(); + int need_restore = current_env && strcmp(env_name, current_env) != 0; + + conda_activate(ctx->storage.conda_install_prefix, env_name); + // Retrieve a listing of python packages installed under "env_name" + int freeze_status = 0; + char *freeze_output = shell_output("python -m pip freeze", &freeze_status); + if (freeze_status) { + guard_free(freeze_output); + guard_free(current_env); + return -1; + } + + if (need_restore) { + // Restore the original conda environment + conda_activate(ctx->storage.conda_install_prefix, current_env); + } + guard_free(current_env); + + struct StrList *frozen_list = strlist_init(); + strlist_append_tokenize(frozen_list, freeze_output, LINE_SEP); + guard_free(freeze_output); + + struct StrList *new_list = strlist_init(); + + // - consume package specs that have no test blocks. + // - these will be third-party packages like numpy, scipy, etc. + // - and they need to be present at the head of the list so they + // get installed first. + for (size_t i = 0; i < strlist_count(ctx->conda.pip_packages); i++) { + char *spec = strlist_item(ctx->conda.pip_packages, i); + char spec_name[255] = {0}; + char *op = find_version_spec(spec); + if (op) { + strncpy(spec_name, spec, op - spec); + } else { + strncpy(spec_name, spec, sizeof(spec_name) - 1); + } + struct Test *test_block = requirement_from_test(ctx, spec_name); + if (!test_block) { + msg(STASIS_MSG_L2 | STASIS_MSG_WARN, "from config without test: %s\n", spec); + strlist_append(&new_list, spec); + } + } + + // now consume packages that have a test block + // if the ini provides a spec, override the environment's version. + // otherwise, use the spec derived from the environment + for (size_t i = 0; i < strlist_count(frozen_list); i++) { + char *frozen_spec = strlist_item(frozen_list, i); + char frozen_name[255] = {0}; + char *op = find_version_spec(frozen_spec); + // we only care about packages with specs here. if something else arrives, ignore it + if (op) { + strncpy(frozen_name, frozen_spec, op - frozen_spec); + } else { + strncpy(frozen_name, frozen_spec, sizeof(frozen_name) - 1); + } + struct Test *test = requirement_from_test(ctx, frozen_name); + if (test && strcmp(test->name, frozen_name) == 0) { + char *config_spec = have_spec_in_config(ctx, frozen_name); + if (config_spec) { + msg(STASIS_MSG_L2, "from config: %s\n", config_spec); + strlist_append(&new_list, config_spec); + } else { + msg(STASIS_MSG_L2, "from environment: %s\n", frozen_spec); + strlist_append(&new_list, frozen_spec); + } + } + } + guard_strlist_free(&ctx->conda.pip_packages); + ctx->conda.pip_packages = strlist_copy(new_list); + guard_strlist_free(&new_list); + guard_strlist_free(&frozen_list); + return 0; +} + int delivery_install_packages(struct Delivery *ctx, char *conda_install_dir, char *env_name, int type, struct StrList **manifest) { char cmd[PATH_MAX]; char pkgs[STASIS_BUFSIZ]; diff --git a/src/stasis_main.c b/src/stasis_main.c index b5d43e3..737fafc 100644 --- a/src/stasis_main.c +++ b/src/stasis_main.c @@ -496,11 +496,12 @@ int main(int argc, char *argv[]) { } msg(STASIS_MSG_L1, "Creating release environment(s)\n"); - if (ctx.meta.based_on && strlen(ctx.meta.based_on)) { + if (!isempty(ctx.meta.based_on)) { if (conda_env_remove(env_name)) { - msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "failed to remove release environment: %s\n", env_name_testing); - exit(1); + msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "failed to remove release environment: %s\n", env_name); + exit(1); } + msg(STASIS_MSG_L2, "Based on release: %s\n", ctx.meta.based_on); if (conda_env_create_from_uri(env_name, ctx.meta.based_on)) { msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "unable to install release environment using configuration file\n"); @@ -508,7 +509,7 @@ int main(int argc, char *argv[]) { } if (conda_env_remove(env_name_testing)) { - msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "failed to remove testing environment\n"); + msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "failed to remove testing environment %s\n", env_name_testing); exit(1); } if (conda_env_create_from_uri(env_name_testing, ctx.meta.based_on)) { @@ -544,10 +545,18 @@ int main(int argc, char *argv[]) { } if (pip_exec("install build")) { - msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "'build' tool installation failed"); + msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "'build' tool installation failed\n"); exit(1); } + if (!isempty(ctx.meta.based_on)) { + msg(STASIS_MSG_L1, "Generating package overlay from environment: %s\n", env_name); + if (delivery_overlay_packages_from_env(&ctx, env_name)) { + msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "%s", "Failed to generate package overlay. Resulting environment integrity cannot be guaranteed.\n"); + exit(1); + } + } + msg(STASIS_MSG_L1, "Filter deliverable packages\n"); delivery_defer_packages(&ctx, DEFER_CONDA); delivery_defer_packages(&ctx, DEFER_PIP); @@ -10,7 +10,7 @@ always_update_base_environment = false conda_fresh_start = true ; (string) Install conda in a custom prefix -; DEFAULT: Conda will be installed under stasis/conda +; DEFAULT: Conda will be installed under stasis/tools/conda ; NOTE: conda_fresh_start will automatically be set to "false" ;conda_install_prefix = /path/to/conda diff --git a/tests/data/generic.ini b/tests/data/generic.ini index 7d77edd..fd67ed7 100644 --- a/tests/data/generic.ini +++ b/tests/data/generic.ini @@ -50,7 +50,7 @@ dest = {{ meta.mission }}/{{ info.build_name }}/ [deploy:docker] -;registry = bytesalad.stsci.edu +registry = bytesalad.stsci.edu image_compression = zstd -v -9 -c build_args = SNAPSHOT_INPUT={{ info.release_name }}.yml |