]> Trent Huber's Code - cbs.git/commitdiff
Arrays instead of variadics, removal of incremental building
authorTrent Huber <trentmhuber@gmail.com>
Mon, 21 Jul 2025 03:42:55 +0000 (23:42 -0400)
committerTrent Huber <trentmhuber@gmail.com>
Mon, 21 Jul 2025 03:42:55 +0000 (23:42 -0400)
README.md
cbs.c

index 578b36f0cba75757fc74134dd44a54a1f6e84afa..9b167350adf3d0e201030cd10724cfe08ed718a9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,96 +6,84 @@ 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 a minimal example of the contents of such a file.
+Build files are always named `build.c`. Here is a minimal example of the contents of such a file.
 
 ```c
+// build.c
+
 #include "cbs.c"
 
 int main(void) {
-   build(NULL);
+    build(NULL);
 
-   compile("main", NULL);
-   load('x', "main", "main", NULL);
+    compile("main");
+    load('x', "main", (char *[]){"main", NULL});
 
-   return 0;
+    return 0;
 }
 ```
 
-To build your project, you first need to manually compile `build.c` and run the resulting executable, called `build`.
+To build your project, you first need to manually compile your build file and run the resulting executable, called `build`.
 
 ```console
 $ cc -o build build.c
 $ ./build
-cc -c -o main.o main.c                                                                                                                   
+cc -c -o build.o build.c
+cc -o build build.o
+build
+cc -c -o main.o main.c
 cc -o main main.o
 $ ./main
 Hello, world!
 ```
 
-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**.
+The inclusion of the `build(NULL);` statement at the top of the build file will cause the build executable to *recompile its own source code* anytime that source code gets modified. This means **you only need to manually compile `build.c` once, even if you later modify it**.
 
 ```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
+$ touch build.c
+$ ./build
+cc -c -o build.o build.c
+cc -o build build.o
+build
 cc -c -o main.o main.c
 cc -o main main.o
 ```
 
-## Detailed usage
-
-The advantage of making a library that only builds C projects is that it can be made dead simple. The build system itself just needs a way of executing build files recursively, and each build file just needs to compile and link C code. Three simple functions have been defined accordingly: `build()` for recursion, `compile()` for compiling, and `load()` for linking.
-
-### Recurring build files
-
-```c
-void build(char *path);
-```
-
-It is often advantageous 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 can be run by the programmer from a shell or run by the build system itself from some other directory. The `build()` function is what performs the latter functionality.
-
-Build files must be named `build.c`, and the resulting build executable will always be named `build`. Each build file is in charge of building the contents of its resident directory. `build()` gets passed the name of the subdirectory you want to build, either as an absolute or relative path. It should be noted that **all relative paths in a build file are always assumed to be with respect to the directory that that build file is in**, not the root directory of the project. If `path` is a null pointer, this has the effect of staying in the current directory and thus (if the build file was modified) recompiling its own build file and rerunning itself. In essence, the `build()` function is what allows the system to be truly automated.
-
-An example of building the contents of the directories `abc/src/` and `/usr/local/proj/src/`.
+Note too that cbs rebuilt `main.c` even though it wasn't modified. This is because **cbs is not an incremental build system**. The bugs these kinds of systems produce can often waste more time for the programmer than they would otherwise save. Besides, cbs is intended for smaller projects where incremental builds really wouldn't save that much time anyway.
 
-```c
-build("abc/src/");
-build("/usr/local/proj/src/");
-```
+## Detailed usage
+The advantage of cbs is its simplicity, which in turn comes from its intentionally limited scope of building C projects. cbs just needs a way of compiling and linking C code and perhaps a way of recursing into project subdirectories where the build process can be repeated.
 
 ### Compiling source files
 
 ```c
-void compile(char *src, ...);
+void compile(char *src);
 ```
 
-The `compile()` function is given a single source file to compile. It will only run if it finds the source file has been modified since the last time it compiled it. This is similar to the caching behavior in most other build systems. When an object file is generated, it will have the same base name as its source file and be in the same directory. The philosophy one should take on file extensions is to only use them when using *non-typical* file extensions, as **typical file extensions are always implicit**. What counts as a "typical" file extension depends on the situation. In the case of compiling, source files have the typical extension `.c` and object files have `.o`. As it turns out dropping the typical file extensions has its advantages in writing concise code.[^1]
-
-[^1]: One example is putting a list of source file names without the `.c` extension in a C macro. We can use it to initialize an array that we iterate through, passing its elements as arguments to `compile()`, but we can also pass the entire macro as arguments to `load()` to link all the object files we just generated, ensuring we don't ever miss one.
-
-If the source files you're compiling use project header files themselves and you wish to trigger recompilation should the header files be modified, you can pass them as additional arguments after the source file. The typical file extension assumed for these arguments is `.h`.
+The `compile()` function is given a single source file to compile. The object file it generates will have the same base name and will be in the same directory as the source file. In general, file extensions are unnecessary for cbs as they can usually be inferred based on the function being called. This has the added benefit that often data structures containing build file names can be reused for compilation and linking steps.
 
