diff options
Diffstat (limited to 'src/lib/index')
-rw-r--r-- | src/lib/index/CMakeLists.txt | 19 | ||||
-rw-r--r-- | src/lib/index/args.c | 38 | ||||
-rw-r--r-- | src/lib/index/helpers.c | 343 | ||||
-rw-r--r-- | src/lib/index/include/args.h | 9 | ||||
-rw-r--r-- | src/lib/index/include/helpers.h | 27 | ||||
-rw-r--r-- | src/lib/index/include/index.h | 14 | ||||
-rw-r--r-- | src/lib/index/include/index_callbacks.h | 9 | ||||
-rw-r--r-- | src/lib/index/include/junitxml_report.h | 8 | ||||
-rw-r--r-- | src/lib/index/include/readmes.h | 8 | ||||
-rw-r--r-- | src/lib/index/include/website.h | 8 | ||||
-rw-r--r-- | src/lib/index/index_callbacks.c | 41 | ||||
-rw-r--r-- | src/lib/index/junitxml_report.c | 148 | ||||
-rw-r--r-- | src/lib/index/readmes.c | 113 | ||||
-rw-r--r-- | src/lib/index/stasis_index_entrypoint.c | 402 | ||||
-rw-r--r-- | src/lib/index/website.c | 68 |
15 files changed, 1255 insertions, 0 deletions
diff --git a/src/lib/index/CMakeLists.txt b/src/lib/index/CMakeLists.txt new file mode 100644 index 0000000..a647d0d --- /dev/null +++ b/src/lib/index/CMakeLists.txt @@ -0,0 +1,19 @@ +add_library(stasis_index STATIC + args.c + helpers.c + index_callbacks.c + junitxml_report.c + readmes.c + stasis_index_entrypoint.c + website.c + +) +target_link_libraries(stasis_index PRIVATE + stasis_delivery +) +target_include_directories(stasis_index PRIVATE + ${core_INCLUDE} + ${delivery_INCLUDE} + ${CMAKE_CURRENT_SOURCE_DIR}/include +) +target_link_libraries(stasis_index PUBLIC LibXml2::LibXml2) diff --git a/src/lib/index/args.c b/src/lib/index/args.c new file mode 100644 index 0000000..2d92ab0 --- /dev/null +++ b/src/lib/index/args.c @@ -0,0 +1,38 @@ +#include "core.h" +#include "args.h" + +struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"destdir", required_argument, 0, 'd'}, + {"verbose", no_argument, 0, 'v'}, + {"unbuffered", no_argument, 0, 'U'}, + {"web", no_argument, 0, 'w'}, + {0, 0, 0, 0}, +}; + +const char *long_options_help[] = { + "Display this usage statement", + "Destination directory", + "Increase output verbosity", + "Disable line buffering", + "Generate HTML indexes (requires pandoc)", + NULL, +}; + +void usage(char *name) { + const int maxopts = sizeof(long_options) / sizeof(long_options[0]); + char *opts = calloc(maxopts + 1, sizeof(char)); + for (int i = 0; i < maxopts; i++) { + opts[i] = (char) long_options[i].val; + } + printf("usage: %s [-%s] {{STASIS_ROOT}...}\n", name, opts); + guard_free(opts); + + for (int i = 0; i < maxopts - 1; i++) { + char line[255] = {0}; + sprintf(line, " --%s -%c %-20s", long_options[i].name, long_options[i].val, long_options_help[i]); + puts(line); + } + +} + diff --git a/src/lib/index/helpers.c b/src/lib/index/helpers.c new file mode 100644 index 0000000..5ae01ca --- /dev/null +++ b/src/lib/index/helpers.c @@ -0,0 +1,343 @@ +// +// Created by jhunk on 11/15/24. +// + +#include "core.h" +#include "helpers.h" + +struct StrList *get_architectures(struct Delivery ctx[], const size_t nelem) { + struct StrList *architectures = strlist_init(); + for (size_t i = 0; i < nelem; i++) { + if (ctx[i].system.arch) { + if (!strstr_array(architectures->data, ctx[i].system.arch)) { + strlist_append(&architectures, ctx[i].system.arch); + } + } + } + return architectures; +} + +struct StrList *get_platforms(struct Delivery ctx[], const size_t nelem) { + struct StrList *platforms = strlist_init(); + for (size_t i = 0; i < nelem; i++) { + if (ctx[i].system.platform) { + if (!strstr_array(platforms->data, ctx[i].system.platform[DELIVERY_PLATFORM_RELEASE])) { + strlist_append(&platforms, ctx[i].system.platform[DELIVERY_PLATFORM_RELEASE]); + } + } + } + return platforms; +} + +int get_pandoc_version(size_t *result) { + *result = 0; + int state = 0; + char *version_str = shell_output("pandoc --version", &state); + if (state || !version_str) { + // an error occurred + guard_free(version_str); + return -1; + } + + // Verify that we're looking at pandoc + if (strlen(version_str) > 7 && !strncmp(version_str, "pandoc ", 7)) { + // we have pandoc + char *v_begin = &version_str[7]; + if (!v_begin) { + SYSERROR("unexpected pandoc output: %s", version_str); + guard_free(version_str); + return -1; + } + char *v_end = strchr(version_str, '\n'); + if (v_end) { + *v_end = 0; + } + + char **parts = split(v_begin, ".", 0); + if (!parts) { + SYSERROR("unable to split pandoc version string, '%s': %s", version_str, strerror(errno)); + guard_free(version_str); + return -1; + } + + size_t parts_total; + for (parts_total = 0; parts[parts_total] != NULL; parts_total++) {} + + // generate the version as an integer + // note: pandoc version scheme never exceeds four elements (or bytes in this case) + for (size_t i = 0; i < 4; i++) { + unsigned char tmp = 0; + if (i < parts_total) { + // only process version elements we have. the rest will be zeros. + tmp = strtoul(parts[i], NULL, 10); + } + // pack version element into result + *result = *result << 8 | tmp; + } + GENERIC_ARRAY_FREE(parts); + } else { + // invalid version string + guard_free(version_str); + return 1; + } + + guard_free(version_str); + return 0; +} + +int pandoc_exec(const char *in_file, const char *out_file, const char *css_file, const char *title) { + if (!find_program("pandoc")) { + fprintf(stderr, "pandoc is not installed: unable to generate HTML indexes\n"); + return 0; + } + + char pandoc_versioned_args[255] = {0}; + size_t pandoc_version = 0; + if (!get_pandoc_version(&pandoc_version)) { + // < 2.19 + if (pandoc_version < 0x02130000) { + strcat(pandoc_versioned_args, "--self-contained "); + } else { + // >= 2.19 + strcat(pandoc_versioned_args, "--embed-resources "); + } + + // >= 1.15.0.4 + if (pandoc_version >= 0x010f0004) { + strcat(pandoc_versioned_args, "--standalone "); + } + + // >= 1.10.0.1 + if (pandoc_version >= 0x010a0001) { + strcat(pandoc_versioned_args, "-f gfm+autolink_bare_uris "); + } + + // > 3.1.9 + if (pandoc_version > 0x03010900) { + strcat(pandoc_versioned_args, "-f gfm+alerts "); + } + } + + // Converts a markdown file to html + char cmd[STASIS_BUFSIZ] = {0}; + strcpy(cmd, "pandoc "); + strcat(cmd, pandoc_versioned_args); + if (css_file && strlen(css_file)) { + strcat(cmd, "--css "); + strcat(cmd, css_file); + } + strcat(cmd, " "); + strcat(cmd, "--metadata title=\""); + strcat(cmd, title); + strcat(cmd, "\" "); + strcat(cmd, "-o "); + strcat(cmd, out_file); + strcat(cmd, " "); + strcat(cmd, in_file); + + if (globals.verbose) { + puts(cmd); + } + + // This might be negative when killed by a signal. + return system(cmd); +} + + +int micromamba_configure(const struct Delivery *ctx, struct MicromambaInfo *m) { + int status = 0; + char *micromamba_prefix = NULL; + if (asprintf(µmamba_prefix, "%s/bin", ctx->storage.tools_dir) < 0) { + return -1; + } + m->conda_prefix = globals.conda_install_prefix; + m->micromamba_prefix = micromamba_prefix; + + const size_t pathvar_len = strlen(getenv("PATH")) + strlen(m->micromamba_prefix) + strlen(m->conda_prefix) + 3 + 4 + 1; + // ^^^^^^^^^^^^^^^^^^ + // 3 = separators + // 4 = chars (/bin) + // 1 = nul terminator + char *pathvar = calloc(pathvar_len, sizeof(*pathvar)); + if (!pathvar) { + SYSERROR("%s", "Unable to allocate bytes for temporary path string"); + exit(1); + } + snprintf(pathvar, pathvar_len, "%s/bin:%s:%s", m->conda_prefix, m->micromamba_prefix, getenv("PATH")); + setenv("PATH", pathvar, 1); + guard_free(pathvar); + + status += micromamba(m, "config prepend --env channels conda-forge"); + if (!globals.verbose) { + status += micromamba(m, "config set --env quiet true"); + } + status += micromamba(m, "config set --env always_yes true"); + status += micromamba(m, "install conda-build pandoc"); + + return status; +} + +int get_latest_rc(struct Delivery ctx[], const size_t nelem) { + int result = 0; + for (size_t i = 0; i < nelem; i++) { + if (ctx[i].meta.rc > result) { + result = ctx[i].meta.rc; + } + } + return result; +} + +int sort_by_latest_rc(const void *a, const void *b) { + const struct Delivery *aa = a; + const struct Delivery *bb = b; + if (aa->meta.rc > bb->meta.rc) { + return -1; + } + if (aa->meta.rc < bb->meta.rc) { + return 1; + } + return 0; +} + +struct Delivery *get_latest_deliveries(struct Delivery ctx[], size_t nelem) { + int latest = 0; + size_t n = 0; + + struct Delivery *result = calloc(nelem + 1, sizeof(*result)); + if (!result) { + fprintf(stderr, "Unable to allocate %zu bytes for result delivery array: %s\n", nelem * sizeof(*result), strerror(errno)); + return NULL; + } + + latest = get_latest_rc(ctx, nelem); + qsort(ctx, nelem, sizeof(*ctx), sort_by_latest_rc); + for (size_t i = 0; i < nelem; i++) { + if (ctx[i].meta.rc == latest) { + result[n] = ctx[i]; + n++; + } + } + return result; +} + +int get_files(struct StrList **out, const char *path, const char *pattern, ...) { + va_list args; + va_start(args, pattern); + char userpattern[PATH_MAX] = {0}; + vsprintf(userpattern, pattern, args); + va_end(args); + if (!strlen(userpattern)) { + userpattern[0] = '*'; + userpattern[1] = 0; + } + struct StrList *list = listdir(path); + if (!list) { + return -1; + } + + if (!*out) { + *out = strlist_init(); + if (!*out) { + guard_strlist_free(&list); + return -1; + } + } + + size_t no_match = 0; + for (size_t i = 0; i < strlist_count(list); i++) { + char *item = strlist_item(list, i); + if (fnmatch(userpattern, item, 0)) { + no_match++; + } else { + strlist_append(out, item); + } + } + if (no_match >= strlist_count(list)) { + fprintf(stderr, "no files matching the pattern: %s\n", userpattern); + guard_strlist_free(&list); + return -1; + } + guard_strlist_free(&list); + return 0; +} + +struct StrList *get_docker_images(struct Delivery *ctx, char *pattern) { + char *tarball = NULL; + asprintf(&tarball, "%s*.tar*", pattern); + if (!tarball) { + SYSERROR("%s", "Unable to allocate bytes for docker image wildcard pattern"); + return NULL; + } + tolower_s(tarball); + replace_text(tarball, "+", "-", 0); + + struct StrList *files = NULL; + get_files(&files, ctx->storage.docker_artifact_dir, tarball); + guard_free(tarball); + + return files; +} + +int load_metadata(struct Delivery *ctx, const char *filename) { + char line[STASIS_NAME_MAX] = {0}; + + FILE *fp = fopen(filename, "r"); + if (!fp) { + return -1; + } + + while (fgets(line, sizeof(line) - 1, fp) != NULL) { + char **parts = split(line, " ", 1); + const char *name = parts[0]; + char *value = parts[1]; + + strip(value); + if (!strcmp(name, "name")) { + ctx->meta.name = strdup(value); + } else if (!strcmp(name, "version")) { + ctx->meta.version = strdup(value); + } else if (!strcmp(name, "rc")) { + ctx->meta.rc = (int) strtol(value, NULL, 10); + } else if (!strcmp(name, "python")) { + ctx->meta.python = strdup(value); + } else if (!strcmp(name, "python_compact")) { + ctx->meta.python_compact = strdup(value); + } else if (!strcmp(name, "mission")) { + ctx->meta.mission = strdup(value); + } else if (!strcmp(name, "codename")) { + ctx->meta.codename = strdup(value); + } else if (!strcmp(name, "platform")) { + ctx->system.platform = split(value, " ", 0); + } else if (!strcmp(name, "arch")) { + ctx->system.arch = strdup(value); + } else if (!strcmp(name, "time")) { + ctx->info.time_str_epoch = strdup(value); + } else if (!strcmp(name, "release_fmt")) { + ctx->rules.release_fmt = strdup(value); + } else if (!strcmp(name, "release_name")) { + ctx->info.release_name = strdup(value); + } else if (!strcmp(name, "build_name_fmt")) { + ctx->rules.build_name_fmt = strdup(value); + } else if (!strcmp(name, "build_name")) { + ctx->info.build_name = strdup(value); + } else if (!strcmp(name, "build_number_fmt")) { + ctx->rules.build_number_fmt = strdup(value); + } else if (!strcmp(name, "build_number")) { + ctx->info.build_number = strdup(value); + } else if (!strcmp(name, "conda_installer_baseurl")) { + ctx->conda.installer_baseurl = strdup(value); + } else if (!strcmp(name, "conda_installer_name")) { + ctx->conda.installer_name = strdup(value); + } else if (!strcmp(name, "conda_installer_version")) { + ctx->conda.installer_version = strdup(value); + } else if (!strcmp(name, "conda_installer_platform")) { + ctx->conda.installer_platform = strdup(value); + } else if (!strcmp(name, "conda_installer_arch")) { + ctx->conda.installer_arch = strdup(value); + } + GENERIC_ARRAY_FREE(parts); + } + fclose(fp); + + return 0; +} diff --git a/src/lib/index/include/args.h b/src/lib/index/include/args.h new file mode 100644 index 0000000..543aa4b --- /dev/null +++ b/src/lib/index/include/args.h @@ -0,0 +1,9 @@ +#ifndef STASIS_ARGS_H +#define STASIS_ARGS_H + +#include <getopt.h> + +extern struct option long_options[]; +void usage(char *name); + +#endif //STASIS_ARGS_H diff --git a/src/lib/index/include/helpers.h b/src/lib/index/include/helpers.h new file mode 100644 index 0000000..46705d2 --- /dev/null +++ b/src/lib/index/include/helpers.h @@ -0,0 +1,27 @@ +#ifndef HELPERS_H +#define HELPERS_H + +#include "delivery.h" + +#define ARRAY_COUNT_DYNAMIC(X, COUNTER) \ + do { \ + for ((COUNTER) = 0; (X) && (X)[COUNTER] != NULL; (COUNTER)++) {} \ + } while(0) + +#define ARRAY_COUNT_BY_STRUCT_MEMBER(X, MEMBER, COUNTER) \ + do { \ + for ((COUNTER) = 0; (X)[COUNTER].MEMBER != NULL; (COUNTER)++) {} \ + } while(0) + +struct StrList *get_architectures(struct Delivery ctx[], size_t nelem); +struct StrList *get_platforms(struct Delivery ctx[], size_t nelem); +int get_pandoc_version(size_t *result); +int pandoc_exec(const char *in_file, const char *out_file, const char *css_file, const char *title); +int get_latest_rc(struct Delivery ctx[], size_t nelem); +struct Delivery *get_latest_deliveries(struct Delivery ctx[], size_t nelem); +int get_files(struct StrList **out, const char *path, const char *pattern, ...); +struct StrList *get_docker_images(struct Delivery *ctx, char *pattern); +int load_metadata(struct Delivery *ctx, const char *filename); +int micromamba_configure(const struct Delivery *ctx, struct MicromambaInfo *m); + +#endif //HELPERS_H diff --git a/src/lib/index/include/index.h b/src/lib/index/include/index.h new file mode 100644 index 0000000..c57fc7b --- /dev/null +++ b/src/lib/index/include/index.h @@ -0,0 +1,14 @@ +#ifndef STASIS_INDEX_H +#define STASIS_INDEX_H + +#include "args.h" +#include "helpers.h" +#include "index_callbacks.h" +#include "index.h" +#include "junitxml_report.h" +#include "readmes.h" +#include "website.h" + +int stasis_index_entrypoint(int argc, char *argv[]); + +#endif diff --git a/src/lib/index/include/index_callbacks.h b/src/lib/index/include/index_callbacks.h new file mode 100644 index 0000000..7d95cbb --- /dev/null +++ b/src/lib/index/include/index_callbacks.h @@ -0,0 +1,9 @@ +#ifndef CALLBACKS_H +#define CALLBACKS_H + +#include "delivery.h" + +int callback_sort_deliveries_cmpfn(const void *a, const void *b); +int callback_sort_deliveries_dynamic_cmpfn(const void *a, const void *b); + +#endif //CALLBACKS_H diff --git a/src/lib/index/include/junitxml_report.h b/src/lib/index/include/junitxml_report.h new file mode 100644 index 0000000..6d2a248 --- /dev/null +++ b/src/lib/index/include/junitxml_report.h @@ -0,0 +1,8 @@ +#ifndef JUNITXML_REPORT_H +#define JUNITXML_REPORT_H + +#include "helpers.h" + +int indexer_junitxml_report(struct Delivery ctx[], size_t nelem); + +#endif //JUNITXML_REPORT_H diff --git a/src/lib/index/include/readmes.h b/src/lib/index/include/readmes.h new file mode 100644 index 0000000..d4fa7ac --- /dev/null +++ b/src/lib/index/include/readmes.h @@ -0,0 +1,8 @@ +#ifndef READMES_H +#define READMES_H + +#include "helpers.h" + +int indexer_readmes(struct Delivery ctx[], size_t nelem); + +#endif //READMES_H diff --git a/src/lib/index/include/website.h b/src/lib/index/include/website.h new file mode 100644 index 0000000..e67d58b --- /dev/null +++ b/src/lib/index/include/website.h @@ -0,0 +1,8 @@ +#ifndef WEBSITE_H +#define WEBSITE_H + +#include "helpers.h" + +int indexer_make_website(const struct Delivery *ctx); + +#endif //WEBSITE_H diff --git a/src/lib/index/index_callbacks.c b/src/lib/index/index_callbacks.c new file mode 100644 index 0000000..1e60863 --- /dev/null +++ b/src/lib/index/index_callbacks.c @@ -0,0 +1,41 @@ +// +// Created by jhunk on 11/15/24. +// + +#include "core.h" +#include "index_callbacks.h" + +// qsort callback to sort delivery contexts by compact python version +int callback_sort_deliveries_cmpfn(const void *a, const void *b) { + const struct Delivery *delivery1 = (struct Delivery *) a; + const size_t delivery1_python = strtoul(delivery1->meta.python_compact, NULL, 10); + const struct Delivery *delivery2 = (struct Delivery *) b; + const size_t delivery2_python = strtoul(delivery2->meta.python_compact, NULL, 10); + + if (delivery2_python > delivery1_python) { + return 1; + } + if (delivery2_python < delivery1_python) { + return -1; + } + return 0; +} + +// qsort callback to sort dynamically allocated delivery contexts by compact python version +int callback_sort_deliveries_dynamic_cmpfn(const void *a, const void *b) { + const struct Delivery *delivery1 = a; + const size_t delivery1_python = strtoul(delivery1->meta.python_compact, NULL, 10); + const int delivery1_rc = delivery1->meta.rc; + const struct Delivery *delivery2 = b; + const size_t delivery2_python = strtoul(delivery2->meta.python_compact, NULL, 10); + const int delivery2_rc = delivery2->meta.rc; + + if (delivery2_python > delivery1_python && delivery2_rc > delivery1_rc) { + return 1; + } + if (delivery2_python < delivery1_python && delivery2_rc < delivery1_rc) { + return -1; + } + return 0; +} + diff --git a/src/lib/index/junitxml_report.c b/src/lib/index/junitxml_report.c new file mode 100644 index 0000000..f774c7c --- /dev/null +++ b/src/lib/index/junitxml_report.c @@ -0,0 +1,148 @@ +// +// Created by jhunk on 11/15/24. +// + +#include "core.h" +#include "index_callbacks.h" +#include "junitxml.h" +#include "junitxml_report.h" + +static int is_file_in_listing(struct StrList *list, const char *pattern) { + for (size_t i = 0; i < strlist_count(list); i++) { + char const *path = strlist_item(list, i); + if (!fnmatch(pattern, path, 0)) { + return 1; + } + } + return 0; +} + +static int write_report_output(struct Delivery *ctx, FILE *destfp, const char *xmlfilename) { + struct JUNIT_Testsuite *testsuite = junitxml_testsuite_read(xmlfilename); + if (testsuite) { + if (globals.verbose) { + printf("%s: duration: %0.4f, total: %d, passed: %d, failed: %d, skipped: %d, errors: %d\n", xmlfilename, + testsuite->time, testsuite->tests, + testsuite->passed, testsuite->failures, + testsuite->skipped, testsuite->errors); + } + + char *bname_tmp = strdup(xmlfilename); + char *bname = strdup(path_basename(bname_tmp)); + if (endswith(bname, ".xml")) { + bname[strlen(bname) - 4] = 0; + } + guard_free(bname_tmp); + + char result_outfile[PATH_MAX] = {0}; + char *short_name_pattern = NULL; + asprintf(&short_name_pattern, "-%s", ctx->info.release_name); + + char short_name[PATH_MAX] = {0}; + strncpy(short_name, bname, sizeof(short_name) - 1); + replace_text(short_name, short_name_pattern, "", 0); + replace_text(short_name, "results-", "", 0); + guard_free(short_name_pattern); + + fprintf(destfp, "|[%s](%s.html)|%0.4f|%d|%d|%d|%d|%d|\n", short_name, bname, + testsuite->time, testsuite->tests, + testsuite->passed, testsuite->failures, + testsuite->skipped, testsuite->errors); + + snprintf(result_outfile, sizeof(result_outfile) - strlen(bname) - 3, "%s.md", + bname); + guard_free(bname); + + FILE *resultfp = fopen(result_outfile, "w+"); + if (!resultfp) { + SYSERROR("Unable to open %s for writing", result_outfile); + return -1; + } + + for (size_t i = 0; i < testsuite->_tc_inuse; i++) { + const char *type_str = NULL; + const int state = testsuite->testcase[i]->tc_result_state_type; + const char *message = NULL; + if (state == JUNIT_RESULT_STATE_FAILURE) { + message = testsuite->testcase[i]->result_state.failure->message; + type_str = "[FAILED]"; + } else if (state == JUNIT_RESULT_STATE_ERROR) { + message = testsuite->testcase[i]->result_state.error->message; + type_str = "[ERROR]"; + } else if (state == JUNIT_RESULT_STATE_SKIPPED) { + message = testsuite->testcase[i]->result_state.skipped->message; + type_str = "[SKIPPED]"; + } else { + message = testsuite->testcase[i]->message ? testsuite->testcase[i]->message : ""; + type_str = "[PASSED]"; + } + fprintf(resultfp, "### %s %s :: %s\n", type_str, + testsuite->testcase[i]->classname, testsuite->testcase[i]->name); + fprintf(resultfp, "\nDuration: %0.04fs\n", testsuite->testcase[i]->time); + fprintf(resultfp, "\n```\n%s\n```\n\n", message); + } + junitxml_testsuite_free(&testsuite); + fclose(resultfp); + } else { + fprintf(stderr, "bad test suite: %s: %s\n", strerror(errno), xmlfilename); + } + return 0; +} + +int indexer_junitxml_report(struct Delivery ctx[], const size_t nelem) { + char indexfile[PATH_MAX] = {0}; + sprintf(indexfile, "%s/README.md", ctx->storage.results_dir); + + struct StrList *file_listing = listdir(ctx->storage.results_dir); + if (!file_listing) { + // no test results to process + return 0; + } + + if (!pushd(ctx->storage.results_dir)) { + FILE *indexfp = fopen(indexfile, "w+"); + if (!indexfp) { + fprintf(stderr, "Unable to open %s for writing\n", indexfile); + return -1; + } + printf("Index %s opened for writing\n", indexfile); + + for (size_t d = 0; d < nelem; d++) { + char pattern[PATH_MAX] = {0}; + snprintf(pattern, sizeof(pattern) - 1, "*%s*", ctx[d].info.release_name); + + // if result directory contains this release name, print it + fprintf(indexfp, "### %s\n", ctx[d].info.release_name); + if (!is_file_in_listing(file_listing, pattern)) { + fprintf(indexfp, "No test results\n"); + continue; + } + fprintf(indexfp, "\n|Suite|Duration|Total|Pass|Fail|Skip|Error|\n"); + fprintf(indexfp, "|:----|:------:|:---:|:--:|:--:|:--:|:---:|\n"); + + for (size_t i = 0; i < strlist_count(file_listing); i++) { + const char *filename = strlist_item(file_listing, i); + // if not a xml file, skip it + if (!endswith(filename, ".xml")) { + continue; + } + if (!fnmatch(pattern, filename, 0)) { + if (write_report_output(&ctx[d], indexfp, filename)) { + // warn only + SYSERROR("Unable to write xml report file using %s", filename); + } + } + } + fprintf(indexfp, "\n"); + } + fclose(indexfp); + popd(); + } else { + fprintf(stderr, "Unable to enter delivery directory: %s\n", ctx->storage.delivery_dir); + guard_strlist_free(&file_listing); + return -1; + } + + guard_strlist_free(&file_listing); + return 0; +} diff --git a/src/lib/index/readmes.c b/src/lib/index/readmes.c new file mode 100644 index 0000000..7daf261 --- /dev/null +++ b/src/lib/index/readmes.c @@ -0,0 +1,113 @@ +#include "core.h" +#include "readmes.h" + +int indexer_readmes(struct Delivery ctx[], const size_t nelem) { + struct Delivery *latest_deliveries = get_latest_deliveries(ctx, nelem); + if (!latest_deliveries) { + if (errno) { + return -1; + } + return 0; + } + + char indexfile[PATH_MAX] = {0}; + sprintf(indexfile, "%s/README.md", ctx->storage.delivery_dir); + + FILE *indexfp = fopen(indexfile, "w+"); + if (!indexfp) { + fprintf(stderr, "Unable to open %s for writing\n", indexfile); + return -1; + } + struct StrList *archs = get_architectures(latest_deliveries, nelem); + struct StrList *platforms = get_platforms(latest_deliveries, nelem); + + fprintf(indexfp, "# %s-%s\n\n", ctx->meta.name, ctx->meta.version); + fprintf(indexfp, "## Current Release\n\n"); + for (size_t p = 0; p < strlist_count(platforms); p++) { + char *platform = strlist_item(platforms, p); + for (size_t a = 0; a < strlist_count(archs); a++) { + char *arch = strlist_item(archs, a); + int have_combo = 0; + for (size_t i = 0; i < nelem; i++) { + if (latest_deliveries[i].system.platform) { + if (strstr(latest_deliveries[i].system.platform[DELIVERY_PLATFORM_RELEASE], platform) && + strstr(latest_deliveries[i].system.arch, arch)) { + have_combo = 1; + } + } + } + if (!have_combo) { + continue; + } + fprintf(indexfp, "### %s-%s\n\n", platform, arch); + for (size_t i = 0; i < nelem; i++) { + char link_name[PATH_MAX] = {0}; + char readme_name[PATH_MAX] = {0}; + char conf_name[PATH_MAX] = {0}; + char conf_name_relative[PATH_MAX] = {0}; + if (!latest_deliveries[i].meta.name) { + continue; + } + sprintf(link_name, "latest-py%s-%s-%s.yml", latest_deliveries[i].meta.python_compact, latest_deliveries[i].system.platform[DELIVERY_PLATFORM_RELEASE], latest_deliveries[i].system.arch); + sprintf(readme_name, "README-py%s-%s-%s.md", latest_deliveries[i].meta.python_compact, latest_deliveries[i].system.platform[DELIVERY_PLATFORM_RELEASE], latest_deliveries[i].system.arch); + sprintf(conf_name, "%s.ini", latest_deliveries[i].info.release_name); + sprintf(conf_name_relative, "../config/%s.ini", latest_deliveries[i].info.release_name); + if (strstr(link_name, platform) && strstr(link_name, arch)) { + fprintf(indexfp, "- Python %s\n", latest_deliveries[i].meta.python); + fprintf(indexfp, " - Info: [README](%s)\n", readme_name); + fprintf(indexfp, " - Release: [Conda Environment YAML](%s)\n", link_name); + fprintf(indexfp, " - Receipt: [STASIS input file](%s)\n", conf_name_relative); + fprintf(indexfp, " - Docker: "); + struct StrList *docker_images = get_docker_images(&latest_deliveries[i], ""); + if (docker_images + && strlist_count(docker_images) + && !strcmp(latest_deliveries[i].system.platform[DELIVERY_PLATFORM_RELEASE], "linux")) { + fprintf(indexfp, "[Archive](../packages/docker/%s)\n", path_basename(strlist_item(docker_images, 0))); + guard_free(docker_images); + } else { + fprintf(indexfp, "N/A\n"); + } + } + } + fprintf(indexfp, "\n"); + } + fprintf(indexfp, "\n"); + } + + fprintf(indexfp, "## Releases\n"); + for (size_t i = 0; ctx[i].meta.name != NULL; i++) { + struct Delivery *current = &ctx[i]; + fprintf(indexfp, "### %s\n", current->info.release_name); + fprintf(indexfp, "- Info: [README](README-%s.html)\n", current->info.release_name); + fprintf(indexfp, "- Release: [Conda Environment YAML](%s.yml)\n", current->info.release_name); + fprintf(indexfp, "- Receipt: [STASIS input file](../config/%s.ini)\n", current->info.release_name); + fprintf(indexfp, "- Docker: \n"); + + char *pattern = NULL; + asprintf(&pattern, "*%s*", current->info.build_number); + if (!pattern) { + SYSERROR("%s", "Unable to allocate bytes for pattern"); + return -1; + } + + struct StrList *docker_images = get_docker_images(current, pattern); + if (docker_images + && strlist_count(docker_images) + && !strcmp(current->system.platform[DELIVERY_PLATFORM_RELEASE], "linux")) { + fprintf(indexfp, "[Archive](../packages/docker/%s)\n", path_basename(strlist_item(docker_images, 0))); + guard_free(docker_images); + } else { + fprintf(indexfp, "N/A\n"); + } + guard_free(pattern); + } + fprintf(indexfp, "\n"); + + guard_strlist_free(&archs); + guard_strlist_free(&platforms); + fclose(indexfp); + + // "latest_deliveries" is an array of pointers to ctxs[]. Do not free the contents of the array. + guard_free(latest_deliveries); + return 0; +} diff --git a/src/lib/index/stasis_index_entrypoint.c b/src/lib/index/stasis_index_entrypoint.c new file mode 100644 index 0000000..87347a3 --- /dev/null +++ b/src/lib/index/stasis_index_entrypoint.c @@ -0,0 +1,402 @@ +#include <getopt.h> +#include "args.h" +#include "index_callbacks.h" +#include "helpers.h" +#include "junitxml_report.h" +#include "website.h" +#include "readmes.h" +#include "delivery.h" + +int indexer_combine_rootdirs(const char *dest, char **rootdirs, const size_t rootdirs_total) { + char cmd[PATH_MAX]; + char destdir_bare[PATH_MAX]; + char destdir_with_output[PATH_MAX]; + char *destdir = destdir_bare; + + memset(cmd, 0, sizeof(cmd)); + memset(destdir_bare, 0, sizeof(destdir_bare)); + memset(destdir_with_output, 0, sizeof(destdir_bare)); + + strcpy(destdir_bare, dest); + strcpy(destdir_with_output, dest); + strcat(destdir_with_output, "/output"); + + if (!access(destdir_with_output, F_OK)) { + destdir = destdir_with_output; + } + + sprintf(cmd, "rsync -ah%s --delete --exclude 'tools/' --exclude 'tmp/' --exclude 'build/' ", globals.verbose ? "v" : "q"); + for (size_t i = 0; i < rootdirs_total; i++) { + char srcdir_bare[PATH_MAX] = {0}; + char srcdir_with_output[PATH_MAX] = {0}; + char *srcdir = srcdir_bare; + strcpy(srcdir_bare, rootdirs[i]); + strcpy(srcdir_with_output, rootdirs[i]); + strcat(srcdir_with_output, "/output"); + + if (access(srcdir_bare, F_OK)) { + fprintf(stderr, "%s does not exist\n", srcdir_bare); + continue; + } + + if (!access(srcdir_with_output, F_OK)) { + srcdir = srcdir_with_output; + } + snprintf(cmd + strlen(cmd), sizeof(srcdir) - strlen(srcdir) + 4, "'%s'/ ", srcdir); + } + snprintf(cmd + strlen(cmd), sizeof(cmd) - strlen(destdir) + 1, " %s/", destdir); + + if (globals.verbose) { + puts(cmd); + } + + if (system(cmd)) { + return -1; + } + return 0; +} + +int indexer_wheels(struct Delivery *ctx) { + return delivery_index_wheel_artifacts(ctx); +} + +int indexer_conda(const struct Delivery *ctx, struct MicromambaInfo m) { + int status = 0; + + status += micromamba(&m, "run conda index %s", ctx->storage.conda_artifact_dir); + return status; +} + +int indexer_symlinks(struct Delivery *ctx, const size_t nelem) { + struct Delivery *data = NULL; + data = get_latest_deliveries(ctx, nelem); + //int latest = get_latest_rc(ctx, nelem); + + if (!pushd(ctx->storage.delivery_dir)) { + for (size_t i = 0; i < nelem; i++) { + char link_name_spec[PATH_MAX]; + char link_name_readme[PATH_MAX]; + + char file_name_spec[PATH_MAX]; + char file_name_readme[PATH_MAX]; + + if (!data[i].meta.name) { + continue; + } + sprintf(link_name_spec, "latest-py%s-%s-%s.yml", data[i].meta.python_compact, data[i].system.platform[DELIVERY_PLATFORM_RELEASE], data[i].system.arch); + sprintf(file_name_spec, "%s.yml", data[i].info.release_name); + + sprintf(link_name_readme, "README-py%s-%s-%s.md", data[i].meta.python_compact, data[i].system.platform[DELIVERY_PLATFORM_RELEASE], data[i].system.arch); + sprintf(file_name_readme, "README-%s.md", data[i].info.release_name); + + if (!access(link_name_spec, F_OK)) { + if (unlink(link_name_spec)) { + fprintf(stderr, "Unable to remove spec link: %s\n", link_name_spec); + } + } + if (!access(link_name_readme, F_OK)) { + if (unlink(link_name_readme)) { + fprintf(stderr, "Unable to remove readme link: %s\n", link_name_readme); + } + } + + if (globals.verbose) { + printf("%s -> %s\n", file_name_spec, link_name_spec); + } + if (symlink(file_name_spec, link_name_spec)) { + fprintf(stderr, "Unable to link %s as %s\n", file_name_spec, link_name_spec); + } + + if (globals.verbose) { + printf("%s -> %s\n", file_name_readme, link_name_readme); + } + if (symlink(file_name_readme, link_name_readme)) { + fprintf(stderr, "Unable to link %s as %s\n", file_name_readme, link_name_readme); + } + } + popd(); + } else { + fprintf(stderr, "Unable to enter delivery directory: %s\n", ctx->storage.delivery_dir); + guard_free(data); + return -1; + } + + // "latest" is an array of pointers to ctx[]. Do not free the contents of the array. + guard_free(data); + return 0; +} + +void indexer_init_dirs(struct Delivery *ctx, const char *workdir) { + path_store(&ctx->storage.root, PATH_MAX, workdir, ""); + path_store(&ctx->storage.tmpdir, PATH_MAX, ctx->storage.root, "tmp"); + if (delivery_init_tmpdir(ctx)) { + fprintf(stderr, "Failed to configure temporary storage directory\n"); + exit(1); + } + + char *user_dir = expandpath("~/.stasis/indexer"); + if (!user_dir) { + SYSERROR("%s", "expandpath failed"); + } + + path_store(&ctx->storage.output_dir, PATH_MAX, ctx->storage.root, ""); + path_store(&ctx->storage.tools_dir, PATH_MAX, user_dir, "tools"); + path_store(&globals.conda_install_prefix, PATH_MAX, user_dir, "conda"); + path_store(&ctx->storage.cfgdump_dir, PATH_MAX, ctx->storage.output_dir, "config"); + path_store(&ctx->storage.meta_dir, PATH_MAX, ctx->storage.output_dir, "meta"); + path_store(&ctx->storage.delivery_dir, PATH_MAX, ctx->storage.output_dir, "delivery"); + path_store(&ctx->storage.package_dir, PATH_MAX, ctx->storage.output_dir, "packages"); + path_store(&ctx->storage.results_dir, PATH_MAX, ctx->storage.output_dir, "results"); + path_store(&ctx->storage.wheel_artifact_dir, PATH_MAX, ctx->storage.package_dir, "wheels"); + path_store(&ctx->storage.conda_artifact_dir, PATH_MAX, ctx->storage.package_dir, "conda"); + path_store(&ctx->storage.docker_artifact_dir, PATH_MAX, ctx->storage.package_dir, "docker"); + guard_free(user_dir); + + char newpath[PATH_MAX] = {0}; + if (getenv("PATH")) { + sprintf(newpath, "%s/bin:%s", ctx->storage.tools_dir, getenv("PATH")); + setenv("PATH", newpath, 1); + } else { + SYSERROR("%s", "environment variable PATH is undefined. Unable to continue."); + exit(1); + } +} + +int stasis_index_entrypoint(const int argc, char *argv[]) { + size_t rootdirs_total = 0; + char *destdir = NULL; + char **rootdirs = NULL; + int do_html = 0; + int c = 0; + int option_index = 0; + while ((c = getopt_long(argc, argv, "hd:vUw", long_options, &option_index)) != -1) { + switch (c) { + case 'h': + usage(path_basename(argv[0])); + exit(0); + case 'd': + if (mkdir(optarg, 0755)) { + if (errno != 0 && errno != EEXIST) { + SYSERROR("Unable to create destination directory, '%s': %s", optarg, strerror(errno)); + exit(1); + } + } + destdir = realpath(optarg, NULL); + break; + case 'U': + fflush(stdout); + fflush(stderr); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + break; + case 'v': + globals.verbose = 1; + break; + case 'w': + do_html = 1; + break; + case '?': + default: + exit(1); + } + } + + const int current_index = optind; + if (optind < argc) { + rootdirs_total = argc - current_index; + rootdirs = calloc(rootdirs_total + 1, sizeof(*rootdirs)); + + int i = 0; + while (optind < argc) { + if (argv[optind]) { + if (access(argv[optind], F_OK) < 0) { + fprintf(stderr, "%s: %s\n", argv[optind], strerror(errno)); + exit(1); + } + } + // use first positional argument + rootdirs[i] = realpath(argv[optind], NULL); + optind++; + break; + } + } + + if (isempty(destdir)) { + if (mkdir("output", 0755)) { + if (errno != 0 && errno != EEXIST) { + SYSERROR("Unable to create destination directory, '%s': %s", "output", strerror(errno)); + exit(1); + } + } + destdir = realpath("output", NULL); + } + + if (!rootdirs || !rootdirs_total) { + fprintf(stderr, "You must specify at least one STASIS root directory to index\n"); + exit(1); + } + + for (size_t i = 0; i < rootdirs_total; i++) { + if (isempty(rootdirs[i]) || !strcmp(rootdirs[i], "/") || !strcmp(rootdirs[i], "\\")) { + SYSERROR("Unsafe directory: %s", rootdirs[i]); + exit(1); + } + + if (access(rootdirs[i], F_OK)) { + SYSERROR("%s: %s", rootdirs[i], strerror(errno)); + exit(1); + } + } + + char stasis_sysconfdir_tmp[PATH_MAX]; + if (getenv("STASIS_SYSCONFDIR")) { + strncpy(stasis_sysconfdir_tmp, getenv("STASIS_SYSCONFDIR"), sizeof(stasis_sysconfdir_tmp) - 1); + } else { + strncpy(stasis_sysconfdir_tmp, STASIS_SYSCONFDIR, sizeof(stasis_sysconfdir_tmp) - 1); + } + + globals.sysconfdir = realpath(stasis_sysconfdir_tmp, NULL); + if (!globals.sysconfdir) { + msg(STASIS_MSG_ERROR | STASIS_MSG_L1, "Unable to resolve path to configuration directory: %s\n", stasis_sysconfdir_tmp); + exit(1); + } + + char workdir_template[PATH_MAX] = {0}; + const char *system_tmp = getenv("TMPDIR"); + if (system_tmp) { + strcat(workdir_template, system_tmp); + } else { + strcat(workdir_template, "/tmp"); + } + strcat(workdir_template, "/stasis-combine.XXXXXX"); + char *workdir = mkdtemp(workdir_template); + if (!workdir) { + SYSERROR("Unable to create temporary directory: %s", workdir_template); + exit(1); + } + if (isempty(workdir) || !strcmp(workdir, "/") || !strcmp(workdir, "\\")) { + SYSERROR("Unsafe directory: %s", workdir); + exit(1); + } + + struct Delivery ctx = {0}; + + printf(BANNER, VERSION, AUTHOR); + + indexer_init_dirs(&ctx, workdir); + + msg(STASIS_MSG_L1, "%s delivery root %s\n", + rootdirs_total > 1 ? "Merging" : "Indexing", + rootdirs_total > 1 ? "directories" : "directory"); + if (indexer_combine_rootdirs(workdir, rootdirs, rootdirs_total)) { + SYSERROR("%s", "Copy operation failed"); + rmtree(workdir); + exit(1); + } + + if (access(ctx.storage.conda_artifact_dir, F_OK)) { + mkdirs(ctx.storage.conda_artifact_dir, 0755); + } + + if (access(ctx.storage.wheel_artifact_dir, F_OK)) { + mkdirs(ctx.storage.wheel_artifact_dir, 0755); + } + + struct MicromambaInfo m; + if (micromamba_configure(&ctx, &m)) { + SYSERROR("%s", "Unable to configure micromamba"); + exit(1); + } + + msg(STASIS_MSG_L1, "Indexing conda packages\n"); + if (indexer_conda(&ctx, m)) { + SYSERROR("%s", "Conda package indexing operation failed"); + exit(1); + } + + msg(STASIS_MSG_L1, "Indexing wheel packages\n"); + if (indexer_wheels(&ctx)) { + SYSERROR("%s", "Python package indexing operation failed"); + exit(1); + } + + msg(STASIS_MSG_L1, "Loading metadata\n"); + struct StrList *metafiles = NULL; + get_files(&metafiles, ctx.storage.meta_dir, "*.stasis"); + strlist_sort(metafiles, STASIS_SORT_LEN_ASCENDING); + + struct Delivery *local = calloc(strlist_count(metafiles) + 1, sizeof(*local)); + if (!local) { + SYSERROR("%s", "Unable to allocate bytes for local delivery context array"); + exit(1); + } + + for (size_t i = 0; i < strlist_count(metafiles); i++) { + char *item = strlist_item(metafiles, i); + // Copy the pre-filled contents of the main delivery context + memcpy(&local[i], &ctx, sizeof(ctx)); + if (globals.verbose) { + puts(item); + } + load_metadata(&local[i], item); + } + qsort(local, strlist_count(metafiles), sizeof(*local), callback_sort_deliveries_cmpfn); + + msg(STASIS_MSG_L1, "Generating links to latest release iteration\n"); + if (indexer_symlinks(local, strlist_count(metafiles))) { + SYSERROR("%s", "Link generation failed"); + exit(1); + } + + msg(STASIS_MSG_L1, "Generating README.md\n"); + if (indexer_readmes(local, strlist_count(metafiles))) { + SYSERROR("%s", "README indexing operation failed"); + exit(1); + } + + msg(STASIS_MSG_L1, "Indexing test results\n"); + if (indexer_junitxml_report(local, strlist_count(metafiles))) { + SYSERROR("%s", "Test result indexing operation failed"); + exit(1); + } + + if (do_html) { + msg(STASIS_MSG_L1, "Generating HTML indexes\n"); + if (indexer_make_website(local)) { + SYSERROR("%s", "Site creation failed"); + exit(1); + } + } + + msg(STASIS_MSG_L1, "Copying indexed delivery to '%s'\n", destdir); + char cmd[PATH_MAX] = {0}; + sprintf(cmd, "rsync -ah%s --delete --exclude 'tmp/' --exclude 'tools/' '%s/' '%s/'", globals.verbose ? "v" : "q", workdir, destdir); + guard_free(destdir); + + if (globals.verbose) { + puts(cmd); + } + + if (system(cmd)) { + SYSERROR("%s", "Copy operation failed"); + rmtree(workdir); + exit(1); + } + + msg(STASIS_MSG_L1, "Removing work directory: %s\n", workdir); + if (rmtree(workdir)) { + SYSERROR("Failed to remove work directory: %s", strerror(errno)); + } + + guard_free(destdir); + GENERIC_ARRAY_FREE(rootdirs); + guard_strlist_free(&metafiles); + guard_free(m.micromamba_prefix); + delivery_free(&ctx); + guard_free(local); + globals_free(); + + msg(STASIS_MSG_L1, "Done!\n"); + + return 0; +} diff --git a/src/lib/index/website.c b/src/lib/index/website.c new file mode 100644 index 0000000..55f0c45 --- /dev/null +++ b/src/lib/index/website.c @@ -0,0 +1,68 @@ +#include "core.h" +#include "website.h" + +int indexer_make_website(const struct Delivery *ctx) { + char *css_filename = calloc(PATH_MAX, sizeof(*css_filename)); + if (!css_filename) { + SYSERROR("unable to allocate string for CSS file path: %s", strerror(errno)); + return -1; + } + + sprintf(css_filename, "%s/%s", globals.sysconfdir, "stasis_pandoc.css"); + const int have_css = access(css_filename, F_OK | R_OK) == 0; + + struct StrList *dirs = strlist_init(); + strlist_append(&dirs, ctx->storage.delivery_dir); + strlist_append(&dirs, ctx->storage.results_dir); + + struct StrList *inputs = NULL; + for (size_t i = 0; i < strlist_count(dirs); i++) { + const char *pattern = "*.md"; + char *dirpath = strlist_item(dirs, i); + if (get_files(&inputs, dirpath, pattern)) { + SYSERROR("%s does not contain files with pattern: %s", dirpath, pattern); + continue; + } + + char *root = strlist_item(dirs, i); + for (size_t x = 0; x < strlist_count(inputs); x++) { + char *filename = path_basename(strlist_item(inputs, x)); + char fullpath_src[PATH_MAX] = {0}; + char fullpath_dest[PATH_MAX] = {0}; + sprintf(fullpath_src, "%s/%s", root, filename); + if (access(fullpath_src, F_OK)) { + continue; + } + + // Replace *.md extension with *.html. + strcpy(fullpath_dest, fullpath_src); + gen_file_extension_str(fullpath_dest, ".html"); + + // Convert markdown to html + if (pandoc_exec(fullpath_src, fullpath_dest, have_css ? css_filename : NULL, "STASIS")) { + msg(STASIS_MSG_L2 | STASIS_MSG_WARN, "Unable to convert %s\n", fullpath_src); + } + + if (file_replace_text(fullpath_dest, ".md", ".html", 0)) { + // inform-only + SYSERROR("%s: failed to rewrite *.md urls with *.html extension", fullpath_dest); + } + + // Link the nearest README.html to index.html + if (!strcmp(filename, "README.md")) { + char link_from[PATH_MAX] = {0}; + char link_dest[PATH_MAX] = {0}; + strcpy(link_from, "README.html"); + sprintf(link_dest, "%s/%s", root, "index.html"); + if (symlink(link_from, link_dest)) { + SYSERROR("Warning: symlink(%s, %s) failed: %s", link_from, link_dest, strerror(errno)); + } + } + } + guard_strlist_free(&inputs); + } + guard_free(css_filename); + guard_strlist_free(&dirs); + + return 0; +} |