diff options
| author | David Moc <personal@cdatgoose.org> | 2026-03-09 17:40:18 +0100 |
|---|---|---|
| committer | David Moc <personal@cdatgoose.org> | 2026-03-09 17:40:18 +0100 |
| commit | 1af8b8a568ba1782c7f54c575dd7cbe352e0d4a4 (patch) | |
| tree | 098fa8507bf30537a1b4cfa0b2389f5f142b17cb /src/lib/stb_cbuild.h | |
Diffstat (limited to 'src/lib/stb_cbuild.h')
| -rw-r--r-- | src/lib/stb_cbuild.h | 1368 |
1 files changed, 1368 insertions, 0 deletions
diff --git a/src/lib/stb_cbuild.h b/src/lib/stb_cbuild.h new file mode 100644 index 0000000..3714437 --- /dev/null +++ b/src/lib/stb_cbuild.h @@ -0,0 +1,1368 @@ +#ifndef _STB_CB_H +#define _STB_CB_H +#include <openssl/md5.h> // Requires OpenSSL +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +//============================= arglist.h +//======================================= +typedef struct { + char **list; + int count; + int capacity; +} CB_ARGLIST; + +static inline CB_ARGLIST *arglist_new(); +static inline void arglist_append(CB_ARGLIST *arglist, ...); // sentinel: NULL +static inline void arglist_append_array(CB_ARGLIST *arglist, const char **arr); +static inline void arglist_free(CB_ARGLIST *arglist); + +//============================= platform & macros +//=============================== +#if defined(_WIN32) +#define OS_WIN 1 +#include <process.h> +#include <windows.h> +#else +#define OS_UNIX 1 +#include <sys/wait.h> +#include <unistd.h> +#endif + +#if defined(__APPLE__) +#define OS_MACOS 1 +#endif + +#ifdef CB_DEBUG +#define CB_DEBUG_LOG(fmt, ...) \ + fprintf(stderr, "\x1b[36m[DEBUG] %s:%d: " fmt "\x1b[0m\n", __FILE__, \ + __LINE__, ##__VA_ARGS__) +#else +#define CB_DEBUG_LOG(fmt, ...) +#endif + +#if OS_UNIX +#include <ctype.h> +#include <errno.h> +#include <glob.h> +#include <limits.h> +#include <sys/types.h> +#endif + +// Allow override by macro or environment +#ifndef CB_COMPILER +#if OS_WIN +#define COMPILER_NAME_DEFAULT "cl" +#elif defined(OS_MACOS) +#define COMPILER_NAME_DEFAULT "clang" +#else +#define COMPILER_NAME_DEFAULT "cc" +#endif +#else +#define COMPILER_NAME_DEFAULT CB_COMPILER +#endif + +#define COLOR_RESET "\x1b[0m" +#define COLOR_RED "\x1b[31m" +#define COLOR_GREEN "\x1b[32m" +#define COLOR_YELLOW "\x1b[33m" +#define COLOR_CYAN "\x1b[36m" + +typedef enum { BUILD_EXEC, BUILD_STATIC, BUILD_SHARED } _CB_BUILD_TYPE; + +typedef struct _CB_PROJECT { + char *name; + char *output; + CB_ARGLIST *files; // may contain wildcard patterns; expanded before build + CB_ARGLIST *buildflags; + CB_ARGLIST *flags; + char *compile_command; + int is_rebuild; + _CB_BUILD_TYPE build_type; + int CB_PYTHON; +} _CB_PROJECT; + +typedef struct { + _CB_PROJECT **projects; + int run; // run produced outputs? + int run_if_skipped; // run even if build skipped (cache hit) + int parallel_build; // NEW: number of parallel build jobs + int parallel_run; // NEW: number of parallel run jobs + int parallel; // DEPRECATED: kept for back-compat; treated as parallel_run if + // >0 +} CB_PROJECT_BUILD_CONFIG; + +#define CB_STRLIST(...) ((const char *[]){__VA_ARGS__, NULL}) +#define CB_PROJECT_LIST(...) ((_CB_PROJECT *[]){__VA_ARGS__, NULL}) + +#define _CB_CREATE_PROJECT(var, ...) \ + _CB_PROJECT *var = (_CB_PROJECT *)malloc(sizeof(_CB_PROJECT)); \ + if (!(var)) { \ + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); \ + exit(1); \ + } \ + memset(var, 0, sizeof(_CB_PROJECT)); \ + struct { \ + char *name; \ + char *output; \ + const char **files; \ + const char **buildflags; \ + const char **flags; \ + int is_rebuild; \ + _CB_BUILD_TYPE build_type; \ + int CB_PYTHON; \ + } var##_init = {__VA_ARGS__}; \ + var->name = var##_init.name; \ + var->output = var##_init.output; \ + var->files = arglist_new(); \ + var->buildflags = arglist_new(); \ + var->flags = arglist_new(); \ + var->is_rebuild = var##_init.is_rebuild; \ + var->build_type = var##_init.build_type; \ + var->CB_PYTHON = var##_init.CB_PYTHON; \ + if (var##_init.files) \ + arglist_append_array(var->files, var##_init.files); \ + if (var##_init.buildflags) \ + arglist_append_array(var->buildflags, var##_init.buildflags); \ + if (var##_init.flags) \ + arglist_append_array(var->flags, var##_init.flags) + +#define CB_NEEDED_LIBS "-lssl -lcrypto" + +// — forward decls — +static char *cb_concat_compile_command(_CB_PROJECT *proj); +#define _CB_BUILD_COMPILE_COMMAND(proj) \ + do { \ + if ((proj)->compile_command) { \ + free((proj)->compile_command); \ + } \ + (proj)->compile_command = cb_concat_compile_command(proj); \ + } while (0) + +static void cb_dump_to_console(const _CB_PROJECT *project); +static void cb_free_project(_CB_PROJECT *project); +static char *cb_compute_md5(const char *data, size_t len); +static char *cb_read_file_content(const char *filepath, size_t *out_len); +static char *cb_compute_project_checksum(_CB_PROJECT *proj); +static char *cb_read_checksum(const char *filename); +static int cb_write_checksum(const char *filename, const char *checksum); +static void cb_expand_project_wildcards(_CB_PROJECT *proj); // NEW + +typedef struct { +#if OS_WIN + PROCESS_INFORMATION pi; +#else + pid_t pid; +#endif + int running; + _CB_PROJECT *project; + int is_build; +} proc_t; + +static int proc_start_run(proc_t *proc, _CB_PROJECT *proj, char **argv); +static int proc_start_build_cmd(proc_t *proc, _CB_PROJECT *proj, + const char *cmdline); +static int proc_poll(proc_t *proc); +static void proc_wait_all(proc_t *procs, int count); + +#define _CB_PROJECT_BUILD(...) \ + _cb_project_build_internal((CB_PROJECT_BUILD_CONFIG){__VA_ARGS__}) +static void _cb_project_build_internal(CB_PROJECT_BUILD_CONFIG config); + +#define _CB_IMPLEMENTATION +//============================= implementation +//================================== +#ifdef _CB_IMPLEMENTATION + +//---------- small utils ---------- +static char *cb_strdup(const char *s) { + if (!s) + return NULL; + size_t n = strlen(s) + 1; + char *p = (char *)malloc(n); + if (!p) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + exit(1); + } + memcpy(p, s, n); + return p; +} +static void *cb_realloc(void *p, size_t sz) { + void *q = realloc(p, sz); + if (!q) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + exit(1); + } + return q; +} +static int cb_str_has_wildcards(const char *s) { + if (!s) + return 0; + for (; *s; ++s) { + if (*s == '*' || *s == '?' || *s == '[') + return 1; +#if OS_WIN + // Windows doesn't support [...] in FindFirstFile, but treat it as pattern + // anyway +#endif + } + return 0; +} + +// Growable string builder append +static void cb_strcatf(char **dst, size_t *cap, size_t *len, const char *fmt, + ...) { + va_list ap; + for (;;) { + va_start(ap, fmt); + int need = vsnprintf((*dst) ? *dst + *len : NULL, (*dst) ? *cap - *len : 0, + fmt, ap); + va_end(ap); + if (need < 0) { // encoding error + return; + } + size_t req = (size_t)need; + if ((*dst) && *len + req < *cap) { + *len += req; + return; + } + size_t newcap = (*cap ? *cap : 64); + while (newcap <= *len + req) + newcap *= 2; + *dst = (char *)cb_realloc(*dst, newcap); + if (*cap == 0) { + (*dst)[0] = '\0'; + } + *cap = newcap; + } +} + +//---------- arglist ---------- +static inline CB_ARGLIST *arglist_new() { + CB_ARGLIST *a = (CB_ARGLIST *)malloc(sizeof(CB_ARGLIST)); + if (!a) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + exit(1); + } + a->count = 0; + a->capacity = 8; + a->list = (char **)malloc(sizeof(char *) * a->capacity); + if (!a->list) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + exit(1); + } + return a; +} +static void arglist__ensure(CB_ARGLIST *a, int need) { + if (a->count + need <= a->capacity) + return; + while (a->count + need > a->capacity) + a->capacity *= 2; + a->list = (char **)cb_realloc(a->list, sizeof(char *) * a->capacity); +} +static inline void arglist_append(CB_ARGLIST *arglist, ...) { + if (!arglist) + return; + va_list args; + va_start(args, arglist); + for (;;) { + char *arg = va_arg(args, char *); + if (!arg) + break; + arglist__ensure(arglist, 1); + arglist->list[arglist->count++] = cb_strdup(arg); + } + va_end(args); +} +static inline void arglist_append_array(CB_ARGLIST *arglist, const char **arr) { + if (!arglist || !arr) + return; + for (int i = 0; arr[i] != NULL; i++) { + arglist_append(arglist, arr[i], NULL); + } +} +static inline void arglist_free(CB_ARGLIST *arglist) { + if (!arglist) + return; + for (int i = 0; i < arglist->count; i++) + free(arglist->list[i]); + free(arglist->list); + free(arglist); +} + +//---------- compiler selection ---------- +static const char *cb_pick_compiler_name(void) { + const char *env = getenv("CB_CC"); + if (env && env[0]) + return env; + return COMPILER_NAME_DEFAULT; +} + +//---------- wildcard expansion ---------- +static void cb_files_add(CB_ARGLIST *files, const char *s) { + arglist_append(files, s, NULL); +} + +#if OS_WIN +static void cb_expand_pattern_win(CB_ARGLIST *out, const char *pattern) { + // Split directory part to preserve prefix + char dir[MAX_PATH]; + const char *slash = strrchr(pattern, '\\'); + const char *slash2 = strrchr(pattern, '/'); + const char *cut = slash ? slash : slash2; + size_t dlen = 0; + if (cut) { + dlen = (size_t)(cut - pattern + 1); + if (dlen >= sizeof(dir)) + dlen = sizeof(dir) - 1; + memcpy(dir, pattern, dlen); + dir[dlen] = '\0'; + } else { + dir[0] = '\0'; + } + + WIN32_FIND_DATAA fd; + HANDLE h = FindFirstFileA(pattern, &fd); + if (h == INVALID_HANDLE_VALUE) { + // no match: keep literal to let compiler report it + cb_files_add(out, pattern); + return; + } + do { + if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + char path[MAX_PATH * 2]; + snprintf(path, sizeof(path), "%s%s", dir, fd.cFileName); + cb_files_add(out, path); + } + } while (FindNextFileA(h, &fd)); + FindClose(h); +} +#else +static void cb_expand_pattern_unix(CB_ARGLIST *out, const char *pattern) { + glob_t gl; + memset(&gl, 0, sizeof(gl)); + int r = glob(pattern, GLOB_TILDE, NULL, &gl); + if (r == 0) { + for (size_t i = 0; i < gl.gl_pathc; i++) { + cb_files_add(out, gl.gl_pathv[i]); + } + globfree(&gl); + return; + } + // No matches or error — keep as literal so the compiler can error out if + // needed. + cb_files_add(out, pattern); +} +#endif + +static void cb_expand_project_wildcards(_CB_PROJECT *proj) { + if (!proj || !proj->files) + return; + CB_ARGLIST *expanded = arglist_new(); + for (int i = 0; i < proj->files->count; i++) { + const char *f = proj->files->list[i]; + if (cb_str_has_wildcards(f)) { +#if OS_WIN + cb_expand_pattern_win(expanded, f); +#else + cb_expand_pattern_unix(expanded, f); +#endif + } else { + cb_files_add(expanded, f); + } + } + // swap lists + arglist_free(proj->files); + proj->files = expanded; + CB_DEBUG_LOG("Wildcard expansion for %s produced %d file(s)", proj->name, + proj->files->count); +} + +#ifdef _CB_PY + +#include "cb_py.h" + + + +typedef struct { + const char *name; + + char *file_data; + int file_size; + + char ***code_blocks; + int code_block_count; + int *code_block_sizes; + int *code_block_starts; + +} _CB_PROJECT_TRANSPILE; + +static void _cb_project_transpile_free(_CB_PROJECT_TRANSPILE *transpile) { + for (int i = 0; i < transpile->code_block_count; i++) { + for (int j = 0; j < transpile->code_block_sizes[i]; j++) { + free(transpile->code_blocks[i][j]); // free each line + } + free(transpile->code_blocks[i]); // free line array + } + free(transpile->code_blocks); + free(transpile->code_block_sizes); + free(transpile->code_block_starts); + free(transpile->file_data); + free(transpile); +} + +static void _cb_project_transpile_append(_CB_PROJECT_TRANSPILE *transpile, + char **code_block, int code_block_size, + int start_line) { + transpile->code_blocks = + (char ***)realloc(transpile->code_blocks, + (transpile->code_block_count + 1) * sizeof(char **)); + transpile->code_block_sizes = + (int *)realloc(transpile->code_block_sizes, + (transpile->code_block_count + 1) * sizeof(int)); + transpile->code_block_starts = + (int *)realloc(transpile->code_block_starts, + (transpile->code_block_count + 1) * sizeof(int)); + + transpile->code_blocks[transpile->code_block_count] = code_block; + transpile->code_block_sizes[transpile->code_block_count] = code_block_size; + transpile->code_block_starts[transpile->code_block_count] = start_line; + transpile->code_block_count++; + + // Debug print + CB_DEBUG_LOG("Captured code block #%d starting at line %d (%d lines):", + transpile->code_block_count, start_line, code_block_size); +} + +static char *cb_strip_comment_prefix(const char *line) { + const char *p = line; + while (*p == ' ' || *p == '\t') + p++; + + if (p[0] == '/' && p[1] == '/') { + p += 2; + if (*p == ' ') + p++; + } + + return strdup(p); // return cleaned line +} +static int _py_to_c99(_CB_PROJECT *proj) { + for (int i = 0; i < proj->files->count; i++) { + const char *f = proj->files->list[i]; + FILE *fp = fopen(f, "r"); + if (!fp) { + CB_DEBUG_LOG("Failed to open file %s", f); + return -1; + } + + // read file into memory + fseek(fp, 0, SEEK_END); + int file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + char *file_data = (char *)malloc(file_size + 1); + if (!file_data) { + CB_DEBUG_LOG("Failed to allocate memory for file %s", f); + fclose(fp); + return -1; + } + fread(file_data, 1, file_size, fp); + file_data[file_size] = '\0'; + fclose(fp); + + // split into lines + int line_count = 0; + char **lines = NULL; + char *saveptr; + char *tok = strtok_r(file_data, "\n", &saveptr); + while (tok) { + lines = (char **)realloc(lines, (line_count + 1) * sizeof(char *)); + lines[line_count++] = strdup(tok); + tok = strtok_r(NULL, "\n", &saveptr); + } + + // process file: detect blocks + int in_block = 0; + char **current_block = NULL; + int current_size = 0; + int start_line = 0; + + // new file buffer (lines replaced) + char **new_lines = NULL; + int new_count = 0; + + for (int j = 0; j < line_count; j++) { + const char *line = lines[j]; + if (!in_block && strstr(line, "__CB_PY_BEGIN")) { + in_block = 1; + start_line = j; + // keep a marker comment in output + new_lines = + (char **)realloc(new_lines, (new_count + 1) * sizeof(char *)); + new_lines[new_count++] = strdup("// transpiled block begin"); + continue; + } + + if (in_block && strstr(line, "__CB_PY_END")) { + in_block = 0; + + CB_DEBUG_LOG("Captured code block #%d starting at line %d (%d lines):", + new_count, start_line, current_size); + for (int k = 0; k < current_size; k++) { + CB_DEBUG_LOG(" %s", current_block[k]); + } + // transpile captured block + int out_size = 0; + char **c_lines = + transpile_py_block(current_block, current_size, &out_size); + + for (int k = 0; k < out_size; k++) { + new_lines = + (char **)realloc(new_lines, (new_count + 1) * sizeof(char *)); + new_lines[new_count++] = c_lines[k]; // already malloc’d + } + + // cleanup block + for (int k = 0; k < current_size; k++) + free(current_block[k]); + free(current_block); + current_block = NULL; + current_size = 0; + + new_lines = + (char **)realloc(new_lines, (new_count + 1) * sizeof(char *)); + new_lines[new_count++] = strdup("// transpiled block end"); + continue; + } + + if (in_block) { + // capture block lines (strip // prefix) + current_block = (char **)realloc(current_block, + (current_size + 1) * sizeof(char *)); + current_block[current_size++] = cb_strip_comment_prefix(line); + } else { + // just copy normal line + new_lines = + (char **)realloc(new_lines, (new_count + 1) * sizeof(char *)); + new_lines[new_count++] = strdup(line); + } + } + + // write file back + fp = fopen(f, "w"); + if (!fp) { + CB_DEBUG_LOG("Failed to write file %s", f); + return -1; + } + for (int j = 0; j < new_count; j++) { + fprintf(fp, "%s\n", new_lines[j]); + free(new_lines[j]); + } + fclose(fp); + + // cleanup + for (int j = 0; j < line_count; j++) + free(lines[j]); + free(lines); + free(new_lines); + free(file_data); + + CB_DEBUG_LOG("Transpiled %s", f); + } + return 0; +} + +#define _CB_PYTHON_TRANSPILE(proj) int ret = _py_to_c99(proj) + +#endif // _CB_PY +//---------- compile command ---------- +static char *cb_concat_compile_command(_CB_PROJECT *proj) { + if (!proj || !proj->files || proj->files->count == 0) + return cb_strdup("[error] No source files"); + +#ifdef _CB_PY + if (proj->CB_PYTHON) { + CB_DEBUG_LOG("Project is set to have _PY() blocks: %s", proj->output); + CB_DEBUG_LOG("Will try to transpile into c99."); + _CB_PYTHON_TRANSPILE(proj); + if (ret != 0) { + return cb_strdup("[error] Failed to transpile project"); + } + CB_DEBUG_LOG("Transpilation successful for %s.", proj->output); + } +#endif // _CB_PY + const char *cc = cb_pick_compiler_name(); + char *cmd = NULL; + size_t cap = 0, len = 0; + +#if OS_WIN + switch (proj->build_type) { + case BUILD_EXEC: + cb_strcatf(&cmd, &cap, &len, "%s", cc); + break; + case BUILD_STATIC: + // compile to .obj files + for (int i = 0; i < proj->files->count; i++) + cb_strcatf(&cmd, &cap, &len, "%s /c %s && ", cc, proj->files->list[i]); + cb_strcatf(&cmd, &cap, &len, "lib /OUT:%s", proj->output); + return cmd; + case BUILD_SHARED: + cb_strcatf(&cmd, &cap, &len, "%s /LD", cc); + break; + } +#else // Unix + switch (proj->build_type) { + case BUILD_EXEC: + cb_strcatf(&cmd, &cap, &len, "%s", cc); + break; + case BUILD_STATIC: + // compile all to .o then archive + for (int i = 0; i < proj->files->count; i++) + cb_strcatf(&cmd, &cap, &len, "%s -c %s && ", cc, proj->files->list[i]); + cb_strcatf(&cmd, &cap, &len, "ar rcs %s *.o", proj->output); + return cmd; + case BUILD_SHARED: + cb_strcatf(&cmd, &cap, &len, "%s -fPIC -shared", cc); + break; + } +#endif + + // Common flags + for (int i = 0; i < proj->buildflags->count; i++) + cb_strcatf(&cmd, &cap, &len, " %s", proj->buildflags->list[i]); + + if (proj->is_rebuild) + cb_strcatf(&cmd, &cap, &len, " %s", CB_NEEDED_LIBS); + + // Add sources + for (int i = 0; i < proj->files->count; i++) + cb_strcatf(&cmd, &cap, &len, " %s", proj->files->list[i]); + + // Output handling + if (proj->output) { +#if OS_WIN + if (proj->build_type == BUILD_EXEC) + cb_strcatf(&cmd, &cap, &len, " /Fe%s", proj->output); + else if (proj->build_type == BUILD_SHARED) + cb_strcatf(&cmd, &cap, &len, " /Fe%s", proj->output); +#else + if (proj->build_type == BUILD_EXEC) + cb_strcatf(&cmd, &cap, &len, " -o %s", proj->output); + else if (proj->build_type == BUILD_SHARED) + cb_strcatf(&cmd, &cap, &len, " -o %s", proj->output); +#endif + } + + CB_DEBUG_LOG("Generated compile command: %s", cmd); + return cmd; +} + +//---------- info & free ---------- +static void _cb_project_dump(_CB_PROJECT *proj) { + if (!proj) + return; + + printf("== Project Dump ==\n"); + printf("Name: %s\n", proj->name ? proj->name : "(unnamed)"); + printf("Output: %s\n", proj->output ? proj->output : "(none)"); + + // Build type + const char *type_str = "EXEC"; + if (proj->build_type == BUILD_STATIC) + type_str = "STATIC"; + else if (proj->build_type == BUILD_SHARED) + type_str = "SHARED"; + printf("Build type: %s\n", type_str); + + // Files + printf("Files (%d):\n", proj->files ? proj->files->count : 0); + if (proj->files) + for (int i = 0; i < proj->files->count; i++) + printf(" %s\n", proj->files->list[i]); + + // Build Flags + printf("Build flags (%d):\n", proj->buildflags ? proj->buildflags->count : 0); + if (proj->buildflags) + for (int i = 0; i < proj->buildflags->count; i++) + printf(" %s\n", proj->buildflags->list[i]); + + // Flags + printf("Flags (%d):\n", proj->flags ? proj->flags->count : 0); + if (proj->flags) + for (int i = 0; i < proj->flags->count; i++) + printf(" %s\n", proj->flags->list[i]); + + // Command + printf("Compile command: %s\n", + proj->compile_command ? proj->compile_command : "(none)"); +} + +static void cb_free_project(_CB_PROJECT *project) { + if (!project) + return; + if (project->files) + arglist_free(project->files); + if (project->buildflags) + arglist_free(project->buildflags); + if (project->flags) + arglist_free(project->flags); + if (project->compile_command) + free(project->compile_command); + free(project); +} + +//---------- checksum ---------- +static char *cb_compute_md5(const char *data, size_t len) { + unsigned char digest[MD5_DIGEST_LENGTH]; + MD5((const unsigned char *)data, len, digest); + char *out = (char *)malloc(MD5_DIGEST_LENGTH * 2 + 1); + if (!out) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + exit(1); + } + for (int i = 0; i < MD5_DIGEST_LENGTH; i++) + sprintf(out + i * 2, "%02x", digest[i]); + out[MD5_DIGEST_LENGTH * 2] = 0; + return out; +} + +static char *cb_read_file_content(const char *filepath, size_t *out_len) { + FILE *f = fopen(filepath, "rb"); + if (!f) + return NULL; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return NULL; + } + long size = ftell(f); + if (size < 0) { + fclose(f); + return NULL; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + char *buffer = (char *)malloc((size_t)size); + if (!buffer) { + fclose(f); + return NULL; + } + size_t read_len = fread(buffer, 1, (size_t)size, f); + fclose(f); + if (out_len) + *out_len = read_len; + return buffer; +} + +static char *cb_compute_project_checksum(_CB_PROJECT *proj) { + if (!proj) + return NULL; + MD5_CTX ctx; + MD5_Init(&ctx); + + // include compile command, which includes flags & file list + if (proj->compile_command) + MD5_Update(&ctx, proj->compile_command, strlen(proj->compile_command)); + + // include file contents + for (int i = 0; i < proj->files->count; i++) { + size_t flen = 0; + char *fcontent = cb_read_file_content(proj->files->list[i], &flen); + if (fcontent) { + MD5_Update(&ctx, fcontent, flen); + free(fcontent); + } + } + + unsigned char digest[MD5_DIGEST_LENGTH]; + MD5_Final(digest, &ctx); + + char *checksum = (char *)malloc(MD5_DIGEST_LENGTH * 2 + 1); + if (!checksum) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + exit(1); + } + for (int i = 0; i < MD5_DIGEST_LENGTH; i++) + sprintf(checksum + i * 2, "%02x", digest[i]); + checksum[MD5_DIGEST_LENGTH * 2] = 0; + return checksum; +} + +static char *cb_read_checksum(const char *filename) { + FILE *f = fopen(filename, "r"); + if (!f) + return NULL; + char buf[MD5_DIGEST_LENGTH * 2 + 1]; + size_t r = fread(buf, 1, MD5_DIGEST_LENGTH * 2, f); + fclose(f); + if (r != MD5_DIGEST_LENGTH * 2) + return NULL; + buf[r] = 0; + return cb_strdup(buf); +} + +static int cb_write_checksum(const char *filename, const char *checksum) { + FILE *f = fopen(filename, "w"); + if (!f) + return -1; + size_t w = fwrite(checksum, 1, strlen(checksum), f); + fclose(f); + return (w == strlen(checksum)) ? 0 : -1; +} + +//---------- process helpers ---------- +static int proc_start_build_cmd(proc_t *proc, _CB_PROJECT *proj, + const char *cmdline) { + if (!cmdline) + return -1; +#if OS_WIN + STARTUPINFOA si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&proc->pi, sizeof(proc->pi)); + + // run through cmd.exe to avoid manual argument parsing + char *full = NULL; + size_t cap = 0, len = 0; + cb_strcatf(&full, &cap, &len, "cmd.exe /C %s", cmdline); + + BOOL ok = CreateProcessA(NULL, full, NULL, NULL, FALSE, 0, NULL, NULL, &si, + &proc->pi); + free(full); + if (!ok) { + fprintf(stderr, COLOR_RED "[error] Failed to start build: %s\n" COLOR_RESET, + cmdline); + proc->running = 0; + return -1; + } + proc->running = 1; + proc->project = proj; + proc->is_build = 1; + return 0; +#else + pid_t pid = fork(); + if (pid == 0) { + // child: run via /bin/sh -c "cmdline" + execl("/bin/sh", "sh", "-c", cmdline, (char *)NULL); + perror("execl"); + _exit(127); + } else if (pid > 0) { + proc->pid = pid; + proc->running = 1; + proc->project = proj; + proc->is_build = 1; + return 0; + } else { + perror("fork"); + proc->running = 0; + return -1; + } +#endif +} + +static int proc_start_run(proc_t *proc, _CB_PROJECT *proj, char **argv) { +#if OS_WIN + STARTUPINFOA si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&proc->pi, sizeof(proc->pi)); + + // Build a quoted commandline + size_t cmdlen = 0; + for (int i = 0; argv[i]; i++) { + size_t arglen = strlen(argv[i]); + int needs_quotes = strchr(argv[i], ' ') != NULL; + cmdlen += arglen + (needs_quotes ? 2 : 0) + 1; + } + char *cmdline = (char *)malloc(cmdlen + 1); + if (!cmdline) + return -1; + cmdline[0] = 0; + for (int i = 0; argv[i]; i++) { + int needs_quotes = strchr(argv[i], ' ') != NULL; + if (i > 0) + strcat(cmdline, " "); + if (needs_quotes) + strcat(cmdline, "\""); + strcat(cmdline, argv[i]); + if (needs_quotes) + strcat(cmdline, "\""); + } + BOOL success = CreateProcessA(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, + &si, &proc->pi); + free(cmdline); + if (!success) { + fprintf(stderr, COLOR_RED "[error] Failed to run %s\n" COLOR_RESET, + argv[0]); + proc->running = 0; + return -1; + } + proc->running = 1; + proc->project = proj; + proc->is_build = 0; + return 0; +#else + pid_t pid = fork(); + if (pid == 0) { + execvp(argv[0], argv); + perror("execvp"); + _exit(127); + } else if (pid > 0) { + proc->pid = pid; + proc->running = 1; + proc->project = proj; + proc->is_build = 0; + return 0; + } else { + perror("fork"); + proc->running = 0; + return -1; + } +#endif +} + +static int proc_poll(proc_t *proc) { + if (!proc->running) + return -1; +#if OS_WIN + DWORD res = WaitForSingleObject(proc->pi.hProcess, 0); + if (res == WAIT_OBJECT_0) { + DWORD code; + GetExitCodeProcess(proc->pi.hProcess, &code); + CloseHandle(proc->pi.hProcess); + CloseHandle(proc->pi.hThread); + proc->running = 0; + return (int)code; + } + return -1; +#else + int status; + pid_t ret = waitpid(proc->pid, &status, WNOHANG); + if (ret == 0) + return -1; // still running + else if (ret == proc->pid) { + proc->running = 0; + if (WIFEXITED(status)) + return WEXITSTATUS(status); + else + return -1; + } + return -1; +#endif +} + +static void proc_wait_all(proc_t *procs, int count) { + int running = count; + while (running > 0) { + running = 0; + for (int i = 0; i < count; i++) { + if (procs[i].running) { + int ret = proc_poll(&procs[i]); + if (ret < 0) + running++; + } + } +#if OS_WIN + Sleep(10); +#else + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 10000000; + nanosleep(&ts, NULL); +#endif + } +} + +//---------- main build driver ---------- +static void _cb_project_build_internal(CB_PROJECT_BUILD_CONFIG config) { + if (!config.projects) { + fprintf(stderr, COLOR_RED "[error] No projects to build.\n" COLOR_RESET); + return; + } + + // Back-compat: old .parallel means parallel_run if caller used it + if (config.parallel_run <= 0 && config.parallel > 0) + config.parallel_run = config.parallel; + if (config.parallel_build <= 0) + config.parallel_build = 1; + if (config.parallel_run <= 0) + config.parallel_run = 1; + +#ifdef _CB_LOG_TO_FILE + FILE *log = fopen(".cb_build.out", "a"); + if (!log) { + perror("fopen"); + return; + } + time_t now = time(NULL); + fprintf(log, "\n=== Build Started: %s", ctime(&now)); +#endif + + // 1) Prepare: expand wildcards, build commands, compute checksums, decide + // build/no-build + typedef struct { + _CB_PROJECT *proj; + int should_build; + char checksum_file[512]; + char *new_checksum; + char *old_checksum; + } item_t; + + int nproj = 0; + while (config.projects[nproj]) + nproj++; + item_t *items = (item_t *)calloc(nproj, sizeof(item_t)); + if (!items) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + return; + } + + for (int i = 0; i < nproj; i++) { + _CB_PROJECT *proj = config.projects[i]; + + // Expand wildcards BEFORE command & checksum + cb_expand_project_wildcards(proj); + _CB_BUILD_COMPILE_COMMAND(proj); + + // checksum filename (safe-ish) + snprintf(items[i].checksum_file, sizeof(items[i].checksum_file), + ".cb_checksum_%s", proj->name ? proj->name : "noname"); + items[i].proj = proj; + + // compute checksums + items[i].new_checksum = cb_compute_project_checksum(proj); + items[i].old_checksum = cb_read_checksum(items[i].checksum_file); + items[i].should_build = 1; + if (items[i].new_checksum && items[i].old_checksum && + strcmp(items[i].new_checksum, items[i].old_checksum) == 0) { + items[i].should_build = 0; + } + } + + // 2) Build stage (parallel if requested) + int build_jobs = 0; + for (int i = 0; i < nproj; i++) + if (items[i].should_build) + build_jobs++; + + if (build_jobs == 0) { + for (int i = 0; i < nproj; i++) { + printf(COLOR_YELLOW + "[build] Skipping %s (no changes detected)\n\n" COLOR_RESET, + items[i].proj->name); +#ifdef _CB_LOG_TO_FILE + if (log) + fprintf(log, "[build] Skipped project: %s\n", items[i].proj->name); +#endif + } + } else if (config.parallel_build <= 1) { + for (int i = 0; i < nproj; i++) { + if (!items[i].should_build) { + printf(COLOR_YELLOW + "[build] Skipping %s (no changes detected)\n\n" COLOR_RESET, + items[i].proj->name); + continue; + } + _CB_PROJECT *proj = items[i].proj; + printf(COLOR_YELLOW "[build] Building %s\n" COLOR_RESET, proj->name); + printf(" %s\n", proj->compile_command); +#ifdef _CB_LOG_TO_FILE + if (log) + fprintf(log, "[build] Project: %s\nCommand: %s\n", proj->name, + proj->compile_command); +#endif + clock_t start = clock(); + // Run synchronously (serial mode) +#if OS_WIN + // Use system here; it will invoke cmd.exe + int ret = system(proj->compile_command); +#else + int ret = system(proj->compile_command); +#endif + clock_t end = clock(); + if (ret != 0) { + fprintf(stderr, COLOR_RED "[error] Build failed for %s\n" COLOR_RESET, + proj->name); +#ifdef _CB_LOG_TO_FILE + if (log) + fprintf(log, "[error] Build failed for %s\n", proj->name); +#endif + continue; + } + double duration = (double)(end - start) / CLOCKS_PER_SEC; + printf(COLOR_GREEN "[success] Built in %.2fs\n" COLOR_RESET, duration); +#ifdef _CB_LOG_TO_FILE + if (log) + fprintf(log, "[success] Built in %.2fs\n", duration); +#endif + cb_write_checksum(items[i].checksum_file, items[i].new_checksum); + } + } else { + // Parallel build pool + int maxp = config.parallel_build; + proc_t *pool = (proc_t *)calloc(maxp, sizeof(proc_t)); + if (!pool) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + goto after_builds; + } + int active = 0, next = 0; + + while (1) { + // launch while capacity + while (active < maxp && next < nproj) { + if (!items[next].should_build) { + next++; + continue; + } + _CB_PROJECT *proj = items[next].proj; + printf(COLOR_YELLOW "[build] Building %s (parallel)\n" COLOR_RESET, + proj->name); + printf(" %s\n", proj->compile_command); +#ifdef _CB_LOG_TO_FILE + if (log) + fprintf(log, "[build] Project: %s\nCommand: %s\n", proj->name, + proj->compile_command); +#endif + // find free slot + int k = -1; + for (int t = 0; t < maxp; t++) + if (!pool[t].running) { + k = t; + break; + } + if (k == -1) + break; // shouldn't happen due to active<maxp + if (proc_start_build_cmd(&pool[k], proj, proj->compile_command) == 0) { + active++; + } else { + fprintf(stderr, + COLOR_RED + "[error] Failed to start build for %s\n" COLOR_RESET, + proj->name); + } + next++; + } + + if (active == 0) { + // maybe there are only skipped projects left + int remaining = 0; + for (int j = next; j < nproj; j++) + if (items[j].should_build) + remaining++; + if (remaining == 0) + break; + } + + // poll + for (int t = 0; t < maxp; t++) { + if (pool[t].running) { + int ret = proc_poll(&pool[t]); + if (ret >= 0) { + active--; + if (ret != 0) { + fprintf(stderr, + COLOR_RED + "[error] Build failed for %s (exit %d)\n" COLOR_RESET, + pool[t].project ? pool[t].project->name : "(unknown)", + ret); +#ifdef _CB_LOG_TO_FILE + if (log) + fprintf(log, "[error] Build failed for %s\n", + pool[t].project ? pool[t].project->name : "(unknown)"); +#endif + } else { + // success → write checksum + for (int i = 0; i < nproj; i++) + if (items[i].proj == pool[t].project) + cb_write_checksum(items[i].checksum_file, + items[i].new_checksum); + printf(COLOR_GREEN "[success] Built %s\n" COLOR_RESET, + pool[t].project ? pool[t].project->name : ""); + } + } + } + } +#if OS_WIN + Sleep(10); +#else + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 10000000; + nanosleep(&ts, NULL); + +#endif + if (next >= nproj && active == 0) + break; + } + free(pool); + after_builds:; + // Print skips that never printed + for (int i = 0; i < nproj; i++) + if (!items[i].should_build) + printf(COLOR_YELLOW + "[build] Skipping %s (no changes detected)\n" COLOR_RESET, + items[i].proj->name); + } + + // 3) Optional run stage (may be parallel) + if (config.run) { + int maxp = (config.parallel_run > 0 ? config.parallel_run : 1); + proc_t *pool = (proc_t *)calloc(maxp, sizeof(proc_t)); + if (!pool) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + goto cleanup; + } + int active = 0; + for (int i = 0; i < nproj; i++) { + _CB_PROJECT *proj = items[i].proj; + + int should_run = proj->output != NULL && + (items[i].should_build || config.run_if_skipped); + if (!should_run) + continue; + + int argc = proj->flags ? proj->flags->count : 0; + char **argv = (char **)malloc(sizeof(char *) * (argc + 2)); + if (!argv) { + fprintf(stderr, COLOR_RED "[error] OOM\n" COLOR_RESET); + continue; + } + + char output_path[512]; +#if OS_WIN + snprintf(output_path, sizeof(output_path), "%s", proj->output); +#else + snprintf(output_path, sizeof(output_path), "./%s", proj->output); +#endif + argv[0] = cb_strdup(output_path); + for (int j = 0; j < argc; j++) + argv[j + 1] = proj->flags->list[j]; + argv[argc + 1] = NULL; + + if (maxp <= 1) { + // run serially and wait + printf(COLOR_CYAN "[run] %s\n" COLOR_RESET, argv[0]); +#if OS_WIN + STARTUPINFOA si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + // Build cmdline + size_t cmdlen = 0; + for (int z = 0; argv[z]; z++) { + size_t L = strlen(argv[z]); + int q = strchr(argv[z], ' ') != NULL; + cmdlen += L + (q ? 2 : 0) + 1; + } + char *cmdline = (char *)malloc(cmdlen + 1); + cmdline[0] = 0; + for (int z = 0; argv[z]; z++) { + int q = strchr(argv[z], ' ') != NULL; + if (z > 0) + strcat(cmdline, " "); + if (q) + strcat(cmdline, "\""); + strcat(cmdline, argv[z]); + if (q) + strcat(cmdline, "\""); + } + if (CreateProcessA(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, + &pi)) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } else { + fprintf(stderr, COLOR_RED "[error] Failed to run %s\n" COLOR_RESET, + argv[0]); + } + free(cmdline); +#else + pid_t pid = fork(); + if (pid == 0) { + execvp(argv[0], argv); + perror("execvp"); + _exit(127); + } else if (pid > 0) { + waitpid(pid, NULL, 0); + } else { + perror("fork"); + } +#endif + free(argv[0]); + free(argv); + if (proj->is_rebuild) { + // as per your debug note: stop the program if it's just a rebuild + exit(1); + } + } else { + // run in parallel pool + // ensure slot + for (;;) { + int used = 0; + for (int k = 0; k < maxp; k++) + if (pool[k].running) + used++; + if (used < maxp) + break; + for (int k = 0; k < maxp; k++) { + if (pool[k].running) { + int ret = proc_poll(&pool[k]); + (void)ret; + } + } +#if OS_WIN + Sleep(10); +#else + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 10000000; + nanosleep(&ts, NULL); +#endif + } + // find free slot + int k = -1; + for (int t = 0; t < maxp; t++) + if (!pool[t].running) { + k = t; + break; + } + if (k >= 0) { + printf(COLOR_CYAN "[run] %s (parallel)\n" COLOR_RESET, argv[0]); + if (proc_start_run(&pool[k], proj, argv) != 0) { + fprintf(stderr, + COLOR_RED "[error] Failed to start %s\n" COLOR_RESET, + argv[0]); + } + } + // argv memory: argv[0] was strdup'ed; the child process duplicates + // memory; we can free now + free(argv[0]); + free(argv); + } + } + if (maxp > 1) { + proc_wait_all(pool, maxp); + } + free(pool); + } + +cleanup: +#ifdef _CB_LOG_TO_FILE + if (log) + fclose(log); +#endif + for (int i = 0; i < nproj; i++) { + free(items[i].old_checksum); + free(items[i].new_checksum); + } + free(items); +} + +#endif // _CB_IMPLEMENTATION +#endif // _STB_CB_H |
