diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 17 | ||||
-rw-r--r-- | src/conda.c | 176 | ||||
-rw-r--r-- | src/deliverable.c | 626 | ||||
-rw-r--r-- | src/download.c | 34 | ||||
-rw-r--r-- | src/environment.c | 445 | ||||
-rw-r--r-- | src/ini.c | 409 | ||||
-rw-r--r-- | src/main.c | 373 | ||||
-rw-r--r-- | src/recipe.c | 63 | ||||
-rw-r--r-- | src/relocation.c | 167 | ||||
-rw-r--r-- | src/str.c | 867 | ||||
-rw-r--r-- | src/strlist.c | 483 | ||||
-rw-r--r-- | src/system.c | 201 | ||||
-rw-r--r-- | src/utils.c | 417 | ||||
-rw-r--r-- | src/wheel.c | 74 |
14 files changed, 4352 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..8f996f2 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,17 @@ +include_directories(${CMAKE_SOURCE_DIR}/include) +include_directories(${PROJECT_BINARY_DIR}) +add_executable(omc + main.c + str.c + strlist.c + ini.c + conda.c + environment.c + utils.c + system.c + download.c + deliverable.c + recipe.c + relocation.c + wheel.c +) diff --git a/src/conda.c b/src/conda.c new file mode 100644 index 0000000..41c03ee --- /dev/null +++ b/src/conda.c @@ -0,0 +1,176 @@ +// +// Created by jhunk on 5/14/23. +// + +#include <unistd.h> +#include "conda.h" + +int python_exec(const char *args) { + char command[PATH_MAX]; + memset(command, 0, sizeof(command)); + snprintf(command, sizeof(command) - 1, "python %s", args); + msg(OMC_MSG_L3, "Executing: %s\n", command); + return system(command); +} + +int pip_exec(const char *args) { + char command[PATH_MAX]; + memset(command, 0, sizeof(command)); + snprintf(command, sizeof(command) - 1, "python -m pip %s", args); + msg(OMC_MSG_L3, "Executing: %s\n", command); + return system(command); +} + +int conda_exec(const char *args) { + char command[PATH_MAX]; + const char *mamba_commands[] = { + "build", + "install", + "update", + "create", + "list", + "search", + "run", + "info", + "clean", + "activate", + "deactivate", + NULL + }; + char conda_as[6]; + memset(conda_as, 0, sizeof(conda_as)); + + strcpy(conda_as, "conda"); + for (size_t i = 0; mamba_commands[i] != NULL; i++) { + if (startswith(args, mamba_commands[i])) { + strcpy(conda_as, "mamba"); + break; + } + } + + snprintf(command, sizeof(command) - 1, "%s %s", conda_as, args); + msg(OMC_MSG_L3, "Executing: %s\n", command); + return system(command); +} + +int conda_activate(const char *root, const char *env_name) { + int fd = -1; + FILE *fp = NULL; + const char *init_script_conda = "/etc/profile.d/conda.sh"; + const char *init_script_mamba = "/etc/profile.d/mamba.sh"; + char path_conda[PATH_MAX] = {0}; + char path_mamba[PATH_MAX] = {0}; + char logfile[PATH_MAX] = {0}; + struct Process proc; + memset(&proc, 0, sizeof(proc)); + + // Where to find conda's init scripts + sprintf(path_conda, "%s%s", root, init_script_conda); + sprintf(path_mamba, "%s%s", root, init_script_mamba); + + // Set the path to our stdout log + // Emulate mktemp()'s behavior. Give us a unique file name, but don't use + // the file handle at all. We'll open it as a FILE stream soon enough. + strcpy(logfile, "/tmp/shell_XXXXXX"); + fd = mkstemp(logfile); + if (fd < 0) { + perror(logfile); + return -1; + } + close(fd); + + // Configure our process for output to a log file + strcpy(proc.stdout, logfile); + + // Verify conda's init scripts are available + if (access(path_conda, F_OK) < 0) { + perror(path_conda); + return -1; + } + + if (access(path_mamba, F_OK) < 0) { + perror(path_mamba); + return -1; + } + + // Fully activate conda and record its effect on the runtime environment + char command[PATH_MAX]; + snprintf(command, sizeof(command) - 1, "source %s; source %s; conda activate %s &>/dev/null; printenv", path_conda, path_mamba, env_name); + int retval = shell2(&proc, command); + if (retval) { + // it didn't work; drop out for cleanup + return retval; + } + + // Parse the log file: + // 1. Extract the environment keys and values from the sub-shell + // 2. Apply it to ohmycal's runtime environment + // 3. Now we're ready to execute conda commands anywhere + fp = fopen(proc.stdout, "r"); + if (!fp) { + perror(logfile); + return -1; + } + static char buf[1024]; + int i = 0; + while (fgets(buf, sizeof(buf) -1, fp) != NULL) { + buf[strlen(buf) - 1] = 0; + if (!strlen(buf)) { + continue; + } + //printf("[%d] %s\n", i, buf); + char *eq = strchr(buf, '='); + if (eq) { + *eq = '\0'; + } + char *key = buf; + char *val = &eq[1]; + setenv(key, val, 1); + i++; + } + fclose(fp); + remove(logfile); + return 0; +} + +void conda_env_create_from_uri(char *name, char *uri) { + char env_command[PATH_MAX]; + sprintf(env_command, "env create -n %s -f %s", name, uri); + if (conda_exec(env_command)) { + fprintf(stderr, "derived environment creation failed\n"); + exit(1); + } +} + +void conda_env_create(char *name, char *python_version, char *packages) { + char env_command[PATH_MAX]; + sprintf(env_command, "create -n %s python=%s %s", name, python_version, packages ? packages : ""); + if (conda_exec(env_command)) { + fprintf(stderr, "conda environment creation failed\n"); + exit(1); + } +} + +void conda_env_remove(char *name) { + char env_command[PATH_MAX]; + sprintf(env_command, "env remove -n %s", name); + if (conda_exec(env_command)) { + fprintf(stderr, "conda environment removal failed\n"); + exit(1); + } +} + +void conda_env_export(char *name, char *output_dir, char *output_filename) { + char env_command[PATH_MAX]; + sprintf(env_command, "env export -n %s -f %s/%s.yml", name, output_dir, output_filename); + if (conda_exec(env_command)) { + fprintf(stderr, "conda environment export failed\n"); + exit(1); + } +} + +int conda_index(const char *path) { + char command[PATH_MAX]; + sprintf(command, "index %s", path); + return conda_exec(command); +} 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 + } + } + } + + +} diff --git a/src/download.c b/src/download.c new file mode 100644 index 0000000..42d1653 --- /dev/null +++ b/src/download.c @@ -0,0 +1,34 @@ +// +// Created by jhunk on 10/5/23. +// + +#include "download.h" + +size_t download_writer(void *fp, size_t size, size_t nmemb, void *stream) { + size_t bytes = fwrite(fp, size, nmemb, (FILE *) stream); + return bytes; +} + +int download(char *url, const char *filename) { + CURL *c; + FILE *fp; + + curl_global_init(CURL_GLOBAL_ALL); + c = curl_easy_init(); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, download_writer); + fp = fopen(filename, "wb"); + if (!fp) { + return 1; + } + //curl_easy_setopt(c, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(c, CURLOPT_WRITEDATA, fp); + curl_easy_perform(c); + fclose(fp); + + curl_easy_cleanup(c); + curl_global_cleanup(); + return 0; +}
\ No newline at end of file diff --git a/src/environment.c b/src/environment.c new file mode 100644 index 0000000..a979886 --- /dev/null +++ b/src/environment.c @@ -0,0 +1,445 @@ +/** + * @file environment.c + */ +#include "environment.h" +#include "utils.h" +#include "strlist.h" + +extern char **__environ; + +/** + * Print a shell-specific listing of environment variables to `stdout` + * + * Example: + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * runtime_export(rt, NULL); + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * Usage: + * ~~~{.sh} + * $ gcc program.c + * $ ./a.out + * PATH="/thing/stuff/bin:/example/please/bin" + * SHELL="/your/shell" + * CC="/your/compiler" + * ...=... + * + * # You can also use this to modify the shell environment + * # (use `runtime_set` to manipulate the output) + * $ source $(./a.out) + * ~~~ + * + * Example of exporting specific keys from the environment: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * + * // inline declaration + * runtime_export(rt, (char *[]) {"PATH", "LS_COLORS", NULL}); + * + * // standard declaration + * char *keys_to_export[] = { + * "PATH", "LS_COLORS", NULL + * } + * runtime_export(rt, keys_to_export); + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param keys Array of keys to export. A value of `NULL` exports all environment keys + */ +void runtime_export(RuntimeEnv *env, char **keys) { + char *borne[] = { + "bash", + "dash", + "zsh", + NULL, + }; + char *unborne[] = { + "csh" + "tcsh", + NULL, + }; + + char output[BUFSIZ]; + char export_command[7]; // export=6 and setenv=6... convenient + char *_sh = getenv("SHELL"); + char *sh = path_basename(_sh); + if (sh == NULL) { + fprintf(stderr, "echo SHELL environment variable is not defined"); + exit(1); + } + + for (size_t i = 0; borne[i] != NULL; i++) { + if (strcmp(sh, borne[i]) == 0) { + strcpy(export_command, "export"); + break; + } + } + for (size_t i = 0; unborne[i] != NULL; i++) { + if (strcmp(sh, unborne[i]) == 0) { + strcpy(export_command, "setenv"); + break; + } + } + + for (size_t i = 0; i < strlist_count(env); i++) { + char **pair = split(strlist_item(env, i), "=", 0); + char *key = pair[0]; + char *value = NULL; + + // We split a potentially large string by "=" so: + // Recombine elements pair[1..N] into a single string by "=" + if (pair[1] != NULL) { + value = join(&pair[1], "="); + } + + if (keys != NULL) { + for (size_t j = 0; keys[j] != NULL; j++) { + if (strcmp(keys[j], key) == 0) { + //sprintf(output, "%s=\"%s\"\n%s %s", key, value ? value : "", export_command, key); + sprintf(output, "%s %s=\"%s\"", export_command, key, value ? value : ""); + puts(output); + } + } + } + else { + sprintf(output, "%s %s=\"%s\"", export_command, key, value ? value : ""); + puts(output); + } + free(value); + split_free(pair); + } +} + +/** + * Populate a `RuntimeEnv` structure + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = NULL; + * // Example 1: Copy the shell environment + * rt = runtime_copy(arge); + * // Example 2: Create your own environment + * rt = runtime_copy((char *[]) {"SHELL=/bin/bash", "PATH=/opt/secure:/bin:/usr/bin"}) + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env Array of strings in `var=value` format + * @return `RuntimeEnv` structure + */ +RuntimeEnv *runtime_copy(char **env) { + RuntimeEnv *rt = NULL; + size_t env_count; + for (env_count = 0; env[env_count] != NULL; env_count++); + + rt = strlist_init(); + for (size_t i = 0; i < env_count; i++) { + strlist_append(rt, env[i]); + } + return rt; +} + +/** + * Replace the contents of `dest` with `src` + * @param dest pointer of type `RuntimeEnv` + * @param src pointer to environment array + * @return 0 on success, <0 on error + */ +int runtime_replace(RuntimeEnv **dest, char **src) { + RuntimeEnv *rt_tmp = runtime_copy(src); + if (!rt_tmp) { + return -1; + } + runtime_free((*dest)); + + (*dest) = runtime_copy(rt_tmp->data); + if (!(*dest)) { + return -1; + } + runtime_free(rt_tmp); + + runtime_apply((*dest)); + return 0; +} + +/** + * Determine whether or not a key exists in the runtime environment + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * if (runtime_contains(rt, "PATH") { + * // $PATH is present + * } + * else { + * // $PATH is NOT present + * } + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param key Environment variable string + * @return -1=no, positive_value=yes + */ +ssize_t runtime_contains(RuntimeEnv *env, const char *key) { + ssize_t result = -1; + for (size_t i = 0; i < strlist_count(env); i++) { + char **pair = split(strlist_item(env, i), "=", 0); + if (pair == NULL) { + break; + } + if (strcmp(pair[0], key) == 0) { + result = i; + split_free(pair); + break; + } + split_free(pair); + } + return result; +} + +/** + * Retrieve the value of a runtime environment variable + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * char *path = runtime_get("PATH"); + * if (path == NULL) { + * // handle error + * } + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param key Environment variable string + * @return success=string, failure=`NULL` + */ +char *runtime_get(RuntimeEnv *env, const char *key) { + char *result = NULL; + ssize_t key_offset = runtime_contains(env, key); + if (key_offset != -1) { + char **pair = split(strlist_item(env, key_offset), "=", 0); + result = join(&pair[1], "="); + split_free(pair); + } + return result; +} + +/** + * Parse an input string and expand any environment variable(s) found + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * char *secure_path = runtime_expand_var(rt, "/opt/secure:$PATH:/aux/bin"); + * if (secure_path == NULL) { + * // handle error + * } + * // secure_path = "/opt/secure:/your/original/path/here:/aux/bin"; + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param input String to parse + * @return success=expanded string, failure=`NULL` + */ +char *runtime_expand_var(RuntimeEnv *env, const char *input) { + const char delim = '$'; + const char *delim_literal = "$$"; + char *expanded = NULL; + + // Input is invalid + if (!input) { + return NULL; + } + + // If there's no environment variables to process return a copy of the input string + if (strchr(input, delim) == NULL) { + return strdup(input); + } + + expanded = calloc(BUFSIZ, sizeof(char)); + if (expanded == NULL) { + perror("could not allocate runtime_expand_var buffer"); + fprintf(SYSERROR); + return NULL; + } + + // Parse the input string + size_t i; + for (i = 0; i < strlen(input); i++) { + char var[MAXNAMLEN]; // environment variable name + memset(var, '\0', MAXNAMLEN); // zero out name + + // Handle literal statement "$$var" + // Value becomes "$var" (unexpanded) + if (strncmp(&input[i], delim_literal, strlen(delim_literal)) == 0) { + strncat(expanded, &delim, 1); + i += strlen(delim_literal); + // Ignore opening brace + if (input[i] == '{') { + i++; + } + } + + // Handle variable when encountering a single $ + // Value expands from "$var" to "environment value of var" + if (input[i] == delim) { + // Ignore opening brace + if (input[i+1] == '{') { + i++; + } + char *tmp = NULL; + i++; + + // Construct environment variable name from input + // "$ var" == no + // "$-*)!@ == no + // "$var" == yes + for (size_t c = 0; isalnum(input[i]) || input[i] == '_'; c++, i++) { + // Ignore closing brace + if (input[i] == '}') { + i++; + } + var[c] = input[i]; + } + + if (env) { + tmp = runtime_get(env, var); + } else { + tmp = getenv(var); + } + if (tmp == NULL) { + // This mimics shell behavior in general. + // Prevent appending whitespace when an environment variable does not exist + if (i > 0) { + i--; + } + continue; + } + // Append expanded environment variable to output + strncat(expanded, tmp, strlen(tmp)); + if (env) { + free(tmp); + } + } + + // Nothing to do so append input to output + if (input[i] == '}') { + // Unless we ended on a closing brace + continue; + } + strncat(expanded, &input[i], 1); + } + + return expanded; +} + +/** + * Set a runtime environment variable. + * + * + * Note: `_value` is passed through `runtime_expand_var` to provide shell expansion + * + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * + * runtime_set(rt, "new_var", "1"); + * char *new_var = runtime_get("new_var"); + * // new_var = 1; + * + * char *path = runtime_get("PATH"); + * // path = /your/path:/here + * + * runtime_set(rt, "PATH", "/opt/secure:$PATH"); + * char *secure_path = runtime_get("PATH"); + * // secure_path = /opt/secure:/your/path:/here + * // NOTE: path and secure_path are COPIES, unlike `getenv()` and `setenv()` that reuse their pointers in `environ` + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * + * @param env `RuntimeEnv` structure + * @param _key Environment variable to set + * @param _value New environment variable value + */ +void runtime_set(RuntimeEnv *env, const char *_key, const char *_value) { + if (_key == NULL) { + return; + } + char *key = strdup(_key); + ssize_t key_offset = runtime_contains(env, key); + char *value = runtime_expand_var(env, _value); + char *now = join((char *[]) {key, value, NULL}, "="); + + if (key_offset < 0) { + strlist_append(env, now); + } + else { + strlist_set(env, key_offset, now); + } + free(now); + free(key); + free(value); +} + +/** + * Update the global `environ` array with data from `RuntimeEnv` + * @param env `RuntimeEnv` structure + */ +void runtime_apply(RuntimeEnv *env) { + for (size_t i = 0; i < strlist_count(env); i++) { + char **pair = split(strlist_item(env, i), "=", 0); + setenv(pair[0], pair[1], 1); + split_free(pair); + } +} + +/** + * Free `RuntimeEnv` allocated by `runtime_copy` + * @param env `RuntimeEnv` structure + */ +void runtime_free(RuntimeEnv *env) { + if (env == NULL) { + return; + } + strlist_free(env); +} diff --git a/src/ini.c b/src/ini.c new file mode 100644 index 0000000..b2df150 --- /dev/null +++ b/src/ini.c @@ -0,0 +1,409 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include "ohmycal.h" +#include "ini.h" +/* +char *strip(char *s) { + size_t len = strlen(s) + 1; + while (--len) { + if (isalnum(s[len])) { + break; + } + if (isblank(s[len])) { + s[len] = '\0'; + } + } + return s; +} + */ + +/* +char *lstrip(char *s) { + size_t i = 0; + char *end = NULL; + do { + end = &s[i]; + if (!isblank(*end)) { + break; + } + i++; + } while (1); + if (i) { + size_t len = strlen(end); + memmove(s, end, len); + if (strlen(s)) { + s[len] = '\0'; + } + } + return s; +} + */ + +/* +int startswith(const char *s1, char *s2) { + size_t i; + for (i = 0; i < strlen(s2); i++) { + if (s1[i] != s2[i]) { + return 0; + } + } + return 1; +} +*/ + +/* +int endswith(const char *s1, char *s2) { + size_t s2x, s1x; + for (s2x = strlen(s2), s1x = strlen(s1); s2x >= 0; s2x--, s1x--) { + char *s1p = &s1[s1x]; + char *s2p = &s2[s2x]; + if (s1[s1x] != s2[s2x]) { + return 0; + } + if (s2x == 0) { + break; + } + } + return 1; +} + */ + +struct INIFILE *ini_init() { + struct INIFILE *ini; + ini = calloc(1, sizeof(*ini)); + ini->section_count = 0; + return ini; +} + +void ini_section_init(struct INIFILE **ini) { + (*ini)->section = calloc((*ini)->section_count + 1, sizeof(**(*ini)->section)); +} + +struct INISection *ini_section_search(struct INIFILE **ini, char *value) { + struct INISection *result = NULL; + for (size_t i = 0; i < (*ini)->section_count; i++) { + if ((*ini)->section[i]->key != NULL) { + if (!strcmp((*ini)->section[i]->key, value)) { + result = (*ini)->section[i]; + } + } + } + return result; +} + +int ini_data_init(struct INIFILE **ini, char *section_name) { + struct INISection *section = ini_section_search(ini, section_name); + if (section == NULL) { + return 1; + } + section->data = calloc(section->data_count + 1, sizeof(**section->data)); + return 0; +} + +struct INIData *ini_data_get(struct INIFILE *ini, char *section_name, char *key) { + struct INISection *section = NULL; + section = ini_section_search(&ini, section_name); + for (size_t i = 0; i < section->data_count; i++) { + if (section->data[i]->key != NULL) { + if (!strcmp(section->data[i]->key, key)) { + return section->data[i]; + } + } + } + return NULL; +} + +struct INIData *ini_getall(struct INIFILE *ini, char *section_name) { + struct INISection *section = NULL; + struct INIData *result = NULL; + static size_t i = 0; + + section = ini_section_search(&ini, section_name); + if (section->data[i]) { + result = section->data[i]; + i++; + } else { + result = NULL; + i = 0; + } + + return result; +} + +int ini_getval(struct INIFILE *ini, char *section_name, char *key, int type, union INIVal *result) { + char *token = NULL; + char tbuf[BUFSIZ]; + char *tbufp = tbuf; + struct INIData *data; + data = ini_data_get(ini, section_name, key); + if (!data) { + result->as_char_p = NULL; + return -1; + } + switch (type) { + case INIVAL_TYPE_INT: + result->as_int = (int) strtol(data->value, NULL, 10); + break; + case INIVAL_TYPE_UINT: + result->as_uint = (unsigned int) strtoul(data->value, NULL, 10); + break; + case INIVAL_TYPE_LONG: + result->as_long = (long) strtol(data->value, NULL, 10); + break; + case INIVAL_TYPE_ULONG: + result->as_ulong = (unsigned long) strtoul(data->value, NULL, 10); + break; + case INIVAL_TYPE_LLONG: + result->as_llong = (long long) strtoll(data->value, NULL, 10); + break; + case INIVAL_TYPE_ULLONG: + result->as_ullong = (unsigned long long) strtoull(data->value, NULL, 10); + break; + case INIVAL_TYPE_DOUBLE: + result->as_double = (double) strtod(data->value, NULL); + break; + case INIVAL_TYPE_FLOAT: + result->as_float = (float) strtod(data->value, NULL); + break; + case INIVAL_TYPE_STR: + result->as_char_p = lstrip(data->value); + break; + case INIVAL_TYPE_STR_ARRAY: + strcpy(tbufp, data->value); + *data->value = '\0'; + for (size_t i = 0; (token = strsep(&tbufp, "\n")) != NULL; i++) { + lstrip(token); + strcat(data->value, token); + strcat(data->value, "\n"); + } + result->as_char_p = data->value; + break; + case INIVAL_TYPE_BOOL: + result->as_bool = false; + if ((!strcmp(data->value, "true") || !strcmp(data->value, "True")) || + (!strcmp(data->value, "yes") || !strcmp(data->value, "Yes")) || + strtol(data->value, NULL, 10)) { + result->as_bool = true; + } + break; + default: + memset(result, 0, sizeof(*result)); + break; + } + return 0; +} + +int ini_data_record(struct INIFILE **ini, char *section_name, char *key, char *value) { + struct INISection *section = ini_section_search(ini, section_name); + if (section == NULL) { + return 1; + } + + struct INIData **tmp = realloc(section->data, (section->data_count + 1) * sizeof(**section->data)); + if (!tmp) { + perror(__FUNCTION__); + exit(1); + } + section->data = tmp; + if (!ini_data_get((*ini), section_name, key)) { + section->data[section->data_count] = calloc(1, sizeof(*section->data[0])); + section->data[section->data_count]->key = key; //strdup(key); + section->data[section->data_count]->value = value; //strdup(value); + section->data_count++; + } else { + struct INIData *data = ini_data_get(*ini, section_name, key); + size_t value_len_old = strlen(data->value); + size_t value_len = strlen(value); + size_t value_len_new = value_len_old + value_len; + /* + char *value_tmp = NULL; + value_tmp = realloc(data->value, value_len_new + 2); + if (!value_tmp) { + perror(__FUNCTION__ ); + exit(1); + } + data->value = value_tmp; + */ + //strcat(data->value, " "); + strcat(data->value, value); + } + return 0; +} + +void ini_section_record(struct INIFILE **ini, char *key) { + struct INISection **tmp = realloc((*ini)->section, ((*ini)->section_count + 1) * sizeof((*ini)->section)); + if (!tmp) { + perror(__FUNCTION__); + exit(1); + } + (*ini)->section = tmp; + (*ini)->section[(*ini)->section_count] = calloc(1, sizeof(*(*ini)->section[0])); + (*ini)->section[(*ini)->section_count]->key = strdup(key); + (*ini)->section_count++; +} + +void ini_show(struct INIFILE *ini) { + for (size_t x = 0; x < ini->section_count; x++) { + printf("[%s]\n", ini->section[x]->key); + for (size_t y = 0; y < ini->section[x]->data_count; y++) { + printf("%s='%s'\n", ini->section[x]->data[y]->key, ini->section[x]->data[y]->value); + } + printf("\n"); + } +} + +char *unquote(char *s) { + int found = 0; + if (startswith(s, "'") && endswith(s, "'")) { + found = 1; + } else if (startswith(s, "\"") && endswith(s, "\"")) { + found = 1; + } + + if (found) { + memmove(s, s + 1, strlen(s)); + s[strlen(s) - 1] = '\0'; + } + return s; +} + +char *collapse_whitespace(char **s) { + size_t len = strlen(*s); + size_t i; + for (i = 0; isblank((int)*s[i]); i++); + memmove(*s, *s + i, strlen(*s)); + if (i) { + *s[len - i] = '\0'; + } + return *s; +} + +void ini_free(struct INIFILE **ini) { + for (size_t section = 0; section < (*ini)->section_count; section++) { + for (size_t data = 0; data < (*ini)->section[section]->data_count; data++) { + if ((*ini)->section[section]->data[data]) { + free((*ini)->section[section]->data[data]->key); + free((*ini)->section[section]->data[data]->value); + free((*ini)->section[section]->data[data]); + } + } + free((*ini)->section[section]->data); + free((*ini)->section[section]->key); + free((*ini)->section[section]); + } + free((*ini)->section); + free((*ini)); +} + +struct INIFILE *ini_open(const char *filename) { + FILE *fp; + char line[BUFSIZ] = {0}; + char current_section[BUFSIZ] = {0}; + char *key_last = NULL; + struct INIFILE *ini = ini_init(); + + ini_section_init(&ini); + + // Create an implicit section. [default] does not need to be present in the INI config + ini_section_record(&ini, "default"); + strcpy(current_section, "default"); + //ini_data_init(&ini, "default"); + + // Open the configuration file for reading + fp = fopen(filename, "r"); + if (!fp) { + perror(filename); + exit(1); + } + + // Read file + for (size_t i = 0; fgets(line, sizeof(line), fp) != NULL; i++) { + // Find pointer to first comment character + char *comment = strpbrk(line, ";#"); + if (comment) { + // Remove comment from line (standalone and inline comments) + if (!(comment - line > 0 && (*(comment - 1) == '\\') || (*comment - 1) == '#')) { + *comment = '\0'; + } else { + // Handle escaped comment characters. Remove the escape character '\' + memmove(comment - 1, comment, strlen(comment)); + comment[strlen(comment) - 1] = '\0'; + } + } + + // Removing comments could have reduced the line's length, so calculate it now + size_t len = strlen(line); + + // Ignore empty lines + if (!len || line[0] == '\n') { + continue; + } + + // Test for section header: [string] + if (startswith(line, "[")) { + // Ignore default section because we already have an implicit one + if (!strncmp(&line[1], "default", strlen("default"))) { + continue; + } + + // Remove section ending: ']' + line[strlen(line) - 2] = '\0'; + + // Create new named section + ini_section_record(&ini, &line[1]); + //ini_data_init(&ini, &line[1]); + + // Record the name of the section. This is used until another section is found. + strcpy(current_section, &line[1]); + continue; + } + + char *key = NULL; + char *value = malloc(BUFSIZ); + char *operator = strchr(line, '='); + + // continuation line + if (startswith(line, " ") || startswith(line, "\t")) { + operator = NULL; + } + + if (operator) { + size_t key_len = operator - line; + key = strndup(line, key_len); + key_last = key; + strcpy(value, &operator[1]); + value[strlen(value) - 1] = '\0'; + } else if (!key && !strlen(value) && ! (startswith(line, " ") || startswith(line, "\t"))) { + fprintf(stderr, "NO OPERATOR OR INDENT: %zu:'%s'\n", i, line); + struct INISection *section = ini_section_search(&ini, current_section); + struct INIData *data = NULL; + //key = key_last; + free(value); + value = NULL; + } else { + struct INISection *section = ini_section_search(&ini, current_section); + struct INIData *data = section->data[section->data_count - 1]; + if (strlen(data->value)) { + data->value[strlen(data->value) - 1] = '\n'; + } + key = key_last; + strcpy(value, line); + if (endswith(value, "\n")) { + value[strlen(value) - 1] = '\n'; + } + } + + // Store key value pair in section's data array + if (key) { + lstrip(key); + strip(key); + unquote(value); + lstrip(value); + ini_data_record(&ini, current_section, key, value); + } + } + + return ini; +}
\ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..c33b351 --- /dev/null +++ b/src/main.c @@ -0,0 +1,373 @@ +#define GNU_SOURCE 1 +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> +#include <time.h> +#include <sys/utsname.h> +#include "ohmycal.h" +#include "wheel.h" + +const char *VERSION = "1.0.0"; +const char *AUTHOR = "Joseph Hunkeler"; +const char *BANNER = "---------------------------------------------------------------------\n" + " ██████╗ ██╗ ██╗ ███╗ ███╗██╗ ██╗ ██████╗ █████╗ ██╗ \n" + "██╔═══██╗██║ ██║ ████╗ ████║╚██╗ ██╔╝ ██╔════╝██╔══██╗██║ \n" + "██║ ██║███████║ ██╔████╔██║ ╚████╔╝ ██║ ███████║██║ \n" + "██║ ██║██╔══██║ ██║╚██╔╝██║ ╚██╔╝ ██║ ██╔══██║██║ \n" + "╚██████╔╝██║ ██║ ██║ ╚═╝ ██║ ██║ ╚██████╗██║ ██║███████╗\n" + " ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝\n" + "---------------------------------------------------------------------\n" + " Delivery Generator \n" + " v%s\n" + "---------------------------------------------------------------------\n" + "Copyright (C) 2023 %s,\n" + "Association of Universities for Research in Astronomy (AURA)\n"; + + +void conda_setup_headless() { + // Configure conda for headless CI + conda_exec("config --system --set auto_update_conda false"); + conda_exec("config --system --set always_yes true"); + conda_exec("config --system --set quiet true"); + conda_exec("config --system --set rollback_enabled false"); + conda_exec("config --system --set report_errors false"); + + if (conda_exec("update --all")) { + perror("update base"); + exit(1); + } +} + +void delivery_install_conda(char *install_script, char *conda_install_dir) { + struct Process proc; + memset(&proc, 0, sizeof(proc)); + + if (!access(conda_install_dir, F_OK)) { + if (rmtree(conda_install_dir)) { + perror("unable to remove previous installation"); + exit(1); + } + } + + // -b = batch mode + if (shell_safe(&proc, (char *[]) {find_program("bash"), install_script, "-b", "-p", conda_install_dir, NULL})) { + fprintf(stderr, "conda installation failed\n"); + exit(1); + } +} + +void delivery_conda_enable(struct Delivery *ctx, char *conda_install_dir) { + if (conda_activate(conda_install_dir, "base")) { + fprintf(stderr, "conda activation failed\n"); + exit(1); + } + + if (runtime_replace(&ctx->runtime.environ, __environ)) { + perror("unable to replace runtime environment after activating conda"); + exit(1); + } + + conda_setup_headless(); +} + +#define DEFER_CONDA 0 +#define DEFER_PIP 1 +void delivery_defer_packages(struct Delivery *ctx, int type) { + struct StrList *dataptr = NULL; + struct StrList *deferred = NULL; + char *name = NULL; + char cmd[PATH_MAX]; + + memset(cmd, 0, sizeof(cmd)); + + char mode[10]; + if (DEFER_CONDA == type) { + dataptr = ctx->conda.conda_packages; + deferred = ctx->conda.conda_packages_defer; + strcpy(mode, "conda"); + } else if (DEFER_PIP == type) { + dataptr = ctx->conda.pip_packages; + deferred = ctx->conda.pip_packages_defer; + strcpy(mode, "pip"); + } + msg(OMC_MSG_L2, "Filtering %s packages by test definition...\n", mode); + + struct StrList *filtered = NULL; + filtered = strlist_init(); + for (size_t i = 0, z = 0; i < strlist_count(dataptr); i++) { + name = strlist_item(dataptr, i); + if (!strlen(name) || isblank(*name) || isspace(*name)) { + continue; + } + msg(OMC_MSG_L3, "package '%s': ", name); + int ignore_pkg = 0; + for (size_t x = 0; x < sizeof(ctx->tests) / sizeof(ctx->tests[0]); x++) { + if (ctx->tests[x].name) { + if (startswith(ctx->tests[x].name, name)) { + ignore_pkg = 1; + z++; + break; + } + } + } + + if (ignore_pkg) { + printf("BUILD FOR HOST\n"); + strlist_append(deferred, name); + } else { + printf("USE EXISTING\n"); + strlist_append(filtered, name); + } + } + + if (!strlist_count(deferred)) { + msg(OMC_MSG_WARN, "No packages were filtered by test definitions"); + } else { + if (DEFER_CONDA == type) { + strlist_free(ctx->conda.conda_packages); + ctx->conda.conda_packages = strlist_copy(filtered); + } else if (DEFER_PIP == type) { + strlist_free(ctx->conda.pip_packages); + ctx->conda.pip_packages = strlist_copy(filtered); + } + } +} + +void testfunc(struct Delivery *ctx, char *env_name) { + struct Wheel *wheel; + wheel = get_wheel_file(ctx->storage.wheel_artifact_dir, "drizzlepac", (char *[]) {"cp310", "x86_64", NULL}); + return; +} + +int main(int argc, char *argv[], char *arge[]) { + struct INIFILE *cfg = NULL; + struct INIFILE *ini = NULL; + struct Delivery ctx; + struct Process proc = { + .stdout = "", + .stderr = "", + .redirect_stderr = 0, + }; + struct tm *tm_info; + time_t timenow; + char env_date[100]; + char env_name[PATH_MAX]; + char env_name_testing[PATH_MAX]; + char env_pyver[10]; + char *delivery_input = argv[1]; + char *config_input = argv[2]; + char installer_url[PATH_MAX]; + + memset(&proc, 0, sizeof(proc)); + memset(&ctx, 0, sizeof(ctx)); + + if (!delivery_input) { + fprintf(stderr, "Missing *.ini file\n"); + exit(1); + } + + msg(OMC_MSG_L1, "Initializing\n"); + struct utsname uts; + uname(&uts); + + msg(OMC_MSG_L2, "Setting architecture\n"); + char archsuffix[255]; + ctx.system.arch = strdup(uts.machine); + if (!strcmp(ctx.system.arch, "x86_64")) { + strcpy(archsuffix, "64"); + } else { + strcpy(archsuffix, ctx.system.arch); + } + + msg(OMC_MSG_L2, "Setting platform\n"); + strcpy(ctx.system.platform[DELIVERY_PLATFORM], uts.sysname); + if (!strcmp(ctx.system.platform[DELIVERY_PLATFORM], "Darwin")) { + sprintf(ctx.system.platform[DELIVERY_PLATFORM_CONDA_SUBDIR], "osx-%s", archsuffix); + strcpy(ctx.system.platform[DELIVERY_PLATFORM_CONDA_INSTALLER], "MacOSX"); + strcpy(ctx.system.platform[DELIVERY_PLATFORM_RELEASE], "macos"); + } else if (!strcmp(ctx.system.platform[DELIVERY_PLATFORM], "Linux")) { + sprintf(ctx.system.platform[DELIVERY_PLATFORM_CONDA_SUBDIR], "linux-%s", archsuffix); + strcpy(ctx.system.platform[DELIVERY_PLATFORM_CONDA_INSTALLER], "Linux"); + strcpy(ctx.system.platform[DELIVERY_PLATFORM_RELEASE], "linux"); + } else { + // Not explicitly supported systems + strcpy(ctx.system.platform[DELIVERY_PLATFORM_CONDA_SUBDIR], ctx.system.platform[DELIVERY_PLATFORM]); + strcpy(ctx.system.platform[DELIVERY_PLATFORM_CONDA_INSTALLER], ctx.system.platform[DELIVERY_PLATFORM]); + strcpy(ctx.system.platform[DELIVERY_PLATFORM_RELEASE], ctx.system.platform[DELIVERY_PLATFORM]); + tolower_s(ctx.system.platform[DELIVERY_PLATFORM_RELEASE]); + } + + msg(OMC_MSG_L2, "Setting up runtime environment...\n"); + setenv("OMC_ARCH", ctx.system.arch, 1); + setenv("OMC_PLATFORM", ctx.system.platform[DELIVERY_PLATFORM], 1); + setenv("OMC_CONDA_ARCH", ctx.system.arch, 1); + setenv("OMC_CONDA_PLATFORM", ctx.system.platform[DELIVERY_PLATFORM_CONDA_INSTALLER], 1); + setenv("OMC_CONDA_PLATFORM_SUBDIR", ctx.system.platform[DELIVERY_PLATFORM_CONDA_SUBDIR], 1); + + if (config_input) { + msg(OMC_MSG_L2, "Reading OMC global configuration: %s\n", config_input); + cfg = ini_open(config_input); + //ini_show(cfg); + } + + msg(OMC_MSG_L2, "Reading OMC delivery configuration: %s\n", delivery_input); + ini = ini_open(delivery_input); + //ini_show(ini); + + printf(BANNER, VERSION, AUTHOR); + + delivery_init(&ctx, ini, cfg); + runtime_apply(ctx.runtime.environ); + msg(OMC_MSG_L1, "Overview\n"); + delivery_meta_show(&ctx); + delivery_conda_show(&ctx); + delivery_tests_show(&ctx); + + msg(OMC_MSG_L1, "Conda setup\n"); + delivery_get_installer_url(&ctx, installer_url); + msg(OMC_MSG_L2, "Downloading: %s\n", installer_url); + delivery_get_installer(installer_url); + + // Unlikely to occur: this should help prevent rmtree() from destroying your entire filesystem + // if path is "/" then, die + // or if empty string, die + if (!strcmp(ctx.storage.conda_install_prefix, DIR_SEP) || !strlen(ctx.storage.conda_install_prefix)) { + fprintf(stderr, "error: ctx.storage.conda_install_prefix is malformed!\n"); + exit(1); + } + + msg(OMC_MSG_L2, "Installing: %s\n", path_basename(installer_url)); + delivery_install_conda(path_basename(installer_url), ctx.storage.conda_install_prefix); + + msg(OMC_MSG_L2, "Configuring: %s\n", ctx.storage.conda_install_prefix); + delivery_conda_enable(&ctx, ctx.storage.conda_install_prefix); + + // Generate release and/or environment name + memset(env_pyver, 0, sizeof(env_pyver)); + char *tmp_pyver = to_short_version(ctx.meta.python); + sprintf(env_pyver, "py%s", tmp_pyver); + free(tmp_pyver); + tmp_pyver = NULL; + + msg(OMC_MSG_L1, "Generating release string\n"); + if (!strcasecmp(ctx.meta.mission, "hst") && ctx.meta.final) { + memset(env_date, 0, sizeof(env_date)); + strftime(env_date, sizeof(env_date) - 1, "%Y%m%d", tm_info); + sprintf(env_name, "%s_%s_rc%d", ctx.meta.name, env_date, ctx.meta.rc); + } else if (!strcasecmp(ctx.meta.mission, "hst")) { + sprintf(env_name, "%s_%s_%s_%s_rc%d", ctx.meta.name, ctx.meta.codename, ctx.system.platform[DELIVERY_PLATFORM_RELEASE], env_pyver, ctx.meta.rc); + } else if (!strcasecmp(ctx.meta.mission, "jwst") && ctx.meta.final) { + sprintf(env_name, "%s_%s_rc%d", ctx.meta.name, ctx.meta.version, ctx.meta.rc); + } else if (!strcasecmp(ctx.meta.mission, "jwst")) { + sprintf(env_name, "%s_%s_final", ctx.meta.name, ctx.meta.version); + } + msg(OMC_MSG_L2, "%s\n", env_name); + sprintf(env_name_testing, "%s_test", env_name); + + msg(OMC_MSG_L1, "Creating release environment(s)\n"); + if (ctx.meta.based_on && strlen(ctx.meta.based_on)) { + conda_env_remove(env_name); + conda_env_create_from_uri(env_name, ctx.meta.based_on); + + conda_env_remove(env_name_testing); + conda_env_create_from_uri(env_name_testing, ctx.meta.based_on); + } else { + conda_env_create(env_name, ctx.meta.python, NULL); + conda_env_create(env_name_testing, ctx.meta.python, NULL); + } + + // Activate test environment + msg(OMC_MSG_L1, "Activating test environment\n"); + if (conda_activate(ctx.storage.conda_install_prefix, env_name_testing)) { + fprintf(stderr, "failed to activate test environment\n"); + exit(1); + } + + msg(OMC_MSG_L2, "Installing build tools\n"); + if (conda_exec("install boa conda-build conda-verify")) { + msg(OMC_MSG_ERROR | OMC_MSG_L2, "conda-build installation failed"); + exit(1); + } + + if (pip_exec("install build")) { + msg(OMC_MSG_ERROR | OMC_MSG_L2, "'build' tool installation failed"); + exit(1); + } + + time(&timenow); + tm_info = localtime(&timenow); + + // Execute configuration-defined tests + msg(OMC_MSG_L1, "Begin test execution\n"); + delivery_tests_run(&ctx); + + msg(OMC_MSG_L1, "Generating deferred package listing\n"); + // Test succeeded so move on to producing package artifacts + delivery_defer_packages(&ctx, DEFER_CONDA); + delivery_defer_packages(&ctx, DEFER_PIP); + + // TODO: wheels would be nice, but can't right now + //if (ctx.conda.pip_packages_defer) { + // if (!delivery_build_wheels(&ctx)) { + // exit(1); + // } + // if (delivery_copy_wheel_artifacts(&ctx)) { + // exit(1); + // } + // if (delivery_index_wheel_artifacts(&ctx)) { + // exit(1); + // } + //} + + if (ctx.conda.conda_packages_defer) { + msg(OMC_MSG_L2, "Building Conda recipe(s)\n"); + if (delivery_build_recipes(&ctx)) { + exit(1); + } + msg(OMC_MSG_L3, "Copying artifacts\n"); + if (delivery_copy_conda_artifacts(&ctx)) { + exit(1); + } + msg(OMC_MSG_L3, "Indexing artifacts\n"); + if (delivery_index_conda_artifacts(&ctx)) { + exit(1); + } + } + + // Populate the release environment + msg(OMC_MSG_L1, "Populating release environment\n"); + + msg(OMC_MSG_L2, "Installing conda packages\n"); + delivery_install_packages(&ctx, ctx.storage.conda_install_prefix, env_name, INSTALL_PKG_CONDA, (struct StrList *[]) {ctx.conda.conda_packages, NULL}); + msg(OMC_MSG_L3, "Installing deferred conda packages\n"); + delivery_install_packages(&ctx, ctx.storage.conda_install_prefix, env_name, INSTALL_PKG_CONDA | INSTALL_PKG_CONDA_DEFERRED, (struct StrList *[]) {ctx.conda.conda_packages_defer, NULL}); + msg(OMC_MSG_L2, "Installing pip packages\n"); + delivery_install_packages(&ctx, ctx.storage.conda_install_prefix, env_name, INSTALL_PKG_PIP, (struct StrList *[]) {ctx.conda.pip_packages, NULL}); + msg(OMC_MSG_L3, "Installing deferred pip packages\n"); + delivery_install_packages(&ctx, ctx.storage.conda_install_prefix, env_name, INSTALL_PKG_PIP | INSTALL_PKG_PIP_DEFERRED, (struct StrList *[]) {ctx.conda.pip_packages_defer, NULL}); + + conda_exec("list"); + + msg(OMC_MSG_L1, "Creating release\n"); + msg(OMC_MSG_L2, "Exporting %s\n", env_name_testing); + conda_env_export(env_name_testing, ctx.storage.delivery_dir, env_name_testing); + + msg(OMC_MSG_L2, "Exporting %s\n", env_name); + conda_env_export(env_name, ctx.storage.delivery_dir, env_name); + + // Rewrite release environment output (i.e. set package origin(s) to point to the deployment server, etc.) + char specfile[PATH_MAX]; + sprintf(specfile, "%s/%s.yml", ctx.storage.delivery_dir, env_name); + msg(OMC_MSG_L3, "Rewriting release file %s\n", path_basename(specfile)); + delivery_rewrite_spec(&ctx, specfile); + + msg(OMC_MSG_L1, "Cleaning up\n"); + ini_free(&ini); + ini_free(&cfg); + + msg(OMC_MSG_L1, "Done!\n"); + return 0; +} + diff --git a/src/recipe.c b/src/recipe.c new file mode 100644 index 0000000..6f6eeab --- /dev/null +++ b/src/recipe.c @@ -0,0 +1,63 @@ +#include "recipe.h" + +int recipe_clone(char *recipe_dir, char *url, char *gitref, char **result) { + struct Process proc; + char destdir[PATH_MAX]; + char *reponame = NULL; + + memset(&proc, 0, sizeof(proc)); + memset(destdir, 0, sizeof(destdir)); + reponame = path_basename(url); + + sprintf(destdir, "%s/%s", recipe_dir, reponame); + if (!*result) { + *result = calloc(PATH_MAX, sizeof(*result)); + if (!*result) { + return -1; + } + } + strncpy(*result, destdir, PATH_MAX - 1); + + if (!access(destdir, F_OK)) { + if (!strcmp(destdir, "/")) { + fprintf(stderr, "OHMYCAL is misconfigured. Please check your output path(s) immediately.\n"); + fprintf(stderr, "recipe_dir = '%s'\nreponame = '%s'\ndestdir = '%s'\n", + recipe_dir, reponame, destdir); + } + if (rmtree(destdir)) { + free(*result); + *result = NULL; + return -1; + } + } + return git_clone(&proc, url, destdir, gitref); +} + + +int recipe_get_type(char *repopath) { + int result; + char path[PATH_MAX]; + // conda-forge is a collection of repositories + // "conda-forge.yml" is guaranteed to exist + const char *marker[] = { + "conda-forge.yml", + "stsci", + "meta.yaml", + NULL + }; + const int type[] = { + RECIPE_TYPE_CONDA_FORGE, + RECIPE_TYPE_ASTROCONDA, + RECIPE_TYPE_GENERIC + }; + + for (size_t i = 0; marker[i] != NULL; i++) { + sprintf(path, "%s/%s", repopath, marker[i]); + result = access(path, F_OK); + if (!result) { + return type[i]; + } + } + + return RECIPE_TYPE_UNKNOWN; +}
\ No newline at end of file diff --git a/src/relocation.c b/src/relocation.c new file mode 100644 index 0000000..a8157fa --- /dev/null +++ b/src/relocation.c @@ -0,0 +1,167 @@ +/** + * @file relocation.c + */ +#include "relocation.h" +#include "str.h" + +void replace_text(char *original, const char *target, const char *replacement) { + char buffer[OHMYCAL_BUFSIZ]; + char *tmp = original; + + memset(buffer, 0, sizeof(buffer)); + while (*tmp != '\0') { + if (!strncmp(tmp, target, strlen(target))) { + size_t replen; + char *stop_at = strchr(tmp, '\n'); + if (stop_at) { + replen = (stop_at - tmp); + } else { + replen = strlen(replacement); + } + strcat(buffer, replacement); + strcat(buffer, "\n"); + tmp += replen; + } else { + strncat(buffer, tmp, 1); + } + tmp++; + } + strcpy(original, buffer); +} + +void file_replace_text(const char* filename, const char* target, const char* replacement) { + FILE *fp = fopen(filename, "r"); + if (fp == NULL) { + fprintf(stderr, "unable to open for reading: %s\n", filename); + return; + } + + char buffer[OHMYCAL_BUFSIZ]; + char tempfilename[] = "tempfileXXXXXX"; + FILE *tfp = fopen(tempfilename, "w"); + + if (tfp == NULL) { + fprintf(stderr, "unable to open temporary fp for writing: %s\n", tempfilename); + fclose(fp); + return; + } + + // Write modified strings to temporary file + while (fgets(buffer, sizeof(buffer), fp)) { + if (strstr(buffer, target)) { + replace_text(buffer, target, replacement); + } + fputs(buffer, tfp); + } + + fclose(fp); + fclose(tfp); + + // Replace original with modified copy + remove(filename); + rename(tempfilename, filename); +} + +/** + * Replace all occurrences of `spattern` with `sreplacement` in `data` + * + * ~~~{.c} + * char *str = (char *)calloc(100, sizeof(char)); + * strcpy(str, "This are a test."); + * replace_line(str, "are", "is"); + * // str is: "This is a test." + * free(str); + * ~~~ + * + * @param data string to modify + * @param spattern string value to replace + * @param sreplacement replacement string value + * @return success=0, error=-1 + */ +ssize_t replace_line(char *data, const char *spattern, const char *sreplacement) { + if (data == NULL || spattern == NULL || sreplacement == NULL) { + return -1; + } + + if (strlen(spattern) == 0 || strlen(sreplacement) == 0) { + return 0; + } + + ssize_t count_replaced = 0; + + char *token = NULL; + char buf[OHMYCAL_BUFSIZ]; + char *bufp = buf; + char output[OHMYCAL_BUFSIZ]; + memset(output, 0, sizeof(output)); + strcpy(buf, data); + for (size_t i = 0; (token = strsep(&bufp, "\n")) != NULL; i++) { + char *match = strstr(token, spattern); + if (match) { + strncat(output, token, strlen(token) - strlen(match)); + strcat(output, sreplacement); + strcat(output, "\n"); + count_replaced++; + } else { + strcat(output, token); + strcat(output, "\n"); + } + } + + strcpy(data, output); + return count_replaced; +} + +/** + * Replace all occurrences of `oldstr` in file `path` with `newstr` + * @param filename file to modify + * @param oldstr string to replace + * @param newstr replacement string + * @return success=0, failure=-1, or value of `ferror()` + */ +int file_replace_line(char *filename, const char *spattern, const char *sreplacement) { + char data[OHMYCAL_BUFSIZ]; + char tempfile[PATH_MAX]; + FILE *fp = NULL; + if ((fp = fopen(filename, "r")) == NULL) { + perror(filename); + return -1; + } + + sprintf(tempfile, "%s.replacement", filename); + FILE *tfp = NULL; + if ((tfp = fopen(tempfile, "w+")) == NULL) { + fclose(fp); + perror(tempfile); + return -1; + } + + // Zero the data buffer + memset(data, '\0', OHMYCAL_BUFSIZ); + while(fgets(data, OHMYCAL_BUFSIZ, fp) != NULL) { + replace_line(data, spattern, sreplacement); + fprintf(tfp, "%s", data); + memset(data, 0, sizeof(data)); + } + fclose(fp); + fflush(tfp); + rewind(tfp); + + // Truncate the original file + if ((fp = fopen(filename, "w+")) == NULL) { + perror(filename); + return -1; + } + // Zero the data buffer once more + memset(data, '\0', OHMYCAL_BUFSIZ); + // Dump the contents of the temporary file into the original file + while(fgets(data, OHMYCAL_BUFSIZ, tfp) != NULL) { + fprintf(fp, "%s", data); + } + fclose(fp); + fclose(tfp); + + // Remove temporary file + unlink(tempfile); + return 0; +} diff --git a/src/str.c b/src/str.c new file mode 100644 index 0000000..8a5a43a --- /dev/null +++ b/src/str.c @@ -0,0 +1,867 @@ +/** + * @file strings.c + */ +#include <unistd.h> +#include "str.h" + +/** + * Determine how many times the character `ch` appears in `sptr` string + * @param sptr string to scan + * @param ch character to find + * @return count of characters found + */ +int num_chars(const char *sptr, int ch) { + int result = 0; + for (int i = 0; sptr[i] != '\0'; i++) { + if (sptr[i] == ch) { + result++; + } + } + return result; +} + +/** + * Scan for `pattern` string at the beginning of `sptr` + * + * @param sptr string to scan + * @param pattern string to search for + * @return 1 = found, 0 = not found, -1 = error + */ +int startswith(const char *sptr, const char *pattern) { + if (!sptr || !pattern) { + return -1; + } + for (size_t i = 0; i < strlen(pattern); i++) { + if (sptr[i] != pattern[i]) { + return 0; + } + } + return 1; +} + +/** + * Scan for `pattern` string at the end of `sptr` + * + * @param sptr string to scan + * @param pattern string to search for + * @return 1 = found, 0 = not found, -1 = error + */ +int endswith(const char *sptr, const char *pattern) { + if (!sptr || !pattern) { + return -1; + } + ssize_t sptr_size = (ssize_t) strlen(sptr); + ssize_t pattern_size = (ssize_t) strlen(pattern); + + if (sptr_size == pattern_size) { + if (strcmp(sptr, pattern) == 0) { + return 1; // yes + } + return 0; // no + } + + ssize_t s = sptr_size - pattern_size; + if (s < 0) { + return 0; + } + + for (size_t p = 0 ; s < sptr_size; s++, p++) { + if (sptr[s] != pattern[p]) { + // sptr does not end with pattern + return 0; + } + } + // sptr ends with pattern + return 1; +} + +/** + * Deletes any characters matching `chars` from `sptr` string + * + * @param sptr string to be modified in-place + * @param chars a string containing characters (e.g. " \n" would delete whitespace and line feeds) + */ +void strchrdel(char *sptr, const char *chars) { + if (sptr == NULL || chars == NULL) { + return; + } + + while (*sptr != '\0') { + for (int i = 0; chars[i] != '\0'; i++) { + if (*sptr == chars[i]) { + memmove(sptr, sptr + 1, strlen(sptr)); + } + } + sptr++; + } +} + +/** + * Find the integer offset of the first occurrence of `ch` in `sptr` + * + * ~~~{.c} + * char buffer[255]; + * char string[] = "abc=123"; + * long int separator_offset = strchroff(string, '='); + * for (long int i = 0; i < separator_offset); i++) { + * buffer[i] = string[i]; + * } + * ~~~ + * + * @param sptr string to scan + * @param ch character to find + * @return offset to character in string, or 0 on failure + */ +long int strchroff(const char *sptr, int ch) { + char *orig = strdup(sptr); + char *tmp = orig; + long int result = 0; + + int found = 0; + size_t i = 0; + + while (*tmp != '\0') { + if (*tmp == ch) { + found = 1; + break; + } + tmp++; + i++; + } + + if (found == 0 && i == strlen(sptr)) { + return -1; + } + + result = tmp - orig; + free(orig); + + return result; +} + +/** + * This function scans `sptr` from right to left removing any matches to `suffix` + * from the string. + * + * @param sptr string to be modified + * @param suffix string to be removed from `sptr` + */ +void strdelsuffix(char *sptr, const char *suffix) { + if (!sptr || !suffix) { + return; + } + size_t sptr_len = strlen(sptr); + size_t suffix_len = strlen(suffix); + intptr_t target_offset = sptr_len - suffix_len; + + // Prevent access to memory below input string + if (target_offset < 0) { + return; + } + + // Create a pointer to + char *target = sptr + target_offset; + if (!strcmp(target, suffix)) { + // Purge the suffix + memset(target, '\0', suffix_len); + // Recursive call continues removing suffix until it is gone + strip(sptr); + } +} + +/** + * Split a string by every delimiter in `delim` string. + * + * Callee must free memory using `split_free()` + * + * @param sptr string to split + * @param delim characters to split on + * @return success=parts of string, failure=NULL + */ +char** split(char *_sptr, const char* delim, size_t max) +{ + if (_sptr == NULL || delim == NULL) { + return NULL; + } + size_t split_alloc = 0; + // Duplicate the input string and save a copy of the pointer to be freed later + char *orig = _sptr; + char *sptr = strdup(orig); + + if (!sptr) { + return NULL; + } + + // Determine how many delimiters are present + for (size_t i = 0; i < strlen(delim); i++) { + if (max && i > max) { + break; + } + split_alloc += num_chars(sptr, delim[i]); + } + + // Preallocate enough records based on the number of delimiters + char **result = (char **)calloc(split_alloc + 2, sizeof(char *)); + if (!result) { + free(sptr); + return NULL; + } + + // No delimiter, but the string was not NULL, so return the original string + if (split_alloc == 0) { + result[0] = sptr; + result[1] = NULL; + return result; + } + + // Separate the string into individual parts and store them in the result array + int i = 0; + char *token = NULL; + while((token = strsep(&sptr, delim)) != NULL) { + if (max && i > max) { + --i; + strcat(result[i], delim); + strcat(result[i], token); + } else { + result[i] = calloc(BUFSIZ, sizeof(char)); + if (!result[i]) { + free(sptr); + return NULL; + } + strcpy(result[i], token); + i++; // next record + } + //memcpy(result[i], token, strlen(token) + 1); // copy the string contents into the record + } + //free(orig); + free(sptr); + return result; +} + +/** + * Frees memory allocated by `split()` + * @param ptr pointer to array + */ +void split_free(char **ptr) { + for (int i = 0; ptr[i] != NULL; i++) { + free(ptr[i]); + } + free(ptr); +} + +/** + * Create new a string from an array of strings + * + * ~~~{.c} + * char *array[] = { + * "this", + * "is", + * "a", + * "test", + * NULL, + * } + * + * char *test = join(array, " "); // "this is a test" + * char *test2 = join(array, "_"); // "this_is_a_test" + * char *test3 = join(array, ", "); // "this, is, a, test" + * + * free(test); + * free(test2); + * free(test3); + * ~~~ + * + * @param arr + * @param separator characters to insert between elements in string + * @return new joined string + */ +char *join(char **arr, const char *separator) { + char *result = NULL; + int records = 0; + size_t total_bytes = 0; + + if (!arr || !separator) { + return NULL; + } + + for (int i = 0; arr[i] != NULL; i++) { + total_bytes += strlen(arr[i]); + records++; + } + total_bytes += (records * strlen(separator)) + 1; + + result = (char *)calloc(total_bytes, sizeof(char)); + for (int i = 0; i < records; i++) { + strcat(result, arr[i]); + if (i < (records - 1)) { + strcat(result, separator); + } + } + return result; +} + +/** + * Join two or more strings by a `separator` string + * @param separator + * @param ... + * @return string + */ +char *join_ex(char *separator, ...) { + va_list ap; // Variadic argument list + size_t separator_len = 0; // Length of separator string + size_t size = 0; // Length of output string + size_t argc = 0; // Number of arguments ^ "..." + char **argv = NULL; // Arguments + char *current = NULL; // Current argument + char *result = NULL; // Output string + + if (separator == NULL) { + return NULL; + } + + // Initialize array + argv = calloc(argc + 1, sizeof(char *)); + if (argv == NULL) { + perror("join_ex calloc failed"); + return NULL; + } + + // Get length of the separator + separator_len = strlen(separator); + + // Process variadic arguments: + // 1. Iterate over argument list `ap` + // 2. Assign `current` with the value of argument in `ap` + // 3. Extend the `argv` array by the latest argument count `argc` + // 4. Sum the length of the argument and the `separator` passed to the function + // 5. Append `current` string to `argv` array + // 6. Update argument counter `argc` + va_start(ap, separator); + for(argc = 0; (current = va_arg(ap, char *)) != NULL; argc++) { + char **tmp = realloc(argv, (argc + 1) * sizeof(char *)); + if (tmp == NULL) { + perror("join_ex realloc failed"); + return NULL; + } + argv = tmp; + size += strlen(current) + separator_len; + argv[argc] = strdup(current); + } + va_end(ap); + + // Generate output string + result = calloc(size + 1, sizeof(char)); + for (size_t i = 0; i < argc; i++) { + // Append argument to string + strcat(result, argv[i]); + + // Do not append a trailing separator when we reach the last argument + if (i < (argc - 1)) { + strcat(result, separator); + } + free(argv[i]); + } + free(argv); + + return result; +} + +/** + * Extract the string encapsulated by characters listed in `delims` + * + * ~~~{.c} + * char *str = "this is [some data] in a string"; + * char *data = substring_between(string, "[]"); + * // data = "some data"; + * ~~~ + * + * @param sptr string to parse + * @param delims two characters surrounding a string + * @return success=text between delimiters, failure=NULL + */ +char *substring_between(char *sptr, const char *delims) { + if (sptr == NULL || delims == NULL) { + return NULL; + } + + // Ensure we have enough delimiters to continue + size_t delim_count = strlen(delims); + if (delim_count != 2) { + return NULL; + } + + // Create pointers to the delimiters + char *start = strchr(sptr, delims[0]); + if (start == NULL || strlen(start) == 0) { + return NULL; + } + + char *end = strchr(start + 1, delims[1]); + if (end == NULL) { + return NULL; + } + + start++; // ignore leading delimiter + + // Get length of the substring + ssize_t length = strlen(start); + if (length < 0) { + return NULL; + } + + char *result = (char *)calloc(length + 1, sizeof(char)); + if (!result) { + return NULL; + } + + // Copy the contents of the substring to the result + char *tmp = result; + while (start != end) { + *tmp = *start; + tmp++; + start++; + } + + return result; +} + +/* + * Comparison functions for `strsort` + */ +static int _strsort_alpha_compare(const void *a, const void *b) { + const char *aa = *(const char **)a; + const char *bb = *(const char **)b; + int result = strcmp(aa, bb); + return result; +} + +static int _strsort_numeric_compare(const void *a, const void *b) { + const char *aa = *(const char **)a; + const char *bb = *(const char **)b; + + if (isdigit(*aa) && isdigit(*bb)) { + long ia = strtol(aa, NULL, 10); + long ib = strtol(bb, NULL, 10); + + if (ia == ib) { + return 0; + } else if (ia < ib) { + return -1; + } else if (ia > ib) { + return 1; + } + } + return 0; +} + +static int _strsort_asc_compare(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + size_t len_a = strlen(aa); + size_t len_b = strlen(bb); + return len_a > len_b; +} + +/* + * Helper function for `strsortlen` + */ +static int _strsort_dsc_compare(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + size_t len_a = strlen(aa); + size_t len_b = strlen(bb); + return len_a < len_b; +} + +/** + * Sort an array of strings + * @param arr + */ +void strsort(char **arr, unsigned int sort_mode) { + if (arr == NULL) { + return; + } + + typedef int (*compar)(const void *, const void *); + // Default mode is alphabetic sort + compar fn = _strsort_alpha_compare; + + if (sort_mode == SPM_SORT_LEN_DESCENDING) { + fn = _strsort_dsc_compare; + } else if (sort_mode == SPM_SORT_LEN_ASCENDING) { + fn = _strsort_asc_compare; + } else if (sort_mode == SPM_SORT_ALPHA) { + fn = _strsort_alpha_compare; // ^ still selectable though ^ + } else if (sort_mode == SPM_SORT_NUMERIC) { + fn = _strsort_numeric_compare; + } + + size_t arr_size = 0; + + // Determine size of array (+ terminator) + for (size_t i = 0; arr[i] != NULL; i++) { + arr_size = i; + } + arr_size++; + + qsort(arr, arr_size, sizeof(char *), fn); +} + +/** + * Search for string in an array of strings + * @param arr array of strings + * @param str string to search for + * @return yes=`pointer to string`, no=`NULL`, failure=`NULL` + */ +char *strstr_array(char **arr, const char *str) { + if (arr == NULL || str == NULL) { + return NULL; + } + + for (int i = 0; arr[i] != NULL; i++) { + if (strstr(arr[i], str) != NULL) { + return arr[i]; + } + } + return NULL; +} + +/** + * Remove duplicate strings from an array of strings + * @param arr + * @return success=array of unique strings, failure=NULL + */ +char **strdeldup(char **arr) { + if (!arr) { + return NULL; + } + + size_t records; + // Determine the length of the array + for (records = 0; arr[records] != NULL; records++); + + // Allocate enough memory to store the original array contents + // (It might not have duplicate values, for example) + char **result = (char **)calloc(records + 1, sizeof(char *)); + if (!result) { + return NULL; + } + + int rec = 0; + size_t i = 0; + while(i < records) { + // Search for value in results + if (strstr_array(result, arr[i]) != NULL) { + // value already exists in results so ignore it + i++; + continue; + } + + // Store unique value + result[rec] = strdup(arr[i]); + if (!result[rec]) { + for (size_t die = 0; result[die] != NULL; die++) { + free(result[die]); + } + free(result); + return NULL; + } + + i++; + rec++; + } + return result; +} + +/** Remove leading whitespace from a string + * @param sptr pointer to string + * @return pointer to first non-whitespace character in string + */ +char *lstrip(char *sptr) { + char *tmp = sptr; + size_t bytes = 0; + + if (sptr == NULL) { + return NULL; + } + + while (isblank(*tmp) || isspace(*tmp)) { + bytes++; + tmp++; + } + if (tmp != sptr) { + memmove(sptr, sptr + bytes, strlen(sptr) - bytes); + memset((sptr + strlen(sptr)) - bytes, '\0', bytes); + } + return sptr; +} + +/** + * Remove trailing whitespace from a string + * @param sptr string + * @return truncated string + */ +char *strip(char *sptr) { + if (sptr == NULL) { + return NULL; + } + + size_t len = strlen(sptr); + if (len == 0) { + return sptr; + } + else if (len == 1 && (isblank(*sptr) || isspace(*sptr))) { + *sptr = '\0'; + return sptr; + } + for (size_t i = len; i != 0; --i) { + if (sptr[i] == '\0') { + continue; + } + if (isspace(sptr[i]) || isblank(sptr[i])) { + sptr[i] = '\0'; + } + else { + break; + } + } + return sptr; +} + +/** + * Determine if a string is empty + * @param sptr pointer to string + * @return 0=not empty, 1=empty + */ +int isempty(char *sptr) { + if (sptr == NULL) { + return -1; + } + + char *tmp = sptr; + while (*tmp) { + if (!isblank(*tmp) && !isspace(*tmp) && !iscntrl(*tmp)) { + return 0; + } + tmp++; + } + return 1; +} + +/** + * Determine if a string is encapsulated by quotes + * @param sptr pointer to string + * @return 0=not quoted, 1=quoted + */ +int isquoted(char *sptr) { + const char *quotes = "'\""; + + if (sptr == NULL) { + return -1; + } + + char *quote_open = strpbrk(sptr, quotes); + if (!quote_open) { + return 0; + } + char *quote_close = strpbrk(quote_open + 1, quotes); + if (!quote_close) { + return 0; + } + return 1; +} + +/** + * Determine whether the input character is a relational operator + * Note: `~` is non-standard + * @param ch + * @return 0=no, 1=yes + */ +int isrelational(char ch) { + char symbols[] = "~!=<>"; + char *symbol = symbols; + while (*symbol != '\0') { + if (ch == *symbol) { + return 1; + } + symbol++; + } + return 0; +} + +/** + * Print characters in `s`, `len` times + * @param s + * @param len + */ +void print_banner(const char *s, int len) { + size_t s_len = strlen(s); + if (!s_len) { + return; + } + for (size_t i = 0; i < (len / s_len); i++) { + for (size_t c = 0; c < s_len; c++) { + putchar(s[c]); + } + } + putchar('\n'); +} + +/** + * Collapse whitespace in `s`. The string is modified in place. + * @param s + * @return pointer to `s` + */ +char *normalize_space(char *s) { + size_t len; + size_t trim_pos; + int add_whitespace = 0; + char *result = s; + char *tmp; + + if (s == NULL) { + return NULL; + } + + if ((tmp = calloc(strlen(s) + 1, sizeof(char))) == NULL) { + perror("could not allocate memory for temporary string"); + return NULL; + } + char *tmp_orig = tmp; + + // count whitespace, if any + for (trim_pos = 0; isblank(s[trim_pos]); trim_pos++); + // trim whitespace from the left, if any + memmove(s, &s[trim_pos], strlen(&s[trim_pos])); + // cull bytes not part of the string after moving + len = strlen(s); + s[len - trim_pos] = '\0'; + + // Generate a new string with extra whitespace stripped out + while (*s != '\0') { + // Skip over any whitespace, but record that we encountered it + if (isblank(*s)) { + s++; + add_whitespace = 1; + continue; + } + // This gate avoids filling tmp with whitespace; we want to make our own + if (add_whitespace) { + *tmp = ' '; + tmp++; + add_whitespace = 0; + } + // Write character in s to tmp + *tmp = *s; + // Increment string pointers + s++; + tmp++; + } + + // Rewrite the input string + strcpy(result, tmp_orig); + free(tmp_orig); + return result; +} + +/** + * Duplicate an array of strings + * @param array + * @return + */ +char **strdup_array(char **array) { + char **result = NULL; + size_t elems = 0; + + // Guard + if (array == NULL) { + return NULL; + } + + // Count elements in `array` + for (elems = 0; array[elems] != NULL; elems++); + + // Create new array + result = calloc(elems + 1, sizeof(char *)); + for (size_t i = 0; i < elems; i++) { + result[i] = strdup(array[i]); + } + + return result; +} + +/** + * Compare two arrays of strings + * + * `a` and/or `b` may be `NULL`. You should test for `NULL` in advance if _your_ program considers this an error condition. + * + * @param a array of strings + * @param b array of strings + * @return 0 = identical + */ +int strcmp_array(const char **a, const char **b) { + size_t a_len = 0; + size_t b_len = 0; + + // This could lead to false-positives depending on what the caller plans to achieve + if (a == NULL && b == NULL) { + return 0; + } else if (a == NULL) { + return -1; + } else if (b == NULL) { + return 1; + } + + // Get length of arrays + for (a_len = 0; a[a_len] != NULL; a_len++); + for (b_len = 0; b[b_len] != NULL; b_len++); + + // Check lengths are equal + if (a_len < b_len) return (int)(b_len - a_len); + else if (a_len > b_len) return (int)(a_len - b_len); + + // Compare strings in the arrays returning the total difference in bytes + int result = 0; + for (size_t ai = 0, bi = 0 ;a[ai] != NULL || b[bi] != NULL; ai++, bi++) { + int status = 0; + if ((status = strcmp(a[ai], b[bi]) != 0)) { + result += status; + } + } + return result; +} + +/** + * Determine whether a string is comprised of digits + * @param s + * @return 0=no, 1=yes + */ +int isdigit_s(const char *s) { + for (size_t i = 0; s[i] != '\0'; i++) { + if (isdigit(s[i]) == 0) { + return 0; // non-digit found, fail + } + } + return 1; // all digits, succeed +} + +/** + * Convert input string to lowercase + * @param s + * @return pointer to input string + */ +char *tolower_s(char *s) { + for (size_t i = 0; s[i] != '\0'; i++) { + s[i] = (char)tolower(s[i]); + } + return s; +} + +char *to_short_version(const char *s) { + char *result; + result = strdup(s); + if (!result) { + return NULL; + } + strchrdel(result, "."); + return result; +} diff --git a/src/strlist.c b/src/strlist.c new file mode 100644 index 0000000..9cb09d0 --- /dev/null +++ b/src/strlist.c @@ -0,0 +1,483 @@ +/** + * String array convenience functions + * @file strlist.c + */ +#include "strlist.h" +//#include "url.h" +#include "utils.h" + +/** + * + * @param pStrList `StrList` + */ +void strlist_free(struct StrList *pStrList) { + if (pStrList == NULL) { + return; + } + for (size_t i = 0; i < pStrList->num_inuse; i++) { + free(pStrList->data[i]); + } + free(pStrList->data); + free(pStrList); +} + +/** + * Append a value to the list + * @param pStrList `StrList` + * @param str + */ +void strlist_append(struct StrList *pStrList, char *str) { + char **tmp = NULL; + + if (pStrList == NULL) { + return; + } + + tmp = realloc(pStrList->data, (pStrList->num_alloc + 1) * sizeof(char *)); + if (tmp == NULL) { + strlist_free(pStrList); + perror("failed to append to array"); + exit(1); + } + pStrList->data = tmp; + pStrList->data[pStrList->num_inuse] = strdup(str); + pStrList->data[pStrList->num_alloc] = NULL; + strcpy(pStrList->data[pStrList->num_inuse], str); + pStrList->num_inuse++; + pStrList->num_alloc++; +} + +static int reader_strlist_append_file(size_t lineno, char **line) { + (void)(lineno); // unused parameter + (void)(line); // unused parameter + return 0; +} + +/** + * Append lines from a local file or remote URL (HTTP/s only) + * @param pStrList + * @param path file path or HTTP/s address + * @param readerFn pointer to a reader function (use NULL to retrieve all data) + * @return 0=success 1=no data, -1=error (spmerrno set) + */ +int strlist_append_file(struct StrList *pStrList, char *_path, ReaderFn *readerFn) { + int retval = 0; + int is_remote = 0; + char *path = NULL; + char *filename = NULL; + char *from_file_tmpdir = NULL; + char **data = NULL; + + if (readerFn == NULL) { + readerFn = reader_strlist_append_file; + } + + path = strdup(_path); + if (path == NULL) { + + retval = -1; + goto fatal; + } + + filename = expandpath(path); + + if (filename == NULL) { + + retval = -1; + goto fatal; + } + + data = file_readlines(filename, 0, 0, readerFn); + if (data == NULL) { + retval = 1; + goto fatal; + } + + for (size_t record = 0; data[record] != NULL; record++) { + strlist_append(pStrList, data[record]); + free(data[record]); + } + free(data); + +fatal: + if (from_file_tmpdir != NULL) { + rmtree(from_file_tmpdir); + free(from_file_tmpdir); + } + if (filename != NULL) { + free(filename); + } + if (path != NULL) { + free(path); + } + + return retval; +} + +/** + * Append the contents of a `StrList` to another `StrList` + * @param pStrList1 `StrList` + * @param pStrList2 `StrList` + */ +void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2) { + size_t count = 0; + + if (pStrList1 == NULL || pStrList2 == NULL) { + return; + } + + count = strlist_count(pStrList2); + for (size_t i = 0; i < count; i++) { + char *item = strlist_item(pStrList2, i); + strlist_append(pStrList1, item); + } +} + +/** + * Append the contents of an array of pointers to char + * @param pStrList `StrList` + * @param arr NULL terminated array of strings + */ + void strlist_append_array(struct StrList *pStrList, char **arr) { + if (!pStrList || !arr) { + return; + } + for (size_t i = 0; arr[i] != NULL; i++) { + strlist_append(pStrList, arr[i]); + } + } + +/** + * Append the contents of a newline delimited string + * @param pStrList `StrList` + * @param str + * @param delim + */ + void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim) { + char **token; + if (!str || !delim) { + return; + } + + token = split(str, delim, 0); + if (token) { + for (size_t i = 0; token[i] != NULL; i++) { + strlist_append(pStrList, token[i]); + } + } + } + +/** + * Produce a new copy of a `StrList` + * @param pStrList `StrList` + * @return `StrList` copy + */ +struct StrList *strlist_copy(struct StrList *pStrList) { + struct StrList *result = strlist_init(); + if (pStrList == NULL || result == NULL) { + return NULL; + } + + for (size_t i = 0; i < strlist_count(pStrList); i++) { + strlist_append(result, strlist_item(pStrList, i)); + } + return result; +} + +/** + * Remove a record by index from a `StrList` + * @param pStrList + * @param index + */ +void strlist_remove(struct StrList *pStrList, size_t index) { + size_t count = strlist_count(pStrList); + if (count == 0) { + return; + } + + for (size_t i = index; i < count; i++) { + char *next = pStrList->data[i + 1]; + pStrList->data[i] = next; + if (next == NULL) { + break; + } + } + + pStrList->num_inuse--; +} + +/** + * Compare two `StrList`s + * @param a `StrList` structure + * @param b `StrList` structure + * @return same=0, different=1, error=-1 (a is NULL), -2 (b is NULL) + */ +int strlist_cmp(struct StrList *a, struct StrList *b) { + if (a == NULL) { + return -1; + } + + if (b == NULL) { + return -2; + } + + if (a->num_alloc != b->num_alloc) { + return 1; + } + + if (a->num_inuse != b->num_inuse) { + return 1; + } + + for (size_t i = 0; i < strlist_count(a); i++) { + if (strcmp(strlist_item(a, i), strlist_item(b, i)) != 0) { + return 1; + } + } + + return 0; +} + +/** + * Sort a `StrList` by `mode` + * @param pStrList + * @param mode Available modes: `STRLIST_DEFAULT` (alphabetic), `STRLIST_ASC` (ascending), `STRLIST_DSC` (descending) + */ +void strlist_sort(struct StrList *pStrList, unsigned int mode) { + void *fn = NULL; + + if (pStrList == NULL) { + return; + } + + strsort(pStrList->data, mode); +} + +/** + * Reverse the order of a `StrList` + * @param pStrList + */ +void strlist_reverse(struct StrList *pStrList) { + char *tmp = NULL; + size_t i = 0; + size_t j = 0; + + if (pStrList == NULL) { + return; + } + + j = pStrList->num_inuse - 1; + for (i = 0; i < j; i++) { + tmp = pStrList->data[i]; + pStrList->data[i] = pStrList->data[j]; + pStrList->data[j] = tmp; + j--; + } +} + +/** + * Get the count of values stored in a `StrList` + * @param pStrList + * @return + */ +size_t strlist_count(struct StrList *pStrList) { + return pStrList->num_inuse; +} + +/** + * Set value at index + * @param pStrList + * @param value string + * @return + */ +void strlist_set(struct StrList *pStrList, size_t index, char *value) { + char *tmp = NULL; + char *item = NULL; + if (pStrList == NULL || index > strlist_count(pStrList)) { + return; + } + if ((item = strlist_item(pStrList, index)) == NULL) { + return; + } + if (value == NULL) { + pStrList->data[index] = NULL; + } else { + if ((tmp = realloc(pStrList->data[index], strlen(value) + 1)) == NULL) { + perror("realloc strlist_set replacement value"); + return; + } + + pStrList->data[index] = tmp; + memset(pStrList->data[index], '\0', strlen(value) + 1); + strncpy(pStrList->data[index], value, strlen(value)); + } +} + +/** + * Retrieve data from a `StrList` + * @param pStrList + * @param index + * @return string + */ +char *strlist_item(struct StrList *pStrList, size_t index) { + if (pStrList == NULL || index > strlist_count(pStrList)) { + return NULL; + } + return pStrList->data[index]; +} + +/** + * Alias of `strlist_item` + * @param pStrList + * @param index + * @return string + */ +char *strlist_item_as_str(struct StrList *pStrList, size_t index) { + return strlist_item(pStrList, index); +} + +/** + * Convert value at index to `char` + * @param pStrList + * @param index + * @return `char` + */ +char strlist_item_as_char(struct StrList *pStrList, size_t index) { + return (char) strtol(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `unsigned char` + * @param pStrList + * @param index + * @return `unsigned char` + */ +unsigned char strlist_item_as_uchar(struct StrList *pStrList, size_t index) { + return (unsigned char) strtol(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `short` + * @param pStrList + * @param index + * @return `short` + */ +short strlist_item_as_short(struct StrList *pStrList, size_t index) { + return (short)strtol(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `unsigned short` + * @param pStrList + * @param index + * @return `unsigned short` + */ +unsigned short strlist_item_as_ushort(struct StrList *pStrList, size_t index) { + return (unsigned short)strtoul(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `int` + * @param pStrList + * @param index + * @return `int` + */ +int strlist_item_as_int(struct StrList *pStrList, size_t index) { + return (int)strtol(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `unsigned int` + * @param pStrList + * @param index + * @return `unsigned int` + */ +unsigned int strlist_item_as_uint(struct StrList *pStrList, size_t index) { + return (unsigned int)strtoul(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `long` + * @param pStrList + * @param index + * @return `long` + */ +long strlist_item_as_long(struct StrList *pStrList, size_t index) { + return strtol(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `unsigned long` + * @param pStrList + * @param index + * @return `unsigned long` + */ +unsigned long strlist_item_as_ulong(struct StrList *pStrList, size_t index) { + return strtoul(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `long long` + * @param pStrList + * @param index + * @return `long long` + */ +long long strlist_item_as_long_long(struct StrList *pStrList, size_t index) { + return strtoll(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `unsigned long long` + * @param pStrList + * @param index + * @return `unsigned long long` + */ +unsigned long long strlist_item_as_ulong_long(struct StrList *pStrList, size_t index) { + return strtoull(strlist_item(pStrList, index), NULL, 10); +} + +/** + * Convert value at index to `float` + * @param pStrList + * @param index + * @return `float` + */ +float strlist_item_as_float(struct StrList *pStrList, size_t index) { + return (float)atof(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `double` + * @param pStrList + * @param index + * @return `double` + */ +double strlist_item_as_double(struct StrList *pStrList, size_t index) { + return atof(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `long double` + * @param pStrList + * @param index + * @return `long double` + */ +long double strlist_item_as_long_double(struct StrList *pStrList, size_t index) { + return (long double)atof(strlist_item(pStrList, index)); +} + +/** + * Initialize an empty `StrList` + * @return `StrList` + */ +struct StrList *strlist_init() { + struct StrList *pStrList = calloc(1, sizeof(struct StrList)); + if (pStrList == NULL) { + perror("failed to allocate array"); + exit(errno); + } + pStrList->num_inuse = 0; + pStrList->num_alloc = 1; + pStrList->data = calloc(pStrList->num_alloc, sizeof(char *)); + return pStrList; +} diff --git a/src/system.c b/src/system.c new file mode 100644 index 0000000..ad89682 --- /dev/null +++ b/src/system.c @@ -0,0 +1,201 @@ +// +// Created by jhunk on 10/4/23. +// + +#include "system.h" + +int shell(struct Process *proc, char *args[]) { + FILE *fp_out, *fp_err; + pid_t pid; + pid_t status; + status = 0; + errno = 0; + + pid = fork(); + if (pid == -1) { + fprintf(stderr, "fork failed\n"); + exit(1); + } else if (pid == 0) { + int retval; + if (proc != NULL) { + if (strlen(proc->stdout)) { + fp_out = freopen(proc->stdout, "w+", stdout); + } + + if (strlen(proc->stderr)) { + fp_err = freopen(proc->stderr, "w+", stderr); + } + + if (proc->redirect_stderr) { + if (fp_err) { + fclose(fp_err); + fclose(stderr); + } + dup2(fileno(stdout), fileno(stderr)); + } + } + + retval = execv(args[0], args); + fprintf(stderr, "# executing: "); + for (size_t x = 0; args[x] != NULL; x++) { + fprintf(stderr, "%s ", args[x]); + } + + if (proc != NULL && strlen(proc->stdout)) { + fflush(fp_out); + fclose(fp_out); + fflush(stdout); + fclose(stdout); + } + if (proc != NULL && strlen(proc->stderr)) { + fflush(fp_err); + fclose(fp_err); + fflush(stderr); + fclose(stderr); + } + exit(retval); + } else { + if (waitpid(pid, &status, WUNTRACED) > 0) { + if (WIFEXITED(status) && WEXITSTATUS(status)) { + if (WEXITSTATUS(status) == 127) { + fprintf(stderr, "execv failed\n"); + } + } else if (WIFSIGNALED(status)) { + fprintf(stderr, "signal received: %d\n", WIFSIGNALED(status)); + } + } else { + fprintf(stderr, "waitpid() failed\n"); + } + } + + + if (proc != NULL) { + proc->returncode = status; + } + return WEXITSTATUS(status); +} + +int shell2(struct Process *proc, char *args) { + FILE *fp_out = NULL; + FILE *fp_err = NULL; + pid_t pid; + pid_t status; + status = 0; + errno = 0; + + char t_name[PATH_MAX]; + strcpy(t_name, "/tmp/ohmycal.XXXXXX"); + int fd = mkstemp(t_name); + + FILE *tp; + tp = fdopen(fd, "w"); + if (!tp) { + return -1; + } + + fprintf(tp, "#!/bin/bash\n%s\n", args); + fflush(tp); + fclose(tp); + chmod(t_name, 0755); + + pid = fork(); + if (pid == -1) { + fprintf(stderr, "fork failed\n"); + exit(1); + } else if (pid == 0) { + int retval; + if (proc != NULL) { + if (strlen(proc->stdout)) { + fp_out = freopen(proc->stdout, "w+", stdout); + } + + if (strlen(proc->stderr)) { + fp_err = freopen(proc->stderr, "w+", stderr); + } + + if (proc->redirect_stderr) { + if (fp_err) { + fclose(fp_err); + fclose(stderr); + } + dup2(fileno(stdout), fileno(stderr)); + } + } + + retval = execl("/bin/bash", "bash", "-c", t_name, (char *) NULL); + if (proc != NULL && strlen(proc->stdout)) { + if (fp_out != NULL) { + fflush(fp_out); + fclose(fp_out); + } + fflush(stdout); + fclose(stdout); + } + if (proc != NULL && strlen(proc->stderr)) { + if (fp_err) { + fflush(fp_err); + fclose(fp_err); + } + fflush(stderr); + fclose(stderr); + } + return retval; + } else { + if (waitpid(pid, &status, WUNTRACED) > 0) { + if (WIFEXITED(status) && WEXITSTATUS(status)) { + if (WEXITSTATUS(status) == 127) { + fprintf(stderr, "execv failed\n"); + } + } else if (WIFSIGNALED(status)) { + fprintf(stderr, "signal received: %d\n", WIFSIGNALED(status)); + } + } else { + fprintf(stderr, "waitpid() failed\n"); + } + } + + remove(t_name); + + if (proc != NULL) { + proc->returncode = status; + } + return WEXITSTATUS(status); +} + +int shell_safe(struct Process *proc, char *args[]) { + FILE *fp; + char buf[1024] = {0}; + int result; + + for (size_t i = 0; args[i] != NULL; i++) { + if (strpbrk(args[i], ";&|()")) { + args[i] = NULL; + break; + } + } + + result = shell(proc, args); + if (strlen(proc->stdout)) { + fp = fopen(proc->stdout, "r"); + if (fp) { + while (fgets(buf, sizeof(buf) - 1, fp)) { + fprintf(stdout, "%s", buf); + buf[0] = '\0'; + } + fclose(fp); + fp = NULL; + } + } + if (strlen(proc->stderr)) { + fp = fopen(proc->stderr, "r"); + if (fp) { + while (fgets(buf, sizeof(buf) - 1, fp)) { + fprintf(stderr, "%s", buf); + buf[0] = '\0'; + } + fclose(fp); + fp = NULL; + } + } + return result; +} diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..c0bb28f --- /dev/null +++ b/src/utils.c @@ -0,0 +1,417 @@ +#include <stdarg.h> +#include "ohmycal.h" + +char *dirstack[1024]; +const size_t dirstack_max = sizeof(dirstack) / sizeof(dirstack[0]); +size_t dirstack_len = 0; +int pushd(const char *path) { + if (dirstack_len + 1 > dirstack_max) { + return -1; + } + dirstack[dirstack_len] = realpath(".", NULL); + dirstack_len++; + return chdir(path); +} + +int popd() { + int result = -1; + if (dirstack_len - 1 < 0) { + return result; + } + dirstack_len--; + result = chdir(dirstack[dirstack_len]); + free(dirstack[dirstack_len]); + dirstack[dirstack_len] = NULL; + return result; +} + +int rmtree(char *_path) { + int status = 0; + char path[PATH_MAX] = {0}; + strncpy(path, _path, sizeof(path)); + DIR *dir; + struct dirent *d_entity; + + dir = opendir(path); + if (!dir) { + return 1; + } + + while ((d_entity = readdir(dir)) != NULL) { + char abspath[PATH_MAX] = {0}; + strcat(abspath, path); + strcat(abspath, DIR_SEP); + strcat(abspath, d_entity->d_name); + + if (!strcmp(d_entity->d_name, ".") || !strcmp(d_entity->d_name, "..") || !strcmp(abspath, path)) { + continue; + } + + // Test for sufficient privilege + if (access(abspath, F_OK) < 0 && errno == EACCES) { + continue; + } + + // Push directories on to the stack first + if (d_entity->d_type == DT_DIR) { + rmtree(abspath); + } else { + remove(abspath); + } + } + closedir(dir); + + if (access(path, F_OK) == 0) { + remove(path); + } + return status; +} + +/** + * Expand "~" to the user's home directory + * + * Example: + * ~~~{.c} + * char *home = expandpath("~"); // == /home/username + * char *config = expandpath("~/.config"); // == /home/username/.config + * char *nope = expandpath("/tmp/test"); // == /tmp/test + * char *nada = expandpath("/~/broken"); // == /~/broken + * + * free(home); + * free(config); + * free(nope); + * free(nada); + * ~~~ + * + * @param _path (Must start with a `~`) + * @return success=expanded path or original path, failure=NULL + */ +char *expandpath(const char *_path) { + if (_path == NULL) { + return NULL; + } + const char *homes[] = { + "HOME", + "USERPROFILE", + }; + char home[PATH_MAX]; + char tmp[PATH_MAX]; + char *ptmp = tmp; + char result[PATH_MAX]; + char *sep = NULL; + + memset(home, '\0', sizeof(home)); + memset(ptmp, '\0', sizeof(tmp)); + memset(result, '\0', sizeof(result)); + + strncpy(ptmp, _path, PATH_MAX - 1); + + // Check whether there's a reason to continue processing the string + if (*ptmp != '~') { + return strdup(ptmp); + } + + // Remove tilde from the string and shift its contents to the left + strchrdel(ptmp, "~"); + + // Figure out where the user's home directory resides + for (size_t i = 0; i < sizeof(homes) / sizeof(*homes); i++) { + char *tmphome; + if ((tmphome = getenv(homes[i])) != NULL) { + strncpy(home, tmphome, PATH_MAX - 1); + break; + } + } + + // A broken runtime environment means we can't do anything else here + if (isempty(home)) { + return NULL; + } + + // Scan the path for a directory separator + if ((sep = strpbrk(ptmp, "/\\")) != NULL) { + // Jump past it + ptmp = sep + 1; + } + + // Construct the new path + strncat(result, home, PATH_MAX - 1); + if (sep) { + strncat(result, DIR_SEP, PATH_MAX - 1); + strncat(result, ptmp, PATH_MAX - 1); + } + + return strdup(result); +} + +/** + * Strip directory from file name + * Note: Caller is responsible for freeing memory + * + * @param _path + * @return success=file name, failure=NULL + */ +char *path_basename(char *path) { + char *result = NULL; + char *last = NULL; + + if ((last = strrchr(path, '/')) == NULL) { + return result; + } + // Perform a lookahead ensuring the string is valid beyond the last separator + if (last++ != NULL) { + result = last; + } + + return result; +} + +char **file_readlines(const char *filename, size_t start, size_t limit, ReaderFn *readerFn) { + FILE *fp = NULL; + char **result = NULL; + char *buffer = NULL; + size_t lines = 0; + int use_stdin = 0; + + if (strcmp(filename, "-") == 0) { + use_stdin = 1; + } + + if (use_stdin) { + fp = stdin; + } else { + fp = fopen(filename, "r"); + } + + if (fp == NULL) { + perror(filename); + fprintf(SYSERROR); + return NULL; + } + + // Allocate buffer + if ((buffer = calloc(BUFSIZ, sizeof(char))) == NULL) { + perror("line buffer"); + fprintf(SYSERROR); + if (!use_stdin) { + fclose(fp); + } + return NULL; + } + + // count number the of lines in the file + while ((fgets(buffer, BUFSIZ - 1, fp)) != NULL) { + lines++; + } + + if (!lines) { + free(buffer); + if (!use_stdin) { + fclose(fp); + } + return NULL; + } + + rewind(fp); + + // Handle invalid start offset + if (start > lines) { + start = 0; + } + + // Adjust line count when start offset is non-zero + if (start != 0 && start < lines) { + lines -= start; + } + + + // Handle minimum and maximum limits + if (limit == 0 || limit > lines) { + limit = lines; + } + + // Populate results array + result = calloc(limit + 1, sizeof(char *)); + for (size_t i = start; i < limit; i++) { + if (i < start) { + continue; + } + + if (fgets(buffer, BUFSIZ - 1, fp) == NULL) { + break; + } + + if (readerFn != NULL) { + int status = readerFn(i - start, &buffer); + // A status greater than zero indicates we should ignore this line entirely and "continue" + // A status less than zero indicates we should "break" + // A zero status proceeds normally + if (status > 0) { + i--; + continue; + } else if (status < 0) { + break; + } + } + result[i] = strdup(buffer); + memset(buffer, '\0', BUFSIZ); + } + + free(buffer); + if (!use_stdin) { + fclose(fp); + } + return result; +} + +char *find_program(const char *name) { + static char result[PATH_MAX] = {0}; + char *_env_path = getenv(PATH_ENV_VAR); + if (!_env_path) { + errno = EINVAL; + return NULL; + } + char *path = strdup(_env_path); + char *path_orig = path; + char *path_elem = NULL; + + if (!path) { + errno = ENOMEM; + return NULL; + } + + result[0] = '\0'; + while ((path_elem = strsep(&path, PATH_SEP))) { + char abspath[PATH_MAX] = {0}; + strcat(abspath, path_elem); + strcat(abspath, DIR_SEP); + strcat(abspath, name); + if (access(abspath, F_OK) < 0) { + continue; + } + strncpy(result, abspath, sizeof(result)); + break; + } + path = path_orig; + free(path); + return strlen(result) ? result : NULL; +} + +int touch(const char *filename) { + if (access(filename, F_OK) == 0) { + return 0; + } + + FILE *fp = fopen(filename, "w"); + if (!fp) { + perror(filename); + return 1; + } + fprintf(stderr, ""); + fclose(fp); + return 0; +} + +int git_clone(struct Process *proc, char *url, char *destdir, char *gitref) { + int result = -1; + char *chdir_to = NULL; + char *program = find_program("git"); + if (!program) { + return result; + } + + static char command[PATH_MAX]; + sprintf(command, "%s clone --recursive %s", program, url); + if (destdir && access(destdir, F_OK) < 0) { + sprintf(command + strlen(command), " %s", destdir); + result = shell2(proc, command); + } + + if (destdir) { + chdir_to = destdir; + } else { + chdir_to = path_basename(url); + } + + pushd(chdir_to); + { + memset(command, 0, sizeof(command)); + sprintf(command, "%s fetch --all", program); + result += shell2(proc, command); + + if (gitref != NULL) { + memset(command, 0, sizeof(command)); + sprintf(command, "%s checkout %s", program, gitref); + result += shell2(proc, command); + } + popd(); + } + return result; +} + + +char *git_describe(const char *path) { + pushd(path); + static char version[NAME_MAX]; + FILE *pp; + pp = popen("git describe --always --tags", "r"); + memset(version, 0, sizeof(version)); + fgets(version, sizeof(version) - 1, pp); + strip(version); + pclose(pp); + popd(); + return version; +} + +#define OMC_COLOR_RED "\e[1;91m" +#define OMC_COLOR_GREEN "\e[1;92m" +#define OMC_COLOR_YELLOW "\e[1;93m" +#define OMC_COLOR_BLUE "\e[1;94m" +#define OMC_COLOR_WHITE "\e[1;97m" +#define OMC_COLOR_RESET "\e[0;37m\e[0m" + +int msg(unsigned type, char *fmt, ...) { + FILE *stream = NULL; + char header[255]; + char status[255]; + + if (type & OMC_MSG_NOP) { + // quiet mode + return 0; + } + + memset(header, 0, sizeof(header)); + memset(status, 0, sizeof(status)); + + va_list args; + va_start(args, fmt); + + stream = stdout; + if (type & OMC_MSG_ERROR) { + // for error output + stream = stderr; + fprintf(stream, "%s", OMC_COLOR_RED); + strcpy(status, " ERROR: "); + } else if (type & OMC_MSG_WARN) { + stream = stderr; + fprintf(stream, "%s", OMC_COLOR_YELLOW); + strcpy(status, " WARNING: "); + } else { + fprintf(stream, "%s", OMC_COLOR_GREEN); + strcpy(status, " "); + } + + if (type & OMC_MSG_L1) { + sprintf(header, "==>%s" OMC_COLOR_RESET OMC_COLOR_WHITE, status); + } else if (type & OMC_MSG_L2) { + sprintf(header, " ->%s" OMC_COLOR_RESET, status); + } else if (type & OMC_MSG_L3) { + sprintf(header, OMC_COLOR_BLUE " ->%s" OMC_COLOR_RESET, status); + } + + fprintf(stream, "%s", header); + vfprintf(stream, fmt, args); + printf("%s", OMC_COLOR_RESET); + printf("%s", OMC_COLOR_RESET); + va_end(args); +} diff --git a/src/wheel.c b/src/wheel.c new file mode 100644 index 0000000..a4ddbff --- /dev/null +++ b/src/wheel.c @@ -0,0 +1,74 @@ +#include "wheel.h" + +struct Wheel *get_wheel_file(const char *basepath, const char *name, char *to_match[]) { + DIR *dp; + struct dirent *rec; + struct Wheel *result = NULL; + char package_path[PATH_MAX]; + char package_name[NAME_MAX]; + + strcpy(package_name, name); + tolower_s(package_name); + sprintf(package_path, "%s/%s", basepath, package_name); + + dp = opendir(package_path); + if (!dp) { + return NULL; + } + + while ((rec = readdir(dp)) != NULL) { + if (!strcmp(rec->d_name, ".") || !strcmp(rec->d_name, "..")) { + continue; + } + char filename[NAME_MAX]; + strcpy(filename, rec->d_name); + char *ext = strstr(filename, ".whl"); + if (ext) { + *ext = '\0'; + } else { + // not a wheel file. nothing to do + continue; + } + + size_t match = 0; + size_t pattern_count = 0; + for (; to_match[pattern_count] != NULL; pattern_count++) { + if (strstr(filename, to_match[pattern_count])) { + match++; + } + } + + if (!startswith(rec->d_name, name) || match != pattern_count) { + continue; + } + + result = calloc(1, sizeof(*result)); + result->path_name = realpath(package_path, NULL); + result->file_name = strdup(rec->d_name); + + size_t parts_total; + char **parts = split(filename, "-", 0); + for (parts_total = 0; parts[parts_total] != NULL; parts_total++); + if (parts_total < 6) { + // no build tag + result->distribution = strdup(parts[0]); + result->version = strdup(parts[1]); + result->build_tag = NULL; + result->python_tag = strdup(parts[2]); + result->abi_tag = strdup(parts[3]); + result->platform_tag = strdup(parts[4]); + } else { + // has build tag + result->distribution = strdup(parts[0]); + result->version = strdup(parts[1]); + result->build_tag = strdup(parts[2]); + result->python_tag = strdup(parts[3]); + result->abi_tag = strdup(parts[4]); + result->platform_tag = strdup(parts[5]); + } + split_free(parts); + break; + } + closedir(dp); + return result; +} |