home · contact · privacy
Update erlehmann's redo scripts.
[plomrogue] / build / redo_scripts / redo-ifchange
1 #!/bin/sh
2 # redo-ifchange – bourne shell implementation of DJB redo
3 # Copyright © 2014  Nils Dagsson Moskopp (erlehmann)
4
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.
9
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.
14
15 _add_dependency() {
16   parent="$1"
17   dependency="$2"
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.
26 }
27
28 _add_dependency_ne() {
29   parent="$1"
30   dependency_ne="$2"
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.
41 }
42
43 _add_target() {
44   local target="$1"
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
53   fi
54 }
55
56 _add_target_buildtime() {
57   [ -z "$1" ] && exit 1
58   # Portable timestamps for systems supporting GNU date format '%N'.
59   date +%s%N | tr -d '%N' > "$REDO_DIR$1".buildtime
60 }
61
62 _dependencies_uptodate() {
63   target="$1"
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."
68     return 0
69   fi
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
81       return 1
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 \
89       2>/dev/null); then
90       rm "$REDO_DIR/$target".dependencies
91       return 1
92     fi
93   done
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 || :)
98   else
99     dependencies=$(cat "$REDO_DIR/$target".dependencies 2>/dev/null || :)
100   fi
101   for dependency in $dependencies; do
102     dependency_relpath=${dependency##$REDO_BASE/}
103     dir=''
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."
108       return 1
109     fi
110     if ( ! _dependencies_uptodate "$dependency"); then
111       _echo_debug_message \
112         "$REDO_DEPTH$dir$target_relpath dependency $dependency_relpath dependencies not uptodate."
113       return 1
114     fi
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";
123     else
124       target_ts=0
125     fi
126     if [ -e "$REDO_DIR/$dependency.buildtime" ]; then
127       read dependency_ts < "$REDO_DIR/$dependency.buildtime"
128     else
129        dependency_ts=0
130     fi
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."
134       return 1
135     fi
136   done
137   _echo_debug_message "$REDO_DEPTH$dir$target_relpath dependencies uptodate."
138   return 0
139 }
140
141 _dirsplit() {
142   base=${1##*/}
143   dir=${1%"$base"}
144 }
145
146 _dir_shovel() {
147   local dir base
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
154   done
155 }
156
157 _do() {
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"
163   fi
164   target_relpath="${target_abspath##$REDO_BASE/}"
165   if ( ! _is_uptodate "$target_abspath" || ! _dependencies_uptodate "$target_abspath" ); then
166     dofile="$target".do
167     base="$target"
168     ext=
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"
178         return 0
179       # If .do file does not exist and target does not exist, stop.
180       else
181         echo "redo: $target: no .do file" >&2
182         exit 1
183       fi
184     # Add .do file to dependencies so target is built when .do file changes.
185     else
186       _add_dependency "$target_abspath" "$PWD/$dofile"
187       _add_target "$PWD/$dofile"
188     fi
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" )
192     rv="$?"
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
199         exit 1
200       fi
201     fi
202     mv "$tmp.tmp" "$target" 2>/dev/null ||
203     ! test -s "$tmp.tmp2" ||
204     mv "$tmp.tmp2" "$target" 2>/dev/null
205     rm -f "$tmp.tmp2"
206     _add_target_buildtime "$target_abspath"
207   else
208     _echo_debug_message "$REDO_DEPTH$dir$target is up to date."
209   fi
210   # Some do files (like all.do) do not usually generate output.
211   if [ -e "$target" ]; then
212     _add_target "$target_abspath"
213   fi
214 }
215
216 _echo_debug_message() {
217   [ "$REDO_DEBUG" = "1" ] && echo "$@" >&2
218 }
219
220 _find_dofile() {
221   local prefix=
222   while :; do
223     _find_dofile_pwd "$1"
224     [ -e "$dofile" ] && break
225     [ "$PWD" = "/" ] && break
226     target=${PWD##*/}/"$target"
227     tmp=${PWD##*/}/"$tmp"
228     prefix=${PWD##*/}/"$prefix"
229     cd ..
230   done
231   base="$prefix""$base"
232 }
233
234 _find_dofile_pwd() {
235   dofile=default."$1".do
236   while :; do
237     dofile=default.${dofile#default.*.}
238     [ -e "$dofile" -o "$dofile" = default.do ] && break
239   done
240   ext=${dofile#default}
241   ext=${ext%.do}
242   base=${1%$ext}
243 }
244
245 _is_uptodate() {
246   target="$1"
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."
251     return 1
252   else
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"
259       return 0
260     else
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
263       # up to date.
264       if [ -e "$REDO_DIR/$target".always ]; then
265         _echo_debug_message "$REDO_DEPTH$dir$target_relpath.always exists."
266         return 1
267       fi
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."
272         return 1
273       fi
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."
278         return 0
279       else
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."
286           return 0
287         else
288           _echo_debug_message "$REDO_DEPTH$dir$target_relpath.md5sum mismatch."
289           return 1
290         fi
291       fi
292     fi
293   fi
294   return 1
295 }
296
297 _run_dofile() {
298   export REDO_DEPTH="$REDO_DEPTH  "
299   export REDO_TARGET="$PWD"/"$target"
300   local line1
301   set -e
302   read line1 <"$PWD/$dofile" || true
303   cmd=${line1#"#!"}
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
307       cmd="/bin/sh -ex"
308     else
309       cmd="/bin/sh -e"
310     fi
311   fi
312   $cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
313 }
314
315 set +e
316 if [ -n "$1" ]; then
317   targets="$@"
318   [ "$REDO_SHUFFLE" = "1" ] && targets=$(shuf -e "$@")
319   for target in $targets; do
320     _dirsplit "$target"
321     _dir_shovel "$dir" "$base"
322     dir="$xdir" base="$xbase" basetmp="$xbasetmp"
323     ( cd "$dir" && _do "$dir" "$base" "$basetmp" )
324     [ "$?" = 0 ] || exit 1
325   done
326 fi