| 1 | # Mage by Matt McCutchen |
| 2 | |
| 3 | # Text utilities |
| 4 | empty := |
| 5 | bs := \$(empty) |
| 6 | define nl |
| 7 | |
| 8 | |
| 9 | endef |
| 10 | # Shell-quote: a'b => 'a'\''b' |
| 11 | sq = '$(subst ','\'',$1)' |
| 12 | # Make-quote: a$\nb => a$$$(nl)b |
| 13 | # This is enough to assign the value, *but not to use it as an argument!* |
| 14 | mqas = $(subst $(nl),$$(nl),$(subst $$,$$$$,$1)) |
| 15 | # Return nonempty if the strings are equal, empty otherwise. |
| 16 | # If the strings have only nice characters, you can do $(filter x$1,x$2). |
| 17 | streq = $(findstring x$1,$(findstring x$2,x$1)) |
| 18 | |
| 19 | # $(call fmt-make-assignment,foo,bar) |
| 20 | # Output a make assignment that stores bar in variable foo. |
| 21 | # The result is like `foo:=bar' but handles leading spaces, $, and |
| 22 | # newlines appearing in bar safely. |
| 23 | fmt-make-assignment = $1:=$$(empty)$(call mqas,$2) |
| 24 | |
| 25 | mg-orig-default-goal := $(.DEFAULT_GOAL) |
| 26 | |
| 27 | # bar.g file format: |
| 28 | # cmd-bar:=cat foo >bar.tmp |
| 29 | # errors-bar:=$(empty)yikes! |
| 30 | # exitcode-bar:=0 |
| 31 | |
| 32 | # $(call mg-check-file,foo.o) |
| 33 | # Make sure foo.o's genfile, if any, has been loaded. |
| 34 | define mg-check-file |
| 35 | $(if $(mg-checked-$1),,$(eval |
| 36 | cmd-$1 := |
| 37 | errors-$1 := |
| 38 | exitcode-$1 := |
| 39 | -include $1.g |
| 40 | mg-checked-$1 := done |
| 41 | )) |
| 42 | endef |
| 43 | |
| 44 | # $(call mg-translate-cmd,touch $$@) |
| 45 | # Currently translates $@, $<, $^, $+ to Mage-ized variables. |
| 46 | # $* needs no translation. We won't support $%, $?, $|. |
| 47 | # There might be false matches, e.g., $$@ => $$(out) ; |
| 48 | # to prevent that, do $$$(empty)@ . |
| 49 | define mg-translate-cmd |
| 50 | $(subst $$@,$$(mg@),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1)))) |
| 51 | endef |
| 52 | |
| 53 | # OOOH!!! .SECONDEXPANSION does let us watch the implicit rule search as it |
| 54 | # happens. |
| 55 | .SECONDEXPANSION: |
| 56 | |
| 57 | # $(call mg-rule,target,prerequisite,cmd) |
| 58 | # Defines a rule. |
| 59 | # If cmd uses $@, quote if necessary so this function sees $@, etc. |
| 60 | # |
| 61 | # When we build bar from foo using a Mage rule: |
| 62 | # Division of work: |
| 63 | # - "bar.g: foo" does the computation. |
| 64 | # - "bar: bar.g" replays errors and exit code. |
| 65 | # Cases: |
| 66 | # - bar is managed and up to date => replay |
| 67 | # - bar doesn't exist or is managed and out of date => compute |
| 68 | # - bar is unmanaged => warn about override |
| 69 | MG-FORCE-TARGET-DNE: |
| 70 | MG-FORCE-CMD-CHANGED: |
| 71 | MG-FORCE-REPLAY: |
| 72 | .PHONY: MG-FORCE-TARGET-DNE MG-FORCE-CMD-CHANGED MG-FORCE-REPLAY |
| 73 | define mg-rule |
| 74 | $(eval |
| 75 | |
| 76 | # Define some target-specific variables. |
| 77 | # It might look like we could use $*, but $1 most likely isn't %. |
| 78 | $1.g: target = $$(@:.g=) |
| 79 | $1.g: cmd = $(call mg-translate-cmd,$3) |
| 80 | $1.g: mg@ = $$(target).tmp |
| 81 | $1.g: mg^ = $$(filter-out MG-% $$(abspath $$(target)),$$^) |
| 82 | $1.g: mg+ = $$(filter-out MG-% $$(abspath $$(target)),$$+) |
| 83 | $1.g: mg< = $$(firstword $$(mg^)) |
| 84 | |
| 85 | # Load the target's genfile if necessary. |
| 86 | $1.g: $$$$(call mg-check-file,$$$$(target)) |
| 87 | |
| 88 | # If the target doesn't exist, we must run the rule; we'll generate it. |
| 89 | # If the target is unmanaged, we must run the rule; we'll see that the |
| 90 | # obfuscated target is in $? and complain. |
| 91 | $1.g: $$$$(if $$$$(wildcard $$$$(target)),$$$$(abspath $$$$(target)),$$$$(if $$(exitcode-$$$$(target)),,MG-FORCE-TARGET-DNE)) |
| 92 | |
| 93 | # If the command changed, we must regenerate. |
| 94 | $1.g: $$$$(if $$$$(call streq,$$$$(cmd),$$$$(cmd-$$$$(target))),,MG-FORCE-CMD-CHANGED) |
| 95 | |
| 96 | $1.g: $2 |
| 97 | $(value mg-commands) |
| 98 | |
| 99 | # Replay the errors and the exit code (if any of either). |
| 100 | # We don't have to worry about .DELETE_ON_ERROR deleting the target because |
| 101 | # it is only touched if we *successfully* regenerate it. |
| 102 | # Recheck the file in case it was regenerated. |
| 103 | # HMMM Maybe errors should go to stderr??? |
| 104 | $1: $1.g MG-FORCE-REPLAY |
| 105 | $$(call mg-check-file,$$@) |
| 106 | $$(if $$(errors-$$@)$$(exitcode-$$@)$$(built-$$@),$$(info $$(cmd-$$@)$$(if $$(built-$$@),, [replay]))$$(if $$(errors-$$@),$$(info $$(errors-$$@)),),) |
| 107 | $$(if $$(exitcode-$$@),@exit $$(exitcode-$$@),) |
| 108 | ) |
| 109 | endef |
| 110 | |
| 111 | # Just add additional prerequisites. |
| 112 | define mg-prereq |
| 113 | $(eval |
| 114 | $1.g: $2 |
| 115 | ) |
| 116 | endef |
| 117 | |
| 118 | # If an override: |
| 119 | # 1. Complain. |
| 120 | # 2. Clear out the errors and exit code so we don't replay them. |
| 121 | # Otherwise: |
| 122 | # 1. Store the command. |
| 123 | # 2. Run the command, storing errors. |
| 124 | # 3. Store exit code. |
| 125 | # 4. Update the real files as applicable. |
| 126 | # 5. Read the new genfile. |
| 127 | define mg-commands |
| 128 | $(if $(filter $(abspath $(target)),$?),\ |
| 129 | $(info mage: warning: Manually created/modified file at $(target) overrides rule.)\ |
| 130 | $(eval cmd-$(target):=)$(eval errors-$(target):=)$(eval exitcode-$(target):=)\ |
| 131 | ,\ |
| 132 | @$(eval mg-checked-$(target) :=)$(eval built-$(target) := yes)\ |
| 133 | exec 3>$@.tmp &&\ |
| 134 | echo $(call sq,$(call fmt-make-assignment,cmd-$(target),$(cmd))) >&3 &&\ |
| 135 | set -o pipefail &&\ |
| 136 | { { ($(cmd)) && { [ -r $(target).tmp ] || { echo 'mage: error: Command for $(target) succeeded without creating it!'; false; }; }; } 2>&1\ |
| 137 | | sed -re '1s/^/errors-$(target):=$$(empty)/; 1!s/^/errors-$(target)+=$$(nl)/' >&3; xc=$$?; } &&\ |
| 138 | { [ $$xc == 0 ] || echo "exitcode-$(target):=$$xc" >&3; } &&\ |
| 139 | if [ $$xc != 0 ] || cmp -s $(target) $(target).tmp; then mv -f $@.tmp $@ && rm -f $(target).tmp;\ |
| 140 | else rm -f $(target) && mv -f $@.tmp $@ && mv -f $(target).tmp $(target); fi\ |
| 141 | ) |
| 142 | endef |
| 143 | |
| 144 | # We don't want anything we defined to become the default goal. |
| 145 | .DEFAULT_GOAL := $(mg-orig-default-goal) |