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