ok : a minimal build system in posix sh ok is a minimal build system that's kinda like make in that you plop a file describing your build in your project's folder, then run a system command that reads the file and builds the project. it's written in posix sh and is designed to be strictly more expressive than posix make, which is awful. [[ INSTALLING ]] ok is just a shell script, so you can mv or cp it to your $PATH. It requires a somewhat-modern posix-compliant shell (i am not going to figure out what "somewhat-modern" means), as well as the following utilities: - sed - grep - cut if you don't have these, congratulations! You are really doing something interesting with your system. [[ USING OK ]] If the project you're hacking on has an okfile (by default, ./ok), you can just run ok and it'll do whatever the author made the default target. You can also run ok with the following flags: - -h shows a help message, then exits - -q runs ok in "quiet" mode; that is, no messages are printed - -x runs ok with "set -x" turned on; this is a shell feature that might help you debug some weirdness - -n runs ok in "dry-run" mode; that is, no recipes are run - -B run all targets as if they were "new," like make -B - -f FILE use FILE as the okfile instead of ./ok - -C DIR change to DIR before doing anything, like make -C ok -h will also print the targets it's found in the okfile, along with explanatory comments provided on the target's first lines. See the example provided below for an idea. [[ WRITING OKFILES ]] of course, the real part of using ok is writing the okfile. This section describes the format of that file, which is a shell script sourced by ok, to help you write good okfiles for your own projects. an okfile is simply a shell script where every function defined is a target. The first function in the script is the default target that runs if no target is specified. ok has a few utility functions to make writing targets easier: - ok : print, then run the command given after it on the command line. if -q has been passed, suppress printing; if -n has been passed, suppress running. if the command fails, ok will exit with 100 + the command's exit code. - dep : define a dependency on the targets specified as arguments. the dependencies will be run in the order of the command line before continuing with the current recipe. - fr : define a series of file rules. See the next section for a description of these. - allfiles : print all the files defined in the fr call. [[ FILE RULES (FR) ]] while writing shell functions is fine and dandy to simulate make's .PHONY rules, it does nothing for the main use of make: generating files from other files. The solution i landed on for ok was the fr function, which is short for file rules. This section describes the format of fr's input, as well as suggestions for use. fr reads lines from standard input, processes them, and outputs them to an frfile in the current directory (which can be safely deleted, and is in fact rebuilt on every invocation). I recommend using a heredoc for building the file rules, since that allows free-form text as well as programmatic rule generation (something make does not do). each line fr reads will be parsed as a TARGET, a JOB, and the JOB's DEPS. The TARGET is the target file to build, JOB is the job to run to build that file, and DEPS are all the dependencies of TARGET. DEPS can include both build and order dependencies. Build dependencies are appended to JOB as arguments, while order dependencies merely force TARGET to be built if it's newer than them. TODO: THERE IS A BUG IN THIS IMPLEMENTATION. ORDER DEPENDENCIES ARE CURRENTLY IGNORED. JOB should be a function defined in the okfile or a one-word command, since fr doesn't do any special $IFS magic. I recommend creating an ok function that encapsulates JOB's logic and including that in the fr block. [[ FR EXAMPLE ]] since fr is possibly the most complex thing to understand for ok, here is an example. This okfile is how i build a simple 'hello, world' program in C[1]: ---------------------------------------------->8 for c in *.c do echo "${c%.c}" build "$c" done | fr : "${CC:=cc}" default() { dep allfiles; } build() { # build an executable from a c source file ok "$CC" "$1" -o "$TARGET" } 8<---------------------------------------------- as you can see, providing a heredoc to fr's input means i can build up the flies i want to build dynamically, rather than having to update the okfile every time. This fr invocation produces the following file[2]: ---------------------------------------------->8 hello ok build hello.c 8<---------------------------------------------- in this simple example, i could've written this just as well: ---------------------------------------------->8 fr<<.. hello build hello.c .. 8<---------------------------------------------- you can depend on all files that need building using the 'allfiles' target, so it's a good idea to put a 'dep allfiles' somewhere in your build functions. you can see it above in the 'default' rule. [[ COPYING AND CONTRIBUTING ]] ok is distributed under a standard BSD 3-clause license. Contributions are welcome; feel free to email me[3] with feedback, questions, or patches. [[ INSPIRATION ]] thanks to akkartik, whose basic-build[4] gave me the inspiration for ok. [1] https://www.helloworldin.com/c.html, e.g. [2] https://www.acdw.net/.fr [3] mailto:git@acdw.net [4] https://git.sr.ht/~akkartik/basic-build