-In all cases, whether or not you pass additional arguments, **the arguments passed to `compile()` must be terminated with a null pointer**.
+To pass flags to the compiler, the predefined `cflags` variable is set equal to a NULL-terminated array of C strings, each string being a compiler flag. Until reinitialized, the same flags will be used in all subsequent `compile()` calls. Setting `cflags` to NULL will prevent any flags from being used, which is its default value.
 
-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 also terminated with a null pointer. Unless reinitialized, the same flags will be used in all subsequent `compile()` calls. If no flags are needed, `cflags` can be set to a null pointer.
-
-An example of compiling `main.c` which depends on `foo.h` and `bar.h`. This function call will produce the file `main.o`.
+An example of using `cflags`:
 
 ```c
 cflags = (char *[]){"-Wall", "-O3", NULL};
-compile("main", "foo", "bar", NULL);
+compile("main");
 ```
 
+> [!NOTE]
+> It is not guaranteed that the object code cbs produces will be position-independent. When compiling source files that will be used in a dynamic library, you will need to include the `-fPIC` flag in `cflags` to ensure compatibility between platforms.
+
 ### Linking object files
 
 ```c
-void load(char type, char *target, char *obj, ...);
+void load(char type, char *target, char **objs);
 ```
 
-The first argument to `load()`[^2] is the type of the target file. The types are as follows.
-
-[^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.
+The first argument to `load()` is the type of the target file. The types are as follows.
 
 ```
 'x' - executable
@@ -103,19 +91,31 @@ The first argument to `load()`[^2] is the type of the target file. The types are
 'd' - dynamically linked library
 ```
 
-The second argument is the file 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 by `load()` if necessary. The idea is to replicate the manner in which you would typically pass system libraries to the linker flag `-l`.
+The second argument is the file name of the target. A file extension is optional as it'll be automatically appended if it's not present based on the target type. It is also common to prepend `lib` to libraries; this is done automatically as well in the case it's not present. This behavior is intended to replicate how you would typically specify system libraries to the linker flag `-l`.
+
+The third and final argument is a NULL-terminated array of C strings, each string being the name of an object file or library to link together to create the target. File extensions for libraries are required here, as otherwise the system will assume it's the name of an object file.
+
+The `lflags` variable works exactly like the `cflags` variable but with respect to the linker. An example using `lflags`:
+
+```c
+lflags = (char *[]){"-lm", NULL};
+load('s', "main", (char *[]){"a" DYEXT, "b", "c.a", NULL});
+```
+The `DYEXT` macro is defined with the platform specific file extension for dynamic libraries to aid portability of build files.
 
-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` since we generally link object files. The only other files that would use a different file extension would be statically linked libraries or dynamically linked libraries[^3] (the linker flag `-l` should be used to link system libraries as opposed to project libraries). The `DYEXT` macro has been defined which represents the platform-dependent file extension for dynamic libraries: `".dylib"` for macOS and `".so"` for anything else. This helps ease the portability of build files.
+### Building subdirectories
 
-[^3]: The `-fPIC` flag is not included by default in `cflags`, so make sure to do so manually when you're compiling the object files that will be used to create a dynamic library.
+```c
+void build(char *path);
+```
 
-As is the case with `compile()`, **the arguments passed to `load()` must be terminated with a null pointer**.
+It is often advantageous to compartmentalize projects into a number of subdirectories both for organizational purposes and for rebuilding parts at a time without needing to rebuild the entire project. The usual way this is done is by placing build scripts in any subdirectory you want to be able to rebuild on its own. These scripts can be run by the programmer from a shell *or* run by the build system itself from a higher-up directory. The latter functionality is performed by `build()`.
 
-The predefined `lflags` variable is used to pass flags to the linker, used in a manner similar to the way `cflags` is used for compiling.
+Every subdirectory you want to build should have its own build file in it which is responsible for producing the final product of that subdirectory. `build()` gets passed a C string which contains the name of the subdirectory to build, either as an absolute or relative path. It should be noted that **all relative paths in a build file are with respect to the directory that that build file itself is in**, not the root directory of the project. If `path` is NULL, this has the effect of staying in the current directory and thus recompiling its own build file and rerunning itself if the build file was modified since the last time it was compiled.
 
-An example of linking `liba.so` (or `liba.dylib` on macOS), `b.o`, and `libc.a`, as well as the system math library into the statically linked library `libmain.a`.
+An example using `build()`:
 
 ```c
-lflags = (char *[]){"-lm", NULL};
-load('s', "main", "a" DYEXT, "b", "c.a", NULL);
+build("abc/src/");
+build("/usr/local/proj/src/");
 ```
diff --git a/cbs.c b/cbs.c
index 36dd4d8db884228635c53d0ef82daf02229c2388..40f4143c084a75f253b0b86d965b1d78cad71899 100644 (file)
--- a/cbs.c
+++ b/cbs.c
@@ -1,9 +1,7 @@
 #include <err.h>
-#include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/errno.h>
 #include <sys/stat.h>
 #include <sys/wait.h>
 #include <unistd.h>
@@ -18,64 +16,6 @@ extern char **environ;
 
 char **cflags, **lflags;
 
-void await(pid_t cpid, char *what, char *who) {
-   int status;
-
-   if (cpid == -1 || waitpid(cpid, &status, 0) == -1)
-       err(EXIT_FAILURE, "Unable to %s `%s'", what, who);
-   if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS)
-       exit(EXIT_FAILURE);
-}
-
-int modified(char *target, char *dep) {
-   char *ext;
-   struct stat tstat, dstat;
-
-   if ((ext = strrchr(dep, '.')) && strcmp(ext, DYEXT) == 0) return 0;
-
-   if (stat(target, &tstat) == -1) {
-       if (errno == ENOENT) return 1;
-       err(EXIT_FAILURE, "Unable to stat `%s'", target);
-   }
-   if (stat(dep, &dstat) == -1) err(EXIT_FAILURE, "Unable to stat `%s'", dep);
-
-   return tstat.st_mtime < dstat.st_mtime;
-}
-
-void run(char *path, char **args, char *what, char *who) {
-   size_t i;
-
-   for (i = 0; args[i]; ++i) printf("%s ", args[i]);
-   putchar('\n');
-
-   if (execve(path, args, environ) == -1)
-       err(EXIT_FAILURE, "Unable to %s `%s'", what, who);
-}
-
-void build(char *path) {
-   pid_t cpid;
-
-   if (path) {
-       if ((cpid = fork())) {
-           await(cpid, "run", "build");
-           puts("cd ..");
-           return;
-       }
-       printf("cd %s\n", path);
-       if (chdir(path))
-           err(EXIT_FAILURE, "Unable to set working directory to `%s'", path);
-   }
-
-   if (modified("build", "build.c")) {
-       if ((cpid = fork()) == 0)
-           run("/usr/bin/cc", (char *[]){"cc", "-o", "build", "build.c", NULL},
-               "compile", "build.c");
-       await(cpid, "compile", "build.c");
-   } else if (!path) return;
-
-   run("build", (char *[]){"./build", NULL}, "run", "build");
-}
-
 void *allocate(size_t s) {
    void *r;
 
@@ -99,10 +39,11 @@ char *extend(char *path, char *ext) {
        if (!(dp = realpath(path, NULL)))
            err(EXIT_FAILURE, "Unable to get the absolute path of `%s'", path);
        free(path);
-       dp[(d = strlen(dp))] = '/';
-       ++d;
+       d = strlen(dp);
+       dp[d++] = '/';
    }
-   l = strcmp(e, ".a") == 0 || strcmp(e, DYEXT) == 0 ? 3 : 0;
+   l = (strcmp(e, ".a") == 0 || strcmp(e, DYEXT) == 0)
+       && (b <= 3 || strncmp(bp, "lib", 3) != 0) ? 3 : 0;
 
    r = allocate(d + l + b + strlen(e) + 1);
    strncat(r, dp, d);
@@ -115,46 +56,56 @@ char *extend(char *path, char *ext) {
    return r;
 }
 
-void compile(char *src, ...) {
+void run(char *file, char **args, char *what, char *who) {
+   size_t i;
+
+   for (i = 0; args[i]; ++i) printf("%s ", args[i]);
+   putchar('\n');
+
+   if (execve(file, args, environ) == -1)
+       err(EXIT_FAILURE, "Unable to %s `%s'", what, who);
+}
+
+void await(pid_t cpid, char *what, char *who) {
+   int status;
+
+   if (cpid == -1 || waitpid(cpid, &status, 0) == -1)
+       err(EXIT_FAILURE, "Unable to %s `%s'", what, who);
+   if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS)
+       exit(EXIT_FAILURE);
+}
+
+void compile(char *src) {
    size_t f;
-   char **args, **p, *obj, *hdr;
-   va_list hdrs;
+   char **args, **p;
    pid_t cpid;
 
-   if (f = 0, cflags) while (cflags[f]) ++f;
-   p = args = allocate((5 + f + 1) * sizeof*args);
+   f = 0;
+   if (cflags) while (cflags[f]) ++f;
+   p = args = allocate((2 + f + 3 + 1) * sizeof*args);
 
    *p++ = "cc";
    *p++ = "-c";
    if (cflags) for (f = 0; cflags[f]; *p++ = cflags[f++]);
    *p++ = "-o";
-   *p++ = obj = extend(src, "!.o");
-   *p++ = hdr = src = extend(src, ".c");
+   *p++ = extend(src, "!.o");
+   *p++ = src = extend(src, ".c");
 
-   va_start(hdrs, src);
-   do if (modified(obj, hdr = extend(hdr, ".h"))) {
-       if ((cpid = fork()) == 0) run("/usr/bin/cc", args, "compile", src);
-       await(cpid, "compile", src);
-       break;
-   } while (free(hdr), hdr = va_arg(hdrs, char *));
-   va_end(hdrs);
+   if ((cpid = fork()) == 0) run("/usr/bin/cc", args, "compile", src);
+   await(cpid, "compile", src);
 
    free(src);
-   free(obj);
    free(args);
 }
 
-void load(char type, char *target, char *obj, ...) {
-   va_list count, objs;
+void load(char type, char *target, char **objs) {
    size_t o, f;
    char **args, **p, **a, **fp, *path;
    pid_t cpid;
 
-   va_start(count, obj);
-   va_copy(objs, count);
-   for (o = 1; va_arg(count, char *); ++o);
-   va_end(count);
-   if (f = 0, lflags) while (lflags[f]) ++f;
+   f = o = 0;
+   if (objs) while (objs[o]) ++o;
+   if (lflags) while (lflags[f]) ++f;
    args = allocate((3 + o + 1 + f + 1) * sizeof*args);
    fp = (a = (p = args) + 3) + o;
 
@@ -173,25 +124,44 @@ void load(char type, char *target, char *obj, ...) {
        *p++ = "ar";
        *p++ = "-r";
        target = extend(target, ".a");
-       fp = p;
-       a = p += f;
+       a = p = (fp = p) + f;
        break;
    default:
        errx(EXIT_FAILURE, "Unknown target type `%c'", type);
    }
    *p++ = target;
-   do *p++ = extend(obj, ".o"); while ((obj = va_arg(objs, char *)));
-   va_end(objs);
-   if (lflags) for (f = 0; lflags[f]; *fp++ = lflags[f++]);
-   fp = p;
-
-   p -= o;
-   while (o--) if (modified(target, *p++)) {
-       if ((cpid = fork()) == 0) run(path, args, "link", target);
-       await(cpid, "link", target);
-       break;
-   }
+   f = o = 0;
+   if (objs) while (objs[o]) *p++ = extend(objs[o++], ".o");
+   if (lflags) while (lflags[f]) *fp++ = lflags[f++];
+
+   if ((cpid = fork()) == 0) run(path, args, "link", target);
+   await(cpid, "link", target);
 
-   while (a < fp) free(*a++);
+   while (a < p) free(*a++);
    free(args);
 }
+
+void build(char *path) {
+   pid_t cpid;
+   struct stat src, obj;
+
+   if (path) {
+       if ((cpid = fork())) {
+           await(cpid, "run", "build");
+           puts("cd ..");
+           return;
+       }
+       printf("cd %s\n", path);
+       if (chdir(path))
+           err(EXIT_FAILURE, "Unable to change directory to `%s'", path);
+   }
+
+   if (stat("build.c", &src) == -1)
+       err(EXIT_FAILURE, "Unable to stat `build.c'");
+   if (stat("build.o", &obj) == -1 && src.st_mtime > obj.st_mtime) {
+       compile("build");
+       load('x', "build", (char *[]){"build", NULL});
+   } else if (!path) return;
+
+   run("build", (char *[]){"build", NULL}, "run", "build");
+}