# 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)