diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | cbs.h | 953 | ||||
-rw-r--r-- | make.c | 153 |
3 files changed, 1109 insertions, 1 deletions
@@ -1,5 +1,7 @@ .cache/ -*.o +*.[ao] +*.so compile_commands.json +make out test.c @@ -0,0 +1,953 @@ +/* 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. + + IMPORTANT NOTE: This library is built with the assumption that the source + file for your build script and your build script are in the SAME DIRECTORY. + + 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(<unistd.h>) +# error "Non-POSIX platform detected" +#endif + +#ifdef __APPLE__ +# define st_mtim st_mtimespec +#endif + +#include <sys/stat.h> +#include <sys/wait.h> + +#include <errno.h> +#include <libgen.h> +#include <limits.h> +#ifdef CBS_PTHREAD +# include <pthread.h> +#endif +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wordexp.h> + +/* 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 <stdbool.h> +# include <stddef.h> +# include <stdnoreturn.h> +# 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. It also + chdir()s into the directory where the build script is located. */ +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. + + NOTE: This function/macro REQUIRES that the source for the build script and + the compiled build script are in the SAME DIRECTORY. */ +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); + memset(v, 0, sizeof(*v)); +} + +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) +{ + char *s; + + _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); + } + } + + /* Cd to dirname(argv[0]). We can’t use dirname(3) because it may modify + the source string. */ + if (s = strrchr(_cbs_argv[0], '/')) { + s[0] = '\0'; + if (chdir(_cbs_argv[0]) == -1) + die("chdir: %s", s); + s[0] = '/'; + } +} + +static size_t +_next_powerof2(size_t x) +{ +#if defined(__has_builtin) && __has_builtin(__builtin_clzl) + x = x <= 1 ? 1 : 1 << (64 - __builtin_clzl(x - 1)); +#else + if (x) { + x--; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + if (sizeof(size_t) >= 4) + x |= x >> 16; + if (sizeof(size_t) >= 8) + x |= x >> 32; + } + x++; +#endif + + return x; +} + +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 (!n || !*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; +} + +void +_rebuild(char *src) +{ + char *bbn, *sbn, *argv0; + cmd_t cmd = {0}; + size_t bufsiz; + + /* We assume that the compiled binary and the source file are in the same + directory. */ + if (sbn = strrchr(src, '/')) + sbn++; + else + sbn = src; + if (bbn = strrchr(*_cbs_argv, '/')) + bbn++; + else + bbn = src; + + if (!foutdated(bbn, sbn)) + return; + + cmdadd(&cmd, "cc"); +#ifdef CBS_PTHREAD + cmdadd(&cmd, "-lpthread"); +#endif + cmdadd(&cmd, "-o", bbn, sbn); + cmdput(cmd); + if (cmdexec(cmd)) + diex("Compilation of build script failed"); + + cmdclr(&cmd); + + argv0 = bufalloc(nullptr, strlen(bbn) + 3, 1); + *_cbs_argv = argv0; + + *argv0++ = '.'; + *argv0++ = '/'; + strcpy(argv0, bbn); + + 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 */ @@ -0,0 +1,153 @@ +#define _GNU_SOURCE +#include <errno.h> +#include <glob.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define CBS_PTHREAD +#include "cbs.h" + +#define CC "gcc" +#define CFLAGS_ALL WARNINGS, "-pipe", "-std=c23", "-Iinclude" +#define CFLAGS_DBG CFLAGS_ALL, "-g", "-ggdb3", "-Og" +#ifdef __APPLE__ +# define CFLAGS_RLS CFLAGS_ALL, "-O3", "-flto" +#else +# define CFLAGS_RLS \ + CFLAGS_ALL, "-O3", "-flto", "-march=native", "-mtune=native" +#endif +#define LIBNAME "libmlib" +#define WARNINGS \ + "-Wall", "-Wextra", "-Wpedantic", "-Werror", "-Wno-attributes", \ + "-Wno-pointer-sign" + +#define CMDPRC(c) \ + do { \ + int ec; \ + cmdput(c); \ + if ((ec = cmdexec(c)) != EXIT_SUCCESS) \ + diex("%s terminated with exit-code %d", *(c)._argv, ec); \ + cmdclr(&(c)); \ + } while (false) + +#define flagset(o) (flags & (1 << ((o) - 'a'))) +#define streq(x, y) (!strcmp(x, y)) + +static int globerr(const char *, int); +static void work(void *); + +static unsigned long flags; + +int +main(int argc, char **argv) +{ + int opt, procs = -1; + + cbsinit(argc, argv); + rebuild(); + + while ((opt = getopt(argc, argv, "fj:r")) != -1) { + switch (opt) { + case '?': + fprintf(stderr, "Usage: %s [-j procs] [-fr]\n", *argv); + exit(EXIT_FAILURE); + case 'j': + procs = atoi(optarg); + break; + default: + flags |= 1 << (opt - 'a'); + } + } + + argc -= optind; + argv += optind; + + if (argc >= 1) { + if (!streq(*argv, "clean")) { + diex("invalid subcommand — ‘%s’", *argv); + exit(EXIT_FAILURE); + } + cmd_t c = {}; + cmdadd(&c, "find", ".", "(", "-name", "*.[ao]", "-or", "-name", "*.so", + ")", "-delete"); + CMDPRC(c); + } else { + cmd_t c = {}; + glob_t g; + tpool_t tp; + + if (glob("lib/*/*.c", 0, globerr, &g)) + die("glob"); + + if (procs == -1 && (procs = nproc()) == -1) { + if (errno) + die("nproc"); + procs = 8; + } + + tpinit(&tp, procs); + for (size_t i = 0; i < g.gl_pathc; i++) + tpenq(&tp, work, g.gl_pathv[i], nullptr); + tpwait(&tp); + tpfree(&tp); + + for (size_t i = 0; i < g.gl_pathc; i++) + g.gl_pathv[i][strlen(g.gl_pathv[i]) - 1] = 'o'; + + if (flagset('f') + || foutdatedv(LIBNAME ".a", (const char **)g.gl_pathv, g.gl_pathc)) + { + cmdadd(&c, "ar", "rcs", LIBNAME ".a"); + cmdaddv(&c, g.gl_pathv, g.gl_pathc); + CMDPRC(c); + } + + if (flagset('f') + || foutdatedv(LIBNAME ".so", (const char **)g.gl_pathv, g.gl_pathc)) + { + struct strv sv = {}; + env_or_default(&sv, "CC", CC); + cmdaddv(&c, sv.buf, sv.len); + cmdadd(&c, "-shared", "-o", LIBNAME ".so"); + cmdaddv(&c, g.gl_pathv, g.gl_pathc); + CMDPRC(c); + } + + globfree(&g); + } + + return EXIT_SUCCESS; +} + +void +work(void *p) +{ + char *dst, *src = p; + cmd_t c = {}; + struct strv sv = {}; + + if (!(dst = strdup(src))) + die("strdup"); + dst[strlen(dst) - 1] = 'o'; + + if (flagset('f') || foutdated(dst, src)) { + env_or_default(&sv, "CC", CC); + if (flagset('r')) + env_or_default(&sv, "CFLAGS", CFLAGS_RLS); + else + env_or_default(&sv, "CFLAGS", CFLAGS_DBG); + cmdaddv(&c, sv.buf, sv.len); + cmdadd(&c, "-Iinclude", "-fPIC", "-o", dst, "-c", src); + CMDPRC(c); + } + + free(dst); +} + +int +globerr(const char *s, int e) +{ + errno = e; + die("glob: %s", s); +} |