about summary refs log tree commit diff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/redo116
-rwxr-xr-xbin/redo-always20
-rwxr-xr-xbin/redo-dot63
-rwxr-xr-xbin/redo-ifchange540
-rwxr-xr-xbin/redo-ifcreate35
-rwxr-xr-xbin/redo-ood30
-rwxr-xr-xbin/redo-sources27
-rwxr-xr-xbin/redo-stamp29
-rwxr-xr-xbin/redo-targets25
-rwxr-xr-xbin/redo-whichdo79
10 files changed, 964 insertions, 0 deletions
diff --git a/bin/redo b/bin/redo
new file mode 100755
index 0000000..0c76a18
--- /dev/null
+++ b/bin/redo
@@ -0,0 +1,116 @@
+#!/bin/sh -eu
+# redo – bourne shell implementation of DJB redo
+# Copyright © 2014-2021  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+: \
+ "${REDO_JOBS_MAX:=1}" \
+ "${REDO_MKDIR:=$(command -v mkdir)}" \
+ "${REDO_RM:=$(command -v rm) -f}" \
+ "${REDO_RMDIR:=$(command -v rmdir)}" \
+ "${REDO_TMP_DIR:=$(mktemp -d /tmp/redo.XXXXXXX)}" \
+
+export \
+ REDO_JOBS_MAX \
+ REDO_MKDIR \
+ REDO_RM \
+ REDO_RMDIR \
+ REDO_TMP_DIR \
+
+while [ $# != 0 ]; do
+ case "${1}" in
+  '-d'|'--debug')
+   export REDO_DEBUG='1'
+  ;;
+  '--debug-jobs')
+   export REDO_DEBUG_JOBS='1'
+  ;;
+  '--debug-locks')
+   export REDO_DEBUG_LOCKS='1'
+  ;;
+  '-h'|'--help')
+   printf >&2 \
+"Usage: redo [OPTIONS] [TARGETS...]
+
+  -d, --debug          print dependency checks as they happen
+      --debug-jobs     print messages about job management
+      --debug-locks    print messages about file locking
+  -h, --help           print usage instructions and exit
+  -j [n], --jobs [n]   execute at most [n] dofiles in parallel
+      --version        print version information and exit
+  -x, --xtrace         print commands as they are executed (variables expanded)
+
+Report bugs to <nils+redo@dieweltistgarnichtso.net>.
+"
+   exit 0
+  ;;
+  '-j'|'--jobs')
+   shift; REDO_JOBS_MAX=${1}
+  ;;
+  '--version')
+   printf >&2 \
+"redo 4.0.4
+Copyright © 2014-2021 Nils Dagsson Moskopp (erlehmann)
+
+License AGPLv3+: GNU Affero GPL version 3 or later <http://www.gnu.org/licenses/agpl-3.0.html>.
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
+"
+   exit 0
+  ;;
+  '-x'|'--xtrace')
+   export REDO_XTRACE='1'
+  ;;
+  *)
+   REDO_HAS_TARGETS='1'
+   export REDO_TARGET=''
+   # If this is build directory, create .redo database directory.
+   case "${REDO_BASE:-}" in
+    '')
+     export REDO_DEPTH=""
+     export REDO_PID="${PPID}"
+     export REDO_BASE="${PWD}"
+     export REDO_DIR="$REDO_BASE/.redo"
+     [ -d "$REDO_DIR" ] || LANG=C "${REDO_MKDIR}" -p "$REDO_DIR"
+   esac
+   break
+  ;;
+ esac
+ shift
+done
+
+[ "${REDO_HAS_TARGETS:-}" = "1" ] || exec redo all
+
+case "${REDO_JOBS_PIPE:-}" in
+ '')
+  export REDO_JOBS_PIPE="${REDO_TMP_DIR}"/jobs_pipe
+  mkfifo "${REDO_JOBS_PIPE}"
+  exec 9<> "${REDO_JOBS_PIPE}"
+  >&9 seq $(( REDO_JOBS_MAX - 1 )) &
+ ;;
+esac
+
+set +e
+redo-ifchange "$@"
+EXITCODE=${?}
+set -e
+
+wait
+
+case ${#REDO_DEPTH} in
+ 0)
+  LANG=C ${REDO_RM} "${REDO_JOBS_PIPE}"
+  LANG=C ${REDO_RMDIR} "${REDO_TMP_DIR}"
+ ;;
+esac
+
+exit "${EXITCODE}"
diff --git a/bin/redo-always b/bin/redo-always
new file mode 100755
index 0000000..f6b088a
--- /dev/null
+++ b/bin/redo-always
@@ -0,0 +1,20 @@
+#!/bin/sh
+# redo-always – bourne shell implementation of DJB redo
+# Copyright © 2014-2016  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+set -eu
+
+# The following code adds a dependency that is impossible to satisfy.
+LANG=C mkdir -p $(LANG=C dirname "$REDO_DIR/$REDO_TARGET".dependencies_ne.tmp)
+printf '%s\t%s\t%s\n' "/dev/null" "0" "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  -" >> \
+ "$REDO_DIR"/"$REDO_TARGET".dependencies.tmp
diff --git a/bin/redo-dot b/bin/redo-dot
new file mode 100755
index 0000000..5526f67
--- /dev/null
+++ b/bin/redo-dot
@@ -0,0 +1,63 @@
+#!/bin/sh
+# redo-dot – bourne shell implementation of DJB redo
+# Copyright © 2018  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+# Prints redo dependencies in dot(1) format. Usage is similar to this:
+# redo-dot |sed s%"$(pwd)"/%%g >deps.dot; dot deps.dot -Tpng >deps.png
+
+[ -d .redo ] || exit 1
+
+pattern=${1:-*}
+
+match() case "${1}" in
+ ${pattern}) : ;;
+ *) ! : ;;
+esac
+
+_escape() {
+ printf '%s' "$1" | sed 's/\\/\\\\/g;s/\"/\\\"/g'
+}
+
+IFS='
+	'
+cat <<EOF
+digraph redo {
+concentrate=true;rankdir=LR;ranksep=2;splines=polyline
+node[shape=rectangle]
+EOF
+printf 'subgraph dependencies { edge[style=solid,minlen=2]\n'
+for depfile in $(find .redo -name '*.dependencies'); do
+ while read -r dependency ctime md5sum; do
+  file="${depfile%.dependencies}"; file="${file#.redo}"
+  match "${file}" || match "${dependency}" || continue
+  case "$ctime" in
+   0) printf '"%s" [style=bold]\n' "$(_escape "${file}")"; break ;;
+   *) printf '"%s" -> "%s"\n' "$(_escape "${file}")" "$(_escape "${dependency}")" ;;
+  esac
+ done <"$depfile"
+done
+printf '}\nsubgraph dependencies_ne { edge[style=dotted,minlen=1]\n'
+for depfile in $(find .redo -name '*.dependencies_ne'); do
+ while read -r dependency_ne; do
+  file="${depfile%.dependencies_ne}"; file="${file#.redo}";
+  match "${file}" || match "${dependency_ne}" || continue
+  printf '"%s" -> "%s"\n' "$(_escape "${file}")" "$(_escape "${dependency_ne}")"
+ done <"$depfile"
+done
+printf '}\n'
+for stampfile in $(find .redo -name '*.stamp'); do
+ file="${stampfile%.stamp}"; file="${file#.redo}"
+ match "${file}" || continue
+ printf '"%s" [style=dashed]\n' "$(_escape "${file}")"
+done
+printf '}\n'
diff --git a/bin/redo-ifchange b/bin/redo-ifchange
new file mode 100755
index 0000000..2dad8ec
--- /dev/null
+++ b/bin/redo-ifchange
@@ -0,0 +1,540 @@
+#!/bin/sh -u
+# redo-ifchange – bourne shell implementation of DJB redo
+# Copyright © 2014-2021  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+: \
+ "${REDO_JOBS_MAX:=1}" \
+ "${REDO_MKDIR:=$(command -v mkdir)}" \
+ "${REDO_MV:=$(command -v mv)}" \
+ "${REDO_RM:=$(command -v rm) -f}" \
+ "${REDO_RMDIR:=$(command -v rmdir)}" \
+ "${REDO_SLEEP:=$(command -v sleep)}" \
+ "${REDO_TMP_DIR:=$(mktemp -d /tmp/redo.XXXXXXX)}" \
+ "${REDO_LOCKS_DIR:=${REDO_TMP_DIR}/locks}" \
+ "${REDO_CANARY:=${REDO_TMP_DIR}/canary}"
+
+export \
+ REDO_JOBS_MAX \
+ REDO_MKDIR \
+ REDO_MV \
+ REDO_RM \
+ REDO_RMDIR \
+ REDO_SLEEP \
+ REDO_TMP_DIR \
+ REDO_LOCKS_DIR \
+ REDO_CANARY
+
+case ${REDO_STAT:-} in
+ '')
+  if (LANG=C stat -c%Y "$0" >/dev/null 2>&1); then
+   REDO_STAT=$(command -v stat)
+  elif (LANG=C gstat -c%Y "$0" >/dev/null 2>&1); then
+   REDO_STAT=$(command -v gstat)
+  else
+   >&2 printf 'redo needs BusyBox stat(1), GNU stat(1) or gstat(1).
+'
+   exit 1
+  fi
+esac
+export REDO_STAT
+
+case ${REDO_MD5SUM:-} in
+ '')
+  if command -v md5sum >/dev/null && [ "$(LANG=C : | md5sum)" = "d41d8cd98f00b204e9800998ecf8427e  -" ]; then
+   REDO_MD5SUM=$(command -v md5sum)
+  elif command -v gmd5sum >/dev/null && [ "$(LANG=C : | gmd5sum)" = "d41d8cd98f00b204e9800998ecf8427e  -" ]; then
+   REDO_MD5SUM=$(command -v gmd5sum)
+  elif command -v openssl >/dev/null && [ "$(LANG=C : | openssl md5 -r)" = "d41d8cd98f00b204e9800998ecf8427e *stdin" ]; then
+   REDO_MD5SUM="$(command -v openssl) md5 -r"
+  elif command -v md5 >/dev/null && [ "$(LANG=C : | md5)" = "d41d8cd98f00b204e9800998ecf8427e" ]; then
+   REDO_MD5SUM="$(command -v md5)"
+  else
+   >&2 printf 'redo needs BusyBox md5sum(1), GNU md5sum(1) or gmd5sum(1), openssl(1) or md5(1).
+'
+   exit 1
+  fi
+esac
+export REDO_MD5SUM
+
+: \
+ "${REDO_BOLD:=}" \
+ "${REDO_PLAIN:=}" \
+ "${REDO_COLOR_SUCCESS:=}" \
+ "${REDO_COLOR_FAILURE:=}"
+
+case "${TERM:-dumb}" in
+ 'dumb')
+ ;;
+ *)
+  if LANG=C tty -s; then
+   : \
+    "${REDO_BOLD:=$(printf '\033[1m')}" \
+    "${REDO_PLAIN:=$(printf '\033[m')}" \
+    "${REDO_COLOR_SUCCESS:=$(printf '\033[36m')}" \
+    "${REDO_COLOR_FAILURE:=$(printf '\033[33m')}"
+  fi
+ ;;
+esac
+export \
+ REDO_BOLD \
+ REDO_PLAIN \
+ REDO_COLOR_SUCCESS \
+ REDO_COLOR_FAILURE
+
+_echo_debug_message() {
+ printf '%s' "$@" >&2
+}
+
+case ${REDO_DEBUG:-} in
+ 1) ;;
+ *) alias _echo_debug_message=:
+esac
+
+_echo_debug_jobs_message() {
+ printf '%s' "$@" >&2
+}
+
+case ${REDO_DEBUG_JOBS:-} in
+ 1) ;;
+ *) alias _echo_debug_jobs_message=:
+esac
+
+_echo_debug_locks_message() {
+ printf '%s' "$@" >&2
+}
+
+case ${REDO_DEBUG_LOCKS:-} in
+ 1) ;;
+ *) alias _echo_debug_locks_message=:
+esac
+
+_exit_if_canary_dead() {
+ if [ -e "${REDO_CANARY:-}" ]; then
+ printf "${REDO_COLOR_FAILURE}redo %s${REDO_BOLD}%s: canary detected, aborting.${REDO_PLAIN}\n" \
+  "${REDO_DEPTH:-}" "${target#$REDO_BASE/}" >&2
+  exit 1
+ fi
+}
+
+_kill_canary() {
+ [ -n "${REDO_CANARY:-}" ] && \
+  : >"${REDO_CANARY}"
+}
+
+_exit_sigint() {
+ printf "${REDO_COLOR_FAILURE}redo %s${REDO_BOLD}%s: received SIGINT.${REDO_PLAIN}\n" \
+  "${REDO_DEPTH:-}" "${target#$REDO_BASE/}" >&2
+ _kill_canary
+ exit 1
+}
+
+trap _exit_sigint INT
+
+_add_dependency() {
+ parent=$1 dependency=$2
+ # Do not record circular dependencies.
+ [ "$parent" = "$dependency" ] && exit 1
+ local base; _dirsplit "$parent"
+ [ -d "$REDO_DIR/$dir" ] || LANG=C "${REDO_MKDIR}" -p "$REDO_DIR/$dir"
+ ctime_md5sum=$(
+  LANG=C $REDO_STAT -c%Y "$dependency"
+  LANG=C $REDO_MD5SUM < "$dependency"
+ )
+ ctime=${ctime_md5sum%%'
+'*}
+ md5sum=${ctime_md5sum#*'
+'}
+ printf "%s\t${ctime}\t${md5sum}\n" "${dependency}" >> \
+  "$REDO_DIR"/"$parent".dependencies.tmp
+ local base; _dirsplit "$dependency"
+ [ -d "$REDO_DIR/$dir" ] || LANG=C "${REDO_MKDIR}" -p "$REDO_DIR/$dir"
+ printf "${md5sum}\n" >"$REDO_DIR/$dependency".md5sum
+}
+
+_lock_acquire() {
+ lock_name=${1}
+ lock_dir="${REDO_LOCKS_DIR}${lock_name}"
+ lock_tries=1
+ lock_wait_total=0
+ while LANG=C "${REDO_MKDIR}" -p "${lock_dir%/*}" && ! LANG=C "${REDO_MKDIR}" "${lock_dir}" >/dev/null 2>&1; do
+  _exit_if_canary_dead
+  _echo_debug_locks_message "wait for lock	ppid: ${PPID}	tried: ${lock_tries}	waited: >${lock_wait_total}s	locked: ${lock_name}	target: ${REDO_TARGET}
+"
+  lock_wait=$(( PPID % ( REDO_JOBS_MAX + 1 ) + 1 )).$(( REDO_JOBS_MAX - 1 ))
+  _jobs_count_decrease "lock sleep"
+  LANG=C "${REDO_SLEEP}" "${lock_wait}"
+  _jobs_count_increase "lock awake"
+  lock_tries=$(( lock_tries + 1 ))
+  lock_wait_total=$(( lock_wait_total + ${lock_wait%.*} ))
+ done
+ _echo_debug_locks_message "acquired lock	ppid: ${PPID}	tried: ${lock_tries}	waited: >${lock_wait_total}s	locked: ${lock_name}	target: ${REDO_TARGET}
+"
+}
+
+_lock_release() {
+ lock_name=${1}
+ lock_dir="${REDO_LOCKS_DIR}${lock_name}"
+ _echo_debug_locks_message "release tried	ppid: ${PPID}	tried: 1	waited: ~0s	locked: ${lock_name}	target: ${REDO_TARGET}
+"
+ LANG=C [ -d "${lock_dir}" ] && ${REDO_RMDIR} -p "${lock_dir}" >/dev/null 2>&1
+ _echo_debug_locks_message "released lock	ppid: ${PPID}	tried: 1	waited: ~0s	locked: ${lock_name}	target: ${REDO_TARGET}
+"
+}
+
+_jobs_count_increase() {
+ _echo_debug_jobs_message "job awaiting	reason: ${1}
+"
+ <&9 read -r _
+ _echo_debug_jobs_message "job starting	reason: ${1}
+"
+}
+
+_jobs_count_decrease() {
+ _echo_debug_jobs_message "job finished	reason: ${1}
+"
+ >&9 echo
+}
+
+case ${REDO_JOBS_MAX} in
+ 1)
+  alias \
+   _jobs_count_increase=: \
+   _jobs_count_decrease=:
+ ;;
+esac
+
+_dependencies_ne_uptodate() {
+ target=$1
+ # If no non-existence dependencies exist, they are by definition up to date.
+ if [ ! -s "$REDO_DIR/$target".dependencies_ne ]; then
+  _echo_debug_message "${target#$REDO_BASE/} has no non-existence dependencies.
+"
+  rv=0
+  return
+ fi
+ _echo_debug_message "${target#$REDO_BASE/} non-existence dependency check:
+"
+ exec 3< "$REDO_DIR/$target".dependencies_ne
+ while read -r dependency_ne <&3; do
+  _echo_debug_message "	${dependency_ne#$REDO_BASE/} should not exist "
+  # If a non-existence dependency exists, it is out of date.
+  # Dependencies, e.g. on default.do files may also be out of date.
+  # Naive implementation: Pretend target is not up to date and rebuild.
+  if [ -e "$dependency_ne" ]; then
+   _echo_debug_message "and exists.
+"
+   rv=1
+   return
+  fi
+  _echo_debug_message "and does not.
+"
+ done
+ exec 3>&-
+ _echo_debug_message "${target#$REDO_BASE/} non-existence dependencies up to date.
+"
+ rv=0
+ return
+}
+
+_target_uptodate() {
+ target=$1
+ # If a target is a top-level target, it is not up to date.
+ case "$REDO_TARGET" in
+  '') return 1
+ esac
+ # If a target does not exist, it is not up to date.
+ if [ ! -e "$target" ]; then
+  _echo_debug_message "$target does not exist.
+"
+  return 1
+ fi
+ [ ! -e "$REDO_DIR/$target".md5sum ] && return 1
+ _echo_debug_message "${target#$REDO_BASE/} ctime "
+ ctime_stored_actual="$(LANG=C $REDO_STAT -c%Y "$REDO_DIR/$target".md5sum "$target")"
+ ctime_stored=${ctime_stored_actual%%'
+'*}
+ ctime_actual=${ctime_stored_actual#*'
+'}
+ # faster [ $ctime_stored -ge $ctime_actual ]
+ case $(( ctime_stored - ctime_actual )) in
+  -*) ;;
+  *)
+   _echo_debug_message "unchanged.
+"
+   return 0
+  ;;
+ esac
+ _echo_debug_message "changed.
+"
+ _echo_debug_message "$target md5sum "
+ read -r md5sum_stored <"$REDO_DIR/$target".md5sum
+ IFS=' ' md5sum_actual="$(LANG=C $REDO_MD5SUM < "$target")"
+ IFS='
+	'
+ case $md5sum_stored in
+  $md5sum_actual)
+   _echo_debug_message "unchanged.
+"
+   # If stored md5sum of target matches actual md5sum, but stored
+   # ctime does not, redo needs to update stored ctime of target.
+   : >>"$REDO_DIR/$target".md5sum
+   return 0
+  esac
+ _echo_debug_message "changed.
+"
+ return 1
+}
+
+_dependencies_from_depfile() {
+ while read -r dependency ctime_stored md5sum_stored; do
+  printf "%s\n" "${dependency}"
+ done
+}
+
+_dependencies_uptodate() {
+ target=$1
+ target_depfile="$REDO_DIR/$target".dependencies
+ # If no dependencies exist, they are by definition up to date.
+ if [ ! -e "$target_depfile" ]; then
+  _echo_debug_message "	${target#$REDO_BASE/} has no dependencies.
+"
+  return 0
+ fi
+ _echo_debug_message "${target#$REDO_BASE/} dependency check:
+"
+ # If any dependency does not exist, the target is out of date.
+ IFS='
+	'
+ LANG=C $REDO_STAT -c%Y $(_dependencies_from_depfile <"$target_depfile") > \
+  "$target_depfile".ctimes 2>&- || return 1
+ exec 3< "$target_depfile".ctimes
+ exec 4< "$target_depfile"
+ while read -r ctime_actual <&3 && read -r dependency ctime_stored md5sum_stored <&4; do
+  # If a dependency of a dependency is out of date, the dependency is out of date.
+  if ( ! _dependencies_uptodate "$dependency" ); then
+   return 1
+  fi
+  # If the ctime of a dependency did not change, the dependency is up to date.
+  _echo_debug_message "	${dependency#$REDO_BASE/} ctime "
+  case $ctime_stored in
+   $ctime_actual)
+    _echo_debug_message "unchanged.
+"
+    continue
+  esac
+  # If the md5sum of a dependency did not change, the dependency is up to date.
+  _echo_debug_message "changed.
+	$dependency md5sum "
+  OLDIFS=$IFS
+  IFS=' ' md5sum_actual="$(LANG=C $REDO_MD5SUM < "$dependency")"
+  IFS=$OLDIFS
+  case $md5sum_stored in
+   $md5sum_actual)
+    _echo_debug_message "unchanged.
+"
+    continue
+  esac
+  # if both ctime and md5sum did change, the dependency is out of date.
+  _echo_debug_message "changed.
+"
+  return 1
+ done
+ exec 4>&-
+ exec 3>&-
+ _echo_debug_message "${target#$REDO_BASE/} dependencies up to date.
+"
+ # If a non-existence dependency is out of date, the target is out of date.
+ _dependencies_ne_uptodate "$target"
+ return $rv
+}
+
+_dirsplit() {
+ base=${1##*/}
+ dir=${1%"$base"}
+}
+
+_do() {
+ local dir="$1" target="$2" tmp="$3"
+ target_abspath="${PWD%/}/$target"
+ target_relpath="${target_abspath#$REDO_BASE/}"
+ # If target is not up to date or its dependencies are not up to date, build it.
+ if (
+  ! _target_uptodate "$target_abspath" || \
+  ! _dependencies_uptodate "$target_abspath"
+ ); then
+  dofile_abspath=$(
+   LANG=C redo-whichdo "${target_abspath}" \
+    |LANG=C xargs -0 \
+      sh -cu '
+dofile=${0}
+for arg; do dofile=${arg}; done
+test -e "${dofile}" && printf "%s" "${dofile}"
+'
+   )
+  ext=
+  case ${dofile_abspath} in
+   *default.*.do)
+    ext=${dofile_abspath%.do}
+    ext=.${ext#*default.}
+   ;;
+  esac
+  base=${target%$ext}
+  if [ -z "$dofile_abspath" ]; then
+   # If .do file does not exist and target exists, it is a source file.
+   if [ -e "$target_abspath" ]; then
+    _add_dependency "$REDO_TARGET" "$target_abspath"
+    # Remove dependencies and non-existence dependencies that the
+    # target file might have had. When a target that was built by
+    # redo has its dofile removed, it becomes a source file and
+    # should not be rebuilt constantly due to a missing dofile.
+    [ -e "$REDO_DIR/$target_abspath".dependencies ] && \
+     LANG=C $REDO_RM "$REDO_DIR/$target_abspath".dependencies >&2
+    [ -e "$REDO_DIR/$target_abspath".dependencies_ne ] && \
+     LANG=C $REDO_RM "$REDO_DIR/$target_abspath".dependencies_ne >&2
+    return 0
+   # If .do file does not exist and target does not exist, stop.
+   else
+    printf "${REDO_COLOR_FAILURE}redo %s${REDO_BOLD}%s: no .do file.${REDO_PLAIN}\n" \
+     "${REDO_DEPTH:-}" "${target_abspath#$REDO_BASE/}" >&2
+    exit 1
+   fi
+  fi
+  : ${REDO_DEPTH:=}
+  case ${#REDO_DEPTH} in
+   200)
+    printf "${REDO_COLOR_FAILURE}redo %s${REDO_BOLD}%s: Maximum recursion depth exceeded.${REDO_PLAIN}\n" \
+     "${REDO_DEPTH:-}" "${target_abspath#$REDO_BASE/}" >&2
+    exit 1
+  esac
+  printf '%sredo %s%s%s%s\n' \
+   "${REDO_COLOR_SUCCESS}" "$REDO_DEPTH" "${REDO_BOLD}" "$target_relpath" "${REDO_PLAIN}" >&2
+  ( _run_dofile "$target" "${base##*/}" "$tmp.tmp" )
+  rv="$?"
+  # Add non existing .do file to non-existence dependencies so
+  # target is built when .do file in question is created.
+  LANG=C redo-whichdo "${target}" \
+   |LANG=C REDO_TARGET=${target_abspath} xargs -0 redo-ifcreate 2>/dev/null
+  # Add .do file to dependencies so target is built when .do file changes.
+  _add_dependency "$target_abspath" "${dofile_abspath}"
+  # Exit code 123 conveys that target was considered up to date at runtime.
+  case ${rv} in
+   0) ;;
+   123)
+    LANG=C $REDO_RM ./"$tmp.tmp" ./"$tmp.tmp2"
+   ;;
+   *)
+    LANG=C $REDO_RM ./"$tmp.tmp" ./"$tmp.tmp2"
+    printf "${REDO_COLOR_FAILURE}redo %s${REDO_BOLD}%s: got exit code %s.${REDO_PLAIN}\n" \
+     "$REDO_DEPTH" "${target_abspath#$REDO_BASE/}" "$rv" >&2
+    exit 1
+   ;;
+  esac
+  if [ -s "$tmp.tmp" ]; then
+   LANG=C "$REDO_MV" ./"$tmp.tmp" ./"$target" 2>&-
+  elif [ -s "$tmp.tmp2" ]; then
+   LANG=C "$REDO_MV" ./"$tmp.tmp2" ./"$target" 2>&-
+  fi
+  [ -e "$tmp.tmp2" ] && LANG=C $REDO_RM ./"$tmp.tmp2"
+  # After build is finished, update dependencies.
+  : >> "$REDO_DIR/$target_abspath".dependencies.tmp
+  : >> "$REDO_DIR/$target_abspath".dependencies_ne.tmp
+  LANG=C "$REDO_MV" "$REDO_DIR/$target_abspath".dependencies.tmp \
+   "$REDO_DIR/$target_abspath".dependencies >&2
+  LANG=C "$REDO_MV" "$REDO_DIR/$target_abspath".dependencies_ne.tmp \
+   "$REDO_DIR/$target_abspath".dependencies_ne >&2
+ fi
+ # Some do files (like all.do) do not usually generate output.
+ if [ -e "$target_abspath" ]; then
+  # Record dependency on parent target.
+  if [ -n "$REDO_TARGET" ]; then
+   _add_dependency "$REDO_TARGET" "$target_abspath"
+  else
+   local base; _dirsplit "$target_abspath"
+   [ -d "$REDO_DIR/$dir" ] || LANG=C "${REDO_MKDIR}" -p "$REDO_DIR/$dir"
+   LANG=C $REDO_MD5SUM <"$target_abspath" > \
+    "$REDO_DIR/$target_abspath".md5sum
+  fi
+ fi
+ _exit_if_canary_dead
+}
+
+_run_dofile() {
+ export REDO_DEPTH="$REDO_DEPTH  "
+ export REDO_TARGET="$PWD"/"$target"
+ local line1
+ set -e
+ read -r line1 <"${dofile_abspath}" || true
+ cmd=${line1#"#!"}
+ # If the first line of a do file does not have a hashbang (#!), use /bin/sh.
+ if [ "$cmd" = "$line1" ] || [ "$cmd" = "/bin/sh" ]; then
+  if [ "${REDO_XTRACE:-}" = "1" ]; then
+   cmd="/bin/sh -ex"
+  else
+   cmd="/bin/sh -e"
+  fi
+ fi
+ $cmd "${dofile_abspath}" "$@" >"$tmp.tmp2"
+}
+
+set +e
+if [ -n "${1:-}" ]; then
+ jobs_pids=""
+ _jobs_count_decrease "redo ${REDO_TARGET#${REDO_BASE}/}"
+ for target; do
+  # If relative path to target is given, convert to absolute absolute path.
+  case "$target" in
+   /*) ;;
+   *)  target="$PWD"/"$target" >&2
+  esac
+  _dirsplit "$target"
+  if [ "${REDO_JOBS_MAX}" -gt "1" ]; then
+    _exit_if_canary_dead
+    _jobs_count_increase "redo ${dir#$REDO_BASE/}${base}"
+    if [ -e "${target}" ] && ! [ -w "${target}" ]; then
+     # An existing file that can not be written to is a dependency. It
+     # does not have to be locked, as redo will never be able to write
+     # to it. No dofile is executed, but job count is still increased.
+     (
+      ( cd "$dir" && _do "$dir" "$base" "$base" )
+      [ "$?" = 0 ] || ( _kill_canary; exit 1 )
+      _jobs_count_decrease "done ${dir#$REDO_BASE/}${base}"
+     ) &
+     jobs_pids="${jobs_pids} $!"
+    else
+     # Any other target may require locks and dofile execution or not.
+     (
+      lock_name="${dir}${base}"
+      _lock_acquire "${lock_name}"
+      ( cd "$dir" && _do "$dir" "$base" "$base" )
+      [ "$?" = 0 ] || ( _kill_canary; exit 1 )
+      _lock_release "${lock_name}"
+      _jobs_count_decrease "done ${dir#$REDO_BASE/}${base}"
+     ) &
+     jobs_pids="${jobs_pids} $!"
+    fi
+  else
+   ( cd "$dir" && _do "$dir" "$base" "$base" )
+   [ "$?" = 0 ] || exit 1
+  fi
+ done
+ for pid in ${jobs_pids}; do
+  wait ${pid}
+  [ "$?" = 0 ] || exit 1
+ done
+ _exit_if_canary_dead
+
+ # This operation may briefly increase jobs count over the given
+ # maximum. This is not a bug, but a measure to prevent a hang of
+ # redo-ifchange before exit, which blocks a parent indefinitely.
+ _jobs_count_increase "done ${REDO_TARGET}" &
+fi
diff --git a/bin/redo-ifcreate b/bin/redo-ifcreate
new file mode 100755
index 0000000..5365044
--- /dev/null
+++ b/bin/redo-ifcreate
@@ -0,0 +1,35 @@
+#!/bin/sh -eu
+# redo-ifcreate – bourne shell implementation of DJB redo
+# Copyright © 2014-2019  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+# redo-ifcreate takes a list of non-existent files (sources) and adds
+# them as non-existence dependencies to the current target (the one
+# calling redo-ifcreate). If a non-existence dependency of a target
+# exists, the target will be rebuilt.
+
+for filename; do
+ # BusyBox v.1.19.3 outputs nothing for “readlink -f” on a nonexistent
+ # file, which means that redo-ifcreate can not canonicalize filenames.
+ case "${filename}" in
+  /*) dependency_ne="${filename}" ;;
+  *) dependency_ne="${PWD%/}/${filename}" ;;
+ esac
+ if [ ! -e "${dependency_ne}" ]; then
+  mkdir -p $(LANG=C dirname "${REDO_DIR}/${REDO_TARGET}".dependencies_ne.tmp)
+  printf '%s\n' "${dependency_ne}" >> "${REDO_DIR}/${REDO_TARGET}".dependencies_ne.tmp
+ else
+  printf "%sredo-ifcreate: %s exists.%s\n" "${REDO_COLOR_FAILURE}" "$dependency_ne" "${REDO_PLAIN}" >&2
+  exit 1
+  fi
+done
+exit 0
diff --git a/bin/redo-ood b/bin/redo-ood
new file mode 100755
index 0000000..22f5f43
--- /dev/null
+++ b/bin/redo-ood
@@ -0,0 +1,30 @@
+#!/bin/sh -eu
+# redo-ood – bourne shell implementation of DJB redo
+# Copyright © 2014-2021  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+# Prints a list of all redo target files that are out of date.
+. "$(which redo-ifchange)"
+
+IFS='
+	'
+for depfile in $(find .redo -name '*.dependencies'); do
+ file="${depfile%.dependencies}"; file="${file#.redo}"
+ export REDO_BASE="$(pwd)"
+ export REDO_DIR="$REDO_BASE/.redo"
+ export REDO_TARGET='dummy_target'
+ if ( ! _target_uptodate "$file" || ! _dependencies_uptodate "$file" ); then
+  if [ -e "$file" ]; then
+   echo "$file"
+  fi
+ fi
+done
diff --git a/bin/redo-sources b/bin/redo-sources
new file mode 100755
index 0000000..02c82b7
--- /dev/null
+++ b/bin/redo-sources
@@ -0,0 +1,27 @@
+#!/bin/sh
+# redo-sources – bourne shell implementation of DJB redo
+# Copyright © 2014  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+# Prints a list of all redo source files that exist. A source file is
+# a file that was listed as a dependency using redo-ifchange, but is
+# not itself a target. A target file is a file that redo can build.
+
+IFS='
+	'
+for filename in $(find .redo -name '*.dependencies'); do
+ while read -r dependency ctime md5sum; do
+  if [ -e "$dependency" ] && [ ! -e ".redo/$dependency.dependencies" ] ; then
+   echo "$dependency"
+  fi
+ done <"$filename"
+done | sort | uniq
diff --git a/bin/redo-stamp b/bin/redo-stamp
new file mode 100755
index 0000000..8705765
--- /dev/null
+++ b/bin/redo-stamp
@@ -0,0 +1,29 @@
+#!/bin/sh
+# redo-stamp – bourne shell implementation of DJB redo
+# Copyright © 2014-2017  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+stamp_new=$(md5sum)
+stamp_old=$(cat "$REDO_DIR/$REDO_TARGET".stamp 2>/dev/null)
+if [ "$stamp_new" != "$stamp_old" ]; then
+ path="$REDO_DIR/$REDO_TARGET"
+ dir=${path%/*}
+ [ ! -d "$dir" ] && mkdir -p "$dir"
+ printf '%s' "$stamp_new" > "$REDO_DIR/$REDO_TARGET".stamp
+ redo-ifchange "$REDO_DIR/$REDO_TARGET".stamp
+ exit 0
+fi
+if [ -e "$REDO_TARGET" ]; then
+ # Exit code 123 conveys that target was considered uptodate at runtime.
+ exit 123
+ # FIXME: redo-stamp may not be the only dependency mechanism
+fi
diff --git a/bin/redo-targets b/bin/redo-targets
new file mode 100755
index 0000000..9ae47ed
--- /dev/null
+++ b/bin/redo-targets
@@ -0,0 +1,25 @@
+#!/bin/sh
+# redo-targets – bourne shell implementation of DJB redo
+# Copyright © 2014  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+# Prints a list of all redo target files that exist. A target file is
+# a file that redo can build.
+
+IFS='
+	'
+for depfile in $(find .redo -name '*.dependencies'); do
+ file="${depfile%.dependencies}"; file="${file#.redo}"
+ if [ -e "$file" ]; then
+  echo "$file"
+ fi
+done
diff --git a/bin/redo-whichdo b/bin/redo-whichdo
new file mode 100755
index 0000000..fa3a4ca
--- /dev/null
+++ b/bin/redo-whichdo
@@ -0,0 +1,79 @@
+#!/bin/sh -eu
+# redo-whichdo – bourne shell implementation of DJB redo
+# Copyright © 2018-2019  Nils Dagsson Moskopp (erlehmann)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
+# steigern. Gelegentlich packe ich sogar einen handfesten Buffer
+# Overflow oder eine Format String Vulnerability zwischen die anderen
+# Codezeilen und schreibe das auch nicht dran.
+
+case ${SEPARATOR:-} in
+ '') [ -t 1 ] && SEPARATOR='\n' || SEPARATOR='\0'
+esac
+
+alias print_filename="printf -- '%s${SEPARATOR}'"
+
+find_default_dofile() {
+ target_abspath=${1}
+ target_basename=${target_abspath##*/}
+ target_dirname=${target_abspath%"${target_basename}"}
+ dofile_candidate=default.${target_basename}.do
+ while :; do
+  dofile_candidate=default.${dofile_candidate#default.*.}
+  dofile_candidate_abspath="${PWD%/}/${dofile_candidate}"
+  case "${dofile_candidate_abspath}" in
+   "${target_abspath}")
+    # A file named “default.do” can not be its own dofile.
+   ;;
+   *)
+    print_filename "${dofile_candidate_abspath}"
+   ;;
+  esac
+
+  [ -f "${dofile_candidate}" ] || [ "${dofile_candidate}" = "default.do" ] && break
+ done
+}
+
+find_dofile() {
+ # BusyBox v.1.19.3 outputs nothing for “readlink -f” on a nonexistent
+ # file, which means that redo-whichdo can not canonicalize filenames.
+ case "${1}" in
+  /*) target_abspath="${1}" ;;
+  *) target_abspath="${PWD%/}/${1}" ;;
+ esac
+ target_basename=${target_abspath##*/}
+ target_dirname=${target_abspath%"${target_basename}"}
+
+ # Skip printing first guess for path names with the prefix “default”
+ # to prevent duplicate output.
+ case "${target_basename}" in
+  default)
+  ;;
+  default.*)
+  ;;
+  *)
+   dofile_candidate="$target_abspath".do
+   print_filename "${dofile_candidate}"
+   [ -f "${dofile_candidate}" ] && return
+  ;;
+ esac
+
+ cd "${target_dirname}"
+
+ while :; do
+  find_default_dofile "${target_abspath}"
+  [ -f "${dofile_candidate}" ] || [ "${PWD}" = "/" ] && break
+  cd ..
+ done
+
+ [ -f "${PWD%/}/${dofile_candidate}" ] || exit 1
+}
+
+for target; do
+ find_dofile "${target}"
+done