Rename Mage to mgear, a less common and thus less confusable name.
[mgear/mgear.git] / mgear.mk
diff --git a/mgear.mk b/mgear.mk
new file mode 100644 (file)
index 0000000..22465ce
--- /dev/null
+++ b/mgear.mk
@@ -0,0 +1,319 @@
+# Mgear Build Tool by Matt McCutchen
+# http://www.kepreon.com/~matt/mgear/
+
+
+# Remember the original default goal so we can restore it at the end of mgear.mk.
+mg-orig-default-goal:=$(.DEFAULT_GOAL)
+
+# We use second-expansion heavily to dynamically compute prerequisites when they
+# are needed.  Second-expansion lets us follow make's implicit rule search
+# instead of trying to anticipate which prerequisites it will need in advance.
+.SECONDEXPANSION:
+
+
+# TEXT UTILITIES
+empty:=
+bs:=\$(empty)
+hash:=\#
+pct:=%
+define nl
+
+
+endef
+# Shell-quote: a'b => 'a'\''b'
+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 $(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))
+
+# $(call fmt-make-assignment,foo,bar)
+# Output a make assignment that stores bar in variable foo.
+# The result is like `foo:=bar' but handles leading spaces, $, and
+# newlines appearing in bar safely.
+fmt-make-assignment=$1:=$$(empty)$(call mqas,$2)
+
+
+# TARGET OBFUSCATION
+
+# Mgear uses two kinds of "obfuscated targets" (those that are not the simple
+# names of real files):
+# - An always-exists target like /.//. is used as a prerequisite of an implicit
+#   rule.  Since it exists, make doesn't second-expand its own prerequisites
+#   until it is actually run.  This way, a target's prerequisites can depend on
+#   the results of previous command scripts.
+# - An alternative-name target like /.//./proc/self/cwd/bar is used to check the
+#   mtime of a file without actually building it or introducing a circular
+#   dependency.
+
+# $(newoid) allocates and returns a new obfuscation ID (oid).  Example:
+#    x:=$(newoid)
+# You can then refer to target $x or $x$(aname)bar (for any file bar).
+# Prerequisites and commands come from $($x@opr) and $($x@ocmd).
+# Target-specific variables $(oid) and $(otgt) (if alternate-name) are
+# available.
+
+opfx:=/./
+# TODO If the system doesn't support /proc/self/cwd, use something else.
+aname:=/proc/self/cwd/
+nextoid:=/./.
+newoid=$(nextoid)$(eval nextoid:=$(subst /.//////,//./,$(patsubst /.//////%,/././%,$(nextoid:.=/.))))
+
+# Target-specific variables for obfuscated targets.
+$(opfx)%: oid=$(word 1,$(subst $(aname), ,$@))
+$(opfx)%: otgt=$(word 2,$(subst $(aname), ,$@))
+
+# High-priority implicit rule for obfuscated targets.  Works for both always-
+# exists and alternate-name targets.
+$(opfx)%: $$($$(oid)@opr)
+       $($(oid)@ocmd)
+
+# Make won't use the same implicit rule more than once on its stack, so provide
+# a second copy of the rule to allow two obfuscated targets on the stack.
+# Needed for $(mg-genfile-oid)$(aname)bar.g <- $(mg-scout-oid)$(aname)bar .
+# The prerequisites must look different before second-expansion so the second
+# rule isn't discarded as a duplicate.
+$(opfx)%: $$(empty) $$($$(oid)@opr)
+       $($(oid)@ocmd)
+
+# MAIN BUILD LOGIC
+
+# DESIGN:
+#
+# - To each generated file bar corresponds a "genfile" bar.g that contains some
+# information about how bar was generated, including the command (for rebuild on
+# command change) and the warnings (for replay).
+#
+# - bar.g's mtime is the last time mgear verified that bar was up to date.  bar
+# needs to be regenerated iff a prerequisite is newer than *bar.g* (not bar).
+# If a prerequisite changes but the command gives the same contents for bar,
+# bar.g is touched but bar itself is not touched because files that depend on it
+# need not be regenerated.
+#
+# - If bar is newer than bar.g, the user has overridden it, and we should leave
+# the override in place but warn the user about it.
+#
+# - The user's Makefile defines mgear rules by calling mg-define-rule.  Each
+# mgear rule becomes one underlying make rule with the same target and a little
+# extra magic.  This is important so that all implicit rule competition takes
+# place at the same target.  Additionally, the prerequisites are passed to an
+# obfuscated target for bar.g to see if any are newer than bar.g.  This is done
+# by the single obfuscated implicit rule, keeping the number of implicit rules
+# low. Then the command script for bar checks for overrides, command change,
+# prerequisite change, etc. and acts accordingly.
+#
+# Target metadata variables for bar: (* means stored in bar.g)
+#
+# bar@cmd:=cat foo >bar.tmp
+#     Generation command as given to the shell.*
+#
+# bar@warnings:=$(empty)yikes!
+#     Data that the command printed to stdout or stderr, presumably warnings.*
+#
+# bar@deps:=included@x oops@
+#     If dependency-logging, list of filename@revision used.  Revision is x for
+#     exists and empty for doesn't exist.  Later perhaps x will be the mtime.*
+#
+# bar@gloaded:=1
+#     Set if mgear has loaded bar.g and hasn't changed it since then.
+#
+# bar@gdeps:=foo
+#     Static dependencies of bar, for checking by bar.g.
+#
+# bar@gq:=foo $(mg-scout-oid)$(aname)bar
+#     $? from the rule for bar.g; used by the rule for bar.
+#
+# bar@checked:=1
+#     Set when mgear determines that a file is up to date or depends on it being
+#     so determined.  Used to decide which prerequisite to check next for a
+#     dependency-logging command.
+#
+# bar@dlc-ran:=1
+#     Indicates that mgear is in the middle of building bar using a dependency-
+#     logging command.  Means that bar.g.tmp, not bar.g, is the most current
+#     genfile.
+#
+## If the rule for bar is overridden, we clear the information from bar.g so
+## that it is as if bar.g didn't exist. -- Not currently needed
+#
+# Some make features with which mgear's compatibility has not been investigated:
+# - Command-line options (especially --dry-run, --question, --touch,
+#   --always-make, --keep-going, --jobs, --assume-old, and --assume-new)
+# - Static pattern rules
+# - Vpath
+
+# $(call gload,foo.o)
+# Make sure foo.o's genfile, if any, has been loaded.
+define gload
+$(if $($1@gloaded),,$(eval 
+$1@cmd:=
+$1@warnings:=
+$1@deps:=
+-include $1.g
+$1@gloaded:=1
+))
+endef
+
+# bar.g: scout bar and its dependencies and store $?.  When implicit rules
+# compete for bar, we depend on the rule make uses being the last one it
+# second-expands so that $(bar@gdeps) is still correct.
+mg-genfile-oid:=$(newoid)
+mg-scout-oid:=$(newoid)
+$(mg-genfile-oid)@opr=$($(otgt:.g=)@gdeps) $(if $(wildcard $(otgt:.g=)),$(mg-scout-oid)$(aname)$(otgt:.g=),)
+$(mg-genfile-oid)@ocmd=$(eval $(otgt:.g=)@gq:=$?)
+
+# Mgear-ized automatic variables.
+# $@: needs no translation
+# NOTE: $(mg@) is *eventual* target.  Commands must write to temp file, $t .
+# $%: haven't thought about it much, but probably needs no translation
+mg< = $(firstword $(mg^))
+mg? = $(filter-out $(opfx)%,$($@@gq))
+mg^ = $(filter-out MG-% $(opfx)%,$^)
+mg+ = $(filter-out MG-% $(opfx)%,$+)
+# $|: needs no translation
+# $*: needs no translation
+
+# $(call mg-translate-cmd,cat $$< >$$t)
+# Replaces references to automatic variables with references to their mgear-ized
+# counterparts.  There might be false matches, e.g., $$@ => $$(mg@) ;
+# to prevent that, write $$$(empty)@ instead.  (c.f. autoconf empty quadrigraph)
+mg-translate-cmd=$(subst $$t,$$@.tmp,$(subst $$?,$$(mg?),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1)))))
+
+# $(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.
+# 2. Saves both existing ($+) and newly computed prerequisites to $(bar@gdeps)
+#    where the rule for bar.g can get them.
+# 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.
+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.
+MG-FORCE:
+.PHONY: MG-FORCE
+
+# $(call mg-define-rule,target,prerequisites,cmd)
+# Defines a rule.  cmd is expanded again when it is run, at which time
+# Mgear-ized automatic variables are available.
+#
+# 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).
+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.  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. 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. 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. 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).
+#           b. Move in the new bar.
+#        7. Now that bar is known to be up to date, move in the new bar.g.
+#     8. Defensively remove both temporary files and exit with the exit code
+#        from before the removal. (trap EXIT)
+# If not:
+#     1. If there were warnings, replay them.  (HMMM To stderr?)
+# HMMM .tmp in displayed command looks ugly
+define mg-rule-cmd
+       $(if $(filter $(mg-scout-oid)$(aname)$@,$($@@gq)),\
+               $(info mgear: warning: Manually created/modified file at $@ overrides rule.)\
+       ,$(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 &&\
+               set -o pipefail && { $(mg-run-cmd) | tee /dev/fd/4 | $(mg-wrap-warnings) >&3; } 4>&1 &&\
+               $(mg-assign-cmd) >&3 &&\
+               $(mg-maybe-move-target) &&\
+               mv -f $@.g.tmp $@.g\
+       ,$(if $($@@warnings),\
+               $(info $($@@cmd) # mgear warning replay$(nl)$($@@warnings))\
+       )))
+endef
+
+# If the command changed, we must regenerate.
+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,$1))
+mg-run-cmd={ ($1) 2>&1 && { [ -r $@.tmp ] || { echo 'mgear: error: Command for $@ succeeded without creating it!'; false; }; }; }
+mg-wrap-warnings=sed -re '1s/^/$@@warnings:=$$(empty)/; 1!s/^/$@@warnings+=$$(nl)/'
+# 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, mgear 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.
+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
+
+#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)