/* Single-header library to help write build scripts in C. This library is POSIX compliant, so it should work on all respectible UNIX-like systems. You can find the CBS git repository at https://git.sr.ht/~mango/cbs and you can include this in your project with the following command: $ wget 'https://git.sr.ht/~mango/cbs/blob/master/cbs.h' This library is licensed under the 0-Clause BSD license, and as such you may do whatever you want to it, however you want to it, whenever you want. You are encouraged in fact to modify this library to suit your specific usecase. All functions and macros are documented. You can figure out the API pretty easily by just reading the comments in this file. In many cases you may want to be able to execute commands on multiple threads to speed up compilation, such as the -j option when using Make. Functions for creating and using thread pools will be made available if the CBS_PTHREAD macro is defined before including this file. Do note that on POSIX platforms it will require linking to the pthreads library when bootstrapping the build script. This file does not support C89. Fuck C89, that shit is ancient. Move on. IMPORTANT NOTE: Any identifiers prefixed with an underscore (e.g. ‘_rebuild’) are meant for internal use only and you should not touch them unless you know what you’re doing. IMPORTANT NOTE: All the functions and macros in this library will terminate the program on error. If this is undesired behavior, feel free to edit the functions to return errors. There are a few exceptions to the above rule, and they are documented. This library does not aim to ever support Windows */ #ifndef C_BUILD_SYSTEM_H #define C_BUILD_SYSTEM_H #ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" # pragma GCC diagnostic ignored "-Wparentheses" #endif /* Assert that the user is building for a supported platform. The only portable way to check for POSIX is to validate that unistd.h exists. This is only possible without compiler extensions in C23 (although some compilers support it as an extension in earlier editions), so people compiling for pre-C23 might not get this error if on a bad platform, and may end up being a bit confused. It’s just a maybe though, this is nothing more than a sanity check for the users sake. */ #if defined(__has_include) && !__has_include() # error "Non-POSIX platform detected" #endif #ifdef __APPLE__ # define st_mtim st_mtimespec #endif #include #include #include #include #include #ifdef CBS_PTHREAD # include #endif #include #include #include #include #include #include #include /* C23 changed a lot so we want to check for it */ #if __STDC_VERSION__ >= 202000 # define CBS_IS_C23 1 #endif /* Some C23 compat. In C23 booleans are actual keywords, and the noreturn attribute is different. */ #if CBS_IS_C23 # define noreturn [[noreturn]] #else # include # include # include # define nullptr NULL #endif /* Give helpful diagnostics when people use die() incorrectly on GCC. C23 introduced C++ attribute syntax, so we need a check for that too. */ #ifdef __GNUC__ # if CBS_IS_C23 # define ATTR_FMT [[gnu::format(printf, 1, 2)]] # else # define ATTR_FMT __attribute__((format(printf, 1, 2))) # endif #else # define ATTR_FMT #endif /* Clang defines this attribute, and while it does nothing it does serve as good documentation. */ #ifndef _Nullable # define _Nullable #endif /* Convert the given variadic arguments to a string array */ #define _vtoa(...) ((char *[]){__VA_ARGS__}) /* Internal global versions of argc and argv, so our functions and macros can access them from anywhere. */ static int _cbs_argc; static char **_cbs_argv; /* A vector of strings used to accumulate output of functions such as pcquery(). The array of strings buf has length len, and is not null-terminated. You should always zero-initialize variables of this type. */ struct strv { char **buf; size_t len; }; /* Free and zero a string vector. Because this function zeros the structure, it is safe to reuse the vector after this function is called. */ static void strvfree(struct strv *); /* A wrapper function around realloc(). It behaves exactly the same except instead of taking a buffer size as an argument, it takes a count n of elements, and a size m of each element. This allows it to properly check for overflow, and errors if overflow would occur. */ static void *bufalloc(void *_Nullable, size_t n, size_t m); /* Error reporting functions. The die() function takes the same arguments as printf() and prints the corresponding string to stderr. It also prefixes the string with the command name followed by a colon, and suffixes the string with a colon and the error string returned from strerror(). If you want to print just the error message and no custom text, NULL may be passed to die(). NULL should not be passed to diex(). diex() is the same as die() but does not print a strerror() error string. */ ATTR_FMT noreturn static void die(const char *_Nullable, ...); ATTR_FMT noreturn static void diex(const char *, ...); /* Initializes some data required for this header to work properly. This should be the first thing called in main() with argc and argv passed. */ static void cbsinit(int, char **); /* Get the number of items in the array a */ #define lengthof(a) (sizeof(a) / sizeof(*(a))) /* Struct representing a CLI command that various functions act on. You should always zero-initialize variables of this type before use. After executing a command, you can reuse the already allocated buffer this command holds by calling cmdclr(). When you’re really done with an object of this type, remember to call free() on ._argv. The ._argv field is a NULL-terminated list of command arguments of length ._len. You may safely read from both of these fields but they should NOT be modified without use of cmdadd() and cmdaddv(). */ typedef struct { char **_argv; size_t _len, _cap; } cmd_t; /* Returns whether or not a binary of the given name exists in the users environment as defined by $PATH. */ static bool binexists(const char *); /* cmdadd() adds the variadic string arguments to the given command. Alternatively, the cmdaddv() function adds the n strings pointed to by p to the given command. */ static void cmdaddv(cmd_t *, char **p, size_t n); #define cmdadd(cmd, ...) \ cmdaddv(cmd, _vtoa(__VA_ARGS__), lengthof(_vtoa(__VA_ARGS__))) /* Clear (but not free) the command c. Useful for reusing the same command struct to minimize allocations. */ static void cmdclr(cmd_t *c); /* The cmdexec() function executes the given command and waits for it to terminate, returning its exit code. The cmdexeca() function executes the given command and returns immediately, returning its process ID. The cmdexecb() function is like cmdexec() except it writes the given commands standard output to the character buffer pointed to by p. It also stores the size of the output in *n. The character buffer p is null-terminated. If the given command produces no output to the standard output, p will be set to NULL. cmdexec() and cmdexecb() have the same return values as cmdwait(). */ static int cmdexec(cmd_t); static pid_t cmdexeca(cmd_t); static int cmdexecb(cmd_t, char **p, size_t *n); /* Wait for the process with the given PID to terminate, and return its exit status. If the process was terminated by a signal 256 is returned. */ static int cmdwait(pid_t); /* Write a representation of the given command to the given file stream. This can be used to mimick the echoing behavior of make(1). The cmdput() function is a nice convenience function so you can avoid writing ‘stdout’ all the time. */ static void cmdput(cmd_t); static void cmdputf(FILE *, cmd_t); /* Expand the environment variable s using /bin/sh expansion rules and append the results in the given string vector. If the environment variable is NULL or empty, then store the strings specified by the array p of length n. env_or_default() is the same as env_or_defaultv() but you provide p as variadic arguments. If the pointer p is null, then no default value is appended to the given string vector when the environment variable is not present. */ static void env_or_defaultv(struct strv *, const char *s, char *_Nullable *p, size_t n); #define env_or_default(sv, s, ...) \ env_or_defaultv((sv), (s), _vtoa(__VA_ARGS__), lengthof(_vtoa(__VA_ARGS__))) /* Returns if a file exists at the given path. A return value of false may also mean you don’t have the proper file access permissions, which will also set errno. */ static bool fexists(const char *); /* Compare the modification dates of the two named files. A return value >0 means the LHS is newer than the RHS. A return value <0 means the LHS is older than the RHS. A return value of 0 means the LHS and RHS have the same modification date. The fmdnewer() and fmdolder() functions are wrappers around fmdcmp() that return true when the LHS is newer or older than the RHS respectively. */ static int fmdcmp(const char *, const char *); static bool fmdolder(const char *, const char *); static bool fmdnewer(const char *, const char *); /* Report if any of n files specified by p render the file base outdated. If the file base does not exist, this function returns true. You will typically call this with a compiled program as base, and C source files as p. The macro foutdated() is a wrapper around foutdatedv() that allows you to specify the sources to base as variadic arguments instead of as an array. */ static bool foutdatedv(const char *base, const char **p, size_t n); #define foutdated(s, ...) \ foutdatedv(s, (const char **)_vtoa(__VA_ARGS__), \ lengthof(_vtoa(__VA_ARGS__))) /* Rebuild the build script if it has been modified, and execute the newly built script. You should call the rebuild() macro at the very beginning of main(), but right after cbsinit(). You probably don’t want to call _rebuild() directly. */ static void _rebuild(char *); #define rebuild() _rebuild(__FILE__) /* Get the number of available CPUs, or -1 on error. This function also returns -1 if the _SC_NPROCESSORS_ONLN flag to sysconf(3) is not available. In that case, errno will not be set. */ static int nproc(void); /* Append the arguments returned by an invokation of pkg-config for the library lib to the given string vector. The flags argument is one-or-more of the flags in the pkg_config_flags enum bitwise-ORed together. If PKGC_CFLAGS is specified, call pkg-config with ‘--cflags’. If PKGC_LIBS is specified, call pkg-config with ‘--libs’. This function returns true on success and false if pkg-config is not found on the system. To check for pkg-configs existance, you can use the binexists() function. */ static bool pcquery(struct strv *, char *lib, int flags); enum pkg_config_flags { PKGC_LIBS = 1 << 0, PKGC_CFLAGS = 1 << 1, }; #ifdef CBS_PTHREAD /* A tfunc_t represents a function to be executed by a threads in a thread pool. It takes an argument in the form of a void * and returns nothing. */ typedef void (*tfunc_t)(void *); /* A tfunc_free_t represents a function which frees the argument passed to a tfunc_t function. */ typedef void (*tfree_func_t)(void *); /* A thread pool job queue. Meant for internal-use only. */ struct _tjob { void *arg; tfunc_t fn; tfree_func_t free; struct _tjob *next; }; /* A basic thread pool. None of its fields should really be touched. */ typedef struct { bool _stop; size_t _tcnt, _left; pthread_t *_thrds; pthread_cond_t _cnd; pthread_mutex_t _mtx; struct _tjob *_head, *_tail; } tpool_t; /* Initialize and destroy a thread pool. The tpinit() function initializes the given thread pool and creates n threads ready to execute work. The tpfree() function should be called after a thread pool has been used to release all resources used by the thread pool. */ static void tpinit(tpool_t *, size_t n); static void tpfree(tpool_t *); /* Wait for all jobs on the given thread pool to be executed. Note that this function does not destroy the threads or free any resources — those are tasks for the tpfree() function. */ static void tpwait(tpool_t *); /* Enqueue and dequeue jobs to the thread pools job queue. The tpenq() function is threadsafe while the _tpdeq() function is not (so don’t use it). When calling the tpenq() function, the function fn will be queued to be executed by a thread in the thread pool with the argument arg. If the given argument needs to be deallocated after the job completes, you can pass the free argument which will be called with the given argument after use. If free is NULL, it will be ignored. The free() function is a valid argument to the free parameter. */ static void tpenq(tpool_t *, tfunc_t fn, void *arg, tfree_func_t _Nullable free); static struct _tjob *_tpdeq(tpool_t *); #endif /* CBS_PTHREAD */ /* BEGIN DEFINITIONS */ void strvfree(struct strv *v) { free(v->buf); *v = (struct strv){0}; } void * bufalloc(void *p, size_t n, size_t m) { if (n && SIZE_MAX / n < m) { errno = EOVERFLOW; die(__func__); } if (!(p = realloc(p, n * m))) die(__func__); return p; } void die(const char *fmt, ...) { int e = errno; va_list ap; va_start(ap, fmt); flockfile(stderr); fprintf(stderr, "%s: ", *_cbs_argv); if (fmt) { vfprintf(stderr, fmt, ap); fprintf(stderr, ": "); } fprintf(stderr, "%s\n", strerror(e)); exit(EXIT_FAILURE); } void diex(const char *fmt, ...) { va_list ap; va_start(ap, fmt); flockfile(stderr); fprintf(stderr, "%s: ", *_cbs_argv); vfprintf(stderr, fmt, ap); fputc('\n', stderr); exit(EXIT_FAILURE); } void cbsinit(int argc, char **argv) { _cbs_argc = argc; _cbs_argv = bufalloc(nullptr, argc, sizeof(char *)); for (int i = 0; i < argc; i++) { if (!(_cbs_argv[i] = strdup(argv[i]))) { /* We might not have set _cbs_argv[0] yet, so we can’t use die() */ fprintf(stderr, "%s: strdup: %s\n", *argv, strerror(errno)); exit(EXIT_FAILURE); } } } static size_t _next_powerof2(size_t n) { if (n && !(n & (n - 1))) return n; n--; for (size_t i = 1; i < sizeof(size_t); i <<= 1) n |= n >> i; return n + 1; } bool binexists(const char *name) { char *p, *path; if (!(path = getenv("PATH"))) diex("PATH environment variable not found"); if (!(path = strdup(path))) die("strdup"); p = strtok(path, ":"); while (p) { char buf[PATH_MAX]; snprintf(buf, sizeof(buf), "%s/%s", p, name); if (fexists(buf)) { free(path); return true; } p = strtok(nullptr, ":"); } free(path); return false; } void cmdaddv(cmd_t *cmd, char **xs, size_t n) { if (cmd->_len + n >= cmd->_cap) { cmd->_cap = _next_powerof2(cmd->_len + n) + 2; cmd->_argv = bufalloc(cmd->_argv, cmd->_cap, sizeof(char *)); } memcpy(cmd->_argv + cmd->_len, xs, n * sizeof(*xs)); cmd->_len += n; cmd->_argv[cmd->_len] = nullptr; } void cmdclr(cmd_t *c) { c->_len = 0; *c->_argv = nullptr; } int cmdexec(cmd_t c) { return cmdwait(cmdexeca(c)); } pid_t cmdexeca(cmd_t c) { pid_t pid; switch (pid = fork()) { case -1: die("fork"); case 0: execvp(*c._argv, c._argv); die("execvp: %s", *c._argv); } return pid; } int cmdexecb(cmd_t c, char **p, size_t *n) { enum { FD_R, FD_W, }; pid_t pid; int fds[2]; char *buf; size_t len, blksize; struct stat sb; if (pipe(fds) == -1) die("pipe"); switch (pid = fork()) { case -1: die("fork"); case 0: close(fds[FD_R]); if (dup2(fds[FD_W], STDOUT_FILENO) == -1) die("dup2"); execvp(*c._argv, c._argv); die("execvp: %s", *c._argv); } close(fds[FD_W]); buf = nullptr; len = 0; blksize = fstat(fds[FD_R], &sb) == -1 ? BUFSIZ : sb.st_blksize; for (;;) { /* This can maybe somewhere somehow break some system. I do not care */ char tmp[blksize]; ssize_t nr; if ((nr = read(fds[FD_R], tmp, blksize)) == -1) die("read"); if (!nr) break; buf = bufalloc(buf, len + nr + 1, sizeof(char)); memcpy(buf + len, tmp, nr); len += nr; } close(fds[FD_R]); if (buf) buf[len] = 0; *p = buf; *n = len; return cmdwait(pid); } int cmdwait(pid_t pid) { for (;;) { int ws; if (waitpid(pid, &ws, 0) == -1) die("waitpid"); if (WIFEXITED(ws)) return WEXITSTATUS(ws); if (WIFSIGNALED(ws)) return 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(cmd_t c) { cmdputf(stdout, c); } void cmdputf(FILE *stream, cmd_t cmd) { flockfile(stream); for (size_t i = 0; i < cmd._len; i++) { bool safe = true; char *p, *q; p = q = cmd._argv[i]; for (; *q; q++) { if (!strchr(SHELL_SAFE, *q)) { safe = false; break; } } if (safe) fputs(p, stream); else { putc('\'', stream); for (q = p; *q; q++) { if (*q == '\'') fputs("'\"'\"'", stream); else putc(*q, stream); } putc('\'', stream); } putc(i == cmd._len - 1 ? '\n' : ' ', stream); } funlockfile(stream); } void env_or_defaultv(struct strv *sv, const char *s, char **p, size_t n) { wordexp_t we; const char *ev; if ((ev = getenv(s)) && *ev) { switch (wordexp(ev, &we, WRDE_NOCMD)) { case WRDE_BADCHAR: case WRDE_BADVAL: case WRDE_SYNTAX: errno = EINVAL; die("wordexp"); case WRDE_NOSPACE: errno = ENOMEM; die("wordexp"); } p = we.we_wordv; n = we.we_wordc; } else if (!p) return; sv->buf = bufalloc(sv->buf, sv->len + n, sizeof(*sv->buf)); for (size_t i = 0; i < n; i++) { if (!(sv->buf[sv->len + i] = strdup(p[i]))) die("strdup"); } sv->len += n; if (ev && *ev) wordfree(&we); } bool fexists(const char *f) { return !access(f, F_OK); } int fmdcmp(const char *lhs, const char *rhs) { struct stat sbl, sbr; if (stat(lhs, &sbl) == -1) die("%s", lhs); if (stat(rhs, &sbr) == -1) die("%s", rhs); 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 foutdatedv(const char *src, const char **deps, size_t n) { if (!fexists(src)) return true; for (size_t i = 0; i < n; i++) { if (fmdolder(src, deps[i])) return true; } return false; } static char * _getcwd(void) { char *buf = nullptr; size_t n = 0; for (;;) { n += PATH_MAX; buf = bufalloc(buf, n, sizeof(char)); if (getcwd(buf, n)) break; if (errno != ERANGE) die("getcwd"); } return buf; } void _rebuild(char *src) { char *cwd, *cpy1, *cpy2, *dn, *bn; cmd_t cmd = {0}; cwd = _getcwd(); if (!(cpy1 = strdup(*_cbs_argv))) die("strdup"); if (!(cpy2 = strdup(*_cbs_argv))) die("strdup"); dn = dirname(cpy1); bn = basename(cpy2); if (chdir(dn) == -1) die("chdir: %s", dn); if (!foutdated(bn, src)) { if (chdir(cwd) == -1) die("chdir: %s", cwd); free(cpy1); free(cpy2); free(cwd); return; } cmdadd(&cmd, "cc"); #ifdef CBS_PTHREAD cmdadd(&cmd, "-lpthread"); #endif cmdadd(&cmd, "-o", bn, src); cmdput(cmd); if (cmdexec(cmd)) diex("Compilation of build script failed"); cmdclr(&cmd); if (chdir(cwd) == -1) die("chdir: %s", cwd); cmdaddv(&cmd, _cbs_argv, _cbs_argc); execvp(*cmd._argv, cmd._argv); die("execvp: %s", *cmd._argv); } int nproc(void) { #ifdef _SC_NPROCESSORS_ONLN return (int)sysconf(_SC_NPROCESSORS_ONLN); #else errno = 0; return -1; #endif } bool pcquery(struct strv *vec, char *lib, int flags) { int ec; char *p; size_t n; cmd_t c = {0}; wordexp_t we; p = nullptr; cmdadd(&c, "pkg-config"); if (flags & PKGC_LIBS) cmdadd(&c, "--libs"); if (flags & PKGC_CFLAGS) cmdadd(&c, "--cflags"); cmdadd(&c, lib); if ((ec = cmdexecb(c, &p, &n))) { if (errno == ENOENT) { free(c._argv); return false; } diex("pkg-config terminated with exit-code %d", ec); } if (!p) return true; /* Remove trailing newline */ p[n - 1] = 0; switch (wordexp(p, &we, WRDE_NOCMD)) { case WRDE_BADCHAR: case WRDE_BADVAL: case WRDE_SYNTAX: errno = EINVAL; die("wordexp"); case WRDE_NOSPACE: errno = ENOMEM; die("wordexp"); } vec->buf = bufalloc(vec->buf, vec->len + we.we_wordc, sizeof(char *)); for (size_t i = 0; i < we.we_wordc; i++) { char *p = strdup(we.we_wordv[i]); if (!p) die(__func__); vec->buf[vec->len++] = p; } wordfree(&we); free(p); return true; } #ifdef CBS_PTHREAD static void * _tpwork(void *arg) { tpool_t *tp = arg; while (!tp->_stop) { struct _tjob *j; 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; } j = _tpdeq(tp); pthread_mutex_unlock(&tp->_mtx); j->fn(j->arg); if (j->free) j->free(j->arg); free(j); pthread_mutex_lock(&tp->_mtx); tp->_left--; pthread_cond_broadcast(&tp->_cnd); pthread_mutex_unlock(&tp->_mtx); } return nullptr; } void tpinit(tpool_t *tp, size_t n) { tp->_tcnt = n; tp->_stop = false; tp->_left = 0; tp->_head = tp->_tail = nullptr; tp->_thrds = bufalloc(nullptr, n, sizeof(pthread_t)); pthread_cond_init(&tp->_cnd, nullptr); pthread_mutex_init(&tp->_mtx, nullptr); for (size_t i = 0; i < n; i++) pthread_create(tp->_thrds + i, nullptr, _tpwork, tp); } void tpfree(tpool_t *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], nullptr); free(tp->_thrds); while (tp->_head) { struct _tjob *j = _tpdeq(tp); if (j->free) j->free(j->arg); free(j); } pthread_cond_destroy(&tp->_cnd); pthread_mutex_destroy(&tp->_mtx); } struct _tjob * _tpdeq(tpool_t *tp) { struct _tjob *j = tp->_head; if (j) { tp->_head = tp->_head->next; if (!tp->_head) tp->_tail = nullptr; } return j; } void tpenq(tpool_t *tp, tfunc_t fn, void *arg, tfree_func_t free) { struct _tjob *j = bufalloc(nullptr, 1, sizeof(struct _tjob)); *j = (struct _tjob){ .fn = fn, .arg = arg, .free = free, }; pthread_mutex_lock(&tp->_mtx); if (tp->_tail) tp->_tail->next = j; if (!tp->_head) tp->_head = j; tp->_tail = j; tp->_left++; pthread_cond_signal(&tp->_cnd); pthread_mutex_unlock(&tp->_mtx); } void tpwait(tpool_t *tp) { pthread_mutex_lock(&tp->_mtx); while (!tp->_stop && tp->_left) pthread_cond_wait(&tp->_cnd, &tp->_mtx); pthread_mutex_unlock(&tp->_mtx); } #endif /* CBS_PTHREAD */ #ifdef __GNUC__ # pragma GCC diagnostic pop #endif #ifdef __APPLE__ # undef st_mtim #endif #endif /* !C_BUILD_SYSTEM_H */