X-Git-Url: https://mattmccutchen.net/mgear/mgear.git/blobdiff_plain/21a503712996ac153f7e7b79596d8f3a372c1a97..099638eb844d4da0ffba1c238ed3904d90a622bc:/mgear.mk diff --git a/mgear.mk b/mgear.mk new file mode 100644 index 0000000..22465ce --- /dev/null +++ b/mgear.mk @@ -0,0 +1,319 @@ +# 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: + + +# TEXT UTILITIES +empty:= +bs:=\$(empty) +hash:=\# +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))) +# 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) +# +# bar@cmd:=cat foo >bar.tmp +# Generation command as given to the shell.* +# +# bar@warnings:=$(empty)yikes! +# Data that the command printed to stdout or stderr, presumably warnings.* +# +# bar@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@gloaded:=1 +# Set if mgear has loaded bar.g and hasn't changed it since then. +# +# 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) +# 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 + +# 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-genfile-oid)@ocmd=$(eval $(otgt:.g=)@gq:=$?) + +# Mgear-ized automatic variables. +# $@: needs no translation +# NOTE: $(mg@) is *eventual* target. Commands must write to temp file, $t . +# $%: 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 $$< >$$t) +# 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 $$t,$$@.tmp,$(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,cmd) +# Defines a rule. cmd is expanded again when it is run, at which time +# Mgear-ized automatic variables are available. +# +# 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 mg-translate-cmd,$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 $($@@cmd) # mgear warning replay$(nl)$($@@warnings))\ + ))) +endef + +# If the command changed, we must regenerate. +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) + + +# 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)