Made a bunch of cleanups/improvements, including:
authorMatt McCutchen <hashproduct@gmail.com>
Mon, 4 Jun 2007 05:42:45 +0000 (01:42 -0400)
committerMatt McCutchen <hashproduct@gmail.com>
Mon, 4 Jun 2007 16:36:30 +0000 (12:36 -0400)
- Changed some variable/function names.  mg-rule -> mg-define-rule.  (Changed
  demo to match.)  Changed Mage target variables: cmd-foo.o -> foo.o@cmd.
- Took out exit code replay because it introduced lots of complexity for little
  benefit.
- Changed scouted target from abspath to /./proc/self/cwd format.  This fits
  with the planned target obfuscation system but isn't so portable; I'll
  reconsider later.
- Massively refactored mg-commands -> mg-generate so that it's understandable.
- FORCEd *.g rules and moved a bunch of processing from second-expansion to the
  command script.  This way the processing only runs if make actually uses the
  rule (not just second-expanding anyway or implicit rule searching).
- Detection of command change for implicit rules was broken because evidently
  only the prereqs given with the command take effect.  I moved all the prereqs
  to the same line but then the new command was evaluated too early to see the
  automatic variables with the user-specified prereqs.  Moving the command check
  into the command script fixed it.
- Put more complete title and Web site at the top.

I have been preparing to add the dependency-logging command support, which is
going to be dauntingly complicated.  I wanted to nail down the basics first.

demo/Makefile
src/mage.mk

index 2e20b36..cfdfc32 100644 (file)
@@ -7,7 +7,7 @@ include mage.mk
 foo.x.h:
 
 cmd-xgrep = grep 'X' '$<' >'$@'
-$(call mg-rule,%.x,%,$(value cmd-xgrep))
+$(call mg-define-rule,%.x,%,$(value cmd-xgrep))
 
 cmd-addheader = echo 'Header:' | cat - '$<' >'$@'
-$(call mg-rule,%.h,%,$(value cmd-addheader))
+$(call mg-define-rule,%.h,%,$(value cmd-addheader))
index 465a05a..47650b3 100644 (file)
@@ -1,6 +1,10 @@
-# Mage by Matt McCutchen
+# Mage Build Tool by Matt McCutchen
+# http://www.kepreon.com/~matt/mage/
 
-# Text utilities
+# 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)
 define nl
@@ -22,29 +26,42 @@ streq = $(findstring x$1,$(findstring x$2,x$1))
 # newlines appearing in bar safely.
 fmt-make-assignment = $1:=$$(empty)$(call mqas,$2)
 
-mg-orig-default-goal := $(.DEFAULT_GOAL)
+# OBFUSCATION
 
-# bar.g file format:
-# cmd-bar:=cat foo >bar.tmp
-# errors-bar:=$(empty)yikes!
-# exitcode-bar:=0
+# Coming soon.
+
+# High-priority implicit rule to ensure that we don't try to actually update
+# obfuscated targets.  TEMP
+/./proc/self/cwd/%:
+       
 
-# $(call mg-check-file,foo.o)
+# MAIN BUILD LOGIC
+
+# bar.g file format:
+#     bar@cmd:=cat foo >bar.tmp
+#     bar@warnings:=$(empty)yikes!
+# And if dependency-logging:
+# List of filename@revision, revision is x for exists and empty for doesn't
+# exist.  Later perhaps x will be the mtime.
+#     bar@deps:=included@x oops@
+# If bar is overridden, we clear all three variables.
+
+# $(call gload,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 :=
+define gload
+$(if $($1@gloaded),,$(eval 
+$1@cmd:=
+$1@warnings:=
+$1@deps:=
 -include $1.g
-mg-checked-$1 := done
+$1@gloaded:=1
 ))
 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) ;
+# Translates $@, $<, $^, $+, $* to Mage-ized variables if necessary.
+# We won't support $%, $?, $|.
+# There might be false matches, e.g., $$@ => $$(mg@) ;
 # to prevent that, do $$$(empty)@ .
 define mg-translate-cmd
 $(subst $$@,$$(mg@),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1))))
@@ -58,19 +75,19 @@ endef
 # Defines a rule.
 # If cmd uses $@, quote if necessary so this function sees $@, etc.
 # 
-# When we build bar from foo using a Mage rule:
+# When we generate bar from foo using a Mage rule:
 # Division of work:
-# - "bar.g: foo" does the computation.
-# - "bar: bar.g" replays errors and exit code.
+# - "bar.g: foo" generates bar if necessary.
+# - "bar: bar.g" loads new genfile and replays warnings if bar.g was
+#   already up to date.
 # Cases:
 # - bar is managed and up to date => replay
-# - bar doesn't exist or is managed and out of date => compute
+# - bar doesn't exist or is managed and out of date => generate
 # - bar is unmanaged => warn about override 
+MG-FORCE:
 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
+.PHONY: MG-FORCE MG-FORCE-TARGET-DNE
+define mg-define-rule
 $(eval 
 
 # Define some target-specific variables.
@@ -78,68 +95,84 @@ $(eval
 $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^ = $$(filter-out MG-% /./%,$$^)
+$1.g: mg+ = $$(filter-out MG-% /./%,$$+)
 $1.g: mg< = $$(firstword $$(mg^))
 
-# Load the target's genfile if necessary.
-$1.g: $$$$(call mg-check-file,$$$$(target))
+# Rule for the genfile.  Evidently all the prerequisites we want second-expanded
+# have to go on the same rule.
+$1.g: $2 $$$$(mg-scout-target) MG-FORCE
+       $$(mg-generate)
+
+# If the file was regenerated, load the new genfile.
+# If not, replay any warnings.
+## We don't have to worry about .DELETE_ON_ERROR deleting the target because
+## it is only touched if we *successfully* regenerate it.
+# - No longer applies because we don't remember errors.
+# HMMM Maybe errors should go to stderr???
+$1: MG-FORCE | $1.g
+       $$(call gload,$$@)
+       $$(if $$($$@@warnings),$$(if $$($$@@built),,$$(info $$($$@@cmd) # warning replay)$$(info $$($$@@warnings))),)
+)
+endef
 
-# 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))
+mg-scout-target=$(if $(wildcard $(target)),/./proc/self/cwd/$(target),)
 
 # 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
+mg-check-cmd=$(if $(call streq,$(cmd),$($(target)@cmd)),,x)
 
 # Just add additional prerequisites.
-define mg-prereq
+# I don't think this works for implicit rules.
+define mg-define-prereq
 $(eval 
 $1.g: $2
 )
 endef
 
+# Procedure to generate bar.  Remember, $@ is bar.g.
 # 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)),$?),\
+#     1. Complain.  (Should we stop "`bar' is up to date" using @:; ?)
+#     2. Clear out the warnings so we don't replay them.
+# 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".
+#     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.
+#        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)
+define mg-generate
+       $(call gload,$(target))\
+       $(if $(filter /./proc/self/cwd/$(target),$?),\
                $(info mage: warning: Manually created/modified file at $(target) overrides rule.)\
-               $(eval cmd-$(target):=)$(eval errors-$(target):=)$(eval exitcode-$(target):=)\
+               $(eval $(target)@cmd:=)$(eval $(target)@warnings:=)$(eval $(target)@deps:=)\
        ,\
-       @$(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\
-       )
+       $(if $?$(if $(wildcard $(target)),,x)$(mg-check-cmd),\
+       $(eval $(target)@gloaded:=)$(eval $(target)@built:=1)$(info $(cmd))\
+       @trap 'rm -f $(target).tmp $@.tmp' EXIT &&\
+       exec 3>$@.tmp && $(mg-assign-cmd) >&3 &&\
+       set -o pipefail && { $(mg-run-cmd) | tee /dev/fd/4 | $(mg-wrap-warnings) >&3; } 4>&1 &&\
+       $(mg-maybe-move-target) && mv -f $@.tmp $@\
+       ))
 endef
 
+# Pieces of mg-generate that I factored out to make mg-generate more readable.
+mg-assign-cmd=echo $(call sq,$(call fmt-make-assignment,$(target)@cmd,$(cmd)))
+mg-run-cmd={ ($(cmd)) 2>&1 && { [ -r $(target).tmp ] || { echo 'mage: error: Command for $(target) succeeded without creating it!'; false; }; }; }
+mg-wrap-warnings=sed -re '1s/^/$(target)@warnings:=$$(empty)/; 1!s/^/$(target)@warnings+=$$(nl)/'
+mg-maybe-move-target={ cmp -s $(target) $(target).tmp || echo >$@ && mv -f $(target).tmp $(target); }
+
+# END
+
 # We don't want anything we defined to become the default goal.
-.DEFAULT_GOAL := $(mg-orig-default-goal)
\ No newline at end of file
+.DEFAULT_GOAL := $(mg-orig-default-goal)