#!/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
# <make && make coccicheck, as above>
# 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"