CBS — The C Build System
CBS is a single-header library for writing build scripts in C. The philosophy behind this project is that the only tool you should ever need to build your C projects is a C compiler. Not Make, not Cmake, not Autoconf — just a C compiler.
Using C for your build system also has numerous advantages. C is portable to almost any platform, C is a turing-complete language that makes performing very specific build steps easy, and anyone working on your C project already knows C.
CBS does not aim to be the most powerful and ultimate high-level API. It simply aims to be a set of potentially useful functions and macros to make writing a build script in C a bit easier. If there is functionality you are missing, then add it. You’re a programmer aren’t you?
All functions and macros are documented in this file.
CBS is very much inspired by Tsoding’s ‘Nob’.
Features
- C99 and POSIX compliant
- Capturing of command output
- Easy command building and execution
- PkgConfig support
- Simple and easy to understand API
- Thread pool support
Important
This library works with the assumption that your compiled build script and your build script source code are located in the same directory. The general project structure of your project is intended to look like so:
.
├── cbs.h # This library
├── make # The compiled build script
├── make.c # The build script source
└── … # Your own files
Example
Assuming you have a source file my-file.c
— you can compile this build
script (called build.c
for example) with cc build.c
to bootstrap —
and then run ./a.out
to build the my-file
binary linked against the
liblux library.
If you make any modifications to the build script or to the cbs.h header, there is no need to manually recompile — the script will rebuild itself.
#include <stdlib.h>
#define CBS_NO_THREADS
#include "cbs.h"
static char *cflags[] = {"-Wall", "-Wextra", "-Werror", "-O3"};
int
main(int argc, char **argv)
{
/* Initialize the library, and rebuild this script if needed */
cbsinit(argc, argv);
rebuild();
/* If the compiled binary isn’t outdated, do nothing */
if (fmdnewer("my-file", "my-file.c"))
return EXIT_SUCCESS;
/* Append ‘cc’ and our cflags to the command, but allow the user to use the
$CC and $CFLAGS environment variables to override them */
struct strs cmd = {0};
strspushenvl(&cmd, "CC", "cc");
strspushenv(&cmd, "CFLAGS", cflags, lengthof(cflags));
/* Call pkg-config with the --libs and --cflags options for the library
‘liblux’, appending the result to our command. If it fails then we
fallback to using -llux */
if (!pcquery(&cmd, "liblux", PC_LIBS | PC_CFLAGS))
strspushl(&cmd, "-llux");
/* Push the final arguments to our command */
strspushl(&cmd, "-o", "my-file", "my-file.c");
/* Print our command to stdout, and execute it */
cmdput(cmd);
return cmdexec(cmd);
}
Example With Threads
This is like the previous example, but you should compile with -lpthread.
#include <stdlib.h>
#include "cbs.h"
static char *cflags[] = {"-Wall", "-Wextra", "-Werror", "-O3"};
static char *sources[] = {"foo.c", "bar.c", "baz.c"};
static void build(void *);
int
main(int argc, char **argv)
{
cbsinit(argc, argv);
rebuild();
if (!foutdated("my-file", sources, lengthof(sources)))
return EXIT_SUCCESS;
/* Get the number of CPUs available. If this fails we fallback to 8. */
int cpus = nproc();
if (cpus == -1)
cpus = 8;
/* Create a thread pool, with one thread per CPU */
tpool tp;
tpinit(&tp, cpus);
/* For each of our source files, add a task to the thread pool to build
the file ‘sources[i]’ with the function ‘build’ */
for (size_t i = 0; i < lengthof(sources); i++)
tpenq(&tp, build, sources[i], NULL);
/* Wait for all the tasks to complete and free the thread pool */
tpwait(&tp);
tpfree(&tp);
struct strs cmd = {0};
strspushenvl(&cmd, "CC", "cc");
strspushl(&cmd, "-o", "my-file");
for (size_t i = 0; i < lengthof(sources); i++)
strspushl(&cmd, swpext(sources[i], "o"));
cmdput(cmd);
return cmdexec(cmd);
}
void
build(void *arg)
{
/* This function will be called by the thread pool with ‘arg’ set to a
filename such as ‘foo.c’ */
struct strs cmd = {0};
strspushenvl(&cmd, "CC", "cc");
strspushenv(&cmd, "CFLAGS", cflags, lengthof(cflags));
/* Allocate a copy of the string ‘arg’, with the file extension replaced.
This will for example return ‘foo.o’ when given ‘foo.c’ */
char *object = swpext(arg, "o");
strspushl(&cmd, "-o", object, "-c", arg);
cmdput(cmd);
if (cmdexec(cmd) != EXIT_SUCCESS)
exit(EXIT_FAILURE);
free(object);
strsfree(&cmd);
}
Documentation
Macros
#define CBS_NO_THREADS
If this macro is defined before including cbs.h
, then support for
thread pools won’t be included meaning you don’t need to link with
-lpthread
when bootstrapping the build script.
#define lengthof(xs) /* … */
Return the number of elements in the static array xs
.
Startup Functions
These two functions should be called at the very beginning of your
main()
function in the order in which they are documented here for
everything to work properly.
void cbsinit(int argc, char **argv)
Should be the first function called in main()
and passed the same
parameters received from main()
. It initializes some internal data,
but it also changes the current working directory so that the running
process is in the same directory as the location of the process. For
example if your build script is called make
and you call it as
./build/make
, this function will change your working directory to
./build
.
#define rebuild() /* … */
Should be called right after cbsinit()
. This function-like macro
checks to see if the build script is outdated compared to its source
file. If it finds that the build script is outdated it rebuilds it
before executing the new build script.
String Array Types and Functions
The following types and functions all work on dynamically-allocated arrays of string, which make gradually composing a complete command that can be executed very simple.
struct strs {
char **buf;
size_t len, cap;
};
A type representing a dynamic array of strings. The len
and cap
fields hold the length and capacity of the string array respectively, and
the buf
field is the actual array itself. Despite being a sized array,
buf
is also guaranteed by all the functions that act on this structure
to always be null-terminated.
There is no initialization function for the strs
structure. To
initialize the structure simply zero-initialize it:
int
main(int argc, char **argv)
{
/* … */
struct strs cmd = {0};
strspush(&cmd, "cc");
/* … */
}
void strsfree(struct strs *xs)
Deallocates all memory associated with the string array xs
. Note that
this does not deallocate memory associated with the individual
elements in the string array — that must still be done manually.
This function also zeros xs
after freeing memory, so that the same
structure can be safely reused afterwards.
void strszero(struct strs *xs)
Zeros the string array xs
without deallocating any memory used by
the string array. This allows you to reuse the same structure for a
different purpose without needing to reallocate a fresh new array,
instead reusing the old one.
void strspush(struct strs *xs, char **ys, size_t n)
Append n
strings from the string array ys
to the end of xs
.
#define strspushl(xs, ...) /* … */
Append the strings specified by the provided variable-arguments to the
end of xs
.
void strspushenv(struct strs *xs, const char *ev, char **ys, size_t n)
Append the value of the environment variable ev
to the end of xs
. If
the provided environment variable doesn’t exist or has the value of the
empty string, then fallback to appending n
strings from ys
to the end
of xs
.
The value of the environment variable ev
will undergo sh
-style
word-splitting so that usages such as the following are legal:
$ CC="zig cc -target x86_64-linux-musl" ./make
NOTE: This function leaks memory!
#define strspushenvl(xs, ev, ...) /* … */
Append the value of the environment variable ev
to the end of xs
. If
the provided environment variable doesn’t exist or has the value of the
empty string, then fallback to appending the strings specified by the
provided variable-arguments to the end of xs
.
The value of the environment variable ev
will undergo sh
-style
word-splitting so that usages such as the following are legal:
$ CC="zig cc -target x86_64-linux-musl" ./make
NOTE: This macro leaks memory!
File Information Functions
The following functions are useful for performing common checks on files.
bool fexists(const char *s);
Returns true
if the file s
exists, and false
otherwise. If you
want to check if a certain binary is present on the host system, you
should use binexists()
instead.
int fmdcmp(const char *x, const char *y);
Returns a value greater than 0 if the file x
was modified more recently
than the file y
, a value lower than 0 if the file y
was modified more
recently than the file x
, and 0 if the two files were modified at the
exact same time.
bool fmdnewer(const char *x, const char *y);
bool fmdolder(const char *x, const char *y);
The fmdnewer()
and fmdolder()
functions return true
if the file x
was modified more- or less recently than the file y
respectively, and
false
otherwise.
bool foutdated(const char *x, char **xs, size_t n);
Returns true
if any of the n
files in the array xs
were modified
more recently than the file x
.
#define foutdatedl(x, ...) /* … */
Returns true
if any of the files specified by the variable-arguments
were modified more recently than the file x
.
Command Execution Functions
The following functions are used to execute commands. It is a common
task that you may want to interface with pkg-config
or change the
extension of a file while building up a command. Functions to perform
these tasks are not listed in this section, but instead listed later on
in this document.
int cmdexec(struct strs cmd);
Execute the command composed by the command-line arguments specified in
cmd
, wait for the command to complete execution, and return its exit
status.
int cmdexec_read(struct strs cmd, char **buf, size_t *bufsz);
Execute the command composed by the command-line arguments specified in
cmd
, wait for the command to complete execution, and return its exit
status. Additionally, the standard output of the command is captured and
stored in the buffer pointed to by buf
, with the length of the buffer
being stored in bufsz
.
Note that buf
will not be null-terminated, and must be freed by a call
to free()
after use.
pid_t cmdexec_async(struct strs cmd);
Execute the command composed by the command-line arguments specified in
cmd
asynchronously and return its process ID.
int cmdwait(pid_t pid);
Wait for the process specified by pid
to terminate and return its exit
status.
void cmdput(struct strs cmd);
void fcmdput(FILE *stream, struct strs cmd);
Print a representation of the command composed by the command-line
arguments specified in cmd
to the standard output, with shell
metacharacters properly quoted.
This function is useful for implementing make(1)
-like command-echoing
behaviour.
The fcmdput()
function is identical to cmdput()
except the output is
written to stream
as opposed to stdout
.
Thread Pool Types and Functions
The following types and functions are used for implementing thread pools.
This will be very helpful for speeding up build times. If you intend to
use thread pools you may want to see the documentation below for the
nproc()
function.
typedef void tjob(void *arg);
A type representing a function that takes a void pointer argument and performs some action.
typedef void tjob_free(void *arg);
A type representing a function that takes a void pointer argument a and frees it and its associated memory.
typedef /* ... */ tpool;
An opaque structure representing a thread pool. A variable of this type needs to be passed to all the thread pool functions.
void tpinit(tpool *tp, size_t cnt);
Initialize the thread pool tp
with cnt
threads. To use the number of
threads available on the system you should query the nproc()
function.
void tpfree(tpool *tp);
Free the resources used by the thread pool tp
, and join all remaining
threads.
void tpwait(tpool *tp);
Block until all tasks in the thread pool tp
have finished execution.
void tpenq(tpool *tp, tjob *job, void *arg, tjob_free *free);
Enqueue a new job job
to the thread pool tp
for execution. job
will be called with the argument arg
. If free
is non-NULL, it will
be called with the argument arg
after the job was completed.
Miscellaneous Functions
The following functions are all useful, but don’t quite fall into any of the specific function categories and namespaces documented above.
bool binexists(const char *s);
Return true
if a binary of the name s
is located anywhere in the
users $PATH
, and false
otherwise.
int nproc(void);
Return the number of available CPUs, or -1
on error.
char *swpext(const char *file, const char *ext);
Return a copy of the string file
with the file extension set to the
string ext
. The file extension is defined to be the contents following
the last occurance of a period in file
.
The returned string is allocated via malloc()
and should be freed by a
call to free()
after use.
enum pkg_config_flags {
PC_CFLAGS = /* --cflags */,
PC_LIBS = /* --libs */,
PC_SHARED = /* --shared */,
PC_STATIC = /* --static */,
};
bool pcquery(struct strs *cmd, const char *lib, int flags);
Query pkg-config
for the library lib
and append the output to the
command specified by cmd
, returning true
if successful and false
if
pkg-config
exited with a failing exit code.
flags
is a bitwise-ORd set of values in the pkg_config_flags
enumeration which control the flags passed to pkg-config
. The above
synopsis documents which enumeration values map to which command-line
flag.
It may be useful to append a default value to cmd
if pkg-config
fails
for whatever reason. As an example you may do the following when linking
to liburiparser:
struct strs cmd = {0};
strspushl(&cmd, "cc");
if (!pcquery(&cmd, "uriparser", PC_CFLAGS | PC_LIBS))
strspushl(&cmd, "-luriparser"); /* fallback */
strspushl(&cmd, "-o", "main", "main.c");
NOTE: This function leaks memory!