aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--CMakeLists.txt6
-rw-r--r--LICENSE30
-rw-r--r--README.md1
-rw-r--r--include/conda.h23
-rw-r--r--include/deliverable.h117
-rw-r--r--include/download.h13
-rw-r--r--include/environment.h23
-rw-r--r--include/ini.h54
-rw-r--r--include/ohmycal.h24
-rw-r--r--include/recipe.h20
-rw-r--r--include/relocation.h16
-rw-r--r--include/str.h47
-rw-r--r--include/strlist.h47
-rw-r--r--include/system.h28
-rw-r--r--include/utils.h43
-rw-r--r--include/wheel.h21
-rw-r--r--src/CMakeLists.txt17
-rw-r--r--src/conda.c176
-rw-r--r--src/deliverable.c626
-rw-r--r--src/download.c34
-rw-r--r--src/environment.c445
-rw-r--r--src/ini.c409
-rw-r--r--src/main.c373
-rw-r--r--src/recipe.c63
-rw-r--r--src/relocation.c167
-rw-r--r--src/str.c867
-rw-r--r--src/strlist.c483
-rw-r--r--src/system.c201
-rw-r--r--src/utils.c417
-rw-r--r--src/wheel.c74
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..409f311
--- /dev/null
+++ b/LICENSE
@@ -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;
+}