aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2023-12-30 20:11:06 +0100
committerThomas Voss <mail@thomasvoss.com> 2023-12-30 20:11:06 +0100
commit1de8c1d2f42d6f6e4ef0c6996030c846266c194d (patch)
treef2c0851683a77e4a0e79a0cce3f9ef3406fbd0d3
parentd70d3214b2b26f6d8e1bd6771ffe5cdca2c99cf3 (diff)
Switch to cbs for building
-rw-r--r--.gitignore1
-rw-r--r--Makefile23
-rw-r--r--README.md14
-rw-r--r--cbs.h783
-rw-r--r--make.c147
5 files changed, 945 insertions, 23 deletions
diff --git a/.gitignore b/.gitignore
index aa21f6a..a93b5f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.clang-format
grab
+make
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 522ea68..0000000
--- a/Makefile
+++ /dev/null
@@ -1,23 +0,0 @@
-.POSIX:
-
-WARNINGS = -Wall -Wextra -Werror -Wpedantic
-
-CC = cc
-CFLAGS = $(WARNINGS) -pipe -O3 -march=native -mtune=native
-
-PREFIX = /usr/local
-DPREFIX = $(DESTDIR)$(PREFIX)
-
-all: grab
-grab: grab.c
-
-debug:
- $(CC) $(WARNINGS) -DGRAB_DEBUG -g -ggdb3 -o grab grab.c
-
-install:
- mkdir -p $(DPREFIX)/bin $(DPREFIX)/share/man/man1
- cp grab $(DPREFIX)/bin
- cp grab.1 $(DPREFIX)/share/man/man1
-
-clean:
- rm -f grab
diff --git a/README.md b/README.md
index 6fc3971..52b9b96 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,20 @@ paper][1]. Grab allows you to be far more precise with your searching
than Grep, as it doesn’t constrain itself to working only on individual
lines.
+
+## Installation
+
+To install grab, all you need is a C compiler:
+
+```sh
+$ cc -o make make.c # Bootstrap the build script
+$ ./make # Build the project
+$ ./make install # Install the project
+```
+
+
+## Description
+
Grab invokations must include a pattern string which specifies which text
to match. A pattern string consists of one or more commands. A command
is an operator followed by a delimiter, a regular expression (regex), and
diff --git a/cbs.h b/cbs.h
new file mode 100644
index 0000000..789f532
--- /dev/null
+++ b/cbs.h
@@ -0,0 +1,783 @@
+/* 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
+
+/* 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
+
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <errno.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, and some idiot decided that
+ __STDC_VERSION__ is an optional macro */
+#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202000
+# define CBS_IS_C23
+#endif
+
+/* Some C23 compat. In C23 booleans are actual keywords, and the noreturn
+ attribute is different. */
+#ifdef CBS_IS_C23
+# define noreturn [[noreturn]]
+#else
+# include <stdbool.h>
+# include <stdnoreturn.h>
+#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__
+# ifdef 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 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;
+
+/* 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);
+
+/* 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 either it, or this header file have 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);
+
+/* Add the arguments returned by an invokation of pkg-config for the library lib
+ to the given command. 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 without doing anything
+ meaningful, you can call this function with flags set to 0 and lib set to a
+ VALID library name.
+
+ The arguments this function appends to the given command are heap-allocated.
+ If you care about deallocating them, you can figure out their indicies in
+ the commands ._argv field by getting cmd._len both before and after calling
+ this function. */
+static bool pcquery(cmd_t *, 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 *
+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 = argv;
+}
+
+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;
+}
+
+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] = NULL;
+}
+
+void
+cmdclr(cmd_t *c)
+{
+ c->_len = 0;
+ *c->_argv = NULL;
+}
+
+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 = NULL;
+ 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);
+}
+
+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)
+{
+ cmd_t cmd = {0};
+
+ if (fmdnewer(*_cbs_argv, src) && fmdnewer(*_cbs_argv, __FILE__))
+ return;
+
+ cmdadd(&cmd, "cc");
+#ifdef CBS_PTHREAD
+ cmdadd(&cmd, "-lpthread");
+#endif
+ cmdadd(&cmd, "-o", *_cbs_argv, src);
+ cmdput(cmd);
+ if (cmdexec(cmd))
+ diex("Compilation of build script failed");
+
+ cmdclr(&cmd);
+ 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(cmd_t *cmd, char *lib, int flags)
+{
+ int ec;
+ char *p;
+ size_t n;
+ cmd_t c = {0};
+ wordexp_t we;
+
+ p = NULL;
+
+ 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, 0)) {
+ case WRDE_BADCHAR:
+ case WRDE_BADVAL:
+ case WRDE_SYNTAX:
+ errno = EINVAL;
+ die("wordexp");
+ case WRDE_NOSPACE:
+ errno = ENOMEM;
+ die("wordexp");
+ }
+
+ for (size_t i = 0; i < we.we_wordc; i++) {
+ char *p = strdup(we.we_wordv[i]);
+ if (!p)
+ die(__func__);
+ cmdadd(cmd, 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 NULL;
+}
+
+void
+tpinit(tpool_t *tp, size_t n)
+{
+ tp->_tcnt = n;
+ tp->_stop = false;
+ tp->_left = 0;
+ tp->_head = tp->_tail = NULL;
+ tp->_thrds = bufalloc(NULL, n, sizeof(pthread_t));
+ pthread_cond_init(&tp->_cnd, NULL);
+ pthread_mutex_init(&tp->_mtx, NULL);
+
+ for (size_t i = 0; i < n; i++)
+ pthread_create(tp->_thrds + i, NULL, _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], NULL);
+
+ 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 = NULL;
+ }
+
+ return j;
+}
+
+void
+tpenq(tpool_t *tp, tfunc_t fn, void *arg, tfree_func_t free)
+{
+ struct _tjob *j = bufalloc(NULL, 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 */
+
+#endif /* !C_BUILD_SYSTEM_H */
diff --git a/make.c b/make.c
new file mode 100644
index 0000000..08c883d
--- /dev/null
+++ b/make.c
@@ -0,0 +1,147 @@
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "cbs.h"
+
+#define CC "cc"
+#define CFLAGS "-pipe", "-O3", "-march=native", "-mtune=native"
+#define DFLAGS "-DGRAB_DEBUG", "-g", "-ggdb3"
+#define WFLAGS "-Wall", "-Wextra", "-Werror", "-Wpedantic"
+#define PREFIX "/usr/local"
+
+#define streq(a, b) (!strcmp(a, b))
+#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 (0)
+
+static size_t _strlcat(char *, const char *, size_t);
+static char *_mkoutpath(const char **, size_t);
+#define mkoutpath(...) \
+ _mkoutpath((const char **)_vtoa(__VA_ARGS__), lengthof(_vtoa(__VA_ARGS__)))
+
+int
+main(int argc, char **argv)
+{
+ int opt;
+ bool debug;
+ cmd_t c = {0};
+
+ cbsinit(argc, argv);
+ rebuild();
+
+ while ((opt = getopt(argc, argv, "d")) != -1) {
+ switch (opt) {
+ case 'd':
+ debug = true;
+ break;
+ default:
+ fputs("Usage: make [-d]\n", stderr);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc > 0) {
+ if (streq(*argv, "clean")) {
+ cmdadd(&c, "rm", "-f", "grab");
+ cmdprc(c);
+ } else if (streq(*argv, "install")) {
+ char *bin, *man;
+ bin = mkoutpath("/bin");
+ man = mkoutpath("/share/man/man1");
+ cmdadd(&c, "mkdir", "-p", bin, man);
+ cmdprc(c);
+ cmdadd(&c, "cp", "grab", bin);
+ cmdprc(c);
+ cmdadd(&c, "cp", "grab.1", man);
+ cmdprc(c);
+ }
+ } else {
+ if (foutdated("grab", "grab.c")) {
+ cmdadd(&c, CC, WFLAGS);
+ if (debug)
+ cmdadd(&c, DFLAGS);
+ else
+ cmdadd(&c, CFLAGS);
+ cmdadd(&c, "-o", "grab", "grab.c");
+ cmdprc(c);
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
+
+char *
+_mkoutpath(const char **xs, size_t n)
+{
+#define trycat(s) \
+ do { \
+ if (_strlcat(buf, (s), PATH_MAX) >= PATH_MAX) \
+ goto toolong; \
+ } while (0)
+
+ char *p, *buf;
+
+ buf = bufalloc(NULL, PATH_MAX, sizeof(char));
+ buf[0] = 0;
+
+ if (p = getenv("DESTDIR"), p && *p)
+ trycat(p);
+
+ p = getenv("PREFIX");
+ trycat(p && *p ? p : PREFIX);
+
+ for (size_t i = 0; i < n; i++)
+ trycat(xs[i]);
+
+ return buf;
+
+toolong:
+ errno = ENAMETOOLONG;
+ die(__func__);
+}
+
+size_t
+_strlcat(char *dst, const char *src, size_t size)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = size;
+ size_t dlen;
+
+ while (n-- != 0 && *d != '\0')
+ d++;
+
+ dlen = d - dst;
+ n = size - dlen;
+
+ if (n == 0) {
+ while (*s++)
+ ;
+ return dlen + (s - src - 1);
+ }
+
+ while (*s != '\0') {
+ if (n != 1) {
+ *d++ = *s;
+ n--;
+ }
+ s++;
+ }
+
+ *d = '\0';
+
+ return dlen + (s - src);
+}