Commit | Line | Data |
---|---|---|
00804b7c MM |
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) |