aboutsummaryrefslogtreecommitdiff

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!