diff options
| author | Joseph Hunkeler <jhunkeler@gmail.com> | 2023-10-26 19:53:29 -0400 | 
|---|---|---|
| committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2023-10-26 19:53:29 -0400 | 
| commit | 17178535cc9df5e834dfd43e3b2b919e02e5798d (patch) | |
| tree | 5e55e8b2c2453ccf6271b190cf45e90d2c25179d | |
| download | stasis-17178535cc9df5e834dfd43e3b2b919e02e5798d.tar.gz | |
Initial commit
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| -rw-r--r-- | LICENSE | 30 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | include/conda.h | 23 | ||||
| -rw-r--r-- | include/deliverable.h | 117 | ||||
| -rw-r--r-- | include/download.h | 13 | ||||
| -rw-r--r-- | include/environment.h | 23 | ||||
| -rw-r--r-- | include/ini.h | 54 | ||||
| -rw-r--r-- | include/ohmycal.h | 24 | ||||
| -rw-r--r-- | include/recipe.h | 20 | ||||
| -rw-r--r-- | include/relocation.h | 16 | ||||
| -rw-r--r-- | include/str.h | 47 | ||||
| -rw-r--r-- | include/strlist.h | 47 | ||||
| -rw-r--r-- | include/system.h | 28 | ||||
| -rw-r--r-- | include/utils.h | 43 | ||||
| -rw-r--r-- | include/wheel.h | 21 | ||||
| -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 | 
31 files changed, 4871 insertions, 0 deletions
| diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56c5f38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build +cmake-* +.idea +*.so +*.dylib +*.dll diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..fcf028a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.21) +project(ohmycal C) + +set(CMAKE_C_STANDARD 99) +link_libraries(curl) +add_subdirectory(src)
\ No newline at end of file @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2023, Joseph Hunkeler, +Association of Universities for Research in Astronomy (AURA) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +   list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +   this list of conditions and the following disclaimer in the documentation +   and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +   contributors may be used to endorse or promote products derived from +   this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5184292 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Oh My CAL diff --git a/include/conda.h b/include/conda.h new file mode 100644 index 0000000..a642702 --- /dev/null +++ b/include/conda.h @@ -0,0 +1,23 @@ +// +// Created by jhunk on 5/14/23. +// + +#ifndef OHMYCAL_CONDA_H +#define OHMYCAL_CONDA_H + +#include <stdio.h> +#include <string.h> +#include "ohmycal.h" + +#define CONDA_INSTALL_PREFIX "conda" + +int python_exec(const char *args); +int pip_exec(const char *args); +int conda_exec(const char *args); +int conda_activate(const char *root, const char *env_name); +void conda_env_create_from_uri(char *name, char *uri); +void conda_env_create(char *name, char *python_version, char *packages); +void conda_env_remove(char *name); +void conda_env_export(char *name, char *output_dir, char *output_filename); +int conda_index(const char *path); +#endif //OHMYCAL_CONDA_H diff --git a/include/deliverable.h b/include/deliverable.h new file mode 100644 index 0000000..1487ace --- /dev/null +++ b/include/deliverable.h @@ -0,0 +1,117 @@ +// +// Created by jhunk on 10/5/23. +// + +#ifndef OHMYCAL_DELIVERABLE_H +#define OHMYCAL_DELIVERABLE_H + +#include <string.h> +#include <stdbool.h> +#include <unistd.h> +#include <sys/utsname.h> +#include "str.h" +#include "ini.h" +#include "environment.h" + +#define DELIVERY_DIR "delivery" +#define DELIVERY_PLATFORM_MAX 4 +#define DELIVERY_PLATFORM_MAXLEN 65 +#define DELIVERY_PLATFORM 0 +#define DELIVERY_PLATFORM_CONDA_SUBDIR 1 +#define DELIVERY_PLATFORM_CONDA_INSTALLER 2 +#define DELIVERY_PLATFORM_RELEASE 3 + +#define INSTALL_PKG_CONDA 1 << 1 +#define INSTALL_PKG_CONDA_DEFERRED 1 << 2 +#define INSTALL_PKG_PIP 1 << 3 +#define INSTALL_PKG_PIP_DEFERRED 1 << 4 + +struct Delivery { +    struct System { +        char *arch; +        char platform[DELIVERY_PLATFORM_MAX][DELIVERY_PLATFORM_MAXLEN]; +    } system; +    struct Storage { +        char *delivery_dir; +        char *conda_install_prefix; +        char *conda_artifact_dir; +        char *conda_staging_dir; +        char *conda_staging_url; +        char *wheel_artifact_dir; +        char *wheel_staging_dir; +        char *wheel_staging_url; +        char *build_dir; +        char *build_recipes_dir; +        char *build_sources_dir; +        char *build_testing_dir; +    } storage; +    struct Meta { +        // delivery name +        char *name; +        // delivery version +        char *version; +        // build iteration +        int rc; +        // version of python to use +        char *python; +        // URL to previous final configuration +        char *based_on; +        // hst, jwst, roman +        char *mission; +        // HST uses codenames +        char *codename; +        // is this a final release? +        bool final; +        // keep going, or don't +        bool continue_on_error; +    } meta; + +    struct Conda { +        char *installer_baseurl; +        char *installer_name; +        char *installer_version; +        char *installer_platform; +        char *installer_arch; +        // packages to install +        struct StrList *conda_packages; +        // conda recipes to be built +        struct StrList *conda_packages_defer; +        // packages to install +        struct StrList *pip_packages; +        // packages to be built +        struct StrList *pip_packages_defer; +    } conda; + +    // global runtime variables +    struct Runtime { +        RuntimeEnv *environ; +    } runtime; + +    struct Test { +        char *name; +        char *version; +        char *repository; +        char *script; +        char *build_recipe; +        // test-specific runtime variables +        struct Runtime runtime; +    } tests[1000]; +}; + +int delivery_init(struct Delivery *ctx, struct INIFILE *ini, struct INIFILE *cfg); +void delivery_meta_show(struct Delivery *ctx); +void delivery_conda_show(struct Delivery *ctx); +void delivery_tests_show(struct Delivery *ctx); +int delivery_build_recipes(struct Delivery *ctx); +struct StrList *delivery_build_wheels(struct Delivery *ctx); +int delivery_index_wheel_artifacts(struct Delivery *ctx); +void delivery_rewrite_spec(struct Delivery *ctx, char *filename); +int delivery_copy_wheel_artifacts(struct Delivery *ctx); +int delivery_copy_conda_artifacts(struct Delivery *ctx); +void delivery_get_installer(char *installer_url); +void delivery_get_installer_url(struct Delivery *delivery, char *result); +void delivery_install_packages(struct Delivery *ctx, char *conda_install_dir, char *env_name, int type, struct StrList *manifest[]); +int delivery_index_conda_artifacts(struct Delivery *ctx); +void delivery_tests_run(struct Delivery *ctx); + +#endif //OHMYCAL_DELIVERABLE_H diff --git a/include/download.h b/include/download.h new file mode 100644 index 0000000..0522aee --- /dev/null +++ b/include/download.h @@ -0,0 +1,13 @@ +// +// Created by jhunk on 10/5/23. +// + +#ifndef OHMYCAL_DOWNLOAD_H +#define OHMYCAL_DOWNLOAD_H + +#include <curl/curl.h> + +size_t download_writer(void *fp, size_t size, size_t nmemb, void *stream); +int download(char *url, const char *filename); + +#endif //OHMYCAL_DOWNLOAD_H diff --git a/include/environment.h b/include/environment.h new file mode 100644 index 0000000..db15d27 --- /dev/null +++ b/include/environment.h @@ -0,0 +1,23 @@ +/** + * @file environment.h + */ +#ifndef SPM_ENVIRONMENT_H +#define SPM_ENVIRONMENT_H + +#include <stdio.h> +#include <stdlib.h> +#include <dirent.h> +#include "environment.h" + +typedef struct StrList RuntimeEnv; + +ssize_t runtime_contains(RuntimeEnv *env, const char *key); +RuntimeEnv *runtime_copy(char **env); +int runtime_replace(RuntimeEnv **dest, char **src); +char *runtime_get(RuntimeEnv *env, const char *key); +void runtime_set(RuntimeEnv *env, const char *_key, const char *_value); +char *runtime_expand_var(RuntimeEnv *env, const char *input); +void runtime_export(RuntimeEnv *env, char **keys); +void runtime_apply(RuntimeEnv *env); +void runtime_free(RuntimeEnv *env); +#endif //SPM_ENVIRONMENT_H diff --git a/include/ini.h b/include/ini.h new file mode 100644 index 0000000..06004e3 --- /dev/null +++ b/include/ini.h @@ -0,0 +1,54 @@ +#ifndef OHMYCAL_INI_H +#define OHMYCAL_INI_H +#include <stddef.h> +#include <stdbool.h> + +#define INIVAL_TYPE_INT 1 +#define INIVAL_TYPE_UINT 2 +#define INIVAL_TYPE_LONG 3 +#define INIVAL_TYPE_ULONG 4 +#define INIVAL_TYPE_LLONG 5 +#define INIVAL_TYPE_ULLONG 6 +#define INIVAL_TYPE_DOUBLE 7 +#define INIVAL_TYPE_FLOAT 8 +#define INIVAL_TYPE_STR 9 +#define INIVAL_TYPE_STR_ARRAY 10 +#define INIVAL_TYPE_BOOL 11 + +#define INIVAL_TO_LIST 1 << 1 + +union INIVal { +    int as_int; +    unsigned as_uint; +    long as_long; +    unsigned long as_ulong; +    long long as_llong; +    unsigned long long as_ullong; +    double as_double; +    float as_float; +    char *as_char_p; +    char **as_char_array_p; +    bool as_bool; +}; + + +struct INIData { +    char *key; +    char *value; +}; +struct INISection { +    size_t data_count; +    char *key; +    struct INIData **data; +}; +struct INIFILE { +    size_t section_count; +    struct INISection **section; +}; + +struct INIFILE *ini_open(const char *filename); +struct INIData *ini_getall(struct INIFILE *ini, char *section_name); +int ini_getval(struct INIFILE *ini, char *section_name, char *key, int type, union INIVal *result); +void ini_show(struct INIFILE *ini); +void ini_free(struct INIFILE **ini); +#endif //OHMYCAL_INI_H diff --git a/include/ohmycal.h b/include/ohmycal.h new file mode 100644 index 0000000..d3e86ec --- /dev/null +++ b/include/ohmycal.h @@ -0,0 +1,24 @@ +#ifndef OHMYCAL_OHMYCAL_H +#define OHMYCAL_OHMYCAL_H +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <unistd.h> + +#define SYSERROR stderr, "%s:%s:%d: %s\n", path_basename(__FILE__), __FUNCTION__, __LINE__, strerror(errno) +#define OHMYCAL_BUFSIZ 8192 + +#include "utils.h" +#include "ini.h" +#include "conda.h" +#include "environment.h" +#include "deliverable.h" +#include "str.h" +#include "strlist.h" +#include "system.h" +#include "download.h" +#include "recipe.h" +#include "relocation.h" + +#endif //OHMYCAL_OHMYCAL_H diff --git a/include/recipe.h b/include/recipe.h new file mode 100644 index 0000000..2a0fe4b --- /dev/null +++ b/include/recipe.h @@ -0,0 +1,20 @@ +// +// Created by jhunk on 10/7/23. +// + +#ifndef OHMYCAL_RECIPE_H +#define OHMYCAL_RECIPE_H + +#include "str.h" +#include "utils.h" + +#define RECIPE_DIR "recipes" +#define RECIPE_TYPE_UNKNOWN 0 +#define RECIPE_TYPE_CONDA_FORGE 1 +#define RECIPE_TYPE_ASTROCONDA 2 +#define RECIPE_TYPE_GENERIC 3 + +int recipe_clone(char *recipe_dir, char *url, char *gitref, char **result); +int recipe_get_type(char *repopath); + +#endif //OHMYCAL_RECIPE_H diff --git a/include/relocation.h b/include/relocation.h new file mode 100644 index 0000000..ecbb38b --- /dev/null +++ b/include/relocation.h @@ -0,0 +1,16 @@ +/** + * @file relocation.h + */ +#ifndef OHMYCAL_RELOCATION_H +#define OHMYCAL_RELOCATION_H + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <linux/limits.h> +#include <unistd.h> + +void replace_text(char *original, const char *target, const char *replacement); +void file_replace_text(const char* filename, const char* target, const char* replacement); + +#endif //OHMYCAL_RELOCATION_H diff --git a/include/str.h b/include/str.h new file mode 100644 index 0000000..1c67eda --- /dev/null +++ b/include/str.h @@ -0,0 +1,47 @@ +/** + * @file str.h + */ +#ifndef SPM_STR_H +#define SPM_STR_H + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <ctype.h> +#include "ohmycal.h" + +#define SPM_SORT_ALPHA 1 << 0 +#define SPM_SORT_NUMERIC 1 << 1 +#define SPM_SORT_LEN_ASCENDING 1 << 2 +#define SPM_SORT_LEN_DESCENDING 1 << 3 + +int num_chars(const char *sptr, int ch); +int startswith(const char *sptr, const char *pattern); +int endswith(const char *sptr, const char *pattern); +char *normpath(const char *path); +void strchrdel(char *sptr, const char *chars); +long int strchroff(const char *sptr, int ch); +void strdelsuffix(char *sptr, const char *suffix); +char** split(char *sptr, const char* delim, size_t max); +void split_free(char **ptr); +char *join(char **arr, const char *separator); +char *join_ex(char *separator, ...); +char *substring_between(char *sptr, const char *delims); +void strsort(char **arr, unsigned int sort_mode); +int isrelational(char ch); +void print_banner(const char *s, int len); +char *strstr_array(char **arr, const char *str); +char **strdeldup(char **arr); +char *lstrip(char *sptr); +char *strip(char *sptr); +int isempty(char *sptr); +int isquoted(char *sptr); +char *normalize_space(char *s); +char **strdup_array(char **array); +int strcmp_array(const char **a, const char **b); +int isdigit_s(const char *s); +char *tolower_s(char *s); +char *to_short_version(const char *s); + +#endif //SPM_STR_H diff --git a/include/strlist.h b/include/strlist.h new file mode 100644 index 0000000..84019b9 --- /dev/null +++ b/include/strlist.h @@ -0,0 +1,47 @@ +/** + * String array convenience functions + * @file strlist.h + */ +#ifndef SPM_STRLIST_H +#define SPM_STRLIST_H +#include <stdlib.h> +#include "utils.h" +#include "str.h" + +struct StrList { +    size_t num_alloc; +    size_t num_inuse; +    char **data; +}; + +struct StrList *strlist_init(); +void strlist_remove(struct StrList *pStrList, size_t index); +long double strlist_item_as_long_double(struct StrList *pStrList, size_t index); +double strlist_item_as_double(struct StrList *pStrList, size_t index); +float strlist_item_as_float(struct StrList *pStrList, size_t index); +unsigned long long strlist_item_as_ulong_long(struct StrList *pStrList, size_t index); +long long strlist_item_as_long_long(struct StrList *pStrList, size_t index); +unsigned long strlist_item_as_ulong(struct StrList *pStrList, size_t index); +long strlist_item_as_long(struct StrList *pStrList, size_t index); +unsigned int strlist_item_as_uint(struct StrList *pStrList, size_t index); +int strlist_item_as_int(struct StrList *pStrList, size_t index); +unsigned short strlist_item_as_ushort(struct StrList *pStrList, size_t index); +short strlist_item_as_short(struct StrList *pStrList, size_t index); +unsigned char strlist_item_as_uchar(struct StrList *pStrList, size_t index); +char strlist_item_as_char(struct StrList *pStrList, size_t index); +char *strlist_item_as_str(struct StrList *pStrList, size_t index); +char *strlist_item(struct StrList *pStrList, size_t index); +void strlist_set(struct StrList *pStrList, size_t index, char *value); +size_t strlist_count(struct StrList *pStrList); +void strlist_reverse(struct StrList *pStrList); +void strlist_sort(struct StrList *pStrList, unsigned int mode); +int strlist_append_file(struct StrList *pStrList, char *path, ReaderFn *readerFn); +void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2); +void strlist_append(struct StrList *pStrList, char *str); +void strlist_append_array(struct StrList *pStrList, char **arr); +void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim); +struct StrList *strlist_copy(struct StrList *pStrList); +int strlist_cmp(struct StrList *a, struct StrList *b); +void strlist_free(struct StrList *pStrList); + +#endif //SPM_STRLIST_H diff --git a/include/system.h b/include/system.h new file mode 100644 index 0000000..a922c69 --- /dev/null +++ b/include/system.h @@ -0,0 +1,28 @@ +// +// Created by jhunk on 10/4/23. +// + +#ifndef OHMYCAL_SYSTEM_H +#define OHMYCAL_SYSTEM_H + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <limits.h> +#include <sys/wait.h> +#include <sys/stat.h> + +struct Process { +    char stdout[PATH_MAX]; +    char stderr[PATH_MAX]; +    int redirect_stderr; +    int returncode; +}; + +int shell(struct Process *proc, char *args[]); +int shell2(struct Process *proc, char *args); +int shell_safe(struct Process *proc, char *args[]); + +#endif //OHMYCAL_SYSTEM_H diff --git a/include/utils.h b/include/utils.h new file mode 100644 index 0000000..eb7604c --- /dev/null +++ b/include/utils.h @@ -0,0 +1,43 @@ +#ifndef OHMYCAL_UTILS_H +#define OHMYCAL_UTILS_H +#include <stdio.h> +#include <stdlib.h> +#include <dirent.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> +#include <errno.h> +#include "system.h" + +#if defined(__WIN32__) +#define PATH_ENV_VAR "path" +#define DIR_SEP "\\" +#define PATH_SEP ";" +#else +#define PATH_ENV_VAR "PATH" +#define DIR_SEP "/" +#define PATH_SEP ":" +#endif + +typedef int (ReaderFn)(size_t line, char **); + +int pushd(const char *path); +int popd(void); +char *expandpath(const char *_path); +int rmtree(char *_path); +char **file_readlines(const char *filename, size_t start, size_t limit, ReaderFn *readerFn); +char *path_basename(char *path); +char *find_program(const char *name); +int touch(const char *filename); +int git_clone(struct Process *proc, char *url, char *destdir, char *gitref); +char *git_describe(const char *path); + +#define OMC_MSG_NOP 1 << 0 +#define OMC_MSG_ERROR 1 << 1 +#define OMC_MSG_WARN 1 << 2 +#define OMC_MSG_L1 1 << 3 +#define OMC_MSG_L2 1 << 4 +#define OMC_MSG_L3 1 << 5 +int msg(unsigned type, char *fmt, ...); + +#endif //OHMYCAL_UTILS_H diff --git a/include/wheel.h b/include/wheel.h new file mode 100644 index 0000000..20ac641 --- /dev/null +++ b/include/wheel.h @@ -0,0 +1,21 @@ +#ifndef OHMYCAL_WHEEL_H +#define OHMYCAL_WHEEL_H + +#include <dirent.h> +#include <string.h> +#include <stdio.h> +#include "str.h" + +struct Wheel { +    char *distribution; +    char *version; +    char *build_tag; +    char *python_tag; +    char *abi_tag; +    char *platform_tag; +    char *path_name; +    char *file_name; +}; + +struct Wheel *get_wheel_file(const char *basepath, const char *name, char *to_match[]); +#endif //OHMYCAL_WHEEL_H 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; +} | 
