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