Here's what I have so far related to the Mage build tool.
authorMatt McCutchen <hashproduct@gmail.com>
Sun, 22 Apr 2007 19:26:24 +0000 (15:26 -0400)
committerMatt McCutchen <hashproduct@gmail.com>
Sun, 22 Apr 2007 19:26:24 +0000 (15:26 -0400)
34 files changed:
demo/Makefile [new file with mode: 0644]
demo/Makefile.plain [new file with mode: 0644]
demo/clean [new file with mode: 0755]
demo/foo [new file with mode: 0644]
demo/mage.mk [new symlink]
experiments/bigint-mage-Makefile [new file with mode: 0644]
experiments/bong [new file with mode: 0644]
experiments/clean [new file with mode: 0755]
experiments/cmd-assign-var.mk [new file with mode: 0644]
experiments/collect.mk [new file with mode: 0644]
experiments/dontcare.mk [new file with mode: 0644]
experiments/eval-commas.mk [new file with mode: 0644]
experiments/flist.mk [new file with mode: 0644]
experiments/genrecord [new file with mode: 0755]
experiments/if-two-commands.mk [new file with mode: 0644]
experiments/implicit-cutoff-cmd [new file with mode: 0755]
experiments/implicit-cutoff.mk [new file with mode: 0644]
experiments/match-command.mk [new file with mode: 0644]
experiments/mk-find-shell-use [new file with mode: 0755]
experiments/mkskeleton [new file with mode: 0755]
experiments/obfuscation-dump.mk [new file with mode: 0644]
experiments/precious.mk [new file with mode: 0644]
experiments/refuted-stamp.mk [new file with mode: 0644]
experiments/remake.mk [new file with mode: 0644]
experiments/required.mk [new file with mode: 0644]
experiments/restat-no-cmd.mk [new file with mode: 0644]
experiments/se-prereq-ts.mk [new file with mode: 0644]
experiments/sip [new file with mode: 0755]
experiments/test-notee [new file with mode: 0755]
experiments/test-tee [new file with mode: 0755]
experiments/transfer-prereqs.mk [new file with mode: 0644]
experiments/two-commands.mk [new file with mode: 0644]
experiments/yucky-ts.mk [new file with mode: 0644]
src/mage.mk [new file with mode: 0644]

diff --git a/demo/Makefile b/demo/Makefile
new file mode 100644 (file)
index 0000000..2e20b36
--- /dev/null
@@ -0,0 +1,13 @@
+# Very simple Mage example
+
+include mage.mk
+
+.SECONDARY:
+
+foo.x.h:
+
+cmd-xgrep = grep 'X' '$<' >'$@'
+$(call mg-rule,%.x,%,$(value cmd-xgrep))
+
+cmd-addheader = echo 'Header:' | cat - '$<' >'$@'
+$(call mg-rule,%.h,%,$(value cmd-addheader))
diff --git a/demo/Makefile.plain b/demo/Makefile.plain
new file mode 100644 (file)
index 0000000..75cdb7a
--- /dev/null
@@ -0,0 +1,14 @@
+# Very simple Mage example
+
+.SECONDARY:
+
+foo.x.h:
+
+cmd-xgrep = grep 'X' '$<' >'$@'
+%.x: %
+       $(cmd-xgrep)
+
+cmd-addheader = echo 'Header:' | cat - '$<' >'$@'
+%.h: %
+       $(cmd-addheader)
+
diff --git a/demo/clean b/demo/clean
new file mode 100755 (executable)
index 0000000..c7f9885
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/bash
+rm -f foo.x foo.x.g foo.x.h foo.x.h.g
diff --git a/demo/foo b/demo/foo
new file mode 100644 (file)
index 0000000..cdde6a7
--- /dev/null
+++ b/demo/foo
@@ -0,0 +1,5 @@
+blah
+Xblah
+foo
+Xfoo
+Xstuffa
diff --git a/demo/mage.mk b/demo/mage.mk
new file mode 120000 (symlink)
index 0000000..73c6b44
--- /dev/null
@@ -0,0 +1 @@
+../src/mage.mk
\ No newline at end of file
diff --git a/experiments/bigint-mage-Makefile b/experiments/bigint-mage-Makefile
new file mode 100644 (file)
index 0000000..d012c7e
--- /dev/null
@@ -0,0 +1,52 @@
+#
+# Matt McCutchen's Big Integer Library
+#
+
+include mage.mk
+
+# Mention default target.
+all :
+
+# Implicit rule to compile C++ files.  Modify to your taste.
+$(call mg-rule,%.o,%.cc,g++ -c -O -Wall -Wextra -pedantic $$< -o $$@)
+
+# Components of the library.
+library-objects = BigUnsigned.o BigInteger.o BigUnsignedInABase.o BigIntegerUtils.o
+library-headers = NumberlikeArray.hh BigUnsigned.hh BigUnsignedInABase.hh BigInteger.hh BigIntegerLibrary.hh
+
+# To ``make the library'', make all its objects using the implicit rule.
+library : $(library-objects)
+
+# Extra dependencies from `#include'.
+$(call mg-prereq,BigUnsigned.o,NumberlikeArray.hh BigUnsigned.hh)
+$(call mg-prereq,BigInteger.o,NumberlikeArray.hh BigUnsigned.hh BigInteger.hh)
+$(call mg-prereq,BigUnsignedInABase.o,NumberlikeArray.hh BigUnsigned.hh BigUnsignedInABase.hh)
+$(call mg-prereq,BigIntegerUtils.o,NumberlikeArray.hh BigUnsigned.hh BigUnsignedInABase.hh BigInteger.hh)
+
+# The rules below build a program that uses the library.  They are preset to
+# build ``sample'' from ``sample.cc''.  You can change the name(s) of the
+# source file(s) and program file to build your own program, or you can write
+# your own Makefile.
+
+# Components of the program.
+program = sample
+program-objects = sample.o
+
+# Conservatively assume all the program source files depend on all the library
+# headers.  You can change this if it is not the case.
+$(call mg-prereq,$(program-objects),$(library-headers))
+
+# How to link the program.  The implicit rule covers individual objects.
+$(call mg-rule,$(program),$(program-objects) $(library-objects),g++ $$(program-objects) $$(library-objects) -o $$@)
+
+# Delete all generated files we know about.
+clean :
+       rm -f $(library-objects) $(program-objects) $(program)
+
+# I removed the *.tag dependency tracking system because it had few advantages
+# over manually entering all the dependencies.  If there were a portable,
+# reliable dependency tracking system, I'd use it, but I know of no such;
+# cons and depcomp are almost good enough.
+
+# Come back and define default target.
+all : library $(program)
diff --git a/experiments/bong b/experiments/bong
new file mode 100644 (file)
index 0000000..fba9db5
--- /dev/null
@@ -0,0 +1,2 @@
+ha: $$(info planning ha) hey
+
diff --git a/experiments/clean b/experiments/clean
new file mode 100755 (executable)
index 0000000..34f46c8
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/bash
+target="${1-.}"
+find "$target" -name '*.g' | while read g; do
+       f="${g%.g}"
+       [ "$f" -nt "$g" ] || echo "$f"
+       echo "$g"
+done | xargs rm -f
diff --git a/experiments/cmd-assign-var.mk b/experiments/cmd-assign-var.mk
new file mode 100644 (file)
index 0000000..acfbe5c
--- /dev/null
@@ -0,0 +1,11 @@
+# It's fine to assign variables after make has snapped deps.
+
+all: foo bar
+
+foo:
+       $(eval MYVAR=value)
+       touch foo
+
+bar:
+       echo "The variable says $(MYVAR)"
+       touch bar
diff --git a/experiments/collect.mk b/experiments/collect.mk
new file mode 100644 (file)
index 0000000..233fa9c
--- /dev/null
@@ -0,0 +1,27 @@
+main: prepare collect
+       touch $@
+
+prepare:
+       echo "Making $@"
+       echo "Try to turn off the switch"
+       touch collect
+
+collect: | t1 t2 t3
+       echo "Making $@"
+
+t%:
+       echo "Making $@"
+       #false
+       touch $@
+
+#dummy:
+
+#collect: | dummy
+#collect: t1
+#collect: | t2
+#collect: | t3
+#collect: t4
+#collect: | t5
+#collect: t6
+#collect: t7
+#collect: | t8
diff --git a/experiments/dontcare.mk b/experiments/dontcare.mk
new file mode 100644 (file)
index 0000000..992b2ed
--- /dev/null
@@ -0,0 +1,23 @@
+# Investigating dontcare (-include) targets to see if they're good for anything.
+
+# Observation 1: If a dontcare target fails during makefile remaking and is
+# needed during the real run, make gives a misleading error that it has no
+# rule instead of reporting the original failure.
+
+# Observation 2: If a required target fails, make makes do with the original
+# error message.
+
+include required.mk
+-include optional.mk
+
+all: required.mk
+
+fail%:
+       @echo "Failing to make $@"
+       false
+
+required.mk: fail1
+       echo "Trying to make $@"
+
+optional.mk: fail1
+       echo "Trying to make $@"
diff --git a/experiments/eval-commas.mk b/experiments/eval-commas.mk
new file mode 100644 (file)
index 0000000..138427f
--- /dev/null
@@ -0,0 +1,13 @@
+all: 
+
+include ../makeman/makeman.mk
+
+define macro
+$(info $(filter $1,$(var)))$(eval $$(info $$(filter $(call mq,$1),$$(var))))
+endef
+
+var := xxx,yyy(
+$(info $(var))
+$(info $(subst $(rp),$$(rp),$(var)))
+$(info $(call mq,$(var)))
+$(call macro,$(var))
diff --git a/experiments/flist.mk b/experiments/flist.mk
new file mode 100644 (file)
index 0000000..734c776
--- /dev/null
@@ -0,0 +1,14 @@
+# Seeing how long it takes Make to recursively list a directory.
+
+define flist1
+$(if $3,$(foreach 4,$3,$(call flist,$1,$4)),$(info $(2:$1/%=%)))
+endef
+
+define flist
+$(call flist1,$1,$2,$(filter-out %/. %/..,$(wildcard $2/.* $2/*)))
+endef
+
+flist-%:
+       $(call flist,$*,$*)
+
+flist-.:
diff --git a/experiments/genrecord b/experiments/genrecord
new file mode 100755 (executable)
index 0000000..9d86947
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/perl
+# FIX: Should use perl from path
+# genrecord: make a Mage genfile
+# { gcc prog.c -o prog.tmp; echo $?; } 2>&1 | genrecord prog 'gcc prog.c -o prog.tmp'
+
+sub mqas($) {
+       my $s = $_[0];
+       $s =~ s/\$/\$\$/g;
+       $s =~ s/\n/\$(nl)/g;
+       return $s;
+}
+sub fmtMakeAssignment($$) {
+       my ($n, $v) = @_;
+       $v = mqas($v);
+       return "$n:=\$(empty)$v";
+}
+
+my ($target, $cmd) = @ARGV;
+
+my @errorLines = <STDIN>;
+my $exitCode = 0;
+if ($errorLines[$#errorLines] =~ /^(\d+)$/) {
+       chomp($exitCode = pop @errorLines);
+}
+my $errorText = join '', @errorLines;
+
+my $genfileText = fmtMakeAssignment("cmd-$target", $cmd) . "\n";
+       . fmtMakeAssignment("errors-$target", $errorText) . "\n"
+       . fmtMakeAssignment("exitcode-$target", $exitCode) . "\n";
+
+open GENFILE, '>', "$target.g.tmp" and print GENFILE $genfileText and close GENFILE or die;
diff --git a/experiments/if-two-commands.mk b/experiments/if-two-commands.mk
new file mode 100644 (file)
index 0000000..c0cf059
--- /dev/null
@@ -0,0 +1,11 @@
+# Make expands the command script as a whole before splitting it into lines
+# and executing each line.
+
+define mm-gen-commands
+exec 3>$@.tmp && echo $(call sq,$(call fmt-make-assignment,cmd-$(target),$(cmd))) >&3 && set -o pipefail && { ($(cmd)) 2>&1 | sed -re '1s/^/errors-$$(target):=$$(empty)/; 1!s/^/errors-$$(target)+=$$(nl)/' >&3; xc=$$?; } && { [ $xc == 0 ] || echo "exitcode-$(target):=$$xc" >&3 } && if [ $xc != 0 ] || cmp -s $(target) $(target).tmp; then mv -f $@.tmp $@; else rm -f $(target) && mv -f $@.tmp $@ && mv -f $(target).tmp $(target); fi
+echo bar
+endef
+
+phooey:
+       $(if x,$(mm-gen-commands),)
+       $(eval include $@)
diff --git a/experiments/implicit-cutoff-cmd b/experiments/implicit-cutoff-cmd
new file mode 100755 (executable)
index 0000000..dc4eda8
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/bash
+make -R -f implicit-cutoff.mk ic.xa
diff --git a/experiments/implicit-cutoff.mk b/experiments/implicit-cutoff.mk
new file mode 100644 (file)
index 0000000..7debf63
--- /dev/null
@@ -0,0 +1,17 @@
+.SECONDEXPANSION:
+
+n=
+s=$n    $n
+
+FORCE: $$(info $$splanning $$@)
+       $(info $srunning $@)
+
+%.xa: $$(info $$splanning $$@) %.xb %.xc $$(if $$(wildcard $$*.xc),,$$(info $$*.xc needs touching -> $$(shell [ -r $$*.xc ] && echo 'already exists' || (sleep 1 && touch $$*.xc))))
+       $(info $srunning $@)
+
+%.xb: $$(info $$splanning $$@) FORCE
+       $(info $srunning $@)
+
+%.xc: $$(info $$splanning $$@) FORCE
+       $(info $srunning $@)
+
diff --git a/experiments/match-command.mk b/experiments/match-command.mk
new file mode 100644 (file)
index 0000000..04aaeec
--- /dev/null
@@ -0,0 +1,16 @@
+# Can I parse makefile lines using Make's macro processor? 
+
+define test-cmd
+$(info $(if $(findstring ^     ,^$1),yes,no): $1)
+endef
+
+empty :=
+cmd-1 := $(empty)      echo bar
+not-cmd-1 := $(empty)  bar: foo
+not-cmd-2 := $(empty)          baz: bar
+
+$(call test-cmd,$(cmd-1))
+$(call test-cmd,$(not-cmd-1))
+$(call test-cmd,$(not-cmd-2))
+
+all:
diff --git a/experiments/mk-find-shell-use b/experiments/mk-find-shell-use
new file mode 100755 (executable)
index 0000000..0a27607
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/bash
+sed -n -r -f /dev/fd/3 3<<'EOS' "$@"
+s/^    (([^$'(){}<>|;&*?`]|'[^']*'|\$[^(]|\$\([^)]*\))*)([(){}<>|;&*?`]|\$\$)(.*)$/\1SHELL!\3!\4/p
+EOS
diff --git a/experiments/mkskeleton b/experiments/mkskeleton
new file mode 100755 (executable)
index 0000000..d3c698c
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -x
+dest="$1"
+shift
+rm -r "$dest"
+src_args="$@"
+parents=("${src_args[@]%/*}")
+mkdir -p "${parents[@]/#/$dest}"
+find "$dest" -type d -exec touch -d @0 +
diff --git a/experiments/obfuscation-dump.mk b/experiments/obfuscation-dump.mk
new file mode 100644 (file)
index 0000000..aaa0953
--- /dev/null
@@ -0,0 +1,25 @@
+.SECONDEXPANSION:
+
+obfn1=/./.
+#obfn=$(obfn1)$(eval obfn1=$(patsubst /.//////%,/.//./%,$(subst /.//////,//./,$(obfn1)/)))
+define obfn
+$(obfn1)$(eval 
+pr$(obfn1)=$1
+cmd$(obfn1)=$2
+obfn1=$(subst /.//////,//./,$(patsubst /.//////%,/././%,$(obfn1:.=/.)))
+)
+endef
+
+digits=0 1 2 3 4 5 6 7 8 9
+
+%yxxx: $(foreach d,$(digits),%$dyxx)
+       
+%yxx: $(foreach d,$(digits),%$dyx)
+       
+%yx: $(foreach d,$(digits),%$dy)
+       
+%y: $$(call obfn,FORCE,$$$$(info Running $$@ $$$$@))
+       
+/./%: $$(pr$$@)
+       $(cmd$@)
+FORCE:
diff --git a/experiments/precious.mk b/experiments/precious.mk
new file mode 100644 (file)
index 0000000..54be133
--- /dev/null
@@ -0,0 +1,24 @@
+all: bar baz
+
+.DELETE_ON_ERROR:
+
+.PRECIOUS: %.g
+
+.SECONDEXPANSION:
+
+%: %.g
+       # updating $@
+       # the error occurs HERE!  do we delete $@?
+       $(info echo bing\
+       echo bong)
+       false
+
+%.g: %.in
+       # updating $@
+       touch $*.g
+       touch $*
+
+%: %.in2
+       # updating baz
+       touch $@
+       false
diff --git a/experiments/refuted-stamp.mk b/experiments/refuted-stamp.mk
new file mode 100644 (file)
index 0000000..6ceaff3
--- /dev/null
@@ -0,0 +1,7 @@
+bar.g: $(wildcard $(abspath bar)) foo
+       $(if $(filter $(abspath bar),$?),$(error ./bar obstructs build),)
+       # Making bar.g
+       touch bar bar.g
+
+bar: bar.g
+       # Making bar
diff --git a/experiments/remake.mk b/experiments/remake.mk
new file mode 100644 (file)
index 0000000..4c61537
--- /dev/null
@@ -0,0 +1,52 @@
+# We've established that
+# bing: $$(eval include bong)
+# causes a segfault.  We don't know why but it isn't important.
+
+# Interesting: make plans any target that is mentioned, even if it isn't
+# needed for any goal or makefile!!!
+
+# Goal: What makefiles get remade?
+
+.SECONDEXPANSION:
+
+#-include bing
+
+#all: $$(info planning $$@) one two three
+#      $(info running $@)
+
+#irrelevant quax zong: %: $$(info planning $$@) other
+#      $(info running irrelevant)
+
+%.xa: $$(info planning $$@ 1) %.xb %.xc signal-%-1
+       $(info running $@ 1)
+
+%.xa: $$(info planning $$@ 2) %.xc %.xd signal-%-2
+       $(info running $@ 2)
+
+signal-%: $$(info good so far $$*)
+       $(info running $@)
+
+%.xb: $$(info planning $$@) %.xe %.xf
+       $(info running $@)
+
+%.xc: $$(info planning $$@) %.xg %.xh
+       $(info running $@)
+
+%.xd: $$(info planning $$@) %.xi %.xj
+       $(info running $@)
+
+landmark: $$(info planning $$@) tat.xa
+       $(info running $@)
+
+#one two three: %: $$(info planning $$@)
+#      $(info running $@)
+
+#do-dontcare-include=$(eval -include $1)
+
+#bing: $$(info planning $$@) $$(call do-dontcare-include,bong) other
+#      echo $@
+#      $(info running $@)
+#other: $$(info planning $$@)
+#      $(info running $@)
+
+#ha: $$(info 1 planning $$@)
diff --git a/experiments/required.mk b/experiments/required.mk
new file mode 100644 (file)
index 0000000..53e344c
--- /dev/null
@@ -0,0 +1 @@
+# This is required!
diff --git a/experiments/restat-no-cmd.mk b/experiments/restat-no-cmd.mk
new file mode 100644 (file)
index 0000000..bbdec9a
--- /dev/null
@@ -0,0 +1,13 @@
+baz: bar
+       touch $@
+
+bar: bar.g.replay ;
+
+.PHONY: bar.g.replay
+bar.g.replay: bar.g
+       # $@
+
+FORCE:
+bar.g: FORCE
+       touch $@
+       touch bar
diff --git a/experiments/se-prereq-ts.mk b/experiments/se-prereq-ts.mk
new file mode 100644 (file)
index 0000000..92414c4
--- /dev/null
@@ -0,0 +1,16 @@
+# References to target-specific variables are recognized in a prerequisite list
+# undergoing second expansion.
+
+.SECONDEXPANSION:
+
+bar.g: target = $(@:.g=)
+
+define nl
+
+
+endef
+
+thetext := hi umm$(nl)there
+
+bar.g: $$(info $$(target)) foo
+       # $(target)
diff --git a/experiments/sip b/experiments/sip
new file mode 100755 (executable)
index 0000000..12e0579
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/bash
+# Simple include processor as an example of a dependency-logging command.
+# sip input output depoutput
+# Follows `include foo' at the beginning of a line.
+
+exec 3>"$3"
+exec >"$2"
+
+function do_read {
+       echo "$1" >&3
+       while IFS='' read line; do
+               if [[ "$line" == "include "* ]]; then
+                       do_read "${line#include }"
+               else
+                       echo "$line"
+               fi
+       done <"$1"
+}
+
+do_read "$1"
diff --git a/experiments/test-notee b/experiments/test-notee
new file mode 100755 (executable)
index 0000000..0efb5cd
--- /dev/null
@@ -0,0 +1,4 @@
+#!/usr/bin/perl
+while (<STDIN>) {
+       print STDOUT $_;
+}
diff --git a/experiments/test-tee b/experiments/test-tee
new file mode 100755 (executable)
index 0000000..62bca0c
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/perl
+open OTHER, ">/dev/stdout";
+while (<STDIN>) {
+       print STDOUT $_;
+       print OTHER $_;
+}
diff --git a/experiments/transfer-prereqs.mk b/experiments/transfer-prereqs.mk
new file mode 100644 (file)
index 0000000..cc901f2
--- /dev/null
@@ -0,0 +1,15 @@
+# Experimenting with the accumulation of prerequisites among explicit and
+# implicit rules.
+
+.SECONDEXPANSION:
+
+%.x: ding3 $$(info implicit2 $$@ $$^) ;
+
+%.x: ding4 $$(info implicit1 $$@ $$^)
+       # 2 $@
+
+foo.x: ding1 $$(info explicit1 $$@ $$^)
+foo.x: ding2 $$(info explicit2 $$@ $$^)
+
+ding%:
+       # $@
diff --git a/experiments/two-commands.mk b/experiments/two-commands.mk
new file mode 100644 (file)
index 0000000..5bf9b30
--- /dev/null
@@ -0,0 +1,7 @@
+# How does Make execute two very simple commands?  Since they don't have any
+# punctuation that looks like it needs a shell, Make word-splits and executes
+# them itself.  Strace me!
+
+all:
+       echo foo
+       echo bar
diff --git a/experiments/yucky-ts.mk b/experiments/yucky-ts.mk
new file mode 100644 (file)
index 0000000..089e480
--- /dev/null
@@ -0,0 +1,5 @@
+# Yes, it's legal to assign a yucky value to a target-specific variable.
+
+all: ts = ),}($$,{
+all:
+       # $(ts)
diff --git a/src/mage.mk b/src/mage.mk
new file mode 100644 (file)
index 0000000..465a05a
--- /dev/null
@@ -0,0 +1,145 @@
+# Mage by Matt McCutchen
+
+# Text utilities
+empty :=
+bs := \$(empty)
+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 $(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)
+
+mg-orig-default-goal := $(.DEFAULT_GOAL)
+
+# bar.g file format:
+# cmd-bar:=cat foo >bar.tmp
+# errors-bar:=$(empty)yikes!
+# exitcode-bar:=0
+
+# $(call mg-check-file,foo.o)
+# Make sure foo.o's genfile, if any, has been loaded.
+define mg-check-file
+$(if $(mg-checked-$1),,$(eval 
+cmd-$1 :=
+errors-$1 :=
+exitcode-$1 :=
+-include $1.g
+mg-checked-$1 := done
+))
+endef
+
+# $(call mg-translate-cmd,touch $$@)
+# Currently translates $@, $<, $^, $+ to Mage-ized variables.
+# $* needs no translation.  We won't support $%, $?, $|.
+# There might be false matches, e.g., $$@ => $$(out) ;
+# to prevent that, do $$$(empty)@ .
+define mg-translate-cmd
+$(subst $$@,$$(mg@),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1))))
+endef
+
+# OOOH!!! .SECONDEXPANSION does let us watch the implicit rule search as it
+# happens.
+.SECONDEXPANSION:
+
+# $(call mg-rule,target,prerequisite,cmd)
+# Defines a rule.
+# If cmd uses $@, quote if necessary so this function sees $@, etc.
+# 
+# When we build bar from foo using a Mage rule:
+# Division of work:
+# - "bar.g: foo" does the computation.
+# - "bar: bar.g" replays errors and exit code.
+# Cases:
+# - bar is managed and up to date => replay
+# - bar doesn't exist or is managed and out of date => compute
+# - bar is unmanaged => warn about override 
+MG-FORCE-TARGET-DNE:
+MG-FORCE-CMD-CHANGED:
+MG-FORCE-REPLAY:
+.PHONY: MG-FORCE-TARGET-DNE MG-FORCE-CMD-CHANGED MG-FORCE-REPLAY
+define mg-rule
+$(eval 
+
+# Define some target-specific variables.
+# It might look like we could use $*, but $1 most likely isn't %.
+$1.g: target = $$(@:.g=)
+$1.g: cmd = $(call mg-translate-cmd,$3)
+$1.g: mg@ = $$(target).tmp
+$1.g: mg^ = $$(filter-out MG-% $$(abspath $$(target)),$$^)
+$1.g: mg+ = $$(filter-out MG-% $$(abspath $$(target)),$$+)
+$1.g: mg< = $$(firstword $$(mg^))
+
+# Load the target's genfile if necessary.
+$1.g: $$$$(call mg-check-file,$$$$(target))
+
+# If the target doesn't exist, we must run the rule; we'll generate it.
+# If the target is unmanaged, we must run the rule; we'll see that the
+# obfuscated target is in $? and complain.
+$1.g: $$$$(if $$$$(wildcard $$$$(target)),$$$$(abspath $$$$(target)),$$$$(if $$(exitcode-$$$$(target)),,MG-FORCE-TARGET-DNE))
+
+# If the command changed, we must regenerate.
+$1.g: $$$$(if $$$$(call streq,$$$$(cmd),$$$$(cmd-$$$$(target))),,MG-FORCE-CMD-CHANGED)
+
+$1.g: $2
+       $(value mg-commands)
+
+# Replay the errors and the exit code (if any of either).
+# We don't have to worry about .DELETE_ON_ERROR deleting the target because
+# it is only touched if we *successfully* regenerate it.
+# Recheck the file in case it was regenerated.
+# HMMM Maybe errors should go to stderr???
+$1: $1.g MG-FORCE-REPLAY
+       $$(call mg-check-file,$$@)
+       $$(if $$(errors-$$@)$$(exitcode-$$@)$$(built-$$@),$$(info $$(cmd-$$@)$$(if $$(built-$$@),, [replay]))$$(if $$(errors-$$@),$$(info $$(errors-$$@)),),)
+       $$(if $$(exitcode-$$@),@exit $$(exitcode-$$@),)
+)
+endef
+
+# Just add additional prerequisites.
+define mg-prereq
+$(eval 
+$1.g: $2
+)
+endef
+
+# If an override:
+#     1. Complain.
+#     2. Clear out the errors and exit code so we don't replay them.
+# Otherwise:
+#     1. Store the command.
+#     2. Run the command, storing errors.
+#     3. Store exit code.
+#     4. Update the real files as applicable.
+#     5. Read the new genfile.
+define mg-commands
+       $(if $(filter $(abspath $(target)),$?),\
+               $(info mage: warning: Manually created/modified file at $(target) overrides rule.)\
+               $(eval cmd-$(target):=)$(eval errors-$(target):=)$(eval exitcode-$(target):=)\
+       ,\
+       @$(eval mg-checked-$(target) :=)$(eval built-$(target) := yes)\
+       exec 3>$@.tmp &&\
+       echo $(call sq,$(call fmt-make-assignment,cmd-$(target),$(cmd))) >&3 &&\
+       set -o pipefail &&\
+       { { ($(cmd)) && { [ -r $(target).tmp ] || { echo 'mage: error: Command for $(target) succeeded without creating it!'; false; }; }; } 2>&1\
+       | sed -re '1s/^/errors-$(target):=$$(empty)/; 1!s/^/errors-$(target)+=$$(nl)/' >&3; xc=$$?; } &&\
+       { [ $$xc == 0 ] || echo "exitcode-$(target):=$$xc" >&3; } &&\
+       if [ $$xc != 0 ] || cmp -s $(target) $(target).tmp; then mv -f $@.tmp $@ && rm -f $(target).tmp;\
+       else rm -f $(target) && mv -f $@.tmp $@ && mv -f $(target).tmp $(target); fi\
+       )
+endef
+
+# We don't want anything we defined to become the default goal.
+.DEFAULT_GOAL := $(mg-orig-default-goal)
\ No newline at end of file