diff options
Diffstat (limited to 'src/deliverable.c')
-rw-r--r-- | src/deliverable.c | 626 |
1 files changed, 626 insertions, 0 deletions
diff --git a/src/deliverable.c b/src/deliverable.c new file mode 100644 index 0000000..15fcb44 --- /dev/null +++ b/src/deliverable.c @@ -0,0 +1,626 @@ +// +// Created by jhunk on 10/5/23. +// + +#include "deliverable.h" +#include "str.h" +#include "strlist.h" +#include "wheel.h" + +#define getter(XINI, SECTION_NAME, KEY, TYPE) \ + { \ + if (ini_getval(XINI, SECTION_NAME, KEY, TYPE, &val)) { \ + fprintf(stderr, "%s:%s not defined\n", SECTION_NAME, KEY); \ + } \ + } + +#define conv_int(X, DEST) X->DEST = val.as_int; +#define conv_str(X, DEST) X->DEST = runtime_expand_var(NULL, val.as_char_p); +#define conv_str_noexpand(X, DEST) X->DEST = val.as_char_p; +#define conv_strlist(X, DEST, TOK) { \ + runtime_expand_var(NULL, val.as_char_p); \ + if (!X->DEST) \ + X->DEST = strlist_init(); \ + strlist_append_tokenize(X->DEST, val.as_char_p, TOK); \ +} +#define conv_bool(X, DEST) X->DEST = val.as_bool; + +void delivery_init_dirs(struct Delivery *ctx) { + mkdir("build", 0755); + mkdir("build/recipes", 0755); + mkdir("build/sources", 0755); + mkdir("build/testing", 0755); + ctx->storage.build_dir = realpath("build", NULL); + ctx->storage.build_recipes_dir = realpath("build/recipes", NULL); + ctx->storage.build_sources_dir = realpath("build/sources", NULL); + ctx->storage.build_testing_dir = realpath("build/testing", NULL); + + mkdir("output", 0755); + mkdir("output/omc", 0755); + mkdir("output/packages", 0755); + mkdir("output/packages/conda", 0755); + mkdir("output/packages/wheels", 0755); + ctx->storage.delivery_dir = realpath("output/omc", NULL); + ctx->storage.conda_artifact_dir = realpath("output/packages/conda", NULL); + ctx->storage.wheel_artifact_dir = realpath("output/packages/wheels", NULL); + + mkdir(CONDA_INSTALL_PREFIX, 0755); + ctx->storage.conda_install_prefix = realpath(CONDA_INSTALL_PREFIX, NULL); +} + +int delivery_init(struct Delivery *ctx, struct INIFILE *ini, struct INIFILE *cfg) { + RuntimeEnv *rt; + struct INIData *rtdata; + union INIVal val; + + if (cfg) { + getter(cfg, "default", "conda_staging_dir", INIVAL_TYPE_STR); + conv_str(ctx, storage.conda_staging_dir); + getter(cfg, "default", "conda_staging_url", INIVAL_TYPE_STR); + conv_str(ctx, storage.conda_staging_url); + getter(cfg, "default", "wheel_staging_dir", INIVAL_TYPE_STR); + conv_str(ctx, storage.wheel_staging_dir); + getter(cfg, "default", "wheel_staging_url", INIVAL_TYPE_STR); + conv_str(ctx, storage.wheel_staging_url); + } + delivery_init_dirs(ctx); + + // Populate runtime variables first they may be interpreted by other + // keys in the configuration + rt = runtime_copy(__environ); + while ((rtdata = ini_getall(ini, "runtime")) != NULL) { + char rec[BUFSIZ]; + sprintf(rec, "%s=%s", lstrip(strip(rtdata->key)), lstrip(strip(rtdata->value))); + runtime_set(rt, rtdata->key, rtdata->value); + } + runtime_apply(rt); + ctx->runtime.environ = rt; + + getter(ini, "meta", "mission", INIVAL_TYPE_STR) + conv_str(ctx, meta.mission) + + if (!strcasecmp(ctx->meta.mission, "hst")) { + getter(ini, "meta", "codename", INIVAL_TYPE_STR) + conv_str(ctx, meta.codename) + } else { + ctx->meta.codename = NULL; + } + + if (!strcasecmp(ctx->meta.mission, "jwst")) { + getter(ini, "meta", "version", INIVAL_TYPE_STR) + conv_str(ctx, meta.version) + + } else { + ctx->meta.version = NULL; + } + + getter(ini, "meta", "name", INIVAL_TYPE_STR) + conv_str(ctx, meta.name) + + getter(ini, "meta", "rc", INIVAL_TYPE_INT) + conv_int(ctx, meta.rc) + + getter(ini, "meta", "final", INIVAL_TYPE_BOOL) + conv_bool(ctx, meta.final) + + getter(ini, "meta", "continue_on_error", INIVAL_TYPE_BOOL) + conv_bool(ctx, meta.continue_on_error) + + getter(ini, "meta", "based_on", INIVAL_TYPE_STR) + conv_str(ctx, meta.based_on) + + getter(ini, "meta", "python", INIVAL_TYPE_STR) + conv_str(ctx, meta.python) + + getter(ini, "conda", "installer_baseurl", INIVAL_TYPE_STR) + conv_str(ctx, conda.installer_baseurl) + + getter(ini, "conda", "installer_name", INIVAL_TYPE_STR) + conv_str(ctx, conda.installer_name) + + getter(ini, "conda", "installer_version", INIVAL_TYPE_STR) + conv_str(ctx, conda.installer_version) + + getter(ini, "conda", "installer_platform", INIVAL_TYPE_STR) + conv_str(ctx, conda.installer_platform) + + getter(ini, "conda", "installer_arch", INIVAL_TYPE_STR) + conv_str(ctx, conda.installer_arch) + + getter(ini, "conda", "conda_packages", INIVAL_TYPE_STR_ARRAY) + conv_strlist(ctx, conda.conda_packages, "\n") + + getter(ini, "conda", "pip_packages", INIVAL_TYPE_STR_ARRAY) + conv_strlist(ctx, conda.pip_packages, "\n") + + ctx->conda.conda_packages_defer = strlist_init(); + ctx->conda.pip_packages_defer = strlist_init(); + + for (size_t z = 0, i = 0; i < ini->section_count; i++ ) { + if (startswith(ini->section[i]->key, "test:")) { + val.as_char_p = strchr(ini->section[i]->key, ':') + 1; + conv_str(ctx, tests[z].name) + + getter(ini, ini->section[i]->key, "version", INIVAL_TYPE_STR) + conv_str(ctx, tests[z].version) + + getter(ini, ini->section[i]->key, "repository", INIVAL_TYPE_STR) + conv_str(ctx, tests[z].repository) + + getter(ini, ini->section[i]->key, "script", INIVAL_TYPE_STR) + conv_str_noexpand(ctx, tests[z].script) + + getter(ini, ini->section[i]->key, "build_recipe", INIVAL_TYPE_STR); + conv_str(ctx, tests[z].build_recipe) + + z++; + } + } + return 0; +} + +void delivery_meta_show(struct Delivery *ctx) { + printf("====DELIVERY====\n"); + printf("%-20s %-10s\n", "Target Python:", ctx->meta.python); + printf("%-20s %-10s\n", "Name:", ctx->meta.name); + printf("%-20s %-10s\n", "Mission:", ctx->meta.mission); + if (ctx->meta.codename) { + printf("%-20s %-10s\n", "Codename:", ctx->meta.codename); + } + if (ctx->meta.version) { + printf("%-20s %-10s\n", "Version", ctx->meta.version); + } + if (!ctx->meta.final) { + printf("%-20s %-10d\n", "RC Level:", ctx->meta.rc); + } + printf("%-20s %-10s\n", "Final Release:", ctx->meta.final ? "Yes" : "No"); + printf("%-20s %-10s\n", "Based On:", ctx->meta.based_on ? ctx->meta.based_on : "New"); +} + +void delivery_conda_show(struct Delivery *ctx) { + char data[BUFSIZ]; + char *datap = data; + + printf("====CONDA====\n"); + printf("%-20s %-10s\n", "Installer:", ctx->conda.installer_baseurl); + + puts("Native Packages:"); + for (size_t i = 0; i < strlist_count(ctx->conda.conda_packages); i++) { + char *token = strlist_item(ctx->conda.conda_packages, i); + if (isempty(token) || isblank(*token) || startswith(token, "-")) { + continue; + } + printf("%21s%s\n", "", token); + } + + puts("PyPi Packages:"); + for (size_t i = 0; i < strlist_count(ctx->conda.pip_packages); i++) { + char *token = strlist_item(ctx->conda.pip_packages, i); + if (isempty(token) || isblank(*token) || startswith(token, "-")) { + continue; + } + printf("%21s%s\n", "", token); + } +} + +void delivery_tests_show(struct Delivery *ctx) { + printf("====TESTS====\n"); + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(ctx->tests[0]); i++) { + if (!ctx->tests[i].name) { + continue; + } + printf("%-20s %-10s %s\n", ctx->tests[i].name, + ctx->tests[i].version, + ctx->tests[i].repository); + } +} + +int delivery_build_recipes(struct Delivery *ctx) { + char *recipe_dir = NULL; + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(ctx->tests[0]); i++) { + if (ctx->tests[i].build_recipe) { // build a conda recipe + int recipe_type; + int status; + if (recipe_clone(ctx->storage.build_recipes_dir, ctx->tests[i].build_recipe, NULL, &recipe_dir)) { + fprintf(stderr, "Encountered an issue while cloning recipe for: %s\n", ctx->tests[i].name); + return -1; + } + recipe_type = recipe_get_type(recipe_dir); + pushd(recipe_dir); + { + if (RECIPE_TYPE_ASTROCONDA == recipe_type) { + pushd(path_basename(ctx->tests[i].repository)); + } else if (RECIPE_TYPE_CONDA_FORGE == recipe_type) { + pushd("recipe"); + } + + char recipe_version[100]; + char recipe_buildno[100]; + char recipe_git_url[PATH_MAX]; + char recipe_git_rev[PATH_MAX]; + + sprintf(recipe_version, "{%% set version = GIT_DESCRIBE_TAG ~ \".dev\" ~ GIT_DESCRIBE_NUMBER ~ \"+\" ~ GIT_DESCRIBE_HASH %%}"); + sprintf(recipe_git_url, " git_url: %s", ctx->tests[i].repository); + sprintf(recipe_git_rev, " git_rev: %s", ctx->tests[i].version); + sprintf(recipe_buildno, " number: 0"); + + //file_replace_text("meta.yaml", "{% set version = ", recipe_version); + if (ctx->meta.final) { + sprintf(recipe_version, "{%% set version = \"%s\" %%}", ctx->tests[i].version); + // TODO: replace sha256 of tagged archive + // TODO: leave the recipe unchanged otherwise. in theory this should produce the same conda package hash as conda forge. + // For now, remove the sha256 requirement + file_replace_text("meta.yaml", " sha256:", "\n"); + } else { + file_replace_text("meta.yaml", "{% set version = ", recipe_version); + file_replace_text("meta.yaml", " url:", recipe_git_url); + file_replace_text("meta.yaml", " sha256:", recipe_git_rev); + file_replace_text("meta.yaml", " number:", recipe_buildno); + } + + char command[PATH_MAX]; + sprintf(command, "build --python=%s .", ctx->meta.python); + status = conda_exec(command); + if (status) { + fprintf(stderr, "failed to build deployment artifact: %s\n", ctx->tests[i].build_recipe); + msg(OMC_MSG_WARN | OMC_MSG_L1, "ENTERING DEBUG SHELL\n"); + system("bash --noprofile --norc"); + exit(1); + if (!ctx->meta.continue_on_error) { + return -1; + } + } + + if (RECIPE_TYPE_GENERIC != recipe_type) { + popd(); + } + popd(); + } + } + } + return 0; +} + +struct StrList *delivery_build_wheels(struct Delivery *ctx) { + struct StrList *result = NULL; + struct Process proc; + memset(&proc, 0, sizeof(proc)); + + result = strlist_init(); + if (!result) { + perror("unable to allocate memory for string list"); + result = NULL; + return NULL; + } + + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(ctx->tests[0]); i++) { + if (!ctx->tests[i].build_recipe && ctx->tests[i].repository) { // build from source + char srcdir[PATH_MAX]; + char wheeldir[PATH_MAX]; + memset(srcdir, 0, sizeof(srcdir)); + memset(wheeldir, 0, sizeof(wheeldir)); + + sprintf(srcdir, "%s/%s", ctx->storage.build_sources_dir, ctx->tests[i].name); + git_clone(&proc, ctx->tests[i].repository, srcdir, ctx->tests[i].version); + pushd(srcdir); + { + if (python_exec("-m build -w ")) { + fprintf(stderr, "failed to generate wheel package for %s-%s\n", ctx->tests[i].name, ctx->tests[i].version); + if (!ctx->meta.continue_on_error) { + strlist_free(result); + result = NULL; + return NULL; + } + } else { + DIR *dp; + struct dirent *rec; + dp = opendir("dist"); + if (!dp) { + fprintf(stderr, "wheel artifact directory does not exist: %s\n", ctx->storage.wheel_artifact_dir); + strlist_free(result); + return NULL; + } + + while ((rec = readdir(dp)) != NULL) { + if (strstr(rec->d_name, ctx->tests[i].name)) { + strlist_append(result, rec->d_name); + } + } + + } + popd(); + } + } + } + return result; +} + +static char *requirement_from_test(struct Delivery *ctx, const char *name) { + static char result[PATH_MAX]; + memset(result, 0, sizeof(result)); + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(ctx->tests[0]); i++) { + if (!strcmp(ctx->tests[i].name, name)) { + sprintf(result, "git+%s@%s", + ctx->tests[i].repository, + ctx->tests[i].version); + break; + } + } + if (!strlen(result)) { + return NULL; + } + return result; +} + +void delivery_install_packages(struct Delivery *ctx, char *conda_install_dir, char *env_name, int type, struct StrList **manifest) { + char cmd[PATH_MAX]; + char pkgs[BUFSIZ]; + char *env_current = getenv("CONDA_DEFAULT_ENV"); + + if (env_current) { + // The requested environment is not the current environment + if (strcmp(env_current, env_name) != 0) { + // Activate the requested environment + printf("Activating: %s\n", env_name); + conda_activate(conda_install_dir, env_name); + //runtime_replace(&ctx->runtime.environ, __environ); + } + } + + memset(cmd, 0, sizeof(cmd)); + memset(pkgs, 0, sizeof(pkgs)); + strcat(cmd, "install"); + + typedef int (*Runner)(const char *); + Runner runner = NULL; + if (INSTALL_PKG_CONDA & type) { + runner = conda_exec; + } else if (INSTALL_PKG_PIP & type) { + runner = pip_exec; + } + + if (INSTALL_PKG_CONDA_DEFERRED & type) { + strcat(cmd, " --use-local"); + } else if (INSTALL_PKG_PIP_DEFERRED & type) { + strcat(cmd, " --upgrade"); + } + + for (size_t x = 0; manifest[x] != NULL; x++) { + char *name = NULL; + for (size_t p = 0; p < strlist_count(manifest[x]); p++) { + name = strlist_item(manifest[x], p); + strip(name); + if (INSTALL_PKG_PIP_DEFERRED & type) { + //DIR *dp; + //struct dirent *rec; + + //dp = opendir(ctx->storage.wheel_artifact_dir); + //if (!dp) { + // perror(ctx->storage.wheel_artifact_dir); + // exit(1); + //} + + //char pyver_compact[100]; + //sprintf(pyver_compact, "-cp%s", ctx->meta.python); + //strchrdel(pyver_compact, "."); + //while ((rec = readdir(dp)) != NULL) { + // struct Wheel *wheelfile = NULL; + // if (!strcmp(rec->d_name, ".") || !strcmp(rec->d_name, "..")) { + // continue; + // } + // if (DT_DIR == rec->d_type && startswith(rec->d_name, name)) { + // wheelfile = get_wheel_file(ctx->storage.wheel_artifact_dir, name, (char *[]) {pyver_compact, NULL}); + // if (wheelfile) { + // sprintf(cmd + strlen(cmd), " %s/%s", wheelfile->path_name, wheelfile->file_name); + // free(wheelfile); + // break; + // } + // } + //} + //closedir(dp); + char *requirement = requirement_from_test(ctx, name); + if (requirement) { + sprintf(cmd + strlen(cmd), " '%s'", requirement); + } + + } else { + if (startswith(name, "--") || startswith(name, "-")) { + sprintf(cmd + strlen(cmd), " %s", name); + } else { + sprintf(cmd + strlen(cmd), " '%s'", name); + } + } + } + if (runner(cmd)) { + fprintf(stderr, "failed to install package: %s\n", name); + exit(1); + } + } +} + +void delivery_get_installer_url(struct Delivery *delivery, char *result) { + if (delivery->conda.installer_version) { + // Use version specified by configuration file + sprintf(result, "%s/%s-%s-%s-%s.sh", delivery->conda.installer_baseurl, + delivery->conda.installer_name, + delivery->conda.installer_version, + delivery->conda.installer_platform, + delivery->conda.installer_arch); + } else { + // Use latest installer + sprintf(result, "%s/%s-%s-%s.sh", delivery->conda.installer_baseurl, + delivery->conda.installer_name, + delivery->conda.installer_platform, + delivery->conda.installer_arch); + } + +} + +void delivery_get_installer(char *installer_url) { + if (access(path_basename(installer_url), F_OK)) { + if (download(installer_url, path_basename(installer_url))) { + fprintf(stderr, "download failed: %s\n", installer_url); + exit(1); + } + } +} + +int delivery_copy_conda_artifacts(struct Delivery *ctx) { + char cmd[PATH_MAX]; + char conda_build_dir[PATH_MAX]; + char subdir[PATH_MAX]; + memset(cmd, 0, sizeof(cmd)); + memset(conda_build_dir, 0, sizeof(conda_build_dir)); + memset(subdir, 0, sizeof(subdir)); + + sprintf(conda_build_dir, "%s/%s", ctx->storage.conda_install_prefix, "conda-bld"); + if (access(conda_build_dir, F_OK) < 0) { + // Conda build was never executed + return 0; + } + + snprintf(cmd, sizeof(cmd) - 1, "rsync -avi --progress %s/%s %s", + conda_build_dir, + ctx->system.platform[DELIVERY_PLATFORM_CONDA_SUBDIR], + ctx->storage.conda_artifact_dir); + + return system(cmd); +} + +int delivery_copy_wheel_artifacts(struct Delivery *ctx) { + char cmd[PATH_MAX]; + memset(cmd, 0, sizeof(cmd)); + snprintf(cmd, sizeof(cmd) - 1, "rsync -avi --progress %s/*/dist/*.whl %s", + ctx->storage.build_sources_dir, + ctx->storage.wheel_artifact_dir); + return system(cmd); +} + +int delivery_index_wheel_artifacts(struct Delivery *ctx) { + struct dirent *rec; + DIR *dp; + dp = opendir(ctx->storage.wheel_artifact_dir); + if (!dp) { + return -1; + } + + while ((rec = readdir(dp)) != NULL) { + // skip directories + if (DT_DIR == rec->d_type || !endswith(rec->d_name, ".whl")) { + continue; + } + char name[NAME_MAX]; + strcpy(name, rec->d_name); + char **parts = split(name, "-", 1); + strcpy(name, parts[0]); + split_free(parts); + + tolower_s(name); + char path_dest[PATH_MAX]; + sprintf(path_dest, "%s/%s/", ctx->storage.wheel_artifact_dir, name); + mkdir(path_dest, 0755); + sprintf(path_dest + strlen(path_dest), "%s", rec->d_name); + + char path_src[PATH_MAX]; + sprintf(path_src, "%s/%s", ctx->storage.wheel_artifact_dir, rec->d_name); + rename(path_src, path_dest); + } + return 0; +} + +void delivery_rewrite_spec(struct Delivery *ctx, char *filename) { + char *package_name = NULL; + char output[PATH_MAX]; + + sprintf(output, " - %s", ctx->storage.conda_staging_url); + file_replace_text(filename, " - local", output); + for (size_t i = 0; i < strlist_count(ctx->conda.pip_packages_defer); i++) { + package_name = strlist_item(ctx->conda.pip_packages_defer, i); + char target[PATH_MAX]; + char replacement[PATH_MAX]; + struct Wheel *wheelfile; + + memset(target, 0, sizeof(target)); + memset(replacement, 0, sizeof(replacement)); + sprintf(target, " - %s", package_name); + // TODO: I still want to use wheels for this but tagging semantics are getting in the way. + // When someone pushes a lightweight tag setuptools_scm will not resolve the expected + // refs unless the following is present in pyproject.toml, setup.cfg, or setup.py: + // + // git_describe_command = "git describe --tags" # at the bare minimum + // + + //char abi[NAME_MAX]; + //strcpy(abi, ctx->meta.python); + //strchrdel(abi, "."); + + //char source_dir[PATH_MAX]; + //sprintf(source_dir, "%s/%s", ctx->storage.build_sources_dir, package_name); + //wheelfile = get_wheel_file(ctx->storage.wheel_artifact_dir, package_name, (char *[]) {git_describe(source_dir), abi, ctx->system.arch, NULL}); + //if (wheelfile) { + // sprintf(replacement, " - %s/%s", ctx->storage.wheel_staging_url, wheelfile->file_name); + // file_replace_text(filename, target, replacement); + //} + // end of TODO + + char *requirement = requirement_from_test(ctx, package_name); + if (requirement) { + sprintf(replacement, " - %s", requirement); + file_replace_text(filename, target, replacement); + } else { + fprintf(stderr, "an error occurred while rewriting a release artifact: %s\n", filename); + fprintf(stderr, "mapping a replacement value for package defined by '[test:%s]' failed: %s\n", package_name, package_name); + fprintf(stderr, "target string in artifact was:\n%s\n", target); + exit(1); + } + } +} + +int delivery_index_conda_artifacts(struct Delivery *ctx) { + return conda_index(ctx->storage.conda_artifact_dir); +} + +void delivery_tests_run(struct Delivery *ctx) { + struct Process proc; + if (!ctx->tests[0].name) { + msg(OMC_MSG_WARN | OMC_MSG_L2, "no tests are defined!\n"); + } else { + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(ctx->tests[0]); i++) { + if (!ctx->tests[i].name && !ctx->tests[i].repository && !ctx->tests[i].script) { + // unused entry + continue; + } + msg(OMC_MSG_L2, "%s %s\n", ctx->tests[i].name, ctx->tests[i].version); + if (!ctx->tests[i].script || !strlen(ctx->tests[i].script)) { + msg(OMC_MSG_WARN | OMC_MSG_L3, "Nothing to do. To fix, declare a 'script' in section: [test:%s]\n", + ctx->tests[i].name); + continue; + } + + char destdir[PATH_MAX]; + sprintf(destdir, "%s/%s", ctx->storage.build_sources_dir, path_basename(ctx->tests[i].repository)); + + msg(OMC_MSG_L3, "Cloning %s\n", ctx->tests[i].repository); + git_clone(&proc, ctx->tests[i].repository, destdir, ctx->tests[i].version); + + if (pushd(destdir) && !ctx->meta.continue_on_error) { + fprintf(stderr, "unable to enter repository directory\n"); + exit(1); + } else { +#if 1 + msg(OMC_MSG_L3, "Running\n"); + memset(&proc, 0, sizeof(proc)); + if (shell2(&proc, ctx->tests[i].script) && !ctx->meta.continue_on_error) { + fprintf(stderr, "continue on error is not enabled. aborting.\n"); + exit(1); + } + popd(); +#else + msg(OMC_MSG_WARNING | OMC_MSG_L3, "TESTING DISABLED BY CODE!\n"); +#endif + } + } + } + + +} |