aboutsummaryrefslogtreecommitdiff
path: root/tools/do
diff options
context:
space:
mode:
Diffstat (limited to 'tools/do')
-rwxr-xr-xtools/do446
1 files changed, 446 insertions, 0 deletions
diff --git a/tools/do b/tools/do
new file mode 100755
index 0000000..f38a2a7
--- /dev/null
+++ b/tools/do
@@ -0,0 +1,446 @@
+#!/bin/sh
+#
+# A minimal alternative to djb redo that doesn't support incremental builds.
+# For the full version, visit http://github.com/apenwarr/redo
+#
+# The author disclaims copyright to this source file and hereby places it in
+# the public domain. (2010 12 14; updated 2019 02 24)
+#
+USAGE="
+usage: do [-d] [-x] [-v] [-c] <targets...>
+ -d print extra debug messages (mostly about dependency checks)
+ -v run .do files with 'set -v'
+ -x run .do files with 'set -x'
+ -c clean up all old targets before starting
+
+ Note: do is an implementation of redo that does *not* check dependencies.
+ It will never rebuild a target it has already built, unless you use -c.
+"
+
+# CDPATH apparently causes unexpected 'cd' output on some platforms.
+unset CDPATH
+
+# By default, no output coloring.
+green=""
+bold=""
+plain=""
+
+if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
+ green="$(printf '\033[32m')"
+ bold="$(printf '\033[1m')"
+ plain="$(printf '\033[m')"
+fi
+
+# The 'seq' command is not available on all platforms.
+_seq() {
+ local x=0 max="$1"
+ while [ "$x" -lt "$max" ]; do
+ x=$((x + 1))
+ echo "$x"
+ done
+}
+
+# Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base)
+_dirsplit() {
+ _dirsplit_base=${1##*/}
+ _dirsplit_dir=${1%$_dirsplit_base}
+}
+
+# Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics.
+qdirname() (
+ _dirsplit "$1"
+ dir=${_dirsplit_dir%/}
+ echo "${dir:-.}"
+)
+
+_dirsplit "$0"
+REDO=$(cd "$(pwd -P)" &&
+ cd "${_dirsplit_dir:-.}" &&
+ echo "$PWD/$_dirsplit_base")
+export REDO
+_cmd=$_dirsplit_base
+
+DO_TOP=
+if [ -z "$DO_BUILT" ]; then
+ export _do_opt_debug=
+ export _do_opt_exec=
+ export _do_opt_verbose=
+ export _do_opt_clean=
+fi
+while getopts 'dxvcj:h?' _opt; do
+ case $_opt in
+ d) _do_opt_debug=1 ;;
+ x) _do_opt_exec=x ;;
+ v) _do_opt_verbose=v ;;
+ c) _do_opt_clean=1 ;;
+ j) ;; # silently ignore, for compat with real redo
+ \?|h|*) printf "%s" "$USAGE" >&2
+ exit 99
+ ;;
+ esac
+done
+shift "$((OPTIND - 1))"
+_debug() {
+ [ -z "$_do_opt_debug" ] || echo "$@" >&2
+}
+
+if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then
+ DO_TOP=1
+ if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then
+ set all # only toplevel redo has a default target
+ fi
+ export DO_STARTDIR="$(pwd -P)"
+ # If starting /bin/pwd != $PWD, this will fix it.
+ # That can happen when $PWD contains symlinks that the shell is
+ # trying helpfully (but unsuccessfully) to hide from the user.
+ cd "$DO_STARTDIR" || exit 99
+ export DO_BUILT="$PWD/.do_built"
+ if [ -z "$_do_opt_clean" -a -e "$DO_BUILT" ]; then
+ echo "do: Incremental mode. Use -c for clean rebuild." >&2
+ fi
+ : >>"$DO_BUILT"
+ sort -u "$DO_BUILT" >"$DO_BUILT.new"
+ while read f; do
+ [ -n "$_do_opt_clean" ] && printf "%s\0%s.did\0" "$f" "$f"
+ printf "%s.did.tmp\0" "$f"
+ done <"$DO_BUILT.new" |
+ xargs -0 rm -f 2>/dev/null
+ mv "$DO_BUILT.new" "$DO_BUILT"
+ export DO_PATH="$DO_BUILT.dir"
+ export PATH="$DO_PATH:$PATH"
+ rm -rf "$DO_PATH"
+ mkdir "$DO_PATH"
+ for d in redo redo-ifchange redo-whichdo; do
+ ln -s "$REDO" "$DO_PATH/$d"
+ done
+ for d in redo-ifcreate redo-stamp redo-always redo-ood \
+ redo-targets redo-sources; do
+ echo "#!/bin/sh" >"$DO_PATH/$d"
+ chmod a+rx "$DO_PATH/$d"
+ done
+fi
+
+
+# Chop the "file" part off a /path/to/file pathname.
+# Note that if the filename already ends in a /, we just remove the slash.
+_updir()
+{
+ local v="${1%/*}"
+ [ "$v" != "$1" ] && echo "$v"
+ # else "empty" which means we went past the root
+}
+
+
+# Returns true if $1 starts with $2.
+_startswith()
+{
+ [ "${1#"$2"}" != "$1" ]
+}
+
+
+# Returns true if $1 ends with $2.
+_endswith()
+{
+ [ "${1%"$2"}" != "$1" ]
+}
+
+
+# Prints $1 if it's absolute, or $2/$1 if $1 is not absolute.
+_abspath()
+{
+ local here="$2" there="$1"
+ if _startswith "$1" "/"; then
+ echo "$1"
+ else
+ echo "$2/$1"
+ fi
+}
+
+
+# Prints $1 as a path relative to $PWD (not starting with /).
+# If it already doesn't start with a /, doesn't change the string.
+_relpath()
+{
+ local here="$2" there="$1" out= hadslash=
+ #echo "RP start '$there' hs='$hadslash'" >&2
+ _startswith "$there" "/" || { echo "$there" && return; }
+ [ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/
+ here=${here%/}/
+ while [ -n "$here" ]; do
+ #echo "RP out='$out' here='$here' there='$there'" >&2
+ [ "${here%/}" = "${there%/}" ] && there= && break;
+ [ "${there#$here}" != "$there" ] && break
+ out=../$out
+ _dirsplit "${here%/}"
+ here=$_dirsplit_dir
+ done
+ there=${there#$here}
+ if [ -n "$there" ]; then
+ echo "$out${there%/}$hadslash"
+ else
+ echo "${out%/}$hadslash"
+ fi
+}
+
+
+# Prints a "normalized relative" path, with ".." resolved where possible.
+# For example, a/b/../c will be reduced to just a/c.
+_normpath()
+(
+ local path="$1" relto="$2" out= isabs=
+ #echo "NP start '$path'" >&2
+ if _startswith "$path" "/"; then
+ isabs=1
+ else
+ path="${relto%/}/$path"
+ fi
+ set -f
+ IFS=/
+ for d in ${path%/}; do
+ #echo "NP out='$out' d='$d'" >&2
+ if [ "$d" = ".." ]; then
+ out=$(_updir "${out%/}")/
+ else
+ out=$out$d/
+ fi
+ done
+ #echo "NP out='$out' (done)" >&2
+ out=${out%/}
+ if [ -n "$isabs" ]; then
+ echo "${out:-/}"
+ else
+ _relpath "${out:-/}" "$relto"
+ fi
+)
+
+
+# Prints a "real" path, with all symlinks resolved where possible.
+_realpath()
+{
+ local path="$1" relto="$2" isabs= rest=
+ if _startswith "$path" "/"; then
+ isabs=1
+ else
+ path="${relto%/}/$path"
+ fi
+ (
+ for d in $(_seq 100); do
+ #echo "Trying: $PWD--$path" >&2
+ if cd -P "$path" 2>/dev/null; then
+ # success
+ pwd=$(pwd -P)
+ #echo " chdir ok: $pwd--$rest" >&2
+ np=$(_normpath "${pwd%/}/$rest" "$relto")
+ if [ -n "$isabs" ]; then
+ echo "$np"
+ else
+ _relpath "$np" "$relto"
+ fi
+ break
+ fi
+ _dirsplit "${path%/}"
+ path=$_dirsplit_dir
+ rest="$_dirsplit_base/$rest"
+ done
+ )
+}
+
+
+# List the possible names for default*.do files in dir $1 matching the target
+# pattern in $2. We stop searching when we find the first one that exists.
+_find_dofiles_pwd()
+{
+ local dodir="$1" dofile="$2"
+ _startswith "$dofile" "default." || dofile=${dofile#*.}
+ while :; do
+ dofile=default.${dofile#default.*.}
+ echo "$dodir$dofile"
+ [ -e "$dodir$dofile" ] && return 0
+ [ "$dofile" = default.do ] && break
+ done
+ return 1
+}
+
+
+# List the possible names for default*.do files in $PWD matching the target
+# pattern in $1. We stop searching when we find the first name that works.
+# If there are no matches in $PWD, we'll search in .., and so on, to the root.
+_find_dofiles()
+{
+ local target="$1" dodir= dofile= newdir=
+ _debug "find_dofile: '$PWD' '$target'"
+ dofile="$target.do"
+ echo "$dofile"
+ [ -e "$dofile" ] && return 0
+
+ # Try default.*.do files, walking up the tree
+ _dirsplit "$dofile"
+ dodir=$_dirsplit_dir
+ dofile=$_dirsplit_base
+ [ -n "$dodir" ] && dodir=${dodir%/}/
+ [ -e "$dodir$dofile" ] && return 0
+ for i in $(_seq 100); do
+ [ -n "$dodir" ] && dodir=${dodir%/}/
+ #echo "_find_dofiles: '$dodir' '$dofile'" >&2
+ _find_dofiles_pwd "$dodir" "$dofile" && return 0
+ newdir=$(_realpath "${dodir}.." "$PWD")
+ [ "$newdir" = "$dodir" ] && break
+ dodir=$newdir
+ done
+ return 1
+}
+
+
+# Print the last .do file returned by _find_dofiles.
+# If that file exists, returns 0, else 1.
+_find_dofile()
+{
+ local files="$(_find_dofiles "$1")"
+ rv=$?
+ #echo "files='$files'" >&2
+ [ "$rv" -ne 0 ] && return $rv
+ echo "$files" | {
+ while read -r linex; do line=$linex; done
+ printf "%s\n" "$line"
+ }
+}
+
+
+# Actually run the given $dofile with the arguments in $@.
+# Note: you should always run this in a subshell.
+_run_dofile()
+{
+ export DO_DEPTH="$DO_DEPTH "
+ export REDO_TARGET="$PWD/$target"
+ local line1
+ set -e
+ read line1 <"$PWD/$dofile" || true
+ cmd=${line1#"#!/"}
+ if [ "$cmd" != "$line1" ]; then
+ set -$_do_opt_verbose$_do_opt_exec
+ exec /$cmd "$PWD/$dofile" "$@"
+ else
+ set -$_do_opt_verbose$_do_opt_exec
+ # If $dofile is empty, "." might not change $? at
+ # all, so we clear it first with ":".
+ :; . "$PWD/$dofile"
+ fi
+}
+
+
+# Find and run the right .do file, starting in dir $1, for target $2,
+# providing a temporary output file as $3. Renames the temp file to $2 when
+# done.
+_do()
+{
+ local dir="$1" target="$1$2" tmp="$1$2.redo.tmp" tdir=
+ local dopath= dodir= dofile= ext=
+ if [ "$_cmd" = "redo" ] ||
+ ( [ ! -e "$target" -o -d "$target" ] &&
+ [ ! -e "$target.did" ] ); then
+ printf '%sdo %s%s%s%s\n' \
+ "$green" "$DO_DEPTH" "$bold" "$target" "$plain" >&2
+ dopath=$(_find_dofile "$target")
+ if [ ! -e "$dopath" ]; then
+ echo "do: $target: no .do file ($PWD)" >&2
+ return 1
+ fi
+ _dirsplit "$dopath"
+ dodir=$_dirsplit_dir dofile=$_dirsplit_base
+ if _startswith "$dofile" "default."; then
+ ext=${dofile#default}
+ ext=${ext%.do}
+ else
+ ext=
+ fi
+ target=$PWD/$target
+ tmp=$PWD/$tmp
+ cd "$dodir" || return 99
+ target=$(_relpath "$target" "$PWD") || return 98
+ tmp=$(_relpath "$tmp" "$PWD") || return 97
+ base=${target%$ext}
+ tdir=$(qdirname "$target")
+ [ ! -e "$DO_BUILT" ] || [ ! -w "$tdir/." ] ||
+ : >>"$target.did.tmp"
+ # $qtmp is a temporary file used to capture stdout.
+ # Since it might be accidentally deleted as a .do file
+ # does its work, we create it, then open two fds to it,
+ # then immediately delete the name. We use one fd to
+ # redirect to stdout, and the other to read from after,
+ # because there's no way to fseek(fd, 0) in sh.
+ qtmp=$DO_PATH/do.$$.tmp
+ (
+ rm -f "$qtmp"
+ ( _run_dofile "$target" "$base" "$tmp" >&3 3>&- 4<&- )
+ rv=$?
+ if [ $rv != 0 ]; then
+ printf "do: %s%s\n" "$DO_DEPTH" \
+ "$target: got exit code $rv" >&2
+ rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did"
+ return $rv
+ fi
+ echo "$PWD/$target" >>"$DO_BUILT"
+ if [ ! -e "$tmp" ]; then
+ # if $3 wasn't created, copy from stdout file
+ cat <&4 >$tmp
+ # if that's zero length too, forget it
+ [ -s "$tmp" ] || rm -f "$tmp"
+ fi
+ ) 3>$qtmp 4<$qtmp # can't use "|| return" here...
+ # ...because "|| return" would mess up "set -e" inside the ()
+ # on some shells. Running commands in "||" context, even
+ # deep inside, will stop "set -e" from functioning.
+ rv=$?
+ [ "$rv" = 0 ] || return "$rv"
+ mv "$tmp" "$target" 2>/dev/null
+ [ -e "$target.did.tmp" ] &&
+ mv "$target.did.tmp" "$target.did" ||
+ : >>"$target.did"
+ else
+ _debug "do $DO_DEPTH$target exists." >&2
+ fi
+}
+
+
+# Implementation of the "redo" command.
+_redo()
+{
+ local i startdir="$PWD" dir base
+ set +e
+ for i in "$@"; do
+ i=$(_abspath "$i" "$startdir")
+ (
+ cd "$DO_STARTDIR" || return 99
+ i=$(_realpath "$(_relpath "$i" "$PWD")" "$PWD")
+ _dirsplit "$i"
+ dir=$_dirsplit_dir base=$_dirsplit_base
+ _do "$dir" "$base"
+ )
+ [ "$?" = 0 ] || return 1
+ done
+}
+
+
+# Implementation of the "redo-whichdo" command.
+_whichdo()
+{
+ _find_dofiles "$1"
+}
+
+
+case $_cmd in
+ do|redo|redo-ifchange) _redo "$@" ;;
+ redo-whichdo) _whichdo "$1" ;;
+ do.test) ;;
+ *) printf "do: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;;
+esac
+[ "$?" = 0 ] || exit 1
+
+if [ -n "$DO_TOP" ]; then
+ if [ -n "$_do_opt_clean" ]; then
+ echo "do: Removing stamp files..." >&2
+ [ ! -e "$DO_BUILT" ] ||
+ while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
+ xargs -0 rm -f 2>/dev/null
+ fi
+fi