shake
minimal build system that generates Ninja build files
git clone https://9o.is/git/shake.git
shake
(7880B)
1 #!/usr/bin/env sh
2 set -eu
3
4 SHAKE_BIN="$0"
5 TARGET=
6 TARGET_ROUTE=
7 SHAKEDIR=./.shake
8 SHAKEFILE=$SHAKEDIR/local
9 DIR=.
10 OUTDIR=.
11 NINJA_FILES=$SHAKEDIR/local.ninja
12 GEN_FILES="./Shakefile $SHAKE_BIN"
13 SHAKE_FORMAT=
14
15 usage() {
16 printf "usage: shake [options] [directory]
17 -C dir change to dir before doing anything
18 -o dir set the output directory
19 -h show this help
20 " >&2
21 exit 1
22 }
23
24 while [ $# -gt 0 ]; do
25 case "$1" in
26 -o)
27 [ $# -lt 2 ] && usage
28 OUTDIR="${2%/}"
29 shift 2
30 ;;
31 -C)
32 [ $# -lt 2 ] && usage
33 cd "$2" 2>/dev/null || {
34 printf "shake: cannot change to directory: %s\n" "$2" >&2
35 exit 1
36 }
37 shift 2
38 ;;
39 -h|--help)
40 usage
41 ;;
42 -*)
43 printf "shake: unknown option: %s\n" "$1" >&2
44 usage
45 ;;
46 *)
47 [ $# -gt 1 ] && usage
48 TARGET="$1"
49 shift
50 ;;
51 esac
52 done
53
54 import() {
55 if [ ! -f "$DIR/$1" ]; then
56 error "import file does not exist: $DIR/$1"
57 fi
58 GEN_FILES="$GEN_FILES $DIR/$1"
59 . $DIR/$1
60 }
61
62 set_target_route() {
63 TARGET_ROUTE="$TARGET_ROUTE $1"
64 if [ $1 = . ]; then return; fi
65 set_target_route $(dirname $1)
66 }
67
68 in_target_route() {
69 [ ! "$TARGET" ] || has $DIR/$1 $TARGET_ROUTE
70 }
71
72 _shake_open() {
73 _f=$SHAKEFILE
74
75 if [ ! "$TARGET" ] || [ $TARGET = "$DIR" ]; then
76 mkdir -p $SHAKEDIR
77 exec 3> $_f.ninja.tmp
78 exec 4> $_f.ins.tmp
79 exec 5> $_f.out.tmp
80 else
81 exec 3> /dev/null
82 exec 4> /dev/null
83 exec 5> /dev/null
84 fi
85 }
86
87 _shake_close() {
88 _f=$SHAKEFILE
89
90 exec 4>&-
91 exec 5>&-
92 wait
93
94 if [ ! "$TARGET" ] || [ $TARGET = "$DIR" ]; then
95 sort -u $_f.ins.tmp -o $_f.ins
96 sort -u $_f.out.tmp -o $_f.out
97 rm -f $_f.ins.tmp
98 rm -f $_f.out.tmp
99
100 if [ "${SHAKE_GROUPIN-}" ]; then
101 set -- $SHAKE_GROUPIN
102 while [ "${1-}" ] && [ "${2-}" ]; do
103 if grep -E "$2" $_f.ins 2>/dev/null >$SHAKEDIR/$1.ins; then
104 printf 'build ' >&3
105 cat $SHAKEDIR/$1.ins | tr '\n' ' ' >&3
106 printf ': phony $dir/%s\n' "$1" >&3
107 fi
108 shift 2
109 done
110 unset -v SHAKE_GROUPIN
111 fi
112
113 if [ "${SHAKE_GROUPOUT-}" ]; then
114 set -- $SHAKE_GROUPOUT
115 while [ "${1-}" ] && [ "${2-}" ]; do
116 if grep -E "$2" $_f.out 2>/dev/null >$SHAKEDIR/$1.out; then
117 printf 'build $dir/%s: phony ' "$1" >&3
118 cat $SHAKEDIR/$1.out | tr '\n' ' ' >&3
119 printf '\n' >&3
120 fi
121 shift 2
122 done
123 unset -v SHAKE_GROUPOUT
124 fi
125
126 printf 'build %s: gen | %s\n' "$shakedir/local.ninja $shakedir/local.ins $shakedir/local.out" "$GEN_FILES" >&3
127 bind description SHAKE $dir
128 printf 'build $dir/ninja: phony %s\n' "$NINJA_FILES" >&3
129 fi
130
131 exec 3>&-
132 wait
133
134 if [ ! "$TARGET" ] || [ $TARGET = "$DIR" ]; then
135 if cmp -s $_f.ninja.tmp $_f.ninja; then
136 rm -f $_f.ninja.tmp
137 else
138 mv $_f.ninja.tmp $_f.ninja
139 fi
140 fi
141 }
142
143 sub() {
144 printf 'subninja %s/%s.ninja\n' "$SHAKEDIR" "$1" >&3
145 SHAKEFILE=$SHAKEDIR/local
146 _shake_open
147 [ "${2-}" ] && eval "$2 $1"
148 $1
149 [ "${3-}" ] && eval "$3 $1"
150 _shake_close
151 }
152
153 shake() {
154 printf 'subninja %s/%s/local.ninja\n' "$SHAKEDIR" "$1" >&3
155 NINJA_FILES="$NINJA_FILES $dir/$1/ninja"
156
157 if in_target_route $1; then
158 {
159 DIR=$DIR/$1
160 OUTDIR=$OUTDIR/$1
161 SHAKEDIR=$SHAKEDIR/$1
162 SHAKEFILE=$SHAKEDIR/local
163 GEN_FILES="$GEN_FILES $DIR/Shakefile"
164 NINJA_FILES=$shakedir/local.ninja
165
166 _shake_open
167 let dir $dir/$1
168 let outdir $outdir/$1
169 let shakedir $shakedir/$1
170
171 [ "${2-}" ] && eval "$2 $1"
172 . $DIR/Shakefile
173 [ "${2-}" ] && eval "$3 $1"
174 _shake_close
175 } &
176 fi
177 }
178
179 let() {
180 printf '%s =' "$1" >&3
181 for _v in ${*:2}; do
182 printf ' %s' "$_v" >&3
183 done
184 printf '\n' >&3
185 eval "$1='\$$1'"
186 }
187
188 bind() {
189 printf ' %s =' "$1" >&3
190 if [ "$1" == description ] && [ -n "$SHAKE_FORMAT" ]; then
191 printf "$SHAKE_FORMAT\n" "$2" "${*:3}" | sed 's| |$ |g' >&3
192 else
193 for _v in ${*:2}; do
194 printf ' %s' "$_v" >&3
195 done
196 printf '\n' >&3
197 fi
198 }
199
200 _shake_prefix() {
201 case "$2" in
202 '$'*|'./'*|'../'*|'/'*)
203 printf ' %s' "$2" >&3
204 case "$1" in
205 $dir) printf '%s\n' "$2" >&4;;
206 $outdir) printf '%s\n' "$2" >&5;;
207 esac
208 ;;
209 *)
210 printf ' %s/%s' "$1" "$2" >&3
211 case "$1" in
212 $dir) printf '$dir/%s\n' "$2" >&4;;
213 $outdir) printf '$outdir/%s\n' "$2" >&5;;
214 esac
215 ;;
216 esac
217 }
218
219 _shake_build() {
220 _mode=bout
221 printf 'build' >&3
222 for _v in $*; do
223 case "$_v" in
224 '-')
225 case "$_mode" in
226 bout) printf ': %s |' $_brule >&3;;
227 bin) printf ' |' >&3;;
228 bord) error 'invalid rule';;
229 esac
230 _mode=bdep
231 ;;
232 '--')
233 case "$_mode" in
234 bout) printf ': %s ||' $_brule >&3;;
235 bin|bdep) printf ' ||' >&3;;
236 esac
237 _mode=bord
238 ;;
239 *:*)
240 if [ "$_mode" != bout ]; then
241 error 'invalid rule'
242 fi
243 if [ "${_v%%:*}" ]; then
244 _shake_prefix $_pre "${_v%%:*}"
245 fi
246
247 printf ': %s' $_brule >&3
248 _mode=bin
249 [ "$_pre" == $dir ] && _pre=$outdir || _pre=$dir
250
251 if [ "${_v##*:}" ]; then
252 _shake_prefix $_pre "${_v##*:}"
253 fi
254 ;;
255 *)
256 _shake_prefix $_pre "$_v"
257 ;;
258 esac
259 done
260 printf '\n' >&3
261
262 }
263
264 build() {
265 _brule=$1
266 _pre=$outdir
267 _shake_build ${*:2}
268 }
269
270 phony() {
271 _brule=phony
272 _pre=$dir
273 _shake_build $*
274 }
275
276 _shake_requires_arg() {
277 case "$2" in
278 *"$1:"*) return 0;;
279 *) return 1;;
280 esac
281 }
282
283 _shake_parse_args() {
284 _optstring="$1"
285 shift
286
287 _opts=
288 _deps=
289 _optshift=0
290 _optcnt=$#
291
292 while [ $# -gt 0 ]; do
293 case "$1" in
294 -*)
295 _optflag="${1#-}"
296 if _shake_requires_arg "$_optflag" "$_optstring"; then
297 _optval="$2"
298 case "$_optval" in
299 \$*dir/*)
300 if [ -z "$_deps" ]; then
301 _deps='-'
302 fi
303 _deps="$_deps $_optval"
304 ;;
305 esac
306 shift 2
307 else
308 _optval=
309 shift 1
310 fi
311 [ -n "$_optval" ] && _opts="$_opts -$_optflag $_optval" || _opts="$_opts -$_optflag"
312 ;;
313 *)
314 break
315 ;;
316 esac
317 done
318
319 _optrem=$#
320 _optshift=$(( _optcnt - _optrem ))
321 }
322
323 rule() {
324 _r=$1
325 shift
326 printf 'rule %s\n' "$_r" >&3
327
328 _optstring=
329 if [ "$1" == '-o' ]; then
330 _optstring="$2"
331 shift 2
332 fi
333
334 bind command "$*"
335
336 _d=
337 for _v in $*; do
338 case "$_v" in
339 \$*dir/*|/*|./*|../*) _d="${_d:-'-'} $_v";;
340 esac
341 done
342
343 if [ -z "$_optstring" ]; then
344 eval "$_r() { build $_r \$* $_d; }"
345 else
346 eval "$_r() { _shake_parse_args '$_optstring' \$*; shift \$_optshift; build $_r \$* \$_deps $_d; bind opts \"\$_opts\"; }"
347 fi
348 }
349
350 default() {
351 printf 'default' >&3
352 for _v in $*; do
353 _shake_prefix $dir "$_v"
354 done
355 printf '\n' >&3
356 }
357
358 groupin() {
359 SHAKE_GROUPIN="${SHAKE_GROUPIN-} $1 $2"
360 }
361
362 groupout() {
363 SHAKE_GROUPOUT="${SHAKE_GROUPOUT-} $1 $2"
364 }
365
366 error() {
367 printf "shake: %s: %s\n" "$DIR" "$*" >&2
368 exit 1
369 }
370
371 has() {
372 _val=$1
373 shift
374 for _v in $*; do
375 if [ $_v = $_val ]; then
376 return 0
377 fi
378 done
379 return 1
380 }
381
382 genfile() {
383 _f="$1"
384 case "$_f" in
385 $dir/*) _f="$DIR/${_f#*/}";;
386 $outdir/*) _f="$OUTDIR/${_f#*/}";;
387 esac
388 GEN_FILES="$GEN_FILES $_f"
389 }
390
391 foreach() {
392 while read -r line; do
393 $2 $line
394 done <$DIR/$1
395 genfile "$DIR/$1"
396 }
397
398 if [ "$TARGET" ] && [ ! -f "$TARGET/Shakefile" ]; then
399 printf "shake: target is missing Shakefile: %s\n" "$TARGET" >&2
400 exit 1
401 fi
402
403 # recursively check if there's a build directory
404 PWD="$(pwd)"
405 ROOTDIR="$PWD"
406 while [ ! -d "$ROOTDIR/$SHAKEDIR" ]; do
407 if [ ! "$ROOTDIR" ]; then
408 ROOTDIR="$(pwd)"
409 break
410 fi
411
412 ROOTDIR="${ROOTDIR%/*}"
413 done
414
415 if [ "$ROOTDIR" != "$PWD" ]; then
416 if [ "$TARGET" ]; then
417 TARGET="${PWD#$ROOTDIR}/$TARGET"
418 fi
419 cd "$ROOTDIR" 2>/dev/null || {
420 printf "shake: cannot change to root directory: %s\n" "$ROOTDIR" >&2
421 exit 1
422 }
423 PWD="$(pwd)"
424 fi
425
426 if [ ! -f Shakefile ]; then
427 printf "shake: cannot find %s/Shakefile\n" "$PWD" >&2
428 exit 1
429 fi
430
431 if [ "$TARGET" ]; then
432 TARGET="$(cd "${TARGET#/}" && pwd)"
433 case "$TARGET" in
434 $PWD) TARGET=.;;
435 $PWD/*) TARGET="./${TARGET#$PWD/}";;
436 esac
437 set_target_route $TARGET
438 fi
439
440 in='$in'
441 out='$out'
442 opts='$opts'
443
444 _shake_open
445 let ninja_required_version 1.8
446 let builddir $SHAKEDIR
447 let dir $DIR
448 let outdir $OUTDIR
449 let shakedir $SHAKEDIR
450
451 rule gen "$SHAKE_BIN $dir"
452 bind generator 1
453 bind restat 1
454
455 . $DIR/Shakefile
456
457 phony build.ninja: $dir/ninja
458 bind generator 1
459 _shake_close
460
461 if [ ! -L build.ninja ]; then
462 rm -f build.ninja
463 ln -s $SHAKEDIR/local.ninja build.ninja
464 fi