From a69fe73d6bcb84b4d3cdbaf8b9207be948cce103 Mon Sep 17 00:00:00 2001 From: Trent Huber Date: Fri, 11 Apr 2025 05:09:30 -0400 Subject: [PATCH] Major refactoring, bug fixes, improved extend() --- README.md | 67 +++++++++---------- cbs.c | 195 +++++++++++++++++++++++++++++------------------------- 2 files changed, 136 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 82d93db..89c5a26 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,13 @@ Many modern programming languages integrate some form of a build system into their own runtimes, allowing developers to use the same language to write their applications and to build them. This library hopes to bring that functionality to the C language. -cbs is a build system for C projects and is itself written in C. This gives the added bonus of only needing a C compiler to build and compile C projects. +cbs is a build system for C projects and is itself written in C. ## Overview -Build "scripts" are written in files called `build.c`. Here is an example of the contents of such a file: +Build "scripts" are written in files called `build.c`. Here is a minimal example of the contents of such a file: ```c -#define CFLAGS "" -#define LFLAGS "" - #include "cbs.c" int main(void) { @@ -24,7 +21,7 @@ int main(void) { } ``` -To build your project, you first need to compile and run `build.c`. The inclusion of the `build(NULL);` statement at the top of the build file will recompile `build.c` every subsequent time you run it (see the [Recursive builds](#recursive-builds) section for details). This allows you to make changes to the build file without having to manually recompile. +To build your project, you first need to manually compile `build.c` and run the resulting executable, called `build`: ```console $ cc -o build build.c @@ -35,50 +32,51 @@ $ ./main Hello, world! ``` -## Detailed usage - -The advantage of making a build system that only compiles C projects is that it can be made dead simple. C code just needs to be compiled and linked, and perhaps it would be nice to recurse the build into subdirectories. cbs uses three simple functions accordingly: [`compile()`](#compiling) for compiling, [`load()`](#linking) for linking, and [`build()`](#recursive-builds) for recursion. - -### Preprocessing +The inclusion of the `build(NULL);` statement at the top of the build file means that when you modify the build file after compiling the build executable, you don't need to recompile the build file again as the old executable will recompile it for you and rerun the new build executable automatically. This means **you only need to manually compile `build.c` once, even if you modify it later**: -Before you include `cbs.c` it is important you define the `CFLAGS` and `LFLAGS` macros. `CFLAGS` gets passed to the compiler and `LFLAGS` gets passed to the linker. If you have no use for one or both of the flags, then they must be left as empty strings. Any flags you want to pass to the compiler or linker must be written as a comma separated list of C strings with no trailing comma, for example: - -```c -#define CFLAGS "-Ifoo/", "-Ibar/", "-Dbaz" -#define LFLAGS "-lm", "-Llib/" +```console +$ touch main.c build.c +$ ./build +cc -o build build.c +./build +cc -c -o main.o main.c +cc -o main main.o ``` -The `CFLAGS` and `LFLAGS` macros will automatically be used in all `compile()` and `load()` calls respectively. +## Detailed usage + +The advantage of making a build system that only compiles C projects is that it can be made dead simple. C code just needs to be compiled and linked, and perhaps it would be nice to recurse the build into subdirectories. cbs uses three simple functions accordingly: `compile()` for compiling, `load()` for linking, and `build()` for recursion. ### Compiling ```c -void compile(char *name, ...); +void compile(char *src, ...); ``` -The `compile()` function is given a single source file to compile. File extensions are redundant and thus implicit in cbs, so don't use them. cbs will generate an object file of the same name and in the same directory as the source file you give it (notice this gives us the ability to reuse the list of source file names as object file names for the linker).[^1] +The `compile()` function is given a single source file to compile. cbs will generate an object file of the same name as the source file and in the same directory. The general philosophy cbs takes on file extensions is to only use them when using *non-typical* file extensions, as **typical file extensions are always implicit**. In the case of compiling, source files have the typical extension `.c` and object files have `.o`. This has the convenient side effect of letting us reuse the list of source file names as object file names for the linker.[^1] -[^1]: Notice if we put the list of comma separated C strings in a macro, then we can pass it both to function calls and to array initialization. Using them in a null terminated array allows us to iterate through the array for names of source files to compile, while using the macro in a function call allows us to link all the object files generated by the compilation. This is the elegance of dropping the file extensions. +[^1]: One example is putting a list of comma-separated C strings in a macro. Then we can pass it both to function calls as arguments and to array initialization as elements. Using the macro in a null-terminated array allows us to iterate through the array and use them as the names of source files to compile, while using the macro in a function call allows us to link all the object files generated by the compilation. `compile()` will only run if it finds the source file has been modified since the last time it compiled the resulting object file. This is similar to the caching behavior in most other build systems. -If the object file also depends on other header files and you wish to trigger recompilation should the headers change, you can add their names (again, no extensions) after the name of the source file. +If the source file uses project header files and you wish to trigger recompilation should they be modified, you can add their names after the name of the source file. The typical file extension assumed for these arguments is `.h`. In all cases, whether or not these additional arguments are included, **the arguments passed to `compile()` must be terminated with a null pointer**. + +To set flags to be passed to the compiler, the predefined `cflags` variable is used by setting it equal to an array of C strings which is terminated with a null pointer. Unless reinitialized, the same flags will be used in all subsequent `compile()` calls. `cflags` can be set to a null pointer when no flags are needed. An example of compiling `main.c` which depends on `foo.h` and `bar.h`. This function call will produce the file `main.o`: ```c +cflags = (char *[]){"-Wall", "-O3", NULL}; compile("main", "foo", "bar", NULL); ``` -It should be noted too that due to variatics in C, `compile()` requires a terminating null pointer in all cases. - ### Linking ```c -void load(char type, char *output, char *input, ...); +void load(char type, char *target, char *obj, ...); ``` -The first argument to `load()`[^2] is the type of file to link. The options are: +The first argument to `load()`[^2] is the type of the target file. The options are: [^2]: Although the term "linking" is far more common to use nowadays, the original term when UNIX was first created was "loading," so I use it here to name the function that does the linking. Also the name "link" is already taken on UNIX based systems. @@ -88,29 +86,28 @@ The first argument to `load()`[^2] is the type of file to link. The options are: 'd' - dynamically linked library ``` -The second argument is the name of the output. As mentioned above, the system will automatically add the correct file extension so there's no need to provide one. It is also common to prepend `lib` to files that are static or dynamic libraries; this is similarly done automatically, so you don't need to include it when naming the libraries (this is even considered when the name of the library is prepended by a path). This allows you to use the same name to refer to the library as you would when passing it to the linker's `-l` flag. +The second argument is the name of the target. There is no assumed typical file extension for the target as executables commonly lack them. It is also common to prepend `lib` to files that are static or dynamic libraries; this is done automatically and in a nature similar to typical file extensions. This allows you to use the same name to refer to a statically linked library in the same way you would when refering to a dynamically linked library in the linker flags. + +The rest of the arguments are the names of the files you want to link together. The typical file extension for these files is `.o`. Generally the only other files that would use a different file extension would be statically linked libraries (note that dynamically linked libraries aren't really linked in the same way as object files nor statically linked libraries and should thus be passed as linker flags instead). As is the case with compiling, **the arguments passed to `load()` must be terminated with a null pointer**. -The rest of the arguments are the names of the files you want to link together (at least one is required). All files are assumed to be object files (static and dynamic libraries are linked through the linker's aforementioned `-l` flag). +In a similar way as compiling, the predefined `lflags` variable can be used to define flags for the linker. -An example of linking `a.o`, `libB.a`, and `c.o` into a static library, `libmain.a`: +An example of linking `liba.dylib`, `b.o`, and `libc.a` as a statically linked library `libmain.a`: ```c -#define LFLAGS "-lB" -... -load('s', "main", "a", "c", NULL); +lflags = (char *[]){"-la", NULL}; +load('s', "main", "b", "c.a", NULL); ``` -For the same reason as `compile()`, the `load()` function requires a null pointer as the last argument. - ### Recursive builds ```c void build(char *path); ``` -It is often adventagous to compartmentalize projects into a number of subdirectories both for organizational purposes and for rebuilding parts at a time without rebuilding the whole thing. The usual way this is done is by placing build scripts in any subdirectory you want to rebuild on its own. But when it comes time for the overall build, we need a way to have the main build script call all the other ones (among other things). This is what the `build()` function is used for. +It is often adventagous to compartmentalize projects into a number of subdirectories both for organizational purposes and for rebuilding parts at a time without needing to rebuild the whole thing. The usual way this is done is by placing build scripts in any subdirectory you want to rebuild on its own. These scripts double as both being able to be ran by the programmer from the shell as well as being able to be run by the build system itself from some parent directory. The `build()` function performs the latter function of the build system. -`build()` gets passed the name of the subdirectory you want to build. The path can be either absolute or relative to the directory the current build executable is being run in. This directory you pass it must have a `build.c` file in it which will be compiled (recompiled if needed) and ran. If `NULL` is passed to build, this has the effect of not switching directories and just recompiling (if necessary) and rerunning (if necessary) the build executable in the current directory. This is why we include `build(NULL);` at the start of build files we want to automatically recompile. +`build()` gets passed the name of the subdirectory you want to build, either as an absolute or relative path. Another philosophy taken by cbs is that **relative paths in a build file are always assumed to be with respect to the directory that that build file is in**. The directory you pass `build()` must have a `build.c` file in it which will be compiled and run (of course being recompiled and rerun if needed). If `path` is a null pointer, this has the effect of staying in the current directory and (again, if necessary) recompiling the build file and rerunning the build executable therein. In other words, in addition to orchestrating recursive builds, this function is also used to automate build executables rebuilding themselves and is thus why we generally include the `build(NULL);` statement at the start of build files. An example of building the contents of the directories `abc/src/` and `/usr/local/proj/src/`: diff --git a/cbs.c b/cbs.c index 6b44de6..9b3a7a5 100644 --- a/cbs.c +++ b/cbs.c @@ -8,143 +8,160 @@ extern char **environ; -void *alloc(int s) { - void *r; +char **cflags; +char **lflags; + +void error(char *fmt, ...) { + va_list args; - if ((r = malloc(s))) return r; + fprintf(stderr, "cbs: "); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + if (errno) fprintf(stderr, ": %s", strerror(errno)); + fprintf(stderr, "\n"); - perror(NULL); exit(EXIT_FAILURE); } -char *extend(char *path, char *ext, int lib) { - char *bp, *rp, *tp; - int d, l, b, e; +void *alloc(size_t size) { + void *r; - if (path == NULL) return NULL; + if (!(r = malloc(size))) error("Memory allocation"); + + return r; +} - bp = rindex(path, '/'); - bp = bp ? bp + 1 : path; +char *extend(char *path, char *ext) { + char *bp, *ep, *rp, *tp; + int d, b, e, l; + + if (!path) return NULL; + + bp = (bp = rindex(path, '/')) ? bp + 1 : path; d = bp - path; - l = lib ? 3 : 0; - b = strlen(bp); - e = strlen(ext); + b = (ep = rindex(bp, '.')) ? ep - bp : (ep = ext, strlen(bp)); + if (*ext == '!') ep = ext + 1; + e = strlen(ep); + l = strncmp(ep, ".a", e) == 0 || strncmp(ep, ".dylib", e) == 0 ? 3 : 0; tp = rp = alloc(d + l + b + e + 1); tp = stpncpy(tp, path, d); tp = stpncpy(tp, "lib", l); tp = stpncpy(tp, bp, b); - stpncpy(tp, ext, e + 1); + stpncpy(tp, ep, e + 1); return rp; } -int modified(char *tpath, char *dpath) { - struct stat dstat, tstat; +int modified(char *target, char *dep) { + struct stat tstat, dstat; - if (stat(dpath, &dstat) == -1) { - fprintf(stderr, "Unable to stat `%s': %s\n", dpath, strerror(errno)); - exit(EXIT_FAILURE); - } - errno = 0; - if (stat(tpath, &tstat) == -1 && errno != ENOENT) { - fprintf(stderr, "Unable to stat `%s': %s\n", tpath, strerror(errno)); - exit(EXIT_FAILURE); - } - return errno == ENOENT || dstat.st_mtime > tstat.st_mtime; + if (stat(target, &tstat) == -1 && errno != ENOENT) + error("Unable to stat `%s'", target); + if (stat(dep, &dstat) == -1) error("Unable to stat `%s'", dep); + + return errno == ENOENT || tstat.st_mtime < dstat.st_mtime; } void run(char *path, char **args, char *what, char *who) { int i; - for (i = 0; args[i]; ++i) if (*args[i] != '\0') printf("%s ", args[i]); + for (i = 0; args[i]; ++i) printf("%s ", args[i]); printf("\n"); - if (execve(path, args, environ) == -1) { - fprintf(stderr, "Unable to run the %s of `%s': %s\n", - what, who, strerror(errno)); - exit(EXIT_FAILURE); - } + if (execve(path, args, environ) == -1) + error("Unable to run %s of `%s'", what, who); } void await(int cpid, char *what, char *who) { int status; - if (cpid == -1) { - fprintf(stderr, "Unable to delegate the %s of `%s': %s\n", - what, who, strerror(errno)); + if (cpid == -1) error("Unable to delegate the %s of `%s'", what, who); + if (waitpid(cpid, &status, 0) == -1) + error("Unable to await the %s of `%s'", what, who); + if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS) exit(EXIT_FAILURE); - } - if (waitpid(cpid, &status, 0) == -1) { - fprintf(stderr, "Unable to await the %s of %s: %s\n", - what, who, strerror(errno)); - exit(EXIT_FAILURE); - } - if (WIFEXITED(status) && WEXITSTATUS(status) != 0) exit(EXIT_FAILURE); } -void compile(char *name, ...) { - char *src, *dep, *obj; +void compile(char *src, ...) { + int fn, cpid; + char **args, **p, *obj, *dep; va_list deps; - int cpid; - dep = src = extend(name, ".c", 0); - obj = extend(name, ".o", 0); + fn = 0; + if (cflags) while (cflags[fn]) ++fn; + p = args = alloc((5 + fn + 1) * sizeof*args); - va_start(deps, name); + *p++ = "cc"; + if (cflags) for (fn = 0; cflags[fn]; *p++ = cflags[fn++]); + *p++ = "-c"; + *p++ = "-o"; + *p++ = obj = extend(src, "!.o"); + *p++ = src = extend(src, ".c"); + *p = NULL; + + dep = strdup(src); + va_start(deps, src); do if (modified(obj, dep)) { - if ((cpid = fork()) == 0) - run("/usr/bin/cc", (char *[]){"cc", CFLAGS, "-c", "-o", obj, src, NULL}, - "compilation", src); + if ((cpid = fork()) == 0) run("/usr/bin/cc", args, "compilation", src); await(cpid, "compilation", src); break; - } while ((dep = extend(va_arg(deps, char *), ".h", 0))); + } while (free(dep), dep = extend(va_arg(deps, char *), ".h")); va_end(deps); + + free(src); + free(obj); + free(args); } -void load(char type, char *output, char *input, ...) { - char **fargs, **args; - int fn, vn, i, cpid; - va_list count, inputs; +void load(char type, char *target, char *obj, ...) { + int fn, vn, cpid; + va_list count, objs; + char **args, **p, *path, **o; - input = extend(input, ".o", 0); + fn = 0; + if (lflags) while (lflags[fn]) ++fn; + va_start(count, obj); + va_copy(objs, count); + for (vn = 0; va_arg(count, char *); ++vn); + va_end(count); + p = args = alloc((5 + fn + vn + 1) * sizeof*args); + + path = "/usr/bin/cc"; + *p++ = "cc"; switch (type) { + case 'd': + *p++ = "-dynamiclib"; case 'x': - fargs = (char *[]){"cc", LFLAGS, "-o", output, input, NULL}; + if (lflags) for (fn = 0; lflags[fn]; *p++ = lflags[fn++]); + *p++ = "-o"; + *p++ = target = type == 'd' ? extend(target, ".dylib") : strdup(target); break; case 's': - output = extend(output, ".a", 1); - fargs = (char *[]){"ar", "-r", output, input, NULL}; - break; - case 'd': - output = extend(output, ".dylib", 1); - fargs = (char *[]){"cc", "-dynamiclib", LFLAGS, "-o", output, input, NULL}; + path = "/usr/bin/ar"; + *(p - 1) = "ar"; + *p++ = "-r"; + *p++ = target = extend(target, ".a"); break; default: - fprintf(stderr, "Unknown load type `%c'\n", type); - exit(EXIT_FAILURE); + error("Unknown linking type `%c'", type); } - for (fn = 0; fargs[fn]; ++fn); - - va_start(count, input); - va_copy(inputs, count); - for (vn = 0; va_arg(count, char *); ++vn); - va_end(count); - - args = alloc((fn + vn + 1) * sizeof(char *)); - memcpy(args, fargs, fn * sizeof(char *)); - for (i = fn; i < fn + vn; ++i) - args[i] = extend(va_arg(inputs, char *), ".o", 0); - args[fn + vn] = NULL; - va_end(inputs); - - for (i = fn - 1; i < fn + vn; ++i) if (modified(output, args[i])) { - if ((cpid = fork()) == 0) - run(type == 's' ? "/usr/bin/ar" : "/usr/bin/cc", - args, "loading", output); - await(cpid, "loading", output); + o = p; + *o++ = extend(obj, ".o"); + while ((*o++ = extend(va_arg(objs, char *), ".o"))); + va_end(objs); + + o = p; + while (*o) if (modified(target, *o++)) { + if ((cpid = fork()) == 0) run(path, args, "linking", target); + await(cpid, "linking", target); break; } + + while (*p) free(*p++); + free(target); + free(args); } void build(char *path) { @@ -157,11 +174,7 @@ void build(char *path) { return; } printf("cd %s\n", path); - if (chdir(path)) { - fprintf(stderr, "Unable to make `%s' the current working directory: %s\n", - path, strerror(errno)); - exit(EXIT_FAILURE); - } + if (chdir(path)) error("Unable to set working directory to `%s'", path); } if (modified("build", "build.c")) { @@ -171,5 +184,5 @@ void build(char *path) { await(cpid, "compilation", "build.c"); } else if (!path) return; - run("build", (char *[]){"build", NULL}, "execution", "build"); + run("build", (char *[]){"./build", NULL}, "execution", "build"); } -- 2.51.0