# Mage Build Tool by Matt McCutchen # http://www.kepreon.com/~matt/mage/ # 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 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) # We use second-expansion heavily to dynamically compute prerequisites when they # are needed. Second-expansion lets us follow make's implicit rule search # instead of trying to anticipate which prerequisites it will need in advance. .SECONDEXPANSION: # TARGET OBFUSCATION # Mage uses two kinds of "obfuscated targets" (those that are not the simple # names of real files): # - An always-exists target like /.//. is used as a prerequisite of an implicit # rule. Since it exists, make doesn't second-expand its own prerequisites # until it is actually run. This way, a target's prerequisites can depend on # the results of previous command scripts. # - An alternative-name target like /.//./proc/self/cwd/bar is used to check the # mtime of a file without actually building it or introducing a circular # dependency. # $(newoid) allocates and returns a new obfuscation ID (oid). Example: # x:=$(newoid) # You can then refer to target $x or $x$(aname)bar (for any file bar). # Prerequisites and commands come from $($x@opr) and $($x@ocmd). # Target-specific variables $(oid) and $(otgt) (if alternate-name) are # available. opfx:=/./ # TODO If the system doesn't support /proc/self/cwd, use something else. aname:=/proc/self/cwd/ nextoid:=/./. newoid=$(nextoid)$(eval nextoid:=$(subst /.//////,//./,$(patsubst /.//////%,/././%,$(nextoid:.=/.)))) # High-priority implicit rule for obfuscated targets. Works for both always- # exists and alternate-name targets. $(opfx)%: oid=$(word 1,$(subst $(aname), ,$@)) $(opfx)%: otgt=$(word 2,$(subst $(aname), ,$@)) $(opfx)%: $$($$(oid)@opr) $($(oid)@ocmd) # 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 gload $(if $($1@gloaded),,$(eval $1@cmd:= $1@warnings:= $1@deps:= -include $1.g $1@gloaded:=1 )) endef # $(call mg-translate-cmd,touch $$@) # 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)))) endef # $(call mg-rule,target,prerequisite,cmd) # Defines a rule. # If cmd uses $@, quote if necessary so this function sees $@, etc. # # When we generate bar from foo using a Mage rule: # Division of work: # - "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 => generate # - bar is unmanaged => warn about override MG-FORCE: .PHONY: MG-FORCE scout-oid:=$(newoid) define mg-define-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-% /./%,$$^) $1.g: mg+ = $$(filter-out MG-% /./%,$$+) $1.g: mg< = $$(firstword $$(mg^)) # 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) $(mg-file-from-genfile) ) endef define mg-file-from-genfile # If the file was regenerated, load the new genfile. # If not, replay any warnings. # 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 is unmanaged, we must run the rule; we'll see that the # obfuscated target is in $? and complain. mg-scout-target=$(if $(wildcard $(target)),$(scout-oid)$(aname)$(target),) # If the command changed, we must regenerate. mg-check-cmd=$(if $(call streq,$(cmd),$($(target)@cmd)),,x) # Just add additional prerequisites. # I don't think this can add patterns to implicit rules, but it should be able # to add specific prerequisites to uses of implicit rules. define mg-define-prereq $(eval $1.g: $2 ) endef # Procedure to generate bar. Remember, $@ is bar.g. # If an override: # 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 $(scout-oid)$(aname)$(target),$?),\ $(info mage: warning: Manually created/modified file at $(target) overrides rule.)\ $(eval $(target)@cmd:=)$(eval $(target)@warnings:=)$(eval $(target)@deps:=)\ ,\ $(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)