diff options
author | Joseph Hunkeler <jhunkeler@users.noreply.github.com> | 2024-07-06 09:49:51 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-06 09:49:51 -0400 |
commit | 8ce824ac4b2f526331093a7150e643700efd4d20 (patch) | |
tree | 99e27dee24e82d78941ded4c510e1bac57c3f015 | |
parent | abe87056faa6ed02aff3bbf77c1fd78b713a0864 (diff) | |
download | stasis-8ce824ac4b2f526331093a7150e643700efd4d20.tar.gz |
Add github.c and github.h (#9)
* Add github.c and github.h
* Implements get_github_release_notes()
* Remove unused variables
* Fix circular dependency on tplfunc_frame
* Remove predeclaration of tplfunc_frame
* tpl_register_func accepts pointer to void instead
* tpl_register_func sets maximum number of arguments
* Frame is generated within tpl_register_func
* Improve template function error handling and return/output management
* Remove redundant extern statement
* Include github.h and template_func_proto.h in core.h
* Expose get_github_release_notes_tplfunc_entrypoint function to template engine
* Add template_func_proto.c and template_func_proto.h
* Replace free() with guard variant
* Fix test_template::test_tpl_register_func
* Fix tests
* Fix tests
* cmd should be at least PATH_MAX in size.
* Magic number caused failure to install conda with a long installation path
* Implement get_github_release_notes_auto function that bases release note data off test contexts
* Disable overwriting releases by default
* Add automatic release note generation function call to release_notes.md.in
* Fix test_tpl_register_func()
* Add enough space for tar command plus a path
* Fix circular include
* Github functions do not require access to core.h anyway
* Add comments to union
* Update README to mention template function availability
* Add EnvCtl structure
* Add runtime checks to avoid running all the way to the end only to be met with a configuration error.
* Rename GITHUB to GH
* Development docs pre-rough-draft
-rw-r--r-- | README.md | 39 | ||||
-rw-r--r-- | docs/Doxyfile | 2 | ||||
-rw-r--r-- | docs/devel.md | 148 | ||||
-rw-r--r-- | include/core.h | 13 | ||||
-rw-r--r-- | include/delivery.h | 7 | ||||
-rw-r--r-- | include/github.h | 10 | ||||
-rw-r--r-- | include/template.h | 52 | ||||
-rw-r--r-- | include/template_func_proto.h | 9 | ||||
-rw-r--r-- | mission/hst/release_notes.md.in | 1 | ||||
-rw-r--r-- | mission/jwst/release_notes.md.in | 1 | ||||
-rw-r--r-- | mission/roman/release_notes.md.in | 1 | ||||
-rw-r--r-- | src/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/conda.c | 2 | ||||
-rw-r--r-- | src/delivery.c | 22 | ||||
-rw-r--r-- | src/github.c | 133 | ||||
-rw-r--r-- | src/globals.c | 16 | ||||
-rw-r--r-- | src/ini.c | 2 | ||||
-rw-r--r-- | src/junitxml.c | 6 | ||||
-rw-r--r-- | src/stasis_main.c | 49 | ||||
-rw-r--r-- | src/template.c | 31 | ||||
-rw-r--r-- | src/template_func_proto.c | 65 | ||||
-rw-r--r-- | tests/test_str.c | 18 | ||||
-rw-r--r-- | tests/test_strlist.c | 4 | ||||
-rw-r--r-- | tests/test_template.c | 41 |
24 files changed, 593 insertions, 81 deletions
@@ -143,21 +143,22 @@ stasis mydelivery.ini ## Environment variables -| Name | Purpose | -|-------------------------------------|-------------------------------------------------------| -| TMPDIR | Change default path to store temporary data | -| STASIS_ROOT | Change default path to write STASIS's data | -| STASIS_SYSCONFDIR | Change default path to search for configuration files | -| STASIS_CPU_COUNT (alias: CPU_COUNT) | Number of available CPUS | -| STASIS_JF_ARTIFACTORY_URL | Artifactory service URL (ending in `/artifactory`) | -| STASIS_JF_ACCESS_TOKEN | Artifactory Access Token | -| STASIS_JF_USER | Artifactory username | -| STASIS_JF_PASSWORD | Artifactory password | -| STASIS_JF_SSH_KEY_PATH | Path to SSH public key file | -| STASIS_JF_SSH_PASSPHRASE | Password associated with SSH public key file | -| STASIS_JF_CLIENT_CERT_CERT_PATH | Path to OpenSSL cert files | -| STASIS_JF_CLIENT_CERT_KEY_PATH | OpenSSL key file (in cert path) | -| STASIS_JF_REPO | Artifactory "generic" repository to write to | +| Name | Purpose | +|-------------------------------------------|-------------------------------------------------------------------------| +| TMPDIR | Change default path to store temporary data | +| STASIS_ROOT | Change default path to write STASIS's data | +| STASIS_SYSCONFDIR | Change default path to search for configuration files | +| STASIS_CPU_COUNT<br/>(alias: CPU_COUNT) | Number of available CPUs | +| STASIS_GH_TOKEN<br/>(alias: GITHUB_TOKEN) | GitHub API token<br/>(Scope: "Contents" repository permissions (write)) | +| STASIS_JF_ARTIFACTORY_URL | Artifactory service URL (ending in `/artifactory`) | +| STASIS_JF_ACCESS_TOKEN | Artifactory Access Token | +| STASIS_JF_USER | Artifactory username | +| STASIS_JF_PASSWORD | Artifactory password | +| STASIS_JF_SSH_KEY_PATH | Path to SSH public key file | +| STASIS_JF_SSH_PASSPHRASE | Password associated with SSH public key file | +| STASIS_JF_CLIENT_CERT_CERT_PATH | Path to OpenSSL cert files | +| STASIS_JF_CLIENT_CERT_KEY_PATH | OpenSSL key file (in cert path) | +| STASIS_JF_REPO | Artifactory "generic" repository to write to | # Variable expansion @@ -205,6 +206,14 @@ version = {{ env:MY_DYNAMIC_DELIVERY_VERSION }} python = {{ env:MY_DYNAMIC_PYTHON_VERSION }} ``` +## Template Functions + +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 | + # Delivery files ## Sections diff --git a/docs/Doxyfile b/docs/Doxyfile index 92fcec2..3964a1e 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -124,7 +124,7 @@ WARN_LOGFILE = #--------------------------------------------------------------------------- # Configuration options related to the input files #--------------------------------------------------------------------------- -INPUT = ../README.md ../src ../include +INPUT = devel.md ../README.md ../src ../include INPUT_ENCODING = UTF-8 INPUT_FILE_ENCODING = FILE_PATTERNS = *.c \ diff --git a/docs/devel.md b/docs/devel.md new file mode 100644 index 0000000..a288ef5 --- /dev/null +++ b/docs/devel.md @@ -0,0 +1,148 @@ +# Developing + +# Using the tpl_* functions + +```c +#include <stdio.h> // for fprintf, free, puts +#include <stdlib.h> // for strdup +#include "template.h" // for tpl_free, tpl_register, tpl_render + +int main(int argc, char *argv[]) { + char *value = strdup("the value"); + tpl_register("my_data", &value); + char *rendered = tpl_render("I am showing you {{ my_data }}."); + if (rendered) { + puts(rendered); + free(rendered); + } else { + fprintf() + } + tpl_free(); +} +``` + +`tpl_register` accepts an address to a heap-allocated pointer of type `char`. One cannot pass the address of a stack-allocated character string, because chances are reasonably high that your C compiler detect this condition and throw an _incompatible pointer_ error. You may, however, register a pointer to a string that has been allocated on the stack (see example). + +```c +// Invalid (stack) +char value[255]; + +// Invalid (stack) +char value[] = "the value"; + +// Valid (pointer to stack) +char value_s[] = "the value"; +char *value = value_s; + +// Valid (heap) +char *value = calloc(255, sizeof(*value)); +strcpy(value, "the value"); + +// Valid (heap) +char *value = strdup("the value"); +``` + +The `tpl_render` function parses an input string and replaces any references encapsulated by double curly braces (`{{}}`) with the _current_ value of a registered template variable. Empty references and undefined variables are ignored, however whitespace surrounding the reference will be preserved in the result. If an unrecoverable error occurs while rendering this function returns `NULL`. + +The following illustrates this effect: +```c +char *abc = strdup("ABC"); +tpl_register("abc", &abc); +char *rendered = tpl_render("{{}} {{ undefined_var }} I know my {{ abc }}'s!"); +// Result: " I know my ABC's!" +// ^^ whitespace +free(rendered); +tpl_free(); +``` + +One should consider using the `normalize_space` function to remove undesired whitespace from the rendered output. + +```c +#include <stdio.h> // for fprintf, stderr +#include "str.h" // for normalize_space + +// ... +char *rendered = tpl_render("{{}} {{ undefined_var }} I know my {{ abc }}'s!"); +if (rendered) { + // Remove leading, trailing, and repeated whitespace + normalize_space(rendered); +} else { + fprintf(stderr, "unable to render input string\n"); + exit(1); +} +free(rendered); +tpl_free(); +// Result: "I know my ABC's!" +``` + +Most calls to `tpl_render` use data read from a file. The examples below should clarify how to achieve this. + +Template file: **index.html.in** + +```html +<!DOCTYPE html> +<html lang="en"> + <head> + <title>{{ site_title }}</title> + </head> + <body> + {{ site_body_text }} + <br/> + {{ site_footer }} + </body> +</html> +``` + +- Using the standard library + +```c +#include <stdio.h> // for fgets +#include <string.h> +#include <stdlib.h> +#include "template.h" // for tpl_render + +struct Site { + char *title; + char *body; + char *footer; +} site; + +void site_setup_template() { + tpl_register("site_title", &site.title); + tpl_register("site_body", &site.body); + tpl_register("site_footer", &site.footer); +} + +void site_setup_data() { + site.title = strdup("My static site"); + site.body = strdup("This is the body."); + site.footer = strdup("Generated with tpl_render()"); +} + +int main(int argc, char *argv[]) { + char line[BUFSIZ] = {0}; + char *filename = argv[1]; + FILE *fp = fopen(filename, "r"); + if (!fp) { + perror(filename); + exit(1); + } + + while (fgets(line, sizeof(line) - 1, fp) != NULL) { + char *rendered = tpl_render(line); + if (rendered) { + normalize_space(rendered); + printf("%s", rendered); + } + } + fclose(fp); +} +``` + +- Using file_readlines + +```c + +#include "util.h" +``` + diff --git a/include/core.h b/include/core.h index ac9ae2f..d065096 100644 --- a/include/core.h +++ b/include/core.h @@ -38,6 +38,8 @@ #include "relocation.h" #include "wheel.h" #include "junitxml.h" +#include "github.h" +#include "template_func_proto.h" #define guard_runtime_free(X) do { if (X) { runtime_free(X); X = NULL; } } while (0) #define guard_strlist_free(X) do { if ((*X)) { strlist_free(X); (*X) = NULL; } } while (0) @@ -57,6 +59,11 @@ } \ } while (0) +struct EnvCtl { + unsigned flags; + const char *name[10]; +}; + struct STASIS_GLOBAL { bool verbose; //!< Enable verbose output bool always_update_base_environment; //!< Update base environment immediately after activation @@ -65,6 +72,7 @@ struct STASIS_GLOBAL { bool enable_docker; //!< Enable docker image builds bool enable_artifactory; //!< Enable artifactory uploads bool enable_testing; //!< Enable package testing + bool enable_overwrite; //!< Enable release file clobbering struct StrList *conda_packages; //!< Conda packages to install after initial activation struct StrList *pip_packages; //!< Pip packages to install after initial activation char *tmpdir; //!< Path to temporary storage directory @@ -85,8 +93,13 @@ struct STASIS_GLOBAL { char *repo; char *url; } jfrog; + struct EnvCtl envctl[]; }; extern struct STASIS_GLOBAL globals; + +#define STASIS_ENVCTL_PASSTHRU 0 << 1 +#define STASIS_ENVCTL_REQUIRED 1 << 1 +#define STASIS_ENVCTL_REDACT 2 << 1 extern const char *VERSION; extern const char *AUTHOR; extern const char *BANNER; diff --git a/include/delivery.h b/include/delivery.h index 971705f..969c803 100644 --- a/include/delivery.h +++ b/include/delivery.h @@ -391,4 +391,11 @@ int *bootstrap_build_info(struct Delivery *ctx); int delivery_dump_metadata(struct Delivery *ctx); +/** + * Determine whether a release on-disk matches the release name in use + * @param ctx Delivery context + * @return 0=no, 1=yes + */ +int delivery_exists(struct Delivery *ctx); + #endif //STASIS_DELIVERY_H diff --git a/include/github.h b/include/github.h new file mode 100644 index 0000000..cebeabf --- /dev/null +++ b/include/github.h @@ -0,0 +1,10 @@ +#ifndef STASIS_GITHUB_H +#define STASIS_GITHUB_H + +#include <curl/curl.h> + +#define STASIS_GITHUB_API_VERSION "2022-11-28" + +int get_github_release_notes(const char *api_token, const char *repo, const char *tag, const char *target_commitish, char **output); + +#endif //STASIS_GITHUB_H
\ No newline at end of file diff --git a/include/template.h b/include/template.h index ba62667..e3d83fb 100644 --- a/include/template.h +++ b/include/template.h @@ -40,28 +40,42 @@ char *tpl_render(char *str); */ int tpl_render_to_file(char *str, const char *filename); -struct tplfunc_frame *tpl_getfunc(char *key); -struct tplfunc_frame; -typedef int tplfunc(struct tplfunc_frame *frame, void *result); +typedef int tplfunc(void *frame, void *data_out); + struct tplfunc_frame { - char *key; - tplfunc *func; - int argc; + char *key; ///< Name of the function + tplfunc *func; ///< Pointer to the function + void *data_in; ///< Pointer to internal data (can be NULL) + int argc; ///< Maximum number of arguments to accept union { - char **t_char_refptr; - char *t_char_ptr; - void *t_void_ptr; - int *t_int_ptr; - unsigned *t_uint_ptr; - float *t_float_ptr; - double *t_double_ptr; - char t_char; - int t_int; - unsigned t_uint; - float t_float; - double t_double; + char **t_char_refptr; ///< &pointer + char *t_char_ptr; ///< pointer + void *t_void_ptr; ///< pointer to void + int *t_int_ptr; ///< pointer to int + unsigned *t_uint_ptr; ///< pointer to unsigned int + float *t_float_ptr; ///< pointer to float + double *t_double_ptr; ///< pointer to double + char t_char; ///< type of char + int t_int; ///< type of int + unsigned t_uint; ///< type of unsigned int + float t_float; ///< type of float + double t_double; ///< type of double } argv[10]; // accept up to 10 arguments }; -void tpl_register_func(char *key, struct tplfunc_frame *frame); + +/** + * Register a template function + * @param key function name to expose to "func:" interface + * @param tplfunc_ptr pointer to function of type tplfunc + * @param argc number of function arguments to accept + */ +void tpl_register_func(char *key, void *tplfunc_ptr, int argc, void *data_in); + +/** + * Get the function frame associated with a template function + * @param key function name + * @return tplfunc_frame structure + */ +struct tplfunc_frame *tpl_getfunc(char *key); #endif //STASIS_TEMPLATE_H diff --git a/include/template_func_proto.h b/include/template_func_proto.h new file mode 100644 index 0000000..212759c --- /dev/null +++ b/include/template_func_proto.h @@ -0,0 +1,9 @@ +#ifndef TEMPLATE_FUNC_PROTO_H +#define TEMPLATE_FUNC_PROTO_H + +#include "template.h" + +int get_github_release_notes_tplfunc_entrypoint(void *frame, void *data_out); +int get_github_release_notes_auto_tplfunc_entrypoint(void *frame, void *data_out); + +#endif //TEMPLATE_FUNC_PROTO_H
\ No newline at end of file diff --git a/mission/hst/release_notes.md.in b/mission/hst/release_notes.md.in index 372b5c4..39b84a2 100644 --- a/mission/hst/release_notes.md.in +++ b/mission/hst/release_notes.md.in @@ -2,3 +2,4 @@ **{{meta.name}} {{meta.version}}** +{{ func:get_github_release_notes_auto() }} diff --git a/mission/jwst/release_notes.md.in b/mission/jwst/release_notes.md.in index 372b5c4..39b84a2 100644 --- a/mission/jwst/release_notes.md.in +++ b/mission/jwst/release_notes.md.in @@ -2,3 +2,4 @@ **{{meta.name}} {{meta.version}}** +{{ func:get_github_release_notes_auto() }} diff --git a/mission/roman/release_notes.md.in b/mission/roman/release_notes.md.in index 372b5c4..39b84a2 100644 --- a/mission/roman/release_notes.md.in +++ b/mission/roman/release_notes.md.in @@ -2,3 +2,4 @@ **{{meta.name}} {{meta.version}}** +{{ func:get_github_release_notes_auto() }} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c42bb0f..a7b06f7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(stasis_core STATIC rules.c docker.c junitxml.c + github.c + template_func_proto.c ) add_executable(stasis diff --git a/src/conda.c b/src/conda.c index 1e8b03f..976bbbc 100644 --- a/src/conda.c +++ b/src/conda.c @@ -32,7 +32,7 @@ int micromamba(struct MicromambaInfo *info, char *command, ...) { sprintf(mmbin, "%s/micromamba", info->micromamba_prefix); if (access(mmbin, F_OK)) { - char untarcmd[PATH_MAX]; + char untarcmd[PATH_MAX * 2]; mkdirs(info->micromamba_prefix, 0755); sprintf(untarcmd, "tar -xvf %s -C %s --strip-components=1 bin/micromamba 1>/dev/null", installer_path, info->micromamba_prefix); system(untarcmd); diff --git a/src/delivery.c b/src/delivery.c index 278647c..7bedef3 100644 --- a/src/delivery.c +++ b/src/delivery.c @@ -1419,7 +1419,7 @@ void delivery_install_conda(char *install_script, char *conda_install_dir) { // Proceed with the installation // -b = batch mode (non-interactive) - char cmd[255] = {0}; + char cmd[PATH_MAX] = {0}; snprintf(cmd, sizeof(cmd) - 1, "%s %s -b -p %s", find_program("bash"), install_script, @@ -1476,7 +1476,7 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { struct StrList *filtered = NULL; filtered = strlist_init(); - for (size_t i = 0, z = 0; i < strlist_count(dataptr); i++) { + for (size_t i = 0; i < strlist_count(dataptr); i++) { int ignore_pkg = 0; name = strlist_item(dataptr, i); @@ -1494,7 +1494,6 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { if (strstr(name, ctx->tests[x].name)) { version = ctx->tests[x].version; ignore_pkg = 1; - z++; break; } } @@ -2164,4 +2163,21 @@ int delivery_fixup_test_results(struct Delivery *ctx) { closedir(dp); return 0; +} + +int delivery_exists(struct Delivery *ctx) { + // TODO: scan artifactory repo for the same information + char release_pattern[PATH_MAX] = {0}; + sprintf(release_pattern, "*%s*", ctx->info.release_name); + struct StrList *files = listdir(ctx->storage.delivery_dir); + for (size_t i = 0; i < strlist_count(files); i++) { + char *filename = strlist_item(files, i); + int release_exists = fnmatch(release_pattern, filename, FNM_PATHNAME); + if (!globals.enable_overwrite && !release_exists) { + guard_strlist_free(&files); + return 1; + } + } + guard_strlist_free(&files); + return 0; }
\ No newline at end of file diff --git a/src/github.c b/src/github.c new file mode 100644 index 0000000..36e2e7c --- /dev/null +++ b/src/github.c @@ -0,0 +1,133 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "core.h" + +struct GHContent { + char *data; + size_t len; +}; + +static size_t writer(void *contents, size_t size, size_t nmemb, void *result) { + const size_t newlen = size * nmemb; + struct GHContent *content = (struct GHContent *) result; + + char *ptr = realloc(content->data, content->len + newlen + 1); + if (!ptr) { + perror("realloc failed"); + return 0; + } + + content->data = ptr; + memcpy(&(content->data[content->len]), contents, newlen); + content->len += newlen; + content->data[content->len] = 0; + + return newlen; +} + +static char *unescape_lf(char *value) { + char *seq = strstr(value, "\\n"); + while (seq != NULL) { + size_t cur_len = strlen(seq); + memmove(seq, seq + 1, strlen(seq) - 1); + *seq = '\n'; + if (strlen(seq) && cur_len) { + seq[cur_len - 1] = 0; + } + seq = strstr(value, "\\n"); + } + return value; +} + +int get_github_release_notes(const char *api_token, const char *repo, const char *tag, const char *target_commitish, char **output) { + const char *field_body = "\"body\":\""; + const char *field_message = "\"message\":\""; + const char *endpoint_header_auth_fmt = "Authorization: Bearer %s"; + const char *endpoint_header_api_version = "X-GitHub-Api-Version: " STASIS_GITHUB_API_VERSION; + const char *endpoint_post_fields_fmt = "{\"tag_name\":\"%s\", \"target_commitish\":\"%s\"}"; + const char *endpoint_url_fmt = "https://api.github.com/repos/%s/releases/generate-notes"; + char endpoint_header_auth[PATH_MAX] = {0}; + char endpoint_post_fields[PATH_MAX] = {0}; + char endpoint_url[PATH_MAX] = {0}; + struct curl_slist *list = NULL; + struct GHContent content; + + CURL *curl = curl_easy_init(); + if (!curl) { + return -1; + } + + // Render the header data + sprintf(endpoint_header_auth, endpoint_header_auth_fmt, api_token); + sprintf(endpoint_post_fields, endpoint_post_fields_fmt, tag, target_commitish); + sprintf(endpoint_url, endpoint_url_fmt, repo); + + // Begin curl configuration + curl_easy_setopt(curl, CURLOPT_URL, endpoint_url); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, endpoint_post_fields); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &content); + + // Append headers to the request + list = curl_slist_append(list, "Accept: application/vnd.github+json"); + list = curl_slist_append(list, endpoint_header_auth); + list = curl_slist_append(list, endpoint_header_api_version); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); + + // Set the user-agent (github requires one) + char user_agent[20] = {0}; + sprintf(user_agent, "stasis/%s", VERSION); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent); + + // Execute curl request + memset(&content, 0, sizeof(content)); + CURLcode res; + res = curl_easy_perform(curl); + + // Clean up + curl_slist_free_all(list); + curl_easy_cleanup(curl); + + if(res != CURLE_OK) { + fprintf(stderr, "curl_easy_perform() failed: %s\n", + curl_easy_strerror(res)); + return -1; + } + + // Replace all "\\n" literals with new line characters + char *line = unescape_lf(content.data); + if (line) { + char *data_offset = NULL; + if ((data_offset = strstr(line, field_body))) { + // Skip past the body field + data_offset += strlen(field_body); + // Remove quotation mark (and trailing comma if it exists) + int trim = 2; + char last_char = data_offset[strlen(data_offset) - trim]; + if (last_char == ',') { + trim++; + } + data_offset[strlen(data_offset) - trim] = 0; + // Extract release notes + *output = strdup(data_offset); + } else if ((data_offset = strstr(line, field_message))) { + // Skip past the message field + data_offset += strlen(field_message); + *(strchr(data_offset, '"')) = 0; + fprintf(stderr, "GitHub API Error: '%s'\n", data_offset); + fprintf(stderr, "URL: %s\n", endpoint_url); + fprintf(stderr, "POST: %s\n", endpoint_post_fields); + guard_free(content.data); + return -1; + } + } else { + fprintf(stderr, "Unknown error\n"); + guard_free(content.data); + return -1; + } + + guard_free(content.data); + return 0; +}
\ No newline at end of file diff --git a/src/globals.c b/src/globals.c index 297598f..18a32b5 100644 --- a/src/globals.c +++ b/src/globals.c @@ -36,6 +36,22 @@ struct STASIS_GLOBAL globals = { .enable_docker = true, .enable_artifactory = true, .enable_testing = true, + .envctl = { + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"TMPDIR", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_ROOT", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_SYSCONFDIR", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_CPU_COUNT", "CPU_COUNT", NULL}}, + {.flags = STASIS_ENVCTL_REQUIRED | STASIS_ENVCTL_REDACT, .name={"STASIS_GH_TOKEN", "GITHUB_TOKEN", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_ACCESS_TOKEN", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_JF_USER", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_PASSWORD", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_SSH_KEY_PATH", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_SSH_PASSPHRASE", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_CLIENT_CERT_CERT_PATH", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_CLIENT_CERT_KEY_PATH", NULL}}, + {.flags = STASIS_ENVCTL_REQUIRED, .name = {"STASIS_JF_REPO", NULL}}, + {.flags = 0, .name = {NULL}}, + } }; void globals_free() { @@ -153,7 +153,7 @@ int ini_getval(struct INIFILE *ini, char *section_name, char *key, int type, uni case INIVAL_TYPE_STR_ARRAY: strcpy(tbufp, data->value); *data->value = '\0'; - for (size_t i = 0; (token = strsep(&tbufp, "\n")) != NULL; i++) { + while ((token = strsep(&tbufp, "\n")) != NULL) { lstrip(token); strcat(data->value, token); strcat(data->value, "\n"); diff --git a/src/junitxml.c b/src/junitxml.c index c1bf1be..9c7e5b4 100644 --- a/src/junitxml.c +++ b/src/junitxml.c @@ -146,16 +146,16 @@ static struct StrList *attributes_to_strlist(xmlTextReaderPtr reader) { static int read_xml_data(xmlTextReaderPtr reader, struct JUNIT_Testsuite **testsuite) { const xmlChar *name; - const xmlChar *value; + //const xmlChar *value; name = xmlTextReaderConstName(reader); if (!name) { // name could not be converted to string name = BAD_CAST "--"; } - value = xmlTextReaderConstValue(reader); + //value = xmlTextReaderConstValue(reader); const char *node_name = (char *) name; - const char *node_value = (char *) value; + //const char *node_value = (char *) value; struct StrList *attrs = attributes_to_strlist(reader); if (attrs && strlist_count(attrs)) { diff --git a/src/stasis_main.c b/src/stasis_main.c index c550982..ce49829 100644 --- a/src/stasis_main.c +++ b/src/stasis_main.c @@ -9,6 +9,7 @@ #define OPT_NO_DOCKER 1001 #define OPT_NO_ARTIFACTORY 1002 #define OPT_NO_TESTING 1003 +#define OPT_OVERWRITE 1004 static struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'V'}, @@ -18,6 +19,7 @@ static struct option long_options[] = { {"verbose", no_argument, 0, 'v'}, {"unbuffered", no_argument, 0, 'U'}, {"update-base", no_argument, 0, OPT_ALWAYS_UPDATE_BASE}, + {"overwrite", no_argument, 0, OPT_OVERWRITE}, {"no-docker", no_argument, 0, OPT_NO_DOCKER}, {"no-artifactory", no_argument, 0, OPT_NO_ARTIFACTORY}, {"no-testing", no_argument, 0, OPT_NO_TESTING}, @@ -33,6 +35,7 @@ const char *long_options_help[] = { "Increase output verbosity", "Disable line buffering", "Update conda installation prior to STASIS environment creation", + "Overwrite an existing release", "Do not build docker images", "Do not upload artifacts to Artifactory", "Do not execute test scripts", @@ -95,7 +98,32 @@ static void usage(char *progname) { } } +static const char *has_envctl_key_(size_t i) { + for (size_t x = 0; globals.envctl[i].name[x] != NULL; x++) { + const char *name = globals.envctl[i].name[x]; + const char *data = getenv(name); + if (data) { + return name; + } + } + return NULL; +} + +static void check_system_env_requirements() { + msg(STASIS_MSG_L1, "Checking environment\n"); + for (size_t i = 0; globals.envctl[i].name[0] != NULL; i++) { + unsigned int flags = globals.envctl[i].flags; + const char *key = has_envctl_key_(i); + if ((flags & STASIS_ENVCTL_REQUIRED) && !(key && strlen(getenv(key)))) { + if (!strcmp(key, "STASIS_JF_REPO") && !globals.enable_artifactory) { + continue; + } + msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "Environment variable '%s' must be configured.\n", globals.envctl[i].name[0]); + exit(1); + } + } +} static void check_system_requirements(struct Delivery *ctx) { const char *tools_required[] = { @@ -139,6 +167,11 @@ static void check_system_requirements(struct Delivery *ctx) { } } +static void check_requirements(struct Delivery *ctx) { + check_system_requirements(ctx); + check_system_env_requirements(); +} + int main(int argc, char *argv[]) { struct Delivery ctx; struct Process proc = { @@ -193,6 +226,9 @@ int main(int argc, char *argv[]) { case 'v': globals.verbose = true; break; + case OPT_OVERWRITE: + globals.enable_overwrite = true; + break; case OPT_NO_DOCKER: globals.enable_docker = false; user_disabled_docker = true; @@ -260,6 +296,11 @@ int main(int argc, char *argv[]) { tpl_register("workaround.tox_posargs", &globals.workaround.tox_posargs); tpl_register("workaround.conda_reactivate", &globals.workaround.conda_reactivate); + // Expose function(s) to the template engine + // Prototypes can be found in template_func_proto.h + tpl_register_func("get_github_release_notes", &get_github_release_notes_tplfunc_entrypoint, 3, NULL); + tpl_register_func("get_github_release_notes_auto", &get_github_release_notes_auto_tplfunc_entrypoint, 1, &ctx); + // Set up PREFIX/etc directory information // The user may manipulate the base directory path with STASIS_SYSCONFDIR // environment variable @@ -325,7 +366,7 @@ int main(int argc, char *argv[]) { msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "Failed to initialize delivery context\n"); exit(1); } - check_system_requirements(&ctx); + check_requirements(&ctx); msg(STASIS_MSG_L2, "Configuring JFrog CLI\n"); if (delivery_init_artifactory(&ctx)) { @@ -346,6 +387,12 @@ int main(int argc, char *argv[]) { //delivery_runtime_show(&ctx); } + // Safety gate: Avoid clobbering a delivery unless the user wants that behavior + if (delivery_exists(&ctx)) { + msg(STASIS_MSG_ERROR | STASIS_MSG_L1, "Refusing to overwrite delivery: %s\nUse --overwrite to enable release clobbering.\n", ctx.info.release_name); + exit(1); + } + msg(STASIS_MSG_L1, "Conda setup\n"); delivery_get_installer_url(&ctx, installer_url); msg(STASIS_MSG_L2, "Downloading: %s\n", installer_url); diff --git a/src/template.c b/src/template.c index 819fe92..8e1357c 100644 --- a/src/template.c +++ b/src/template.c @@ -25,10 +25,14 @@ extern void tpl_reset() { tpl_pool_func_used = 0; } -void tpl_register_func(char *key, struct tplfunc_frame *frame) { - (void) key; // TODO: placeholder - tpl_pool_func[tpl_pool_func_used] = calloc(1, sizeof(*tpl_pool_func[tpl_pool_func_used])); - memcpy(tpl_pool_func[tpl_pool_func_used], frame, sizeof(*frame)); +void tpl_register_func(char *key, void *tplfunc_ptr, int argc, void *data_in) { + struct tplfunc_frame *frame = calloc(1, sizeof(*frame)); + frame->key = strdup(key); + frame->argc = argc; + frame->func = tplfunc_ptr; + frame->data_in = data_in; + + tpl_pool_func[tpl_pool_func_used] = frame; tpl_pool_func_used++; } @@ -220,7 +224,7 @@ char *tpl_render(char *str) { strcpy(func_name_temp, type_stop + 1); char *param_begin = strchr(func_name_temp, '('); if (!param_begin) { - fprintf(stderr, "offset %zu: function name must be followed by a '('\n", off); + fprintf(stderr, "At position %zu in %s\nfunction name must be followed by a '('\n", off, key); guard_free(output); return NULL; } @@ -228,7 +232,7 @@ char *tpl_render(char *str) { param_begin++; char *param_end = strrchr(param_begin, ')'); if (!param_end) { - fprintf(stderr, "offset %zu: function arguments must be closed with a ')'\n", off); + fprintf(stderr, "At position %zu in %s\nfunction arguments must be closed with a ')'\n", off, key); guard_free(output); return NULL; } @@ -239,8 +243,8 @@ char *tpl_render(char *str) { for (params_count = 0; params[params_count] != NULL; params_count++); struct tplfunc_frame *frame = tpl_getfunc(k); - if (params_count > frame->argc) { - fprintf(stderr, "offset %zu: Too many arguments for function: %s()\n", off, frame->key); + if (params_count > frame->argc || params_count < frame->argc) { + fprintf(stderr, "At position %zu in %s\nIncorrect number of arguments for function: %s (expected %d, got %d)\n", off, key, frame->key, frame->argc, params_count); value = strdup(""); } else { for (size_t p = 0; p < sizeof(frame->argv) / sizeof(*frame->argv) && params[p] != NULL; p++) { @@ -248,10 +252,13 @@ char *tpl_render(char *str) { strip(params[p]); frame->argv[p].t_char_ptr = params[p]; } - char func_result[100]; - char *fres = func_result; - frame->func(frame, fres); - value = strdup(fres); + char *func_result = NULL; + int func_status = 0; + if ((func_status = frame->func(frame, &func_result))) { + fprintf(stderr, "%s returned non-zero status: %d\n", frame->key, func_status); + } + value = strdup(func_result ? func_result : ""); + guard_free(func_result); } GENERIC_ARRAY_FREE(params); } else { diff --git a/src/template_func_proto.c b/src/template_func_proto.c new file mode 100644 index 0000000..140a5e0 --- /dev/null +++ b/src/template_func_proto.c @@ -0,0 +1,65 @@ +#include "template_func_proto.h" + +int get_github_release_notes_tplfunc_entrypoint(void *frame, void *data_out) { + int result; + char **output = (char **) data_out; + struct tplfunc_frame *f = (struct tplfunc_frame *) frame; + char *api_token = getenv("STASIS_GH_TOKEN"); + if (!api_token) { + api_token = getenv("GITHUB_TOKEN"); + } + result = get_github_release_notes(api_token ? api_token : "anonymous", + (const char *) f->argv[0].t_char_ptr, + (const char *) f->argv[1].t_char_ptr, + (const char *) f->argv[2].t_char_ptr, + output); + return result; +} + +int get_github_release_notes_auto_tplfunc_entrypoint(void *frame, void *data_out) { + int result = 0; + char **output = (char **) data_out; + struct tplfunc_frame *f = (struct tplfunc_frame *) frame; + char *api_token = getenv("STASIS_GITHUB_TOKEN"); + if (!api_token) { + api_token = getenv("GITHUB_TOKEN"); + } + + const struct Delivery *ctx = (struct Delivery *) f->data_in; + struct StrList *notes_list = strlist_init(); + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(*ctx->tests); i++) { + // Get test context + const struct Test *test = &ctx->tests[i]; + if (test->name && test->version && test->repository) { + char *repository = strdup(test->repository); + char *match = strstr(repository, "spacetelescope/"); + // Cull repository URL + if (match) { + replace_text(repository, "https://github.com/", "", 0); + if (endswith(repository, ".git")) { + replace_text(repository, ".git", "", 0); + } + // Record release notes for version relative to HEAD + // Using HEAD, GitHub returns the previous tag + char *note = NULL; + char h1_title[NAME_MAX] = {0}; + sprintf(h1_title, "# %s", test->name); + strlist_append(¬es_list, h1_title); + result += get_github_release_notes(api_token ? api_token : "anonymous", + repository, + test->version, + "HEAD", + ¬e); + if (note) { + strlist_append(¬es_list, note); + guard_free(note); + } + } + } + } + // Return all notes as a single string + if (strlist_count(notes_list)) { + *output = join(notes_list->data, "\n\n"); + } + return result; +} diff --git a/tests/test_str.c b/tests/test_str.c index be3f3e1..85c3b78 100644 --- a/tests/test_str.c +++ b/tests/test_str.c @@ -204,7 +204,7 @@ void test_split() { for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { char **result; result = split(tc[i].data, tc[i].delim, tc[i].max_split); - STASIS_ASSERT(strcmp_array(result, tc[i].expected) == 0, "Split failed"); + STASIS_ASSERT(strcmp_array((const char **) result, tc[i].expected) == 0, "Split failed"); GENERIC_ARRAY_FREE(result); } } @@ -223,7 +223,7 @@ void test_join() { }; for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { char *result; - result = join(tc[i].data, tc[i].delim); + result = join((char **) tc[i].data, tc[i].delim); STASIS_ASSERT(strcmp(result ? result : "", tc[i].expected) == 0, "failed to join array"); guard_free(result); } @@ -276,19 +276,19 @@ void test_substring_between() { void test_strdeldup() { struct testcase { - const char **data; + char **data; const char **expected; }; struct testcase tc[] = { {.data = NULL, .expected = NULL}, - {.data = (const char *[]) {"a", "a", "a", "b", "b", "b", "c", "c", "c", NULL}, .expected = (const char *[]) {"a", "b", "c", NULL}}, - {.data = (const char *[]) {"a", "b", "c", "a", "b", "c", "a", "b", "c", NULL}, .expected = (const char *[]) {"a", "b", "c", NULL}}, - {.data = (const char *[]) {"apple", "banana", "coconut", NULL}, .expected = (const char *[]) {"apple", "banana", "coconut", NULL}}, - {.data = (const char *[]) {"apple", "banana", "apple", "coconut", NULL}, .expected = (const char *[]) {"apple", "banana", "coconut", NULL}}, + {.data = (char *[]) {"a", "a", "a", "b", "b", "b", "c", "c", "c", NULL}, .expected = (const char *[]) {"a", "b", "c", NULL}}, + {.data = (char *[]) {"a", "b", "c", "a", "b", "c", "a", "b", "c", NULL}, .expected = (const char *[]) {"a", "b", "c", NULL}}, + {.data = (char *[]) {"apple", "banana", "coconut", NULL}, .expected = (const char *[]) {"apple", "banana", "coconut", NULL}}, + {.data = (char *[]) {"apple", "banana", "apple", "coconut", NULL}, .expected = (const char *[]) {"apple", "banana", "coconut", NULL}}, }; for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { char **result = strdeldup(tc[i].data); - STASIS_ASSERT(strcmp_array(result, tc[i].expected) == 0, "incorrect number of duplicates removed"); + STASIS_ASSERT(strcmp_array((const char **) result, tc[i].expected) == 0, "incorrect number of duplicates removed"); GENERIC_ARRAY_FREE(result); } } @@ -374,4 +374,4 @@ int main(int argc, char *argv[]) { }; STASIS_TEST_RUN(tests); STASIS_TEST_END_MAIN(); -}
\ No newline at end of file +} diff --git a/tests/test_strlist.c b/tests/test_strlist.c index 723c904..90219bd 100644 --- a/tests/test_strlist.c +++ b/tests/test_strlist.c @@ -144,7 +144,7 @@ void test_strlist_append_file() { right = expected[z]; STASIS_ASSERT(strcmp(left, right) == 0, "file content is different than expected"); } - STASIS_ASSERT(strcmp_array(list->data, expected) == 0, "file contents does not match expected values"); + STASIS_ASSERT(strcmp_array((const char **) list->data, expected) == 0, "file contents does not match expected values"); guard_strlist_free(&list); } } @@ -614,4 +614,4 @@ int main(int argc, char *argv[]) { }; STASIS_TEST_RUN(tests); STASIS_TEST_END_MAIN(); -}
\ No newline at end of file +} diff --git a/tests/test_template.c b/tests/test_template.c index 05d8242..79fce5e 100644 --- a/tests/test_template.c +++ b/tests/test_template.c @@ -9,28 +9,36 @@ extern unsigned tpl_pool_func_used; static int adder(struct tplfunc_frame *frame, void *result) { int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); - sprintf(result, "%d", a + b); + char **ptr = (char **) result; + *ptr = calloc(100, sizeof(*ptr)); + sprintf(*ptr, "%d", a + b); return 0; } static int subtractor(struct tplfunc_frame *frame, void *result) { int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); - sprintf(result, "%d", a - b); + char **ptr = (char **) result; + *ptr = calloc(100, sizeof(*ptr)); + sprintf(*ptr, "%d", a - b); return 0; } static int multiplier(struct tplfunc_frame *frame, void *result) { int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); - sprintf(result, "%d", a * b); + char **ptr = (char **) result; + *ptr = calloc(100, sizeof(*ptr)); + sprintf(*ptr, "%d", a * b); return 0; } static int divider(struct tplfunc_frame *frame, void *result) { int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); - sprintf(result, "%d", a / b); + char **ptr = (char **) result; + *ptr = calloc(100, sizeof(*ptr)); + sprintf(*ptr, "%d", a / b); return 0; } @@ -59,17 +67,22 @@ void test_tpl_register() { void test_tpl_register_func() { tpl_reset(); - struct tplfunc_frame tasks[] = { - {.key = "add", .argc = 2, .func = adder}, - {.key = "sub", .argc = 2, .func = subtractor}, - {.key = "mul", .argc = 2, .func = multiplier}, - {.key = "div", .argc = 2, .func = divider}, + struct testcase { + const char *key; + int argc; + void *func; }; - tpl_register_func("add", &tasks[0]); - tpl_register_func("sub", &tasks[1]); - tpl_register_func("mul", &tasks[2]); - tpl_register_func("div", &tasks[3]); - STASIS_ASSERT(tpl_pool_func_used == sizeof(tasks) / sizeof(*tasks), "unexpected function pool used"); + struct testcase tc[] = { + {.key = "add", .argc = 2, .func = &adder}, + {.key = "sub", .argc = 2, .func = &subtractor}, + {.key = "mul", .argc = 2, .func = &multiplier}, + {.key = "div", .argc = 2, .func = ÷r}, + }; + tpl_register_func("add", tc[0].func, tc[0].argc, NULL); + tpl_register_func("sub", tc[1].func, tc[1].argc, NULL); + tpl_register_func("mul", tc[2].func, tc[2].argc, NULL); + tpl_register_func("div", tc[3].func, tc[3].argc, NULL); + STASIS_ASSERT(tpl_pool_func_used == sizeof(tc) / sizeof(*tc), "unexpected function pool used"); char *result = NULL; result = tpl_render("{{ func:add(0,3) }}"); |