aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@gmail.com>2024-05-16 12:13:35 -0400
committerJoseph Hunkeler <jhunkeler@gmail.com>2024-05-16 12:17:46 -0400
commiteaaae2c0f77fe371b1da8c2c248888103d488961 (patch)
tree1137d0e51c214fb9aec6f321c40250a49c6fdba9
parent9ad649be44c568a00f2f407d715d07cd585c2b25 (diff)
downloadstasis-eaaae2c0f77fe371b1da8c2c248888103d488961.tar.gz
First pass at test result creation, and optional markdown->html conversion
-rw-r--r--CMakeLists.txt7
-rw-r--r--include/junitxml.h47
-rw-r--r--include/omc.h1
-rw-r--r--src/CMakeLists.txt6
-rw-r--r--src/junitxml.c217
-rw-r--r--src/omc_indexer.c177
6 files changed, 445 insertions, 10 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e9a001e..5b8376e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,8 +5,11 @@ include(GNUInstallDirs)
set(nix_cflags -Wall -Wextra -fPIC)
set(win_cflags /Wall)
set(CMAKE_C_STANDARD 99)
-
-link_libraries(curl)
+find_package(libxml2)
+find_package(CURL)
+link_libraries(CURL::libcurl)
+link_libraries(LibXml2::LibXml2)
+include_directories(${LIBXML2_INCLUDE_DIR})
if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
message("gnu options")
diff --git a/include/junitxml.h b/include/junitxml.h
new file mode 100644
index 0000000..d5e7708
--- /dev/null
+++ b/include/junitxml.h
@@ -0,0 +1,47 @@
+// @file junitxml.h
+#ifndef OMC_JUNITXML_H
+#define OMC_JUNITXML_H
+#include <libxml/xmlreader.h>
+
+struct JUNIT_Failure {
+ char *message;
+};
+
+struct JUNIT_Skipped {
+ char *type;
+ char *message;
+};
+
+#define JUNIT_RESULT_STATE_NONE 0
+#define JUNIT_RESULT_STATE_FAILURE 1
+#define JUNIT_RESULT_STATE_SKIPPED 2
+struct JUNIT_Testcase {
+ char *classname;
+ char *name;
+ float time;
+ char *message;
+ int tc_result_state_type;
+ union tc_state_ptr {
+ struct JUNIT_Failure *failure;
+ struct JUNIT_Skipped *skipped;
+ } result_state;
+};
+
+struct JUNIT_Testsuite {
+ char *name;
+ int errors;
+ int failures;
+ int skipped;
+ int tests;
+ float time;
+ char *timestamp;
+ char *hostname;
+ struct JUNIT_Testcase **testcase;
+ size_t _tc_inuse;
+ size_t _tc_alloc;
+};
+
+struct JUNIT_Testsuite *junitxml_testsuite_read(const char *filename);
+void junitxml_testsuite_free(struct JUNIT_Testsuite **testsuite);
+
+#endif //OMC_JUNITXML_H
diff --git a/include/omc.h b/include/omc.h
index 45f5671..a116296 100644
--- a/include/omc.h
+++ b/include/omc.h
@@ -38,6 +38,7 @@
#include "recipe.h"
#include "relocation.h"
#include "wheel.h"
+#include "junitxml.h"
#define guard_runtime_free(X) do { if (X) { runtime_free(X); X = NULL; } } while (0)
#define guard_strlist_free(X) do { if ((*X)) { strlist_free(X); (*X) = NULL; } } while (0)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0c43392..f075102 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -21,14 +21,16 @@ add_library(ohmycal STATIC
template.c
rules.c
docker.c
+ junitxml.c
)
add_executable(omc
omc_main.c
)
-target_link_libraries(omc ohmycal)
+target_link_libraries(omc PRIVATE ohmycal)
+target_link_libraries(omc PUBLIC LibXml2::LibXml2)
add_executable(omc_indexer
omc_indexer.c
)
-target_link_libraries(omc_indexer ohmycal)
+target_link_libraries(omc_indexer PRIVATE ohmycal)
install(TARGETS omc omc_indexer RUNTIME)
diff --git a/src/junitxml.c b/src/junitxml.c
new file mode 100644
index 0000000..df0514b
--- /dev/null
+++ b/src/junitxml.c
@@ -0,0 +1,217 @@
+#include <stdlib.h>
+#include <string.h>
+#include "strlist.h"
+#include "junitxml.h"
+
+static void testcase_result_state_free(struct JUNIT_Testcase **testcase) {
+ struct JUNIT_Testcase *tc = (*testcase);
+ if (tc->tc_result_state_type == JUNIT_RESULT_STATE_FAILURE) {
+ guard_free(tc->result_state.failure->message);
+ guard_free(tc->result_state.failure);
+ } else if (tc->tc_result_state_type == JUNIT_RESULT_STATE_SKIPPED) {
+ guard_free(tc->result_state.skipped->message);
+ guard_free(tc->result_state.skipped);
+ }
+}
+
+static void testcase_free(struct JUNIT_Testcase **testcase) {
+ struct JUNIT_Testcase *tc = (*testcase);
+ guard_free(tc->name);
+ guard_free(tc->message);
+ guard_free(tc->classname);
+ testcase_result_state_free(&tc);
+ guard_free(tc);
+}
+
+void junitxml_testsuite_free(struct JUNIT_Testsuite **testsuite) {
+ struct JUNIT_Testsuite *suite = (*testsuite);
+ guard_free(suite->name);
+ guard_free(suite->hostname);
+ guard_free(suite->timestamp);
+ for (size_t i = 0; i < suite->_tc_alloc; i++) {
+ testcase_free(&suite->testcase[i]);
+ }
+ guard_free(suite);
+}
+
+static int testsuite_append_testcase(struct JUNIT_Testsuite **testsuite, struct JUNIT_Testcase *testcase) {
+ struct JUNIT_Testsuite *suite = (*testsuite);
+ struct JUNIT_Testcase **tmp = realloc(suite->testcase, (suite->_tc_alloc + 1 ) * sizeof(*testcase));
+ if (!tmp) {
+ return -1;
+ } else if (tmp != suite->testcase) {
+ suite->testcase = tmp;
+ }
+ suite->testcase[suite->_tc_inuse] = testcase;
+ suite->_tc_inuse++;
+ suite->_tc_alloc++;
+ return 0;
+}
+
+static struct JUNIT_Failure *testcase_failure_from_attributes(struct StrList *attrs) {
+ struct JUNIT_Failure *result;
+
+ result = calloc(1, sizeof(*result));
+ if(!result) {
+ return NULL;
+ }
+ for (size_t x = 0; x < strlist_count(attrs); x += 2) {
+ char *attr_name = strlist_item(attrs, x);
+ char *attr_value = strlist_item(attrs, x + 1);
+ if (!strcmp(attr_name, "message")) {
+ result->message = strdup(attr_value);
+ }
+ }
+ return result;
+}
+
+static struct JUNIT_Skipped *testcase_skipped_from_attributes(struct StrList *attrs) {
+ struct JUNIT_Skipped *result;
+
+ result = calloc(1, sizeof(*result));
+ if(!result) {
+ return NULL;
+ }
+ for (size_t x = 0; x < strlist_count(attrs); x += 2) {
+ char *attr_name = strlist_item(attrs, x);
+ char *attr_value = strlist_item(attrs, x + 1);
+ if (!strcmp(attr_name, "message")) {
+ result->message = strdup(attr_value);
+ }
+ }
+ return result;
+}
+
+static struct JUNIT_Testcase *testcase_from_attributes(struct StrList *attrs) {
+ struct JUNIT_Testcase *result;
+
+ result = calloc(1, sizeof(*result));
+ if(!result) {
+ return NULL;
+ }
+ for (size_t x = 0; x < strlist_count(attrs); x += 2) {
+ char *attr_name = strlist_item(attrs, x);
+ char *attr_value = strlist_item(attrs, x + 1);
+ if (!strcmp(attr_name, "name")) {
+ result->name = strdup(attr_value);
+ } else if (!strcmp(attr_name, "classname")) {
+ result->classname = strdup(attr_value);
+ } else if (!strcmp(attr_name, "time")) {
+ result->time = strtof(attr_value, NULL);
+ } else if (!strcmp(attr_name, "message")) {
+ result->message = strdup(attr_value);
+ }
+ }
+ return result;
+}
+
+static struct StrList *attributes_to_strlist(xmlTextReaderPtr reader) {
+ struct StrList *list;
+ xmlNodePtr node = xmlTextReaderCurrentNode(reader);
+ if (!node) {
+ return NULL;
+ }
+
+ list = strlist_init();
+ if (xmlTextReaderNodeType(reader) == 1 && node->properties) {
+ xmlAttr *attr = node->properties;
+ while (attr && attr->name && attr->children) {
+ char *attr_name = (char *) attr->name;
+ char *attr_value = (char *) xmlNodeListGetString(node->doc, attr->children, 1);
+ strlist_append(&list, attr_name ? attr_name : "");
+ strlist_append(&list, attr_value ? attr_value : "");
+ xmlFree((xmlChar *) attr_value);
+ attr = attr->next;
+ }
+ }
+ return list;
+}
+
+static int read_xml_data(xmlTextReaderPtr reader, struct JUNIT_Testsuite **testsuite) {
+ const xmlChar *name;
+ const xmlChar *value;
+
+ name = xmlTextReaderConstName(reader);
+ if (!name) {
+ name = BAD_CAST "--";
+ }
+ value = xmlTextReaderConstValue(reader);
+ const char *node_name = (char *) name;
+ const char *node_value = (char *) value;
+
+ struct StrList *attrs = attributes_to_strlist(reader);
+ if (attrs && strlist_count(attrs)) {
+ if (!strcmp(node_name, "testsuite")) {
+ for (size_t x = 0; x < strlist_count(attrs); x += 2) {
+ char *attr_name = strlist_item(attrs, x);
+ char *attr_value = strlist_item(attrs, x + 1);
+ if (!strcmp(attr_name, "name")) {
+ (*testsuite)->name = strdup(attr_value);
+ } else if (!strcmp(attr_name, "errors")) {
+ (*testsuite)->errors = (int) strtol(attr_value, NULL, 10);
+ } else if (!strcmp(attr_name, "failures")) {
+ (*testsuite)->failures = (int) strtol(attr_value, NULL, 0);
+ } else if (!strcmp(attr_name, "skipped")) {
+ (*testsuite)->skipped = (int) strtol(attr_value, NULL, 0);
+ } else if (!strcmp(attr_name, "tests")) {
+ (*testsuite)->tests = (int) strtol(attr_value, NULL, 0);
+ } else if (!strcmp(attr_name, "time")) {
+ (*testsuite)->time = strtof(attr_value, NULL);
+ } else if (!strcmp(attr_name, "timestamp")) {
+ (*testsuite)->timestamp = strdup(attr_value);
+ } else if (!strcmp(attr_name, "hostname")) {
+ (*testsuite)->hostname = strdup(attr_value);
+ }
+ }
+ } else if (!strcmp(node_name, "testcase")) {
+ struct JUNIT_Testcase *testcase = testcase_from_attributes(attrs);
+ testsuite_append_testcase(testsuite, testcase);
+ } else if (!strcmp(node_name, "failure")) {
+ size_t cur_tc = (*testsuite)->_tc_inuse > 0 ? (*testsuite)->_tc_inuse - 1 : (*testsuite)->_tc_inuse;
+ struct JUNIT_Failure *failure = testcase_failure_from_attributes(attrs);
+ (*testsuite)->testcase[cur_tc]->tc_result_state_type = JUNIT_RESULT_STATE_FAILURE;
+ (*testsuite)->testcase[cur_tc]->result_state.failure = failure;
+ } else if (!strcmp(node_name, "skipped")) {
+ size_t cur_tc = (*testsuite)->_tc_inuse > 0 ? (*testsuite)->_tc_inuse - 1 : (*testsuite)->_tc_inuse;
+ struct JUNIT_Skipped *skipped = testcase_skipped_from_attributes(attrs);
+ (*testsuite)->testcase[cur_tc]->tc_result_state_type = JUNIT_RESULT_STATE_SKIPPED;
+ (*testsuite)->testcase[cur_tc]->result_state.skipped = skipped;
+ }
+ }
+ guard_strlist_free(&attrs);
+ return 0;
+}
+
+static int read_xml_file(const char *filename, struct JUNIT_Testsuite **testsuite) {
+ xmlTextReaderPtr reader;
+ int result;
+
+ reader = xmlReaderForFile(filename, NULL, 0);
+ if (!reader) {
+ return -1;
+ }
+
+ result = xmlTextReaderRead(reader);
+ while (result == 1) {
+ read_xml_data(reader, testsuite);
+ result = xmlTextReaderRead(reader);
+ }
+
+ xmlFreeTextReader(reader);
+ return 0;
+}
+
+struct JUNIT_Testsuite *junitxml_testsuite_read(const char *filename) {
+ struct JUNIT_Testsuite *result;
+
+ if (access(filename, F_OK)) {
+ return NULL;
+ }
+
+ result = calloc(1, sizeof(*result));
+ if (!result) {
+ return NULL;
+ }
+ read_xml_file(filename, &result);
+ return result;
+} \ No newline at end of file
diff --git a/src/omc_indexer.c b/src/omc_indexer.c
index ebf3ba6..971f389 100644
--- a/src/omc_indexer.c
+++ b/src/omc_indexer.c
@@ -7,6 +7,7 @@ static struct option long_options[] = {
{"destdir", required_argument, 0, 'd'},
{"verbose", no_argument, 0, 'v'},
{"unbuffered", no_argument, 0, 'U'},
+ {"web", no_argument, 0, 'w'},
{0, 0, 0, 0},
};
@@ -15,6 +16,7 @@ const char *long_options_help[] = {
"Destination directory",
"Increase output verbosity",
"Disable line buffering",
+ "Generate HTML indexes (requires pandoc)",
NULL,
};
@@ -169,7 +171,7 @@ int indexer_get_files(struct StrList **out, const char *path, const char *patter
int get_latest_rc(struct Delivery ctx[], size_t nelem) {
int result = 0;
for (size_t i = 0; i < nelem; i++) {
- if (&ctx[i] && ctx[i].meta.rc > result) {
+ if (ctx[i].meta.rc > result) {
result = ctx[i].meta.rc;
}
}
@@ -249,6 +251,44 @@ int micromamba(const char *write_to, const char *prefix, char *command, ...) {
return status;
}
+int indexer_make_website(struct Delivery *ctx) {
+ char cmd[PATH_MAX];
+ char *inputs[] = {
+ ctx->storage.delivery_dir, "/README.md",
+ ctx->storage.results_dir, "/README.md",
+ };
+
+ if (!find_program("pandoc")) {
+ return 0;
+ }
+
+ for (size_t i = 0; i < sizeof(inputs) / sizeof(*inputs); i += 2) {
+ char fullpath[PATH_MAX];
+ memset(fullpath, 0, sizeof(fullpath));
+ sprintf(fullpath, "%s/%s", inputs[i], inputs[i + 1]);
+ if (access(fullpath, F_OK)) {
+ continue;
+ }
+ // Converts a markdown file to html
+ strcpy(cmd, "pandoc ");
+ strcat(cmd, fullpath);
+ strcat(cmd, " ");
+ strcat(cmd, "-o ");
+ strcat(cmd, inputs[i]);
+ strcat(cmd, "/index.html");
+ if (globals.verbose) {
+ puts(cmd);
+ }
+ // This might be negative when killed by a signal.
+ // Otherwise, the return code is not critical to us.
+ if (system(cmd) < 0) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
int indexer_conda(struct Delivery *ctx) {
int status = 0;
char prefix[PATH_MAX];
@@ -361,6 +401,7 @@ int indexer_readmes(struct Delivery ctx[], size_t nelem) {
struct StrList *platforms = get_platforms(*latest, nelem);
fprintf(indexfp, "# %s-%s\n\n", ctx->meta.name, ctx->meta.version);
+ fprintf(indexfp, "## Current Release\n\n");
for (size_t p = 0; p < strlist_count(platforms); p++) {
char *platform = strlist_item(platforms, p);
for (size_t a = 0; a < strlist_count(archs); a++) {
@@ -374,10 +415,10 @@ int indexer_readmes(struct Delivery ctx[], size_t nelem) {
if (!have_combo) {
continue;
}
- fprintf(indexfp, "## %s-%s\n\n", platform, arch);
+ fprintf(indexfp, "### %s-%s\n\n", platform, arch);
fprintf(indexfp, "|Release|Info|Receipt|\n");
- fprintf(indexfp, "|----|----|----|\n");
+ fprintf(indexfp, "|:----:|:----:|:----:|\n");
for (size_t i = 0; i < nelem; i++) {
char link_name[PATH_MAX];
char readme_name[PATH_MAX];
@@ -413,6 +454,102 @@ int indexer_readmes(struct Delivery ctx[], size_t nelem) {
return 0;
}
+int indexer_junitxml_report(struct Delivery ctx[], size_t nelem) {
+ struct Delivery **latest = NULL;
+ latest = get_latest_deliveries(ctx, nelem);
+
+ char indexfile[PATH_MAX] = {0};
+ sprintf(indexfile, "%s/README.md", ctx->storage.results_dir);
+
+ struct StrList *file_listing = listdir(ctx->storage.results_dir);
+ if (!file_listing) {
+ // no test results to process
+ return 0;
+ }
+
+ if (!pushd(ctx->storage.results_dir)) {
+ FILE *indexfp;
+ indexfp = fopen(indexfile, "w+");
+ if (!indexfp) {
+ fprintf(stderr, "Unable to open %s for writing\n", indexfile);
+ return -1;
+ }
+ struct StrList *archs = get_architectures(*latest, nelem);
+ struct StrList *platforms = get_platforms(*latest, nelem);
+
+ fprintf(indexfp, "# %s-%s Test Report\n\n", ctx->meta.name, ctx->meta.version);
+ fprintf(indexfp, "## Current Release\n\n");
+ for (size_t p = 0; p < strlist_count(platforms); p++) {
+ char *platform = strlist_item(platforms, p);
+ for (size_t a = 0; a < strlist_count(archs); a++) {
+ char *arch = strlist_item(archs, a);
+ int have_combo = 0;
+ for (size_t i = 0; i < nelem; i++) {
+ if (strstr(latest[i]->system.platform[DELIVERY_PLATFORM_RELEASE], platform) && strstr(latest[i]->system.arch, arch)) {
+ have_combo = 1;
+ break;
+ }
+ }
+ if (!have_combo) {
+ continue;
+ }
+ fprintf(indexfp, "### %s-%s\n\n", platform, arch);
+
+ fprintf(indexfp, "|Suite|Duration|Fail |Skip |Error |\n");
+ fprintf(indexfp, "|:----|:------:|:------:|:---:|:----:|\n");
+ for (size_t f = 0; f < strlist_count(file_listing); f++) {
+ char *filename = strlist_item(file_listing, f);
+ if (!endswith(filename, ".xml")) {
+ continue;
+ }
+
+ if (strstr(filename, platform) && strstr(filename, arch)) {
+ struct JUNIT_Testsuite *testsuite = junitxml_testsuite_read(filename);
+ if (testsuite) {
+ if (globals.verbose) {
+ printf("%s: duration: %0.4f, failed: %d, skipped: %d, errors: %d\n", filename, testsuite->time, testsuite->failures, testsuite->skipped, testsuite->errors);
+ }
+ fprintf(indexfp, "|[%s](%s)|%0.4f|%d|%d|%d|\n", filename, filename, testsuite->time, testsuite->failures, testsuite->skipped, testsuite->errors);
+ /*
+ * TODO: Display failure/skip/error output.
+ *
+ for (size_t i = 0; i < testsuite->_tc_inuse; i++) {
+ if (testsuite->testcase[i]->tc_result_state_type) {
+ printf("testcase: %s :: %s\n", testsuite->testcase[i]->classname, testsuite->testcase[i]->name);
+ if (testsuite->testcase[i]->tc_result_state_type == JUNIT_RESULT_STATE_FAILURE) {
+ printf("failure: %s\n", testsuite->testcase[i]->result_state.failure->message);
+ } else if (testsuite->testcase[i]->tc_result_state_type == JUNIT_RESULT_STATE_SKIPPED) {
+ printf("skipped: %s\n", testsuite->testcase[i]->result_state.skipped->message);
+ }
+ }
+ }
+ */
+ junitxml_testsuite_free(&testsuite);
+ } else {
+ fprintf(stderr, "bad test suite: %s: %s\n", strerror(errno), filename);
+ continue;
+ }
+ }
+ }
+ fprintf(indexfp, "\n");
+ }
+ fprintf(indexfp, "\n");
+ }
+ guard_strlist_free(&archs);
+ guard_strlist_free(&platforms);
+ fclose(indexfp);
+ popd();
+ } else {
+ fprintf(stderr, "Unable to enter delivery directory: %s\n", ctx->storage.delivery_dir);
+ guard_free(latest);
+ return -1;
+ }
+
+ // "latest" is an array of pointers to ctxs[]. Do not free the contents of the array.
+ guard_free(latest);
+ return 0;
+}
+
void indexer_init_dirs(struct Delivery *ctx, const char *workdir) {
path_store(&ctx->storage.root, PATH_MAX, workdir, "");
path_store(&ctx->storage.tmpdir, PATH_MAX, ctx->storage.root, "tmp");
@@ -425,6 +562,7 @@ void indexer_init_dirs(struct Delivery *ctx, const char *workdir) {
path_store(&ctx->storage.meta_dir, PATH_MAX, ctx->storage.output_dir, "meta");
path_store(&ctx->storage.delivery_dir, PATH_MAX, ctx->storage.output_dir, "delivery");
path_store(&ctx->storage.package_dir, PATH_MAX, ctx->storage.output_dir, "packages");
+ path_store(&ctx->storage.results_dir, PATH_MAX, ctx->storage.output_dir, "results");
path_store(&ctx->storage.wheel_artifact_dir, PATH_MAX, ctx->storage.package_dir, "wheels");
path_store(&ctx->storage.conda_artifact_dir, PATH_MAX, ctx->storage.package_dir, "conda");
}
@@ -433,9 +571,10 @@ int main(int argc, char *argv[]) {
size_t rootdirs_total = 0;
char *destdir = NULL;
char **rootdirs = NULL;
+ int do_html = 0;
int c = 0;
int option_index = 0;
- while ((c = getopt_long(argc, argv, "hd:vU", long_options, &option_index)) != -1) {
+ while ((c = getopt_long(argc, argv, "hd:vUw", long_options, &option_index)) != -1) {
switch (c) {
case 'h':
usage(path_basename(argv[0]));
@@ -452,6 +591,9 @@ int main(int argc, char *argv[]) {
case 'v':
globals.verbose = 1;
break;
+ case 'w':
+ do_html = 1;
+ break;
case '?':
default:
exit(1);
@@ -515,6 +657,15 @@ int main(int argc, char *argv[]) {
if (indexer_combine_rootdirs(workdir, rootdirs, rootdirs_total)) {
SYSERROR("%s", "Copy operation failed");
rmtree(workdir);
+ exit(1);
+ }
+
+ if (access(ctx.storage.conda_artifact_dir, F_OK)) {
+ mkdirs(ctx.storage.conda_artifact_dir, 0755);
+ }
+
+ if (access(ctx.storage.wheel_artifact_dir, F_OK)) {
+ mkdirs(ctx.storage.wheel_artifact_dir, 0755);
}
msg(OMC_MSG_L1, "Indexing conda packages\n");
@@ -559,10 +710,24 @@ int main(int argc, char *argv[]) {
exit(1);
}
+ msg(OMC_MSG_L1, "Indexing test results\n");
+ if (indexer_junitxml_report(local, strlist_count(metafiles))) {
+ SYSERROR("%s", "Test result indexing operation failed");
+ exit(1);
+ }
+
+ if (do_html) {
+ msg(OMC_MSG_L1, "Generating HTML indexes\n");
+ if (indexer_make_website(local)) {
+ SYSERROR("%s", "Site creation failed");
+ exit(1);
+ }
+ }
+
msg(OMC_MSG_L1, "Copying indexed delivery to '%s'\n", destdir);
char cmd[PATH_MAX];
memset(cmd, 0, sizeof(cmd));
- sprintf(cmd, "rsync -ah%s --delete --exclude 'tmp/' --exclude 'tools/' '%s/output/' '%s/'", globals.verbose ? "v" : "q", workdir, destdir);
+ sprintf(cmd, "rsync -ah%s --delete --exclude 'tmp/' --exclude 'tools/' '%s/' '%s/'", globals.verbose ? "v" : "q", workdir, destdir);
guard_free(destdir);
if (globals.verbose) {
@@ -585,4 +750,4 @@ int main(int argc, char *argv[]) {
globals_free();
msg(OMC_MSG_L1, "Done!\n");
return 0;
-} \ No newline at end of file
+}