2 # redo-ifchange – bourne shell implementation of DJB redo
3 # Copyright © 2014 Nils Dagsson Moskopp (erlehmann)
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
11 # steigern. Gelegentlich packe ich sogar einen handfesten Buffer
12 # Overflow oder eine Format String Vulnerability zwischen die anderen
13 # Codezeilen und schreibe das auch nicht dran.
18 # Do not record circular dependencies.
19 [ "$parent" = "$dependency" ] && exit 1
20 local base; _dirsplit "$parent"
21 [ -d "$REDO_DIR/$dir" ] || mkdir -p "$REDO_DIR/$dir"
22 # Naive implementation: Append dependency, then remove double dependencies.
23 dependencies=$( (echo "$dependency"; cat "$REDO_DIR/$parent".dependencies 2>/dev/null ) | sort -u)
24 echo "$dependencies" > "$REDO_DIR/$parent".dependencies
25 # FIXME: Remove added dependency from non-existence dependencies.
28 _add_dependency_ne() {
31 # Do not record circular dependencies.
32 [ "$parent" = "$dependency_ne" ] && exit 1
33 # Do not record existing files as non-existence dependencies.
34 [ -e "$dependency_ne" ] && return
35 local base; _dirsplit "$parent"
36 [ -d "$REDO_DIR/$dir" ] || mkdir -p "$REDO_DIR/$dir"
37 # Naive implementation: Append dependency, then remove double dependencies.
38 dependencies_ne=$( (echo "$dependency_ne"; cat "$REDO_DIR/$parent".dependencies_ne 2>/dev/null ) | sort -u)
39 echo "$dependencies_ne" > "$REDO_DIR/$parent".dependencies_ne
40 # FIXME: Remove added non-existence dependency from dependencies.
45 # Exit if called without a target.
46 [ -z "$target" ] && exit 1
47 local base; _dirsplit "$target"
48 [ -d "$REDO_DIR/$dir" ] || mkdir -p "$REDO_DIR/$dir"
49 # If target file exists, record ctime and md5sum.
50 if [ -e "$target" ]; then
51 stat -c%Y "$target" > "$REDO_DIR/$target".ctime
52 md5sum < "$target" > "$REDO_DIR/$target".md5sum
56 _add_target_buildtime() {
58 # Portable timestamps for systems supporting GNU date format '%N'.
59 date +%s%N | tr -d '%N' > "$REDO_DIR$1".buildtime
62 _dependencies_uptodate() {
64 target_relpath=${target##$REDO_BASE/}
65 # If no dependencies exist, they are by definition up to date.
66 if [ ! -e "$REDO_DIR/$target".dependencies ]; then
67 _echo_debug_message "$REDO_DEPTH$dir$target_relpath has no dependencies."
70 _echo_debug_message "$REDO_DEPTH$dir$target_relpath non-existant dependency check:"
71 dependencies_ne=$(cat "$REDO_DIR/$target".dependencies_ne 2>/dev/null || :)
72 for dependency_ne in $dependencies_ne; do
73 dependency_ne_relpath=${dependency_ne##$REDO_BASE/}
74 _echo_debug_message "$REDO_DEPTH$dir$target_relpath depends on non-existence of $dependency_ne_relpath"
75 # If a non-existence dependency exists, it is out of date.
76 # Dependencies, e.g. on default.do files may also be out of date.
77 # Naive implementation: Delete dependency information and rebuild.
78 if [ -e "$dependency_ne" ]; then
79 rm "$REDO_DIR/$target".dependencies
80 rm "$REDO_DIR/$target".dependencies_ne
82 # If a non-existence dependency does not exist, but is also a
83 # dependency, it existed in the past. Naive implementation: Delete
84 # dependency information and pretend the (actually up-to-date)
85 # dependency is out of date to generate dependency information.
86 # TODO: Only remove non-existence dependency from dependencies,
87 # and do no return at this point; maybe escape filename properly.
88 elif (grep -q '^'"$dependency_ne"'$' "$REDO_DIR/$target".dependencies \
90 rm "$REDO_DIR/$target".dependencies
94 _echo_debug_message "$REDO_DEPTH$dir$target_relpath non-existence dependencies uptodate."
95 _echo_debug_message "$REDO_DEPTH$dir$target_relpath dependency check:"
96 if [ "$REDO_SHUFFLE" = "1" ]; then
97 dependencies=$(shuf "$REDO_DIR/$target".dependencies 2>/dev/null || :)
99 dependencies=$(cat "$REDO_DIR/$target".dependencies 2>/dev/null || :)
101 for dependency in $dependencies; do
102 dependency_relpath=${dependency##$REDO_BASE/}
104 _echo_debug_message "$REDO_DEPTH$dir$target_relpath depends on $dependency_relpath"
105 if ( ! _is_uptodate "$dependency" ); then
106 _echo_debug_message \
107 "$REDO_DEPTH$dir$target_relpath dependency $dependency_relpath not uptodate."
110 if ( ! _dependencies_uptodate "$dependency"); then
111 _echo_debug_message \
112 "$REDO_DEPTH$dir$target_relpath dependency $dependency_relpath dependencies not uptodate."
115 # In the case that two targets depend on the same target and are
116 # built after another, when the second target is built, redo finds
117 # its dependencies to be up-to-date. This implies that the second
118 # target would not be built. If a dependency build timestamp is
119 # greater than or equal to a targets build timestamp, assume the
120 # dependency has been rebuilt and the target should be rebuilt.
121 if [ -e "$REDO_DIR/$target_abspath.buildtime" ]; then \
122 read target_ts < "$REDO_DIR/$target_abspath.buildtime";
126 if [ -e "$REDO_DIR/$dependency.buildtime" ]; then
127 read dependency_ts < "$REDO_DIR/$dependency.buildtime"
131 if [ "$dependency_ts" -ge "$target_ts" ]; then
132 _echo_debug_message \
133 "$REDO_DEPTH$dir$target_relpath dependency $dependency_relpath uptodate, but built later than $target_relpath."
137 _echo_debug_message "$REDO_DEPTH$dir$target_relpath dependencies uptodate."
148 xdir="$1" xbase="$2" xbasetmp="$2"
149 while [ ! -d "$xdir" -a -n "$xdir" ]; do
150 _dirsplit "${xdir%/}"
151 xbasetmp=${base}__"$xbase"
152 xdir="$dir" xbase="$base"/"$xbase"
153 echo "xbasetmp='$xbasetmp'" >&2
158 local dir="$1" target="$2" tmp="$3"
159 # Add target to parent targets dependencies.
160 target_abspath="$PWD/$target"
161 if [ -n "$REDO_TARGET" ]; then
162 _add_dependency "$REDO_TARGET" "$target_abspath"
164 target_relpath="${target_abspath##$REDO_BASE/}"
165 if ( ! _is_uptodate "$target_abspath" || ! _dependencies_uptodate "$target_abspath" ); then
169 [ -e "$target.do" ] || _find_dofile "$target"
170 # Add non existing .do file to non-existence dependencies so target is built when
171 # .do file in question is created.
172 _add_dependency_ne "$target_abspath" "$PWD/$target.do"
173 if [ ! -e "$dofile" ]; then
174 # If .do file does not exist and target exists, it is a source file.
175 if [ -e "$target" ]; then
176 _add_target "$target_abspath"
177 _add_target_buildtime "$target_abspath"
179 # If .do file does not exist and target does not exist, stop.
181 echo "redo: $target: no .do file" >&2
184 # Add .do file to dependencies so target is built when .do file changes.
186 _add_dependency "$target_abspath" "$PWD/$dofile"
187 _add_target "$PWD/$dofile"
189 printf '%sredo %s%s%s%s%s\n' \
190 "$green" "$REDO_DEPTH" "$bold" "$target_relpath" "$plain" >&2
191 ( _run_dofile "$target" "$base" "$tmp.tmp" )
193 if [ $rv != 0 ]; then
194 rm -f "$tmp.tmp" "$tmp.tmp2"
195 # Exit code 123 conveys that target was considered uptodate at runtime.
196 if [ $rv != 123 ]; then
197 printf "%sredo: %s%s: got exit code %s.%s\n" \
198 "$red" "$REDO_DEPTH" "$target" "$rv" "$plain" >&2
202 mv "$tmp.tmp" "$target" 2>/dev/null ||
203 ! test -s "$tmp.tmp2" ||
204 mv "$tmp.tmp2" "$target" 2>/dev/null
206 _add_target_buildtime "$target_abspath"
208 _echo_debug_message "$REDO_DEPTH$dir$target is up to date."
210 # Some do files (like all.do) do not usually generate output.
211 if [ -e "$target" ]; then
212 _add_target "$target_abspath"
216 _echo_debug_message() {
217 [ "$REDO_DEBUG" = "1" ] && echo "$@" >&2
223 _find_dofile_pwd "$1"
224 [ -e "$dofile" ] && break
225 [ "$PWD" = "/" ] && break
226 target=${PWD##*/}/"$target"
227 tmp=${PWD##*/}/"$tmp"
228 prefix=${PWD##*/}/"$prefix"
231 base="$prefix""$base"
235 dofile=default."$1".do
237 dofile=default.${dofile#default.*.}
238 [ -e "$dofile" -o "$dofile" = default.do ] && break
240 ext=${dofile#default}
247 target_relpath=${target##$REDO_BASE/}
248 # If a target does not exist, it is by definition out of date.
249 if [ ! -e "$target" ]; then
250 _echo_debug_message "$REDO_DEPTH$dir$target_relpath does not exist."
253 # If a file exists, but has no build date, it is by a previously unseen
254 # source file. A previously unseen source file is by definition up to date.
255 if [ ! -e "$REDO_DIR/$target".ctime ]; then
256 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.ctime does not exist."
257 # Add source file to dependencies so target is built when source file changes.
258 _add_target "$target_abspath"
261 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.ctime exists."
262 # If a file exists and has an entry in the always database, it is never
264 if [ -e "$REDO_DIR/$target".always ]; then
265 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.always exists."
268 # If a file exists and has an entry in the stamp database, it might not
269 # be up to date. redo-stamp decides if the file is up to date at runtime.
270 if [ -e "$REDO_DIR/$target".stamp ]; then
271 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.stamp exists."
274 # If a file exists and has an entry in the dependency database and ctime
275 # is the same as in the entry in the ctime database, it is up to date.
276 if (export LC_ALL=C; stat -c%Y "$target" | grep -Fqx -f "$REDO_DIR/$target".ctime); then
277 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.ctime match."
280 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.ctime mismatch."
281 # If a file exists and has an entry in the dependency database and
282 # ctime is different from the entry in the ctime database, but md5sum
283 # is the same as md5sum in the md5sum database, it is up to date.
284 if (md5sum < "$target" | grep -Fqx -f "$REDO_DIR/$target".md5sum); then
285 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.md5sum match."
288 _echo_debug_message "$REDO_DEPTH$dir$target_relpath.md5sum mismatch."
298 export REDO_DEPTH="$REDO_DEPTH "
299 export REDO_TARGET="$PWD"/"$target"
302 read line1 <"$PWD/$dofile" || true
304 # If the first line of a do file does not have a hashbang (#!), use /bin/sh.
305 if [ "$cmd" = "$line1" ] || [ "$cmd" = "/bin/sh" ]; then
306 if [ "$REDO_XTRACE" = "1" ]; then
312 $cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
318 [ "$REDO_SHUFFLE" = "1" ] && targets=$(shuf -e "$@")
319 for target in $targets; do
321 _dir_shovel "$dir" "$base"
322 dir="$xdir" base="$xbase" basetmp="$xbasetmp"
323 ( cd "$dir" && _do "$dir" "$base" "$basetmp" )
324 [ "$?" = 0 ] || exit 1