#!/bin/sh # redo-ifchange – 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. _add_ctime_md5sum() { target=$1 local base; _dirsplit "$target" [ -d "$REDO_DIR/$dir" ] || LANG=C mkdir -p "$REDO_DIR/$dir" LANG=C stat -c%Y "$target" >"$REDO_DIR/$target".ctime LANG=C md5sum <"$target" >"$REDO_DIR/$target".md5sum } _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 mkdir -p "$REDO_DIR/$dir" ctime="$(LANG=C stat -c%Y "$dependency")" md5sum="$(LANG=C md5sum < "$dependency")" printf '%s\t%s\t%s\n' "$dependency" "$ctime" "$md5sum" >> \ "$REDO_DIR"/"$parent".dependencies.tmp } _add_dependency_ne() { parent="$1" dependency_ne="$2" # Do not record circular dependencies. [ "$parent" = "$dependency_ne" ] && exit 1 # Do not record existing files as non-existence dependencies. [ -e "$dependency_ne" ] && return local base; _dirsplit "$parent" [ -d "$REDO_DIR/$dir" ] || mkdir -p "$REDO_DIR/$dir" printf '%s\n' "$dependency_ne" >> "$REDO_DIR/$parent".dependencies_ne.tmp } _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 has no non-existence dependencies." return 0 fi _echo_debug_message "$target non-existence dependency check:" while read dependency_ne; do _echo_debug_message "\t$dependency_ne 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 "\t$dependency_ne does exist." return 1 fi _echo_debug_message "\t$dependency_ne does not exist." done < "$REDO_DIR/$target".dependencies_ne _echo_debug_message "$target non-existence dependencies uptodate." return 0 } _target_uptodate() { target=$1 # If a target is a top-level target, it is not up to date. if [ -z "$REDO_TARGET" ]; then return 1 fi # 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 # If an .always file exists, the target is out of date. if [ -e "$REDO_DIR/$target".always ]; then _echo_debug_message "$target is always out of date." return 1 fi # If .stamp file exists, the target is out of date. if [ -e "$REDO_DIR/$target".stamp ]; then _echo_debug_message "$target is out of date due to redo-stamp." return 1 fi if [ -e "$REDO_DIR/$target".ctime ]; then read ctime_stored <"$REDO_DIR/$target".ctime ctime_actual="$(LANG=C stat -c%Y "$target")" _echo_debug_message "$target stored ctime $ctime_stored" _echo_debug_message "$target actual ctime $ctime_actual" if [ "$ctime_stored" = "$ctime_actual" ]; then _echo_debug_message "$target up to date." return 0 elif [ -e "$REDO_DIR/$target".md5sum ]; then read md5sum_stored <"$REDO_DIR/$target".md5sum md5sum_actual="$(LANG=C md5sum < "$target")" _echo_debug_message "$target stored md5sum $md5sum_stored" _echo_debug_message "$target actual md5sum $md5sum_actual" if [ "$md5sum_stored" = "$md5sum_actual" ]; then # If stored md5sum of target matches actual md5sum, but stored # ctime does not, redo needs to update stored ctime of target. LANG=C stat -c%Y "$target" >"$REDO_DIR/$target".md5sum return 0 fi _echo_debug_message "$target out of date." return 1 fi fi } _dependencies_uptodate() { target=$1 # If no dependencies exist, they are by definition up to date. if [ ! -e "$REDO_DIR/$target".dependencies ]; then _echo_debug_message "$target has no dependencies." return 0 fi if [ "$REDO_SHUFFLE" = "1" ]; then shuf "$REDO_DIR/$target".dependencies -o "$REDO_DIR/$target".dependencies \ 2>&- fi _echo_debug_message "$target dependency check:" # If any dependency does not exist, the target is out of date. LANG=C stat -c%Y $(LANG=C cut -f1 "$REDO_DIR/$target".dependencies) > \ "$REDO_DIR/$target".dependencies.ctimes 2>&- || return 1 exec 3< "$REDO_DIR/$target".dependencies.ctimes exec 4< "$REDO_DIR/$target".dependencies while read ctime_actual <&3 && read dependency ctime_stored md5sum_stored <&4; do # If a .always file exists, the dependency is out of date. if [ -e "$REDO_DIR/$dependency".always ]; then _echo_debug_message "\t$dependency is always out of date." return 1 fi # If a .stamp file exists, the dependency is out of date. if [ -e "$REDO_DIR/$dependency".stamp ]; then _echo_debug_message "\t$dependency is always out of date." return 1 fi # 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 "\t$dependency stored ctime $ctime_stored" _echo_debug_message "\t$dependency actual ctime $ctime_actual" if [ "$ctime_stored" = "$ctime_actual" ]; then _echo_debug_message "\t$dependency up to date." continue fi # If the md5sum of a dependency did not change, the dependency is up to date. md5sum_actual="$(LANG=C md5sum < "$dependency")" _echo_debug_message "\t$dependency stored md5sum $md5sum_stored" _echo_debug_message "\t$dependency actual md5sum $md5sum_actual" if [ "$md5sum_stored" = "$md5sum_actual" ]; then _echo_debug_message "\t$dependency up to date." continue else # if both ctime and md5sum did change, the dependency is out of date. _echo_debug_message "\t$dependency out of date." return 1 fi done exec 4>&- exec 3>&- _echo_debug_message "$target dependencies up to date." # If a non-existence dependency is out of date, the target is out of date. if ( ! _dependencies_ne_uptodate "$target" ); then return 1 fi return 0 } _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="$target".do base="$target" ext= [ -e "$target.do" ] || _find_dofile "$target" if [ ! -e "$dofile" ]; 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" _add_ctime_md5sum "$target_abspath" return 0 # If .do file does not exist and target does not exist, stop. else echo "redo: $target: no .do file" >&2 exit 1 fi fi printf '%sredo %s%s%s%s%s\n' \ "$green" "$REDO_DEPTH" "$bold" "$target_relpath" "$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. [ -e "$target.do" ] || _add_dependency_ne "$target_abspath" "$PWD/$target.do" # Add .do file to dependencies so target is built when .do file changes. _add_dependency "$target_abspath" "$PWD/$dofile" if [ $rv != 0 ]; then rm -f "$tmp.tmp" "$tmp.tmp2" # Exit code 123 conveys that target was considered uptodate at runtime. if [ $rv != 123 ]; then printf "%sredo: %s%s: got exit code %s.%s\n" \ "$red" "$REDO_DEPTH" "$target" "$rv" "$plain" >&2 exit 1 fi fi mv "$tmp.tmp" "$target" 2>&- || ! test -s "$tmp.tmp2" || mv "$tmp.tmp2" "$target" 2>&- rm -f "$tmp.tmp2" # After build is finished, update dependencies. touch "$REDO_DIR/$target_abspath".dependencies.tmp touch "$REDO_DIR/$target_abspath".dependencies_ne.tmp mv "$REDO_DIR/$target_abspath".dependencies.tmp \ "$REDO_DIR/$target_abspath".dependencies >&2 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. _add_dependency "$REDO_TARGET" "$target_abspath" _add_ctime_md5sum "$target_abspath" fi } _echo_debug_message() { [ "$REDO_DEBUG" = "1" ] && echo "$@" >&2 } _find_dofile() { local prefix= while :; do _find_dofile_pwd "$1" [ -e "$dofile" ] && break [ "$PWD" = "/" ] && break target=${PWD##*/}/"$target" tmp=${PWD##*/}/"$tmp" prefix=${PWD##*/}/"$prefix" cd .. done base="$prefix""$base" } _find_dofile_pwd() { dofile=default."$1".do while :; do dofile=default.${dofile#default.*.} [ -e "$dofile" -o "$dofile" = default.do ] && break done ext=${dofile#default} ext=${ext%.do} base=${1%$ext} } _run_dofile() { export REDO_DEPTH="$REDO_DEPTH " export REDO_TARGET="$PWD"/"$target" local line1 set -e read line1 <"$PWD/$dofile" || 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 "$PWD/$dofile" "$@" >"$tmp.tmp2" } set +e if [ -n "$1" ]; then targets="$@" [ "$REDO_SHUFFLE" = "1" ] && targets=$(LANG=C shuf -e "$@") for target in $targets; do # If relative path to target is given, convert to absolute absolute path. case "$target" in /*) ;; *) target="$PWD"/"$target" >&2;; esac _dirsplit "$target" ( cd "$dir" && _do "$dir" "$base" "$base" ) [ "$?" = 0 ] || exit 1 done fi