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