#!/bin/sh # # spatchcache: a poor-man's "ccache"-alike for "spatch" in git.git # # This caching command relies on the peculiarities of the Makefile # driving "spatch" in git.git, in particular if we invoke: # # make # # See "spatchCache.cacheWhenStderr" for why "--very-quiet" is # # used # make coccicheck SPATCH_FLAGS=--very-quiet # # We can with COMPUTE_HEADER_DEPENDENCIES (auto-detected as true with # "gcc" and "clang") write e.g. a .depend/grep.o.d for grep.c, when we # compile grep.o. # # The .depend/grep.o.d will have the full header dependency tree of # grep.c, and we can thus cache the output of "spatch" by: # # 1. Hashing all of those files # 2. Hashing our source file, and the *.cocci rule we're # applying # 3. Running spatch, if suggests no changes (by far the common # case) we invoke "spatchCache.getCmd" and # "spatchCache.setCmd" with a hash SHA-256 to ask "does this # ID have no changes" or "say that ID had no changes> # 4. If no "spatchCache.{set,get}Cmd" is specified we'll use # "redis-cli" and maintain a SET called "spatch-cache". Set # appropriate redis memory policies to keep it from growing # out of control. # # This along with the general incremental "make" support for # "contrib/coccinelle" makes it viable to (re-)run coccicheck # e.g. when merging integration branches. # # Note that the "--very-quiet" flag is currently critical. The cache # will refuse to cache anything that has output on STDERR (which might # be errors from spatch), but see spatchCache.cacheWhenStderr below. # # The STDERR (and exit code) could in principle be cached (as with # ccache), but then the simple structure in the Redis cache would need # to change, so just supply "--very-quiet" for now. # # To use this, simply set SPATCH to # contrib/coccinelle/spatchcache. Then optionally set: # # [spatchCache] # # Optional: path to a custom spatch # spatch = ~/g/coccicheck/spatch.opt # # As well as this trace config (debug implies trace): # # cacheWhenStderr = true # trace = false # debug = false # # The ".depend/grep.o.d" can also be customized, as a string that will # be eval'd, it has access to a "$dirname" and "$basename": # # [spatchCache] # dependFormat = "$dirname/.depend/${basename%.c}.o.d" # # Setting "trace" to "true" allows for seeing when we have a cache HIT # or MISS. To debug whether the cache is working do that, and run e.g.: # # redis-cli FLUSHALL # # grep -hore HIT -e MISS -e SET -e NOCACHE -e CANTCACHE .build/contrib/coccinelle | sort | uniq -c # 600 CANTCACHE # 7365 MISS # 7365 SET # # A subsequent "make cocciclean && make coccicheck" should then have # all "HIT"'s and "CANTCACHE"'s. # # The "spatchCache.cacheWhenStderr" option is critical when using # spatchCache.{trace,debug} to debug whether something is set in the # cache, as we'll write to the spatch logs in .build/* we'd otherwise # always emit a NOCACHE. # # Reading the config can make the command much slower, to work around # this the config can be set in the environment, with environment # variable name corresponding to the config key. "default" can be used # to use whatever's the script default, e.g. setting # spatchCache.cacheWhenStderr=true and deferring to the defaults for # the rest is: # # export GIT_CONTRIB_SPATCHCACHE_DEBUG=default # export GIT_CONTRIB_SPATCHCACHE_TRACE=default # export GIT_CONTRIB_SPATCHCACHE_CACHEWHENSTDERR=true # export GIT_CONTRIB_SPATCHCACHE_SPATCH=default # export GIT_CONTRIB_SPATCHCACHE_DEPENDFORMAT=default # export GIT_CONTRIB_SPATCHCACHE_SETCMD=default # export GIT_CONTRIB_SPATCHCACHE_GETCMD=default set -e env_or_config () { env="$1" shift if test "$env" = "default" then # Avoid expensive "git config" invocation return elif test -n "$env" then echo "$env" else git config $@ || : fi } ## Our own configuration & options debug=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_DEBUG" --bool "spatchCache.debug") if test "$debug" != "true" then debug= fi if test -n "$debug" then set -x fi trace=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_TRACE" --bool "spatchCache.trace") if test "$trace" != "true" then trace= fi if test -n "$debug" then # debug implies trace trace=true fi cacheWhenStderr=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_CACHEWHENSTDERR" --bool "spatchCache.cacheWhenStderr") if test "$cacheWhenStderr" != "true" then cacheWhenStderr= fi trace_it () { if test -z "$trace" then return fi echo "$@" >&2 } spatch=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_SPATCH" --path "spatchCache.spatch") if test -n "$spatch" then if test -n "$debug" then trace_it "custom spatchCache.spatch='$spatch'" fi else spatch=spatch fi dependFormat='$dirname/.depend/${basename%.c}.o.d' dependFormatCfg=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_DEPENDFORMAT" "spatchCache.dependFormat") if test -n "$dependFormatCfg" then dependFormat="$dependFormatCfg" fi set=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_SETCMD" "spatchCache.setCmd") get=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_GETCMD" "spatchCache.getCmd") ## Parse spatch()-like command-line for caching info arg_sp= arg_file= args="$@" spatch_opts() { while test $# != 0 do arg_file="$1" case "$1" in --sp-file) arg_sp="$2" ;; esac shift done } spatch_opts "$@" if ! test -f "$arg_file" then arg_file= fi hash_for_cache() { # Parameters that should affect the cache echo "args=$args" echo "config spatchCache.spatch=$spatch" echo "config spatchCache.debug=$debug" echo "config spatchCache.trace=$trace" echo "config spatchCache.cacheWhenStderr=$cacheWhenStderr" echo # Our target file and its dependencies git hash-object "$1" "$2" $(grep -E -o '^[^:]+:$' "$3" | tr -d ':') } # Sanity checks if ! test -f "$arg_sp" && ! test -f "$arg_file" then echo $0: no idea how to cache "$@" >&2 exit 128 fi # Main logic dirname=$(dirname "$arg_file") basename=$(basename "$arg_file") eval "dep=$dependFormat" if ! test -f "$dep" then trace_it "$0: CANTCACHE have no '$dep' for '$arg_file'!" exec "$spatch" "$@" fi if test -n "$debug" then trace_it "$0: The full cache input for '$arg_sp' '$arg_file' '$dep'" hash_for_cache "$arg_sp" "$arg_file" "$dep" >&2 fi sum=$(hash_for_cache "$arg_sp" "$arg_file" "$dep" | git hash-object --stdin) trace_it "$0: processing '$arg_file' with '$arg_sp' rule, and got hash '$sum' for it + '$dep'" getret= if test -z "$get" then if test $(redis-cli SISMEMBER spatch-cache "$sum") = 1 then getret=0 else getret=1 fi else $set "$sum" getret=$? fi if test "$getret" = 0 then trace_it "$0: HIT for '$arg_file' with '$arg_sp'" exit 0 else trace_it "$0: MISS: for '$arg_file' with '$arg_sp'" fi out="$(mktemp)" err="$(mktemp)" set +e "$spatch" "$@" >"$out" 2>>"$err" ret=$? cat "$out" cat "$err" >&2 set -e nocache= if test $ret != 0 then nocache="exited non-zero: $ret" elif test -s "$out" then nocache="had patch output" elif test -z "$cacheWhenStderr" && test -s "$err" then nocache="had stderr (use --very-quiet or spatchCache.cacheWhenStderr=true?)" fi if test -n "$nocache" then trace_it "$0: NOCACHE ($nocache): for '$arg_file' with '$arg_sp'" exit "$ret" fi trace_it "$0: SET: for '$arg_file' with '$arg_sp'" setret= if test -z "$set" then if test $(redis-cli SADD spatch-cache "$sum") = 1 then setret=0 else setret=1 fi else "$set" "$sum" setret=$? fi if test "$setret" != 0 then echo "FAILED to set '$sum' in cache!" >&2 exit 128 fi exit "$ret"