# Mgear Build Tool by Matt McCutchen # http://www.kepreon.com/~matt/mgear/ # Remember the original default goal so we can restore it at the end of mgear.mk. mg-orig-default-goal:=$(.DEFAULT_GOAL) # 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: # Deletion of intermediate files messes everything up. # TODO: See if we can relax this requirement. .SECONDARY: # TEXT UTILITIES empty:= bs:=\$(empty) hash:=\# comma:=, pct:=% 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 $(hash),$$(hash),$(subst $(nl),$$(nl),$(subst $$,$$$$,$1))) # Make the value safe to use as an argument without doubling $. # *The implementation is incomplete*; I will improve it as needed. msarg=$(subst $(comma),$$(comma),$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) # TARGET OBFUSCATION # Mgear 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:.=/.)))) # Target-specific variables for obfuscated targets. $(opfx)%: oid=$(word 1,$(subst $(aname), ,$@)) $(opfx)%: otgt=$(word 2,$(subst $(aname), ,$@)) # High-priority implicit rule for obfuscated targets. Works for both always- # exists and alternate-name targets. $(opfx)%: $$($$(oid)@opr) $($(oid)@ocmd) # Make won't use the same implicit rule more than once on its stack, so provide # a second copy of the rule to allow two obfuscated targets on the stack. # Needed for $(mg-genfile-oid)$(aname)bar.g <- $(mg-scout-oid)$(aname)bar . # The prerequisites must look different before second-expansion so the second # rule isn't discarded as a duplicate. $(opfx)%: $$(empty) $$($$(oid)@opr) $($(oid)@ocmd) # MAIN BUILD LOGIC # DESIGN: # # - To each generated file bar corresponds a "genfile" bar.g that contains some # information about how bar was generated, including the command (for rebuild on # command change) and the warnings (for replay). # # - bar.g's mtime is the last time mgear verified that bar was up to date. bar # needs to be regenerated iff a prerequisite is newer than *bar.g* (not bar). # If a prerequisite changes but the command gives the same contents for bar, # bar.g is touched but bar itself is not touched because files that depend on it # need not be regenerated. # # - If bar is newer than bar.g, the user has overridden it, and we should leave # the override in place but warn the user about it. # # - The user's Makefile defines mgear rules by calling mg-define-rule. Each # mgear rule becomes one underlying make rule with the same target and a little # extra magic. This is important so that all implicit rule competition takes # place at the same target. Additionally, the prerequisites are passed to an # obfuscated target for bar.g to see if any are newer than bar.g. This is done # by the single obfuscated implicit rule, keeping the number of implicit rules # low. Then the command script for bar checks for overrides, command change, # prerequisite change, etc. and acts accordingly. # # Target metadata variables for bar: (* means stored in bar.g) # Now some variables are reused instead of remembered for every target. # # @name:=bar # Name of the target to which @cmd, @warnings, @deps refer. # # @cmd:=cat foo >bar.tmp # Generation command as given to the shell.* # # @warnings:=$(empty)yikes! # Data that the command printed to stdout or stderr, presumably warnings.* # # @deps:=included@x oops@ # If dependency-logging, list of filename@revision used. Revision is x for # exists and empty for doesn't exist. Later perhaps x will be the mtime.* # # bar@gdeps:=foo # Static dependencies of bar, for checking by bar.g. # # bar@gq:=foo $(mg-scout-oid)$(aname)bar # $? from the rule for bar.g; used by the rule for bar. # # bar@checked:=1 # Set when mgear determines that a file is up to date or depends on it being # so determined. Used to decide which prerequisite to check next for a # dependency-logging command. # # bar@dlc-ran:=1 # Indicates that mgear is in the middle of building bar using a dependency- # logging command. Means that bar.g.tmp, not bar.g, is the most current # genfile. # ## If the rule for bar is overridden, we clear the information from bar.g so ## that it is as if bar.g didn't exist. -- Not currently needed # # Some make features with which mgear's compatibility has not been investigated: # - Command-line options (especially --dry-run, --question, --touch, # --always-make, --keep-going, --jobs, --assume-old, and --assume-new) # - Static pattern rules # - Vpath # $(call gload,foo.o) # Load foo.o's genfile, if any, into $(@cmd), etc. define gload $(if $(filter $(@name),$1),,$(eval # TODO: Store and check the version of mgear that wrote the genfile. #@mgear-version:= @cmd:= @warnings:= @deps:= -include $1.g @name:=$1 )) endef @name:= # bar.g: scout bar and its dependencies and store $?. When implicit rules # compete for bar, we depend on the rule make uses being the last one it # second-expands so that $(bar@gdeps) is still correct. mg-genfile-oid:=$(newoid) mg-scout-oid:=$(newoid) $(mg-genfile-oid)@opr=$($(otgt:.g=)@gdeps) $(if $(wildcard $(otgt:.g=)),$(mg-scout-oid)$(aname)$(otgt:.g=),) MG-FORCE $(mg-genfile-oid)@ocmd=$(eval $(otgt:.g=)@gq:=$(filter-out MG-%,$?)) $(mg-scout-oid)@opr:= $(mg-scout-oid)@ocmd:= # Mgear-ized automatic variables. # For now, if you really want the eventual target, write $$(@). mg@ = $@.tmp # $%: haven't thought about it much, but probably needs no translation mg< = $(firstword $(mg^)) mg? = $(filter-out $(opfx)%,$($@@gq)) mg^ = $(filter-out MG-% $(opfx)%,$^) mg+ = $(filter-out MG-% $(opfx)%,$+) # $|: needs no translation # $*: needs no translation # $(call mg-translate-cmd,cat $$< >$$@) # Replaces references to automatic variables with references to their mgear-ized # counterparts. There might be false matches, e.g., $$@ => $$(mg@) ; # to prevent that, write $$$(empty)@ instead. (c.f. autoconf empty quadrigraph) mg-translate-cmd=$(subst $$@,$$(mg@),$(subst $$?,$$(mg?),$(subst $$<,$$(mg<),$(subst $$^,$$(mg^),$(subst $$+,$$(mg+),$1))))) # $(call mg-prereq-predict,target,prerequisites) # Expands to code that does the following at second-expansion time: # 1. Computes the actual prerequisites from the given prerequisite patterns by # translating % to $* and prepending a directory if appropriate. # 2. Saves both existing ($+) and newly computed prerequisites to $(bar@gdeps) # where the rule for bar.g can get them. # Factor out set-gdeps because make gets confused if the macro for the # prerequisites has a colon. Make replaces the first % with the stem *before # expansion*, so use $(pct) to protect some %s. mg-set-gdeps=$(eval $@@gdeps:=$1) mg-prereq-predict=$$$$(call mg-set-gdeps,$$$$+ $(if $(findstring /,$1),$$$$(subst $$$$(pct),$$$*,$2),$$$$(addprefix $$$$(dir $$$$@),$$$$(subst $$$$(pct),$$$$*,$2)))) # Used to make the rule for bar always run so we can act based on $(bar@gdeps) # and check for command change. MG-FORCE: .PHONY: MG-FORCE # $(call mg-define-rule,target,prerequisites,cmdvar) # Defines a rule. cmdvar is *the name of a variable* containing the command. # The variable is read immediately with $(value) and then expanded in the scope # of mgear-ized automatic variables each time the rule is run. # # I eradicated the target-specific variables because they fail when there are # multiple implicit rules with the same target pattern. # # Rule for the target. Set $(bar@gdeps) to all prerequisites. Apply the new # prerequisites. The command script always runs. Depend on bar.g to get # $(bar@gq). define mg-define-rule $(eval $1: $(call mg-prereq-predict,$1,$2) $2 MG-FORCE $(mg-genfile-oid)$(aname)$$$$@.g $$(call mg-rule-cmd,$(call msarg,$(call mg-translate-cmd,$(value $3))))) endef # TODO Provide a way to define static pattern rules. # Procedure to generate bar. Now $@ is bar, so we say $@.g for bar.g. # If an override: # 1. Complain. (Should we stop "`bar' is up to date" using @:; ?) # 2. This would be the place to clear bar's metadata if we wanted to. # Otherwise, check prereqs, command, and nonexistence to decide whether bar # needs to be regenerated. If so: # 1. Note that the genfile is about to change. # 2. Echo the command being run. # On error, skip to 8: # 3. Open the new genfile bar.g.tmp for writing. # 4. Run the command to bar.tmp, storing warnings in bar.g.tmp. # 5. Store the command in bar.g.tmp. Do it here to ensure that bar.g.tmp # is at least as new as bar.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) # If not: # 1. If there were warnings, replay them. (HMMM To stderr?) # HMMM .tmp in displayed command looks ugly define mg-rule-cmd $(if $(filter $(mg-scout-oid)$(aname)$@,$($@@gq)),\ $(info mgear: warning: Manually created/modified file at $@ overrides rule.)\ ,$(call gload,$@)$(if $($@@gq)$(if $(wildcard $@),,TARGET-DNE)$(mg-check-cmd),\ $(eval $@@gloaded:=)$(info $1)\ @trap 'rm -f $@.tmp $@.g.tmp' EXIT &&\ exec 3>$@.g.tmp &&\ set -o pipefail && { $(mg-run-cmd) | tee /dev/fd/4 | $(mg-wrap-warnings) >&3; } 4>&1 &&\ $(mg-assign-cmd) >&3 &&\ $(mg-maybe-move-target) &&\ mv -f $@.g.tmp $@.g\ ,$(if $(@warnings),\ $(info $1 # mgear warning replay$(nl)$(@warnings))\ ))) endef # If the command changed, we must regenerate. # HMMM What if the working directory changes? Most likely, the command will # also change and mgear will do the right thing. mg-check-cmd=$(if $(call streq,$1,$(@cmd)),,COMMAND-CHANGED) # Pieces of mg-generate that I factored out to make mg-generate more readable. mg-assign-cmd=echo $(call sq,$(call fmt-make-assignment,@cmd,$1)) mg-run-cmd={ ($1) 2>&1 && { [ -r $@.tmp ] || { echo 'mgear: error: Command for $@ succeeded without creating it!'; false; }; }; } mg-wrap-warnings=sed -re '1s/^/@warnings:=$$(empty)/; 1!s/^/@warnings+=$$(nl)/' # Drat bash's lack of precedence between || and &&. Extra braces necessary. mg-maybe-move-target={ cmp -s $@ $@.tmp || { echo >$@.g && mv -f $@.tmp $@; }; } # Just add additional prerequisites. This cannot add prerequisite patterns to # an implicit rule, but it can add specific prerequisites to an individual use # of an implicit rule. Currently, mgear picks up the target's prerequisites # from make, so this just attaches the given prerequisites to the target, but # the implementation might change in the future. mg-define-prereq=$(eval $1: $2) # CLEAN # A command that finds all genfiles in a given directory and deletes them and # the corresponding generated files. If a file is overridden, only the genfile # is deleted. # # Example: # # clean: # $(call mg-clean-cmd,.) # .PHONY: clean define mg-clean-cmd @exec 3>&1 &&\ find $1 -name '*.g' | while read gf; do\ f="$${gf%.g}" &&\ if ! [ "$$f" -nt "$$gf" ]; then\ echo "rm -f '$$f'" >&3 && echo "$$f";\ fi &&\ echo "$$gf";\ done | xargs rm -f endef # DEPENDENCY-LOGGING COMMANDS mg-dlc-static-run-oid:=$(mg-genfile-oid) # $(call mg-define-rule-dlc,target,static-prerequisites,cmd,dep-converter) # Analogue of mg-define-rule for a dependency-logging command. # I haven't decided on the format for the dep-converter yet. define mg-define-rule-dlc $(eval # FINISH # Rule for the target. Set $(bar@gdeps) to all prerequisites. Apply the new # prerequisites. The command script always runs. Finally, do the static run # and then the first dynamic check. $1: $(call mg-prereq-predict,$1,$2) $2 MG-FORCE $(mg-dlc-static-run-oid)$(aname)$$$$@.g $$$$(call dlc-next-dchk,$$$$(target)) # TODO: Move the finished file into place or something # $$(call mg-rule-cmd,$3) ) endef #mg-dlc-next-dchk=$(call mg-dlc-next-dchk-1,$1,$(newoid)) #mg-dlc-next-dchk-1=$(eval $2@opr=$$(call mg-dlc-dchk-pr,$1,$2))$2 # ## $(call mg-dlc-dchk-pr,target,oid) #mg-dlc-dchk-pr=$(call mg-dlc-dchk-pr-1,$1,$2,$(call next-unchecked-prereq,$($1@deps))) ## $(call mg-next-unchecked-prereq,foo.c bar.h baz.h) #mg-next-unchecked-prereq=$(firstword $(foreach p,$1,$(if $($p@checked),,$p))) #define mg-dlc-dchk-pr-1 #$(if $3,$(call mg-dlc-drun,$1,$2,$3) $(call mg-dlc-next-dchk,$1),) #endef # ## $(call mg-dlc-drun,target,oid,prereq). FINISH #define mg-dlc-drun #$(eval #$2$(aname)$3@opr:=$3 #$2$(aname)$3@ocmd:= #)$2$(aname)$3 #endef # # ## END # We don't want anything we defined to become the default goal. .DEFAULT_GOAL := $(mg-orig-default-goal)