home · contact · privacy
Upgrade to newest version of 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_ctime_md5sum() {
16   target=$1
17   local base; _dirsplit "$target"
18   [ -d "$REDO_DIR/$dir" ] || LANG=C mkdir -p "$REDO_DIR/$dir"
19   LANG=C stat -c%Y "$target" >"$REDO_DIR/$target".ctime
20   LANG=C md5sum <"$target" >"$REDO_DIR/$target".md5sum
21 }
22
23 _add_dependency() {
24   parent=$1
25   dependency=$2
26   # Do not record circular dependencies.
27   [ "$parent" = "$dependency" ] && exit 1
28   local base; _dirsplit "$parent"
29   [ -d "$REDO_DIR/$dir" ] || LANG=C mkdir -p "$REDO_DIR/$dir"
30   ctime="$(LANG=C stat -c%Y "$dependency")"
31   md5sum="$(LANG=C md5sum < "$dependency")"
32   printf '%s\t%s\t%s\n' "$dependency" "$ctime" "$md5sum" >> \
33     "$REDO_DIR"/"$parent".dependencies.tmp
34 }
35
36 _add_dependency_ne() {
37   parent="$1"
38   dependency_ne="$2"
39   # Do not record circular dependencies.
40   [ "$parent" = "$dependency_ne" ] && exit 1
41   # Do not record existing files as non-existence dependencies.
42   [ -e "$dependency_ne" ] && return
43   local base; _dirsplit "$parent"
44   [ -d "$REDO_DIR/$dir" ] || mkdir -p "$REDO_DIR/$dir"
45   printf '%s\n' "$dependency_ne" >> "$REDO_DIR/$parent".dependencies_ne.tmp
46 }
47
48 _dependencies_ne_uptodate() {
49   target=$1
50   # If no non-existence dependencies exist, they are by definition up to date.
51   if [ ! -s "$REDO_DIR/$target".dependencies_ne ]; then
52     _echo_debug_message "$target has no non-existence dependencies."
53     return 0
54   fi
55   _echo_debug_message "$target non-existence dependency check:"
56   while read dependency_ne; do
57     _echo_debug_message "\t$dependency_ne should not exist."
58     # If a non-existence dependency exists, it is out of date.
59     # Dependencies, e.g. on default.do files may also be out of date.
60     # Naive implementation: Pretend target is not up to date and rebuild.
61     if [ -e "$dependency_ne" ]; then
62       _echo_debug_message "\t$dependency_ne does exist."
63       return 1
64     fi
65     _echo_debug_message "\t$dependency_ne does not exist."
66   done < "$REDO_DIR/$target".dependencies_ne
67   _echo_debug_message "$target non-existence dependencies uptodate."
68   return 0
69 }
70
71 _target_uptodate() {
72   target=$1
73   # If a target does not exist, it is not up to date.
74   if [ ! -e "$target" ]; then
75     _echo_debug_message "$target does not exist."
76     return 1
77   fi
78   # If an .always file exists, the target is out of date.
79   if [ -e "$REDO_DIR/$target".always ]; then
80     _echo_debug_message "$target is always out of date."
81     return 1
82   fi
83   # If .stamp file exists, the target is out of date.
84   if [ -e "$REDO_DIR/$target".stamp ]; then
85     _echo_debug_message "$target is out of date due to redo-stamp."
86     return 1
87   fi
88   if [ -e "$REDO_DIR/$target".ctime ]; then
89     read ctime_stored <"$REDO_DIR/$target".ctime
90     ctime_actual="$(LANG=C stat -c%Y "$target")"
91     _echo_debug_message "$target stored ctime $ctime_stored"
92     _echo_debug_message "$target actual ctime $ctime_actual"
93     if [ "$ctime_stored" = "$ctime_actual" ]; then
94       _echo_debug_message "$target up to date."
95       return 0
96     elif [ -e "$REDO_DIR/$target".md5sum ]; then
97       read md5sum_stored <"$REDO_DIR/$target".md5sum
98       md5sum_actual="$(LANG=C md5sum < "$target")"
99       _echo_debug_message "$target stored md5sum $md5sum_stored"
100       _echo_debug_message "$target actual md5sum $md5sum_actual"
101       if [ "$md5sum_stored" = "$md5sum_actual" ]; then
102         # If stored md5sum of target matches actual md5sum, but stored
103         # ctime does not, redo needs to update stored ctime of target.
104         LANG=C stat -c%Y "$target" >"$REDO_DIR/$target".md5sum
105         return 0
106       fi
107       _echo_debug_message "$target out of date."
108       return 1
109     fi
110   fi
111 }
112
113 _dependencies_uptodate() {
114   target=$1
115   # If no dependencies exist, they are by definition up to date.
116   if [ ! -e "$REDO_DIR/$target".dependencies ]; then
117     _echo_debug_message "$target has no dependencies."
118     return 0
119   fi
120   if [ "$REDO_SHUFFLE" = "1" ]; then
121     shuf "$REDO_DIR/$target".dependencies -o "$REDO_DIR/$target".dependencies \
122       2>&-
123   fi
124   _echo_debug_message "$target dependency check:"
125   # If any dependency does not exist, the target is out of date.
126   LANG=C stat -c%Y $(LANG=C cut -f1 "$REDO_DIR/$target".dependencies) > \
127     "$REDO_DIR/".dependencies.ctimes 2>&- || return 1
128   exec 3< "$REDO_DIR/".dependencies.ctimes
129   exec 4< "$REDO_DIR/$target".dependencies
130   while read ctime_actual <&3 && read dependency ctime_stored md5sum_stored <&4; do
131     # If a .always file exists, the dependency is out of date.
132     if [ -e "$REDO_DIR/$dependency".always ]; then
133       _echo_debug_message "\t$dependency is always out of date."
134       return 1
135     fi
136     # If a .stamp file exists, the dependency is out of date.
137     if [ -e "$REDO_DIR/$dependency".stamp ]; then
138       _echo_debug_message "\t$dependency is always out of date."
139       return 1
140     fi
141     # If a dependency of a dependency is out of date, the dependency is out of date.
142     if ( ! _dependencies_uptodate "$dependency" ); then
143       return 1
144     fi
145     # If the ctime of a dependency did not change, the dependency is up to date.
146     _echo_debug_message "\t$dependency stored ctime $ctime_stored"
147     _echo_debug_message "\t$dependency actual ctime $ctime_actual"
148     if [ "$ctime_stored" = "$ctime_actual" ]; then
149       _echo_debug_message "\t$dependency up to date."
150       continue
151     fi
152     # If the md5sum of a dependency did not change, the dependency is up to date.
153     md5sum_actual="$(LANG=C md5sum < "$dependency")"
154     _echo_debug_message "\t$dependency stored md5sum $md5sum_stored"
155     _echo_debug_message "\t$dependency actual md5sum $md5sum_actual"
156     if [ "$md5sum_stored" = "$md5sum_actual" ]; then
157       _echo_debug_message "\t$dependency up to date."
158       continue
159     else
160       # if both ctime and md5sum did change, the dependency is out of date.
161       _echo_debug_message "\t$dependency out of date."
162       return 1
163     fi
164   done
165   exec 4>&-
166   exec 3>&-
167   _echo_debug_message "$target dependencies up to date."
168   # If a non-existence dependency is out of date, the target is out of date.
169   if ( ! _dependencies_ne_uptodate "$target" ); then
170     return 1
171   fi
172   return 0
173 }
174
175 _dirsplit() {
176   base=${1##*/}
177   dir=${1%"$base"}
178 }
179
180 _do() {
181   local dir="$1" target="$2" tmp="$3"
182   target_abspath="$PWD/$target"
183   target_relpath="${target_abspath##$REDO_BASE/}"
184   # If target is not up to date or its dependencies are not up to date, build it.
185   if (
186     ! _target_uptodate "$target_abspath" || \
187     ! _dependencies_uptodate "$target_abspath"
188   ); then
189     dofile="$target".do
190     base="$target"
191     ext=
192     [ -e "$target.do" ] || _find_dofile "$target"
193     if [ ! -e "$dofile" ]; then
194       # If .do file does not exist and target exists, it is a source file.
195       if [ -e "$target" ]; then
196         return 0
197       # If .do file does not exist and target does not exist, stop.
198       else
199         echo "redo: $target: no .do file" >&2
200         exit 1
201       fi
202     fi
203     printf '%sredo %s%s%s%s%s\n' \
204       "$green" "$REDO_DEPTH" "$bold" "$target_relpath" "$plain" >&2
205     ( _run_dofile "$target" "$base" "$tmp.tmp" )
206     rv="$?"
207     # Add non existing .do file to non-existence dependencies so
208     # target is built when .do file in question is created.
209     [ -e "$target.do" ] || _add_dependency_ne "$target_abspath" "$PWD/$target.do"
210     # Add .do file to dependencies so target is built when .do file changes.
211     _add_dependency "$target_abspath" "$PWD/$dofile"
212     if [ $rv != 0 ]; then
213       rm -f "$tmp.tmp" "$tmp.tmp2"
214       # Exit code 123 conveys that target was considered uptodate at runtime.
215       if [ $rv != 123 ]; then
216         printf "%sredo: %s%s: got exit code %s.%s\n" \
217           "$red" "$REDO_DEPTH" "$target" "$rv" "$plain" >&2
218         exit 1
219       fi
220     fi
221     mv "$tmp.tmp" "$target" 2>&- ||
222     ! test -s "$tmp.tmp2" ||
223     mv "$tmp.tmp2" "$target" 2>&-
224     rm -f "$tmp.tmp2"
225     # After build is finished, update dependencies.
226     touch "$REDO_DIR/$target_abspath".dependencies.tmp
227     touch "$REDO_DIR/$target_abspath".dependencies_ne.tmp
228     mv "$REDO_DIR/$target_abspath".dependencies.tmp \
229       "$REDO_DIR/$target_abspath".dependencies >&2
230     mv "$REDO_DIR/$target_abspath".dependencies_ne.tmp \
231       "$REDO_DIR/$target_abspath".dependencies_ne >&2
232   fi
233   # Some do files (like all.do) do not usually generate output.
234   if [ -e "$target" ]; then
235     # Record dependency on parent target.
236     _add_dependency "$REDO_TARGET" "$target_abspath"
237     _add_ctime_md5sum "$target_abspath"
238   fi
239 }
240
241 _echo_debug_message() {
242   [ "$REDO_DEBUG" = "1" ] && echo "$@" >&2
243 }
244
245 _find_dofile() {
246   local prefix=
247   while :; do
248     _find_dofile_pwd "$1"
249     [ -e "$dofile" ] && break
250     [ "$PWD" = "/" ] && break
251     target=${PWD##*/}/"$target"
252     tmp=${PWD##*/}/"$tmp"
253     prefix=${PWD##*/}/"$prefix"
254     cd ..
255   done
256   base="$prefix""$base"
257 }
258
259 _find_dofile_pwd() {
260   dofile=default."$1".do
261   while :; do
262     dofile=default.${dofile#default.*.}
263     [ -e "$dofile" -o "$dofile" = default.do ] && break
264   done
265   ext=${dofile#default}
266   ext=${ext%.do}
267   base=${1%$ext}
268 }
269
270 _run_dofile() {
271   export REDO_DEPTH="$REDO_DEPTH  "
272   export REDO_TARGET="$PWD"/"$target"
273   local line1
274   set -e
275   read line1 <"$PWD/$dofile" || true
276   cmd=${line1#"#!"}
277   # If the first line of a do file does not have a hashbang (#!), use /bin/sh.
278   if [ "$cmd" = "$line1" ] || [ "$cmd" = "/bin/sh" ]; then
279     if [ "$REDO_XTRACE" = "1" ]; then
280       cmd="/bin/sh -ex"
281     else
282       cmd="/bin/sh -e"
283     fi
284   fi
285   $cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
286 }
287
288 set +e
289 if [ -n "$1" ]; then
290   targets="$@"
291   [ "$REDO_SHUFFLE" = "1" ] && targets=$(LANG=C shuf -e "$@")
292   for target in $targets; do
293     # If relative path to target is given, convert to absolute absolute path.
294     case "$target" in
295       /*) ;;
296       *)  target="$PWD"/"$target" >&2;;
297     esac
298     _dirsplit "$target"
299     ( cd "$dir" && _do "$dir" "$base" "$base" )
300     [ "$?" = 0 ] || exit 1
301   done
302 fi