summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/cdeps.sh234
-rw-r--r--tools/lib.sh49
-rwxr-xr-xtools/makehelp.sh149
-rwxr-xr-xtools/manpages.sh126
4 files changed, 558 insertions, 0 deletions
diff --git a/tools/cdeps.sh b/tools/cdeps.sh
new file mode 100755
index 0000000..689c82d
--- /dev/null
+++ b/tools/cdeps.sh
@@ -0,0 +1,234 @@
+#!/bin/sh
+set -eu
+
+. tools/lib.sh
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ tools/cdeps.sh FILE...
+ tools/cdeps.sh -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ FILE toplevel entrypoint file
+
+
+ Given a list of C FILEs, generate the Makefile dependencies
+ between them.
+
+ We have 3 types of object files:
+ - .o: plain object files;
+ - .lo: object files compiled as relocatable so it can be
+ included in a shared library;
+ - .to: compiled with -DTEST so it can expose its embedded unit
+ tests;
+
+ We also have 1 aggregate file:
+ - .xa: an e**x**ecutable **a**rchive, made with ar(1), that
+ includes all the .o dependencies required for linking, so that
+ one can have only the archive as the linker input, i.e.
+ `cc -o example.bin example.xa`. This way we don't need
+ separate targets for each executable, and we can instead deal
+ separately with dependencies and linking, but without specific
+ file listing for each case. We use an ar-chive to exploit the
+ fact that a) it can replace existing files with the same name
+ and b) the $? macro in make(1) gives us all the out-of-date
+ dependencies, so our rule in the Makefile is usually a simple
+ `$(AR) $(ARFLAGS) $@ $?`. This way each .xa file lists its
+ dependency separately, and the building of the .xa file is
+ taken care of, in the same way that the linkage into an
+ executable is also taken care of. For running unit tests, we
+ include as a dependency of "$NAME.xa" the "$NAME.to" file,
+ which is the object code of "$NAME.c" compiled with the -DTEST
+ flag. This exposes a main() function for running unit tests.
+
+ Also in order to run the unit tests without having to relink
+ them on each run, we have:
+ - .bin-check: a dedicated virtual target that does nothing but
+ execute the tests. In order to assert the binaries exist,
+ each "$NAME.bin-check" virtual target depends on the
+ equivalent "$NAME.check" physical target.
+
+ There are 2 types of dependencies that are generated:
+ 1. self dependencies;
+ 2. inter dependencies.
+
+ The self dependencies are the ones across different
+ manifestations of the same file so all derived assets are
+ correctly kept up-to-date:
+ - $NAME.o $NAME.lo $NAME.to: $NAME.h
+
+ As the .SUFFIXES rule already covers the dependency to the
+ orinal $NAME.c file, all we do is say that whenever the public
+ interface of these binaries change, they need to be
+ recompiled;
+
+ - $NAME.xa: $NAME.to
+
+ We make sure to include in each executable archive (.xa) file
+ its own binary with unit tests. We include the "depN.o"
+ dependencies later;
+
+ - $NAME.bin-check: $NAME.bin
+
+ Enforce that the binary exists before we run them.
+
+ After we establish the self dependencies, we scrub each file's
+ content looking for `#include "..."` lines that denote
+ dependency to other C file. Once we do that we'll have:
+ - $NAME.o $NAME.lo $NAME.to: dep1.h dep2.h ... depN.h
+
+ We'll recompile our file when its public header changes. When
+ only the body of the code changes we don't recompile, only
+ later relink;
+
+ - $NAME.xa: dep1.o dep2.o ... depN.o
+
+ Make sure to include all required dependencies in the
+ $NAME.bin binary so that the later linking works properly.
+
+ So if we have file1.c, file2.c and file3.c with their respective
+ headers, where file2.c and file3.c depend of file1.c, i.e. they
+ have `#include "file.h"` in their code, and file3.c depend of
+ file2.c, the expected output is:
+
+ file1.o file1.lo file1.to: file1.h
+ file2.o file2.lo file2.to: file2.h
+ file3.o file3.lo file3.to: file3.h
+
+ file1.xa: file1.to
+ file2.xa: file2.to
+ file3.xa: file3.to
+
+ file1.bin-check: file1.bin
+ file2.bin-check: file2.bin
+ file3.bin-check: file3.bin
+
+
+ file1.o file1.lo file1.to:
+ file2.o file2.lo file2.to: file1.h
+ file3.o file3.lo file3.to: file1.h file2.h
+
+ file1.xa:
+ file2.xa: file1.o
+ file3.xa: file1.o file2.o
+
+ This ensures that only the minimal amount of files need to get
+ recompiled, but no less.
+
+
+ Examples:
+
+ Get deps for all files in 'src/' but 'src/main.c':
+
+ $ sh tools/cdeps.sh `find src/*.c -not -name 'main.c'`
+
+
+ Emit dependencies for all C files in a Git repository:
+
+ $ sh tools/cdeps.sh `git ls-files | grep '\.c$'`
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ (--)
+ break
+ ;;
+ (--help)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ (h)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+FILE="${1:-}"
+eval "$(assert_arg "$FILE" 'FILE')"
+
+
+
+each_f() {
+ fn="$1"
+ shift
+ for file in "$@"; do
+ f="${file%.c}"
+ "$fn" "$f"
+ done
+ printf '\n'
+}
+
+self_header_deps() {
+ printf '%s.o\t%s.lo\t%s.to:\t%s.h\n' "$1" "$1" "$1" "$1"
+}
+
+self_xa_deps() {
+ printf '%s.xa:\t%s.to\n' "$1" "$1"
+}
+
+self_bincheck_deps() {
+ printf '%s.bin-check:\t%s.bin\n' "$1" "$1"
+}
+
+deps_for() {
+ ext="$2"
+ for file in $(awk -F'"' '/^#include "/ { print $2 }' "$1.c"); do
+ if [ "$file" = 'config.h' ]; then
+ continue
+ fi
+ if [ "$(basename "$file")" = 'tests-lib.h' ]; then
+ continue
+ fi
+ f="$(dirname "$1")/$file"
+ if [ "$f" = "$1.h" ]; then
+ continue
+ fi
+ printf '%s\n' "${f%.h}$2"
+ done
+}
+
+rebuild_deps() {
+ printf '\n'
+ printf '%s.o\t%s.lo\t%s.to:' "$1" "$1" "$1"
+ printf ' %s' $(deps_for "$1" .h) | sed 's| *$||'
+}
+
+archive_deps() {
+ printf '\n'
+ printf '%s.xa:' "$1"
+ printf ' %s' $(deps_for "$1" .o) | sed 's| *$||'
+}
+
+
+each_f self_header_deps "$@"
+each_f self_xa_deps "$@"
+each_f self_bincheck_deps "$@"
+
+each_f rebuild_deps "$@"
+each_f archive_deps "$@"
diff --git a/tools/lib.sh b/tools/lib.sh
new file mode 100644
index 0000000..0412d7c
--- /dev/null
+++ b/tools/lib.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+assert_arg() {
+ if [ -z "$1" ]; then
+ printf 'Missing %s.\n\n' "$2" >&2
+ cat <<-'EOF'
+ usage >&2
+ exit 2
+ EOF
+ fi
+}
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+tmpname() {
+ echo "${TMPDIR:-/tmp}/uuid-tmpname with spaces.$(uuid)"
+}
+
+mkstemp() {
+ name="$(tmpname)"
+ touch "$name"
+ echo "$name"
+}
+
+mkdtemp() {
+ name="$(tmpname)"
+ mkdir "$name"
+ echo "$name"
+}
+
+END="\033[0m"
+yellow() {
+ YELLOW="\033[0;33m"
+ printf "${YELLOW}${1}${END}"
+}
+
+green() {
+ GREEN="\033[0;32m"
+ printf "${GREEN}${1}${END}"
+}
+
+red() {
+ RED="\033[0;31m"
+ printf "${RED}${1}${END}"
+}
diff --git a/tools/makehelp.sh b/tools/makehelp.sh
new file mode 100755
index 0000000..e6118de
--- /dev/null
+++ b/tools/makehelp.sh
@@ -0,0 +1,149 @@
+#!/bin/sh
+set -eu
+
+. tools/lib.sh
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ makehelp.sh < MAKEFILE
+ makehelp.sh -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Generate a help message from the given Makefile.
+
+ Any target or variable commented with two "#" characters gets
+ picked up. Multi-line comments are supported:
+
+ VAR1 = 1
+ # a comment
+ VAR2 = 2
+ ## another comment -> this one is included in the docs
+ VAR3 = 3
+
+ ## with a big
+ ## comment, which is also included
+ a-target:
+
+
+ Examples:
+
+ Generate help messages from "Makefile":
+
+ $ aux/makehelp.sh < Makefile
+
+
+ Generate help messages for all targets:
+
+ $ cat Makefile dev.mk | aux/makehelp.sh
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ (--)
+ break
+ ;;
+ (--help)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ (h)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+TARGETS="$(mkstemp)"
+VARIABLES="$(mkstemp)"
+trap 'rm -f "$TARGETS" "$VARIABLES"' EXIT
+
+awk -vCOLUMN=15 -vTARGETS="$TARGETS" -vVARIABLES="$VARIABLES" '
+function indent(n, where) {
+ for (INDENT = 0; INDENT < n; INDENT++) {
+ printf " " > where
+ }
+}
+
+/^## / { doc[len++] = substr($0, 4) }
+
+/^[-_a-zA-Z]+:/ && len {
+ printf "\033[36m%s\033[0m", substr($1, 1, length($1) - 1) > TARGETS
+ for (i = 0; i < len; i++) {
+ n = COLUMN - (i == 0 ? length($1) - 1 : 0)
+ indent(n, TARGETS)
+ printf "%s\n", doc[i] > TARGETS
+ }
+ len = 0
+}
+
+/^.++=/ && len {
+ printf "\033[36m%s\033[0m", $1 > VARIABLES
+ for (i = 0; i < len; i++) {
+ n = COLUMN - (i == 0 ? length($1) : 0)
+ indent(n, VARIABLES)
+ printf "%s\n", doc[i] > VARIABLES
+ }
+ len = 0
+}'
+
+
+
+indent() {
+ sed 's|^| |'
+}
+
+cat <<-EOF
+ Usage:
+
+ make [VARIABLE=value...] [target...]
+
+
+ Targets:
+
+ $(indent < "$TARGETS")
+
+
+ Variables:
+
+ $(indent < "$VARIABLES")
+
+
+ Examples:
+
+ Build "all", the default target:
+
+ $ make
+
+
+ Test and install, with \$(DESTDIR) set to "tmp/":
+
+ $ make DESTDIR=tmp check install
+EOF
diff --git a/tools/manpages.sh b/tools/manpages.sh
new file mode 100755
index 0000000..755ff77
--- /dev/null
+++ b/tools/manpages.sh
@@ -0,0 +1,126 @@
+#!/bin/sh
+set -eu
+
+. tools/lib.sh
+
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ sh doc/manpages.sh -i|-u -p MANDIR FILE...
+ sh doc/manpages.sh -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -i set ACTION=install
+ -u set ACTION=uninstall
+ -p MANDIR use $MANDIR as the prefix directory
+ -h, --help show this message
+
+
+ Installs/uninstalls manpage files into MANDIR using the original
+ filename as a hint towards which subdirectory to pick: the
+ `prog.ru.1` file is installed under the `man1/` directory
+ hierarchy under the `ru/` language folder. When then language
+ is `en`, a the toplevel `man1/` gets a symlink to the `en/`
+ language folder:
+
+ man/
+ en/
+ man1/
+ prog.1
+ ru/
+ man1/
+ prog.1
+ man1/
+ prog.1 -> ../en/man1/prog.1
+
+
+ Examples:
+
+ Install prog.en.1 under `/usr/man`:
+
+ $ sh doc/manpages.sh -ip/usr/man prog.en.1
+
+
+ Uninstall all files listed under doc/ from `$PREFIX`:
+
+ sh doc/manpages.sh -up "$PREFIX" doc/*.[0-9]
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ (--)
+ break
+ ;;
+ (--help)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ ;;
+ esac
+done
+
+while getopts 'iup:h' flag; do
+ case "$flag" in
+ (i)
+ ACTION=install
+ ;;
+ (u)
+ ACTION=uninstall
+ ;;
+ (p)
+ MANDIR="$OPTARG"
+ ;;
+ (h)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+eval "$(assert_arg "${ACTION:-}" '-[iu] for choosing action')"
+eval "$(assert_arg "${MANDIR:-}" '-p MANDIR')"
+eval "$(assert_arg "${1:-}" 'FILE...')"
+
+
+for f in "$@"; do
+ l="$(echo "$f" | awk -F. '{print $(NF-1)}')"
+ n="$(echo "$f" | awk -F. '{print $NF}')"
+ case "$ACTION" in
+ (install)
+ to_name="$(basename "${f%."$l"."$n"}.$n")"
+ mkdir -p "$MANDIR/$l/man$n" "$MANDIR/man$n"
+ cp "$f" "$MANDIR/$l/man$n/$to_name"
+ if [ "$l" = 'en' ]; then
+ ln -fs "../en/man$n/$to_name" \
+ "$MANDIR/man$n/$to_name"
+ fi
+ ;;
+ (uninstall)
+ to_name="$(basename "${f%."$l"."$n"}.$n")"
+ rm -f \
+ "$MANDIR/$l/man$n/$to_name" \
+ "$MANDIR/man$n/$to_name"
+ ;;
+ (*)
+ echo "Bad ACTION: $ACTION"
+ exit 1
+ ;;
+ esac
+done