about summary refs log tree commit diff stats

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