# Mage Build Tool by Matt McCutchen
# http://www.kepreon.com/~matt/mage/
+
# Remember the original default goal so we can restore it at the end of mage.mk.
mg-orig-default-goal:=$(.DEFAULT_GOAL)
# TEXT UTILITIES
empty:=
bs:=\$(empty)
+hash:=\#
pct:=%
define nl
sq='$(subst ','\'',$1)'
# Make-quote: a$\nb => a$$$(nl)b
# This is enough to assign the value, *but not to use it as an argument!*
-mqas=$(subst $(nl),$$(nl),$(subst $$,$$$$,$1))
+mqas=$(subst $(hash),$$(hash),$(subst $(nl),$$(nl),$(subst $$,$$$$,$1)))
# Return nonempty if the strings are equal, empty otherwise.
# If the strings have only nice characters, you can do $(filter x$1,x$2).
streq=$(findstring x$1,$(findstring x$2,x$1))
# Replaces references to automatic variables with references to their Mage-ized
# counterparts. There might be false matches, e.g., $$@ => $$(mg@) ;
# to prevent that, write $$$(empty)@ instead. (c.f. autoconf empty quadrigraph)
-define mg-translate-cmd
-$(subst $$?,$$(mg?),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1))))
-endef
+mg-translate-cmd=$(subst $$t,$$@.tmp,$(subst $$?,$$(mg?),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1)))))
-# $(call prereq-predict,target,prerequisites)
+# $(call mg-prereq-predict,target,prerequisites)
# Expands to code that does the following at second-expansion time:
# 1. Computes the actual prerequisites from the given prerequisite patterns by
# translating % to $* and prepending a directory if appropriate.
# Factor out set-gdeps because make gets confused if the macro for the
# prerequisites has a colon. Make replaces the first % with the stem *before
# expansion*, so use $(pct) to protect some %s.
-set-gdeps=$(eval $@@gdeps:=$1)
-prereq-predict=$$$$(call set-gdeps,$$$$+ $(if $(findstring /,$1),$$$$(subst $$$$(pct),$$$*,$2),$$$$(addprefix $$$$(dir $$$$@),$$$$(subst $$$$(pct),$$$$*,$2))))
+mg-set-gdeps=$(eval $@@gdeps:=$1)
+mg-prereq-predict=$$$$(call mg-set-gdeps,$$$$+ $(if $(findstring /,$1),$$$$(subst $$$$(pct),$$$*,$2),$$$$(addprefix $$$$(dir $$$$@),$$$$(subst $$$$(pct),$$$$*,$2))))
# Used to make the rule for bar always run so we can act based on $(bar@gdeps)
# and check for command change.
# $(call mg-define-rule,target,prerequisites,cmd)
# Defines a rule. cmd is expanded again when it is run, at which time
# Mage-ized automatic variables are available.
-define mg-define-rule
-$(eval
-
-# Store the new command.
-$1: cmd=$(call mg-translate-cmd,$3)
-# Provide variable for the temporary file. FIX
-$1: t=$$@.tmp
-
+#
+# I eradicated the target-specific variables because they fail when there are
+# multiple implicit rules with the same target pattern.
+#
# Rule for the target. Set $(bar@gdeps) to all prerequisites. Apply the new
# prerequisites. The command script always runs. Depend on bar.g to get
# $(bar@gq).
-$1: $(call prereq-predict,$1,$2) $2 MG-FORCE $(mg-genfile-oid)$(aname)$$$$@.g
- $$(mg-rule-cmd)
-)
+define mg-define-rule
+$(eval $1: $(call mg-prereq-predict,$1,$2) $2 MG-FORCE $(mg-genfile-oid)$(aname)$$$$@.g
+ $$(call mg-rule-cmd,$(call mg-translate-cmd,$3)))
endef
# TODO Provide a way to define static pattern rules.
-# Procedure to generate bar. Remember, $@ is bar.g.
+# Procedure to generate bar. Now $@ is bar, so we say $@.g for bar.g.
# If an override:
# 1. Complain. (Should we stop "`bar' is up to date" using @:; ?)
-# 2. Clear out the warnings so we don't replay them.
+# 2. This would be the place to clear bar's metadata if we wanted to.
# Otherwise, check prereqs, command, and nonexistence to decide whether bar
# needs to be regenerated. If so:
-# 1. Set a flag so Mage knows to reread the genfile when make runs target
-# "bar".
+# 1. Note that the genfile is about to change.
# 2. Echo the command being run.
# On error, skip to 8:
# 3. Open the new genfile bar.g.tmp for writing.
-# 4. Store the command in bar.g.tmp.
-# 5. Run the command to bar.tmp, storing warnings in bar.g.tmp.
+# 4. Run the command to bar.tmp, storing warnings in bar.g.tmp.
+# 5. Store the command in bar.g.tmp. Do it here to ensure that bar.g.tmp
+# is at least as new as bar.tmp.
# 6. If bar.tmp differs from bar:
# a. Clear and touch bar.g (so bar doesn't become an override if we're
# killed between steps 6b and 7).
# 1. If there were warnings, replay them. (HMMM To stderr?)
# HMMM .tmp in displayed command looks ugly
define mg-rule-cmd
- $(foreach x,$@ $(mg+),$(eval $x@checked:=1))
$(if $(filter $(mg-scout-oid)$(aname)$@,$($@@gq)),\
$(info Mage: warning: Manually created/modified file at $@ overrides rule.)\
- ,$(call gload,$@)$(if $($@@gq)$(if $(wildcard $@),,x)$(mg-check-cmd),\
- $(eval $@@gloaded:=)$(info $(cmd))\
+ ,$(call gload,$@)$(if $($@@gq)$(if $(wildcard $@),,TARGET-DNE)$(mg-check-cmd),\
+ $(eval $@@gloaded:=)$(info $1)\
@trap 'rm -f $@.tmp $@.g.tmp' EXIT &&\
- exec 3>$@.g.tmp && $(mg-assign-cmd) >&3 &&\
+ exec 3>$@.g.tmp &&\
set -o pipefail && { $(mg-run-cmd) | tee /dev/fd/4 | $(mg-wrap-warnings) >&3; } 4>&1 &&\
- $(mg-maybe-move-target) && mv -f $@.g.tmp $@.g\
+ $(mg-assign-cmd) >&3 &&\
+ $(mg-maybe-move-target) &&\
+ mv -f $@.g.tmp $@.g\
,$(if $($@@warnings),\
$(info $($@@cmd) # Mage warning replay$(nl)$($@@warnings))\
)))
endef
# If the command changed, we must regenerate.
-mg-check-cmd=$(if $(call streq,$(cmd),$($@@cmd)),,x)
+mg-check-cmd=$(if $(call streq,$1,$($@@cmd)),,COMMAND-CHANGED)
# Pieces of mg-generate that I factored out to make mg-generate more readable.
-mg-assign-cmd=echo $(call sq,$(call fmt-make-assignment,$@@cmd,$(cmd)))
-mg-run-cmd={ ($(cmd)) 2>&1 && { [ -r $@.tmp ] || { echo 'Mage: error: Command for $@ succeeded without creating it!'; false; }; }; }
+mg-assign-cmd=echo $(call sq,$(call fmt-make-assignment,$@@cmd,$1))
+mg-run-cmd={ ($1) 2>&1 && { [ -r $@.tmp ] || { echo 'Mage: error: Command for $@ succeeded without creating it!'; false; }; }; }
mg-wrap-warnings=sed -re '1s/^/$@@warnings:=$$(empty)/; 1!s/^/$@@warnings+=$$(nl)/'
-mg-maybe-move-target={ cmp -s $@ $@.tmp || echo >$@.g && mv -f $@.tmp $@; }
+# Drat bash's lack of precedence between || and &&. Extra braces necessary.
+mg-maybe-move-target={ cmp -s $@ $@.tmp || { echo >$@.g && mv -f $@.tmp $@; }; }
# Just add additional prerequisites. This cannot add prerequisite patterns to
# an implicit rule, but it can add specific prerequisites to an individual use
# of an implicit rule. Currently, Mage picks up the target's prerequisites from
# make, so this just attaches the given prerequisites to the target, but the
# implementation might change in the future.
-define mg-define-prereq
-$(eval $1: $2)
+mg-define-prereq=$(eval $1: $2)
+
+
+# DEPENDENCY-LOGGING COMMANDS
+
+mg-dlc-static-run-oid:=$(mg-genfile-oid)
+
+# $(call mg-define-rule-dlc,target,static-prerequisites,cmd,dep-converter)
+# Analogue of mg-define-rule for a dependency-logging command.
+# I haven't decided on the format for the dep-converter yet.
+define mg-define-rule-dlc
+$(eval
+
+# FINISH
+
+# Rule for the target. Set $(bar@gdeps) to all prerequisites. Apply the new
+# prerequisites. The command script always runs. Finally, do the static run
+# and then the first dynamic check.
+$1: $(call mg-prereq-predict,$1,$2) $2 MG-FORCE $(mg-dlc-static-run-oid)$(aname)$$$$@.g $$$$(call dlc-next-dchk,$$$$(target))
+# TODO: Move the finished file into place or something
+# $$(call mg-rule-cmd,$3)
+)
endef
-# END
+#mg-dlc-next-dchk=$(call mg-dlc-next-dchk-1,$1,$(newoid))
+#mg-dlc-next-dchk-1=$(eval $2@opr=$$(call mg-dlc-dchk-pr,$1,$2))$2
+#
+## $(call mg-dlc-dchk-pr,target,oid)
+#mg-dlc-dchk-pr=$(call mg-dlc-dchk-pr-1,$1,$2,$(call next-unchecked-prereq,$($1@deps)))
+## $(call mg-next-unchecked-prereq,foo.c bar.h baz.h)
+#mg-next-unchecked-prereq=$(firstword $(foreach p,$1,$(if $($p@checked),,$p)))
+#define mg-dlc-dchk-pr-1
+#$(if $3,$(call mg-dlc-drun,$1,$2,$3) $(call mg-dlc-next-dchk,$1),)
+#endef
+#
+## $(call mg-dlc-drun,target,oid,prereq). FINISH
+#define mg-dlc-drun
+#$(eval
+#$2$(aname)$3@opr:=$3
+#$2$(aname)$3@ocmd:=
+#)$2$(aname)$3
+#endef
+#
+#
+## END
# We don't want anything we defined to become the default goal.
.DEFAULT_GOAL := $(mg-orig-default-goal)
--- /dev/null
+#!/bin/bash
+# Test suite for Mage.
+
+echo "Test suite for Mage"
+
+cd "$(dirname "$0")"
+Z=test-zone
+rm -rf $Z
+mkdir $Z
+cd $Z
+
+#exec 3>&2
+#exec 1>test.log 2>&1
+exec </dev/null
+set -e
+set -o errtrace
+section=Initialization
+trap 'echo; echo "TEST SUITE FAILED in section $section!" >&2' ERR
+#set -x
+
+ln -s ../mage.mk mage.mk
+
+function fail {
+ false
+}
+
+function start_section {
+ section="$1"
+ echo
+ echo "SECTION: $1"
+}
+
+function do_mage {
+ echo "Running: make $*"
+ make "$@" 2>&1 | tee mage.log
+}
+
+function assert_contents {
+ if diff -u - "$1"; then
+ echo "File '$1' looks good."
+ else
+ echo "File '$1' has the wrong contents!"
+ fail
+ fi
+}
+
+# Options can be passed to grep: assert_saw -i override
+function assert_saw {
+ if grep -q "$@" mage.log; then
+ echo "Good, saw '${@:$#}' in build log."
+ else
+ echo "Expected '${@:$#}' in build log but didn't see it! Log:"
+ cat mage.log
+ fail
+ fi
+}
+function assert_not_saw {
+ if ! grep -q "$@" mage.log; then
+ echo "Good, saw '${@:$#}' in build log."
+ else
+ echo "Did not expect '${@:$#}' in build log but saw it! Log:"
+ cat mage.log
+ fail
+ fi
+}
+function assert_uptodate {
+ assert_saw "make: \`$1' is up to date."
+}
+function assert_generated {
+ assert_saw "$1.tmp"
+}
+
+function remember_mtime {
+ while [ $# != 0 ]; do
+ eval "orig_mtime_${1//[^A-Za-z]/}=$(stat --format=%Y $1)"
+ shift
+ done
+}
+function assert_touched {
+ mtvar=orig_mtime_${1//[^A-Za-z]/}
+ t1="${!mtvar}"
+ t2="$(stat --format=%Y $1)"
+ echo "Times for '$1': $t1, $t2"
+ if [ "$t1" != "$t2" ]; then
+ echo "Good, '$1' was touched."
+ else
+ echo "Expected '$1' to be touched but it wasn't!"
+ fail
+ fi
+}
+function assert_not_touched {
+ mtvar=orig_mtime_${1//[^A-Za-z]/}
+ t1="${!mtvar}"
+ t2="$(stat --format=%Y $1)"
+ echo "Times for '$1': $t1, $t2"
+ if [ "$t1" == "$t2" ]; then
+ echo "Good, '$1' was not touched."
+ else
+ echo "Expected '$1' to be not touched but it was!"
+ fail
+ fi
+}
+
+# Simple makefile for stripping two kinds of comments.
+# Tests an implicit rule and two competing explicit rules.
+# Watch those dollar signs!
+cat >Makefile <<'EOF'
+include mage.mk
+.SECONDARY:
+include hc-rule.mk
+$(call mg-define-rule,%,%.ssc,sleep 1 && grep 'warn' $$< && sed -e 's_//.*$$$$__' $$< >$$t)
+$(call mg-define-rule,index,index.in,sort $$< >$$t)
+EOF
+cat >hc-rule.mk <<'EOF'
+$(call mg-define-rule,%,%.hc,sed -e 's_#.*$$$$__' $$< >$$t)
+EOF
+
+# Input files.
+cat >foo.hc <<'EOF'
+This is the foo file.
+# You don't get to see this.
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+EOF
+cat >bar.ssc <<'EOF'
+the bar file has a different personality
+// hey there
+# I slip through
+warn: tell me about it!
+EOF
+cat >index.in <<'EOF'
+foo
+bar
+EOF
+
+# Run and make sure the files were compiled correctly.
+
+start_section "Initial full build"
+do_mage foo bar index
+
+assert_contents foo <<'EOF'
+This is the foo file.
+
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+EOF
+assert_contents bar <<'EOF'
+the bar file has a different personality
+
+# I slip through
+warn: tell me about it!
+EOF
+assert_contents index <<'EOF'
+bar
+foo
+EOF
+assert_generated foo
+assert_generated bar
+assert_generated index
+assert_saw "sed -e 's_#.*\$__' foo.hc >foo.tmp"
+assert_saw "sleep 1 && grep 'warn' bar.ssc && sed -e 's_//.*\$__' bar.ssc >bar.tmp"
+assert_saw 'warn: tell me about it!'
+assert_saw sort index.in >index.tmp
+
+# Run it again. Make sure the warning is replayed and bar isn't overridden due
+# to bar.g accidentally being too old.
+
+start_section "Replay bar warning"
+do_mage foo bar index
+
+assert_uptodate foo
+assert_uptodate bar
+assert_uptodate index
+assert_saw -i '#.*warning.*replay' # Indication of warning replay
+assert_saw 'warn: tell me about it!' # Actual warning
+assert_not_saw -i overrid
+
+# Now override bar and make sure it stays that way and the warning isn't replayed.
+start_section "Override bar"
+sleep 1 # No racy cleanliness
+echo NEWCONTENT >bar
+do_mage bar
+
+assert_saw -i overrid
+assert_contents bar <<<NEWCONTENT
+
+# Change foo.hc and make sure foo is updated properly.
+
+start_section "Change foo.hc"
+sleep 1 # No racy cleanliness
+echo 'look: # Last-minute addition.' >>foo.hc
+do_mage index foo
+
+assert_uptodate index
+assert_generated foo
+assert_contents foo <<'EOF'
+This is the foo file.
+
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+look:
+EOF
+
+# Change the rule for # comments to strip spaces before a #.
+# Make sure foo is updated properly.
+
+start_section "Command change for % <- %.hc"
+cat >hc-rule.mk <<'EOF'
+$(call mg-define-rule,%,%.hc,sed -e 's_ *#.*$$$$__' $$< >$$t)
+EOF
+do_mage foo
+
+assert_generated foo
+assert_contents foo <<'EOF'
+This is the foo file.
+
+But you do get to see this!
+// Needles: you can lean but you can't hide!
+look:
+EOF
+
+# Remove a space before a # from foo.hc. This is an inconsequential change.
+# Make sure that foo.g is touched but foo is not.
+
+start_section "Inconsequential change to foo.hc"
+sleep 1 # No racy cleanliness
+remember_mtime foo foo.g
+sed -e '$s/look: /look:/' -i foo.hc
+do_mage foo
+
+assert_generated foo
+assert_touched foo.g
+assert_not_touched foo
+
+cd ..
+rm -rf test-zone
+echo
+echo "TEST SUITE SUCCEEDED"