autoload -Uz colors; colors setopt no_unset warn_create_global extended_glob (($+xtrace)) && (($xtrace)) && set -x typeset -g FILE BASE OUT FUNCDIR TARGET FILE=$1 BASE=$2 OUT=$3 FUNCDIR=${0:h:a}/functions TARGET=${BASE##*/}.${${${DO:t}%.do}#default.} typeset -g value_counter last_var __check_deps__ value_counter=0 typeset -ga finish_hooks args typeset -gA vars vals # helper that prints out error message and exits die() { print -r - "$fg_bold[red]* $fg[cyan]${${${DO:t}%.do}#default.}:$reset_color $@" >&2 exit 1 } info() { print -r - "$fg_bold[green]* $fg[cyan]${${${DO:t}%.do}#default.}:$reset_color $@" >&2 } autoload_functions() { local f fpath=( $FUNCDIR $fpath ) for f in $FUNCDIR/*(.); do [[ $f:t =~ '^[-_[:alnum:]]*$' ]] || continue functions[$f:t]='ifchange $FUNCDIR/'$f:t' && autoload -UXz' done } # Logical variable handling ground() { (($# == 1)) || die "usage: $0 " (($+vars[$1])) || die "$0: undefined variable: ${(qqq)1}" (($+vals[$vars[$1]])) } getvars() { local -a ground_vars local k v if (($#)); then for k in $@; do ground $k && ground_vars+=( $k ) done else for k v in ${(kv)vars}; do (($+vals[$v])) && ground_vars+=( $k ) done fi printf 'local %s\n' ${(j: :)ground_vars} for k in $ground_vars; do printf '%s=$vals[$vars[%s]]\n' $k $k done } setvar() { (($# == 2)) || die "usage: $0 " if ground $1; then [[ $vals[$vars[$1]] == $2 ]] \ || die "$0: Could not unify $1 (value ${(qqq)vals[$vars[$1]]}) with ${(qqq)2}" else vals[$vars[$1]]=$2 fi } defvar() { (($# == 2)) || die "usage: $0 " # set variable only if unbound if ! (($+vars[$1])); then fresh $1 fi if ! ground $1; then setvar $1 "$2" fi } fresh() { (($# < 2)) || die "usage: $0 []" value_counter=$[value_counter + 1] last_var=${1:-_$value_counter} (($+vars[$last_var])) && die "$0: Variable already defined: $1" vars[$last_var]=$value_counter } unify_replace() { # replace internal representation of var $1 with $2, effectively unifying them local k v for k v in ${(kv)vars}; do [[ $v == $1 ]] && vars[$k]=$2 done } unify() { (($# == 2)) || die "usage: $0 " if ground $1; then if ground $2; then [[ $vals[$vars[$1]] == $vals[$vars[$2]] ]] \ || die "$0: Could not unify $1 (value ${(qqq)vals[$vars[$1]]}) with $2 (value ${(qqq)vals[$vars[$2]]})" # deduplicate value unify_replace $vars[$1] $vars[$2] unset "vals[$vars[$1]]" else unify_replace $vars[$2] $vars[$1] fi else unify_replace $vars[$1] $vars[$2] fi } args() { (($#args)) && die "$0: args already defined" local arg local -A unseen unseen=( "${(kv@)vars}" ) for arg in "$@"; do case $arg in (*=*) args+=( ${arg%%=*} ) defvar ${arg%%=*} "${arg#*=}" ;; (*!) args+=( ${arg%!} ) ground ${arg%!} \ || die "$0: Required argument ${(qqq)${arg%!}} not defined" ;; (*) args+=( $arg ) if ! (($+vars[$arg])); then fresh $arg fi ;; esac done for arg in $args; do unset "unseen[$arg]" done (($#unseen)) && die "$0: Undefined arguments passed: ${(k)unseen}" } # argument formatting decode_argstr() { local base base=${BASE##*/} [[ -z $base || $base == '%' ]] && return cat meta/$base } decode_args() { local arg decoded decoded=$(decode_argstr) \ || die "Unable to decode: ${(qqq)${BASE##*/}}" (($#decoded)) || return for arg in "${(s::Q)decoded}"; do if [[ "$arg" != *=* ]]; then echo >&2 "Malformed argument: ${(qqq)arg}" exit 1 fi fresh ${arg%%=*} setvar ${arg%%=*} "${arg#*=}" done } print_meta() { local -a arglist unifications local -A names local k v for k in ${(o)args}; do if (($+names[$vars[$k]])); then names[$vars[$k]]+=" $k" else names[$vars[$k]]=$k fi done for k v in ${(kv)names}; do [[ $v == *' '* ]] && unifications+=( $v ) (( $+vals[$k] )) && arglist+=( ${v%% *}=$vals[$k] ) done arglist=( "${(o)arglist[@]}" __check_deps__=$__check_deps__ ) (( $#unifications )) && arglist=( __unified__=${(vF)} "$arglist[@]" ) arglist=( "${(q)arglist[@]}" ) printf "%s" "${(j::)arglist}" } start_info(){ info "called: ${(qqq)$(decode_argstr)}" } add_finish_hook() { finish_hooks+=( ${(pj:\0:)@} ) } run_finish_hooks() { local hook for hook in "$finish_hooks[@]"; do "${(0@)hook}" || die "Hook failed: ${(0)hook}" done } finish_info() { local -a info info=( finished ) (($+vars[build_dir])) && eval $(getvars build_dir) (($+build_dir)) && info+=( build_dir=$build_dir ) (($+vars[filename])) && eval $(getvars filename) (($+filename)) && info+=( filename=$filename ) (($+vars[sha256sum])) && eval $(getvars sha256sum) (($+sha256sum)) && info+=( sha256sum=$sha256sum ) info $info } finish() { run_finish_hooks finish_info print_meta >$OUT exit 0 } # dependency tracking dep_add_file() { (($# < 3)) || die "usage: $0 []" local out out=$(sha256sum $1) || die "$0: Unable to checksum: ${(qqq)1}" __check_deps__+='if { test -e '${(qqq)1}$' }\n' __check_deps__+='if { pipeline -d { printf "%s\n" '${(qqq)out}$' } sha256sum -c }\n' if (($# == 2)); then setvar $2 "${out%% *}" fi } dep_add_missing() { [[ -e $1 ]] || die "$0: ${(qqq)1} exists" __check_deps__+='if -n { test -e '${(qqq)1}$' }\n' } dep_add_file_or_missing() { if [[ -f $1 ]]; then dep_add_file $1 elif [[ -e $1 ]]; then die "$0: ${(qqq)1} exists and is not a file" else dep_add_missing fi } dep_add_dir() { (($# < 3)) || die "usage: $0 []" [[ -d $1 ]] || die "$0: ${(qqq)1} does not exist or not a directory" local out out=$(tar -cp -C $1 --mtime='1970-01-01 00:00:00' . | sha256sum -) (( ${(j.|.)pipestatus} )) \ && die "$0: Failed to checksum directory: ${(qqq)1}" __check_deps__+='if { pipeline -d { printf "%s\n" '${(qqq)out}' } fdmove 3 0 pipeline -d { tar -cp "--mtime=1970-01-01 00:00:00" -C '${(qqq)1}$' . } sha256sum -c /proc/self/fd/3 }\n' if (($# == 2)); then setvar $2 "${out%% *}" fi } dep_add_dir_mtimes() { (($# < 3)) || die "usage: $0 []" [[ -d $1 ]] || die "$0: ${(qqq)1} does not exist or not a directory" local out out=$(tar -cp -C $1 . | sha256sum -) (( ${(j.|.)pipestatus} )) \ && die "$0: Failed to checksum directory: ${(qqq)1}" __check_deps__+='if { pipeline -d { printf "%s\n" '${(qqq)out}' } fdmove 3 0 pipeline -d { tar -cp -C '${(qqq)1}$' . } sha256sum -c /proc/self/fd/3 }\n' if (($# == 2)); then setvar $2 "${out%% *}" fi } ifchange() { local arg redo-ifchange "$@" || die "failed to build one or more dependencies: $*" for arg in "$@"; do __check_deps__+='if { redo-ifchange '${(qqq)arg}$' }\n' dep_add_file $arg done } exit_unchanged() { info "$DO:t unchanged, skipping build" cp $FILE $OUT exit 0 } depend() { # usage: depend [] # where: # is a dependency name as defined by dofile `default..do` # are named arguments to pass to dependency in following forms: # foo=bar -- sets argument foo to value "bar" # foo: -- unifies argument foo with variable foo in the caller # foo:bar -- unifies argument foo with variable bar in the caller local name local target local -a new_vars run local -A lift name=$1 shift # parse variable assignments while (( $# )); do case $1 in (*=*) new_vars+=( ${1%%=*}=${1#*=} );; (*:) if ! (($+vars[${1%:}])); then fresh ${1%:} fi if ground ${1%:}; then new_vars+=( ${1%:}=$vals[$vars[${1%:}]] ) else lift[${1%:}]=${1%:} fi ;; (*:*) if ! (($+vars[${1#*:}])); then fresh ${1#*:} fi if ground ${1#*:}; then new_vars+=( ${1%:*}=$vals[$vars[${1#*:}]] ) else lift[${1%:*}]=${1#*:} fi ;; (--) shift; break;; (*) die "$name: unrecognised argument: ${(qqq)1}";; esac shift done if (($#)); then run=( "$@" ) else run=( ifchange ) fi if target=$(encode-args "${(o@)new_vars}").$name \ || die "Failed to generate target name" "$run[@]" meta/$target || die "Building $name failed ($new_vars)" if (($#lift)); then [[ -f meta/$target ]] || die "Building $name produced no output ($new_vars)" # lift %variables from calee to caller local -A lift_vars local arg for arg in "${(s::Q)$(>> execline -c '"${(qqq)previous[__check_deps__]}" strace -s 512 -fe trace=execve execlineb -c $previous[__check_deps__] \ && exit_unchanged else execlineb -c $previous[__check_deps__] && exit_unchanged fi fi main || exit $? finish fi