/* * BSD Zero Clause License * * Copyright © 2023–2024 Thomas Voss * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ #ifndef C_BUILD_SYSTEM_H #define C_BUILD_SYSTEM_H #define _GNU_SOURCE #include #include #include #include #include #ifndef CBS_NO_THREADS # include #endif #include #include #include #include #include #include #include #define _vtoxs(...) ((char *[]){__VA_ARGS__}) #define lengthof(xs) (sizeof(xs) / sizeof(*(xs))) struct strs { char **buf; size_t len, cap; }; enum pkg_config_flags { PC_CFLAGS = 1 << 0, PC_LIBS = 1 << 1, PC_SHARED = 1 << 2, PC_STATIC = 1 << 3, }; static void cbsinit(int, char **); static void rebuild(const char *); /* Always call via macro wrapper */ #define rebuild() rebuild(__FILE__) static void strsfree(struct strs *); static void strszero(struct strs *); static void strspush(struct strs *, char **, size_t); static void strspushenv(struct strs *, const char *, char **, size_t); #define strspushl(xs, ...) \ strspush((xs), _vtoxs(__VA_ARGS__), lengthof(_vtoxs(__VA_ARGS__))) #define strspushenvl(xs, ev, ...) \ strspushenv((xs), (ev), _vtoxs(__VA_ARGS__), lengthof(_vtoxs(__VA_ARGS__))) static bool fexists(const char *); static int fmdcmp(const char *, const char *); static bool fmdolder(const char *, const char *); static bool fmdnewer(const char *, const char *); static bool foutdated(const char *, char **, size_t); #define foutdatedl(s, ...) \ foutdated((s), _vtoxs(__VA_ARGS__), lengthof(_vtoxs(__VA_ARGS__))) static int cmdexec(struct strs); static pid_t cmdexec_async(struct strs); static int cmdexec_read(struct strs, char **, size_t *); static int cmdwait(pid_t); static void cmdput(struct strs); static void cmdfput(FILE *, struct strs); static char *swpext(const char *, const char *); static bool pcquery(struct strs *, const char *, int); static bool binexists(const char *); static int nproc(void); #ifndef CBS_NO_THREADS typedef void tjob(void *); typedef void tjob_free(void *); struct _tqueue { void *arg; tjob *fn; tjob_free *free; struct _tqueue *next; }; typedef struct { bool stop; size_t tcnt, left; pthread_t *thrds; pthread_cond_t cnd; pthread_mutex_t mtx; struct _tqueue *head, *tail; } tpool; static void tpinit(tpool *, size_t); static void tpfree(tpool *); static void tpwait(tpool *); static void tpenq(tpool *, tjob *, void *, tjob_free *); #endif /* !CBS_NO_THREADS */ static int _cbs_argc; static char **_cbs_argv; /* Implementation */ #ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif #ifdef __APPLE__ # define st_mtim st_mtimespec #endif void cbsinit(int argc, char **argv) { _cbs_argc = argc; _cbs_argv = malloc(sizeof(char *) * (argc + 1)); assert(_cbs_argv != NULL); for (int i = 0; i < argc; i++) { _cbs_argv[i] = strdup(argv[i]); assert(_cbs_argv[i] != NULL); } _cbs_argv[argc] = NULL; char *s = strrchr(_cbs_argv[0], '/'); if (s != NULL) { s[0] = 0; assert(chdir(_cbs_argv[0]) != -1); s[0] = '/'; } } void (rebuild)(const char *path) { char *src, *dst; if ((src = strrchr(path, '/')) != NULL) src++; else src = (char *)path; if ((dst = strrchr(*_cbs_argv, '/')) != NULL) dst++; else dst = *_cbs_argv; if (!foutdatedl(dst, src)) return; struct strs xs = {0}; strspushenvl(&xs, "CC", "cc"); #if __STDC_VERSION__ >= 202000L strspushl(&xs, "-std=c23"); #endif #ifndef CBS_NO_THREADS strspushl(&xs, "-lpthread"); #endif strspushl(&xs, "-o", dst, src); cmdput(xs); assert(cmdexec(xs) == EXIT_SUCCESS); execvp(*_cbs_argv, _cbs_argv); assert(!"failed to execute process"); } void strsfree(struct strs *xs) { free(xs->buf); xs->buf = NULL; xs->len = xs->cap = 0; } void strszero(struct strs *xs) { xs->len = 0; if (xs->cap > 0) xs->buf[0] = NULL; } void strspush(struct strs *xs, char **ys, size_t n) { if (n == 0) return; if (xs->len + n >= xs->cap) { xs->cap = (xs->len + n) * 2; xs->buf = realloc(xs->buf, sizeof(char *) * (xs->cap + 1)); assert(xs->buf != NULL); } memcpy(xs->buf + xs->len, ys, n * sizeof(char *)); xs->len += n; assert(xs->len <= xs->cap); xs->buf[xs->len] = NULL; } void strspushenv(struct strs *xs, const char *ev, char **ys, size_t n) { /* NOTE: Do your best to NOT modify any pushed envvar! */ char *p = getenv(ev); if (p == NULL || *p == 0) { strspush(xs, ys, n); return; } wordexp_t we; assert(wordexp(p, &we, WRDE_NOCMD) == 0); /* TODO: Memory leak! */ for (size_t i = 0; i < we.we_wordc; i++) { char *w = strdup(we.we_wordv[i]); assert(w != NULL); strspushl(xs, w); } wordfree(&we); } bool fexists(const char *f) { return !access(f, F_OK); } int fmdcmp(const char *lhs, const char *rhs) { int errnol, errnor; struct stat sbl, sbr; stat(lhs, &sbl); errnol = errno; errno = 0; stat(rhs, &sbr); errnor = errno; assert(errnol == 0 || errnol == ENOENT); assert(errnor == 0 || errnor == ENOENT); if (errnol == ENOENT && errnor == ENOENT) return 0; if (errnol == ENOENT) return -1; if (errnor == ENOENT) return +1; return sbl.st_mtim.tv_sec == sbr.st_mtim.tv_sec ? sbl.st_mtim.tv_nsec - sbr.st_mtim.tv_nsec : sbl.st_mtim.tv_sec - sbr.st_mtim.tv_sec; } bool fmdnewer(const char *lhs, const char *rhs) { return fmdcmp(lhs, rhs) > 0; } bool fmdolder(const char *lhs, const char *rhs) { return fmdcmp(lhs, rhs) < 0; } bool foutdated(const char *src, char **deps, size_t n) { for (size_t i = 0; i < n; i++) { if (fmdolder(src, deps[i])) return true; } return false; } int cmdexec(struct strs xs) { flockfile(stderr); int ret = cmdwait(cmdexec_async(xs)); funlockfile(stderr); return ret; } pid_t cmdexec_async(struct strs xs) { pid_t pid = fork(); assert(pid != -1); if (pid == 0) { execvp(xs.buf[0], xs.buf); assert(!"failed to execute process"); } return pid; } int cmdexec_read(struct strs xs, char **p, size_t *n) { enum { R, W, }; int fds[2]; assert(pipe(fds) != -1); pid_t pid = fork(); assert(pid != -1); if (pid == 0) { close(fds[R]); close(STDOUT_FILENO); assert(dup2(fds[W], STDOUT_FILENO) != -1); execvp(xs.buf[0], xs.buf); assert(!"failed to execute process"); } close(fds[W]); struct stat sb; assert(fstat(fds[R], &sb) != -1); *p = NULL, *n = 0; char *buf = malloc(sb.st_blksize); assert(buf != NULL); for (;;) { ssize_t nr; if ((nr = read(fds[R], buf, sb.st_blksize)) == 0) break; assert(nr != -1); *p = realloc(*p, *n + nr + 1); assert(*p != NULL); memcpy(*p + *n, buf, nr); *n += nr; } close(fds[R]); free(buf); return cmdwait(pid); } int cmdwait(pid_t pid) { int ws; assert(waitpid(pid, &ws, 0) != -1); if (WIFEXITED(ws)) return WEXITSTATUS(ws); return WIFEXITED(ws) ? WEXITSTATUS(ws) : 256; } /* * import shlex * * s = '#define _SHELL_SAFE "' * for c in map(chr, range(128)): * if not shlex._find_unsafe(c): * s += c * print(s + '"') */ #define _SHELL_SAFE \ "%+,-./0123456789:=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" void cmdput(struct strs xs) { cmdfput(stdout, xs); } void cmdfput(FILE *fp, struct strs xs) { flockfile(fp); for (size_t i = 0; i < xs.len; i++) { bool safe = true; const char *p = xs.buf[i]; for (const char *q = p; *q; q++) { if (!strchr(_SHELL_SAFE, *q)) { safe = false; break; } } if (safe) fputs(p, fp); else { putc('\'', fp); for (const char *q = p; *q; q++) { if (*q == '\'') fputs("'\"'\"'", fp); else putc(*q, fp); } putc('\'', fp); } putc(i == xs.len - 1 ? '\n' : ' ', fp); } funlockfile(fp); } bool pcquery(struct strs *xs, const char *lib, int flags) { struct strs ys = {0}; strspushl(&ys, "pkg-config", "--silence-errors"); if (flags & PC_CFLAGS) strspushl(&ys, "--cflags"); if (flags & PC_LIBS) strspushl(&ys, "--libs"); if (flags & PC_SHARED) strspushl(&ys, "--shared"); if (flags & PC_STATIC) strspushl(&ys, "--static"); strspushl(&ys, (char *)lib); char *buf; size_t bufsz; int ec = cmdexec_read(ys, &buf, &bufsz); strsfree(&ys); if (ec != EXIT_SUCCESS) return false; /* Remove trailing newline */ buf[bufsz - 1] = 0; wordexp_t we; assert(wordexp(buf, &we, WRDE_NOCMD) == 0); char **words = malloc(sizeof(char *) * we.we_wordc); assert(words != NULL); /* TODO: Memory leak! */ for (size_t i = 0; i < we.we_wordc; i++) assert((words[i] = strdup(we.we_wordv[i])) != NULL); strspush(xs, words, we.we_wordc); wordfree(&we); free(words); free(buf); return true; } bool binexists(const char *s) { const char *path = getenv("PATH"); assert(path != NULL); char *p = strdup(path), *it; assert(p != NULL); for (it = strtok(p, ":"); it != NULL; it = strtok(NULL, ":")) { static char buf[PATH_MAX]; memset(buf, 0, sizeof(buf)); snprintf(buf, sizeof(buf), "%s/%s", it, s); if (fexists(buf)) { free(p); return true; } } free(p); return false; } int nproc(void) { #ifdef _SC_NPROCESSORS_ONLN return (int)sysconf(_SC_NPROCESSORS_ONLN); #else errno = 0; return -1; #endif } char * swpext(const char *file, const char *ext) { const char *p = strrchr(file, '.'); if (p == NULL) { p = strdup(file); assert(p != NULL); return (char *)p; } size_t noextlen = p - file; char *s = malloc(noextlen + strlen(ext) + 2); assert(s != NULL); sprintf(s, "%.*s.%s", (int)noextlen, file, ext); return s; } #ifndef CBS_NO_THREADS static struct _tqueue * _tpdeq(tpool *tp) { struct _tqueue *q = tp->head; if (q != NULL) { tp->head = tp->head->next; if (!tp->head) tp->tail = NULL; } return q; } static void * _tpwork(void *arg) { tpool *tp = arg; while (!tp->stop) { struct _tqueue *q; pthread_mutex_lock(&tp->mtx); while (!tp->stop && !tp->head) pthread_cond_wait(&tp->cnd, &tp->mtx); if (tp->stop) { pthread_mutex_unlock(&tp->mtx); break; } q = _tpdeq(tp); pthread_mutex_unlock(&tp->mtx); q->fn(q->arg); if (q->free) q->free(q->arg); free(q); pthread_mutex_lock(&tp->mtx); tp->left--; pthread_cond_broadcast(&tp->cnd); pthread_mutex_unlock(&tp->mtx); } return NULL; } void tpinit(tpool *tp, size_t n) { tp->tcnt = n; tp->stop = false; tp->left = 0; tp->head = tp->tail = NULL; tp->thrds = malloc(sizeof(pthread_t) * n); assert(tp->thrds != NULL); pthread_cond_init(&tp->cnd, NULL); pthread_mutex_init(&tp->mtx, NULL); for (size_t i = 0; i < n; i++) assert(pthread_create(tp->thrds + i, NULL, _tpwork, tp) == 0); } void tpfree(tpool *tp) { tp->stop = true; pthread_mutex_lock(&tp->mtx); pthread_cond_broadcast(&tp->cnd); pthread_mutex_unlock(&tp->mtx); for (size_t i = 0; i < tp->tcnt; i++) pthread_join(tp->thrds[i], NULL); free(tp->thrds); while (tp->head != NULL) { struct _tqueue *q = _tpdeq(tp); if (q->free) q->free(q->arg); free(q); } pthread_cond_destroy(&tp->cnd); pthread_mutex_destroy(&tp->mtx); } void tpwait(tpool *tp) { pthread_mutex_lock(&tp->mtx); while (!tp->stop && tp->left) pthread_cond_wait(&tp->cnd, &tp->mtx); pthread_mutex_unlock(&tp->mtx); } void tpenq(tpool *tp, tjob *fn, void *arg, tjob_free *free) { struct _tqueue *q = malloc(sizeof(*q)); assert(q != NULL); *q = (struct _tqueue){ .fn = fn, .arg = arg, .free = free, }; pthread_mutex_lock(&tp->mtx); if (tp->tail) tp->tail->next = q; if (!tp->head) tp->head = q; tp->tail = q; tp->left++; pthread_cond_signal(&tp->cnd); pthread_mutex_unlock(&tp->mtx); } #endif /* !CBS_NO_THREADS */ #ifdef __GNUC__ # pragma GCC diagnostic pop #endif #ifdef __APPLE__ # undef st_mtim #endif #endif /* !C_BUILD_SYSTEM_H */