Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 771 lines (683 sloc) 14.1 KB
#!/bin/sh -e
OFF=0
ERROR=1
WARN=2
INFO=3
DEBUG=4
TRACE=5
REDO_LOG_LEVEL=${REDO_LOG_LEVEL:-${INFO}}
DEFAULT_LEVEL=$TRACE
main() {
if [ -z "$REDO" ]; then
export REDO=`readlink -f $0`
export REDO_START_DIR="$PWD"
export REDO_ROOT_DIR="$PWD"
export REDO_META_DIR="$REDO_ROOT_DIR/.redo"
mkdir -p "$REDO_META_DIR"
export REDO_BIN_DIR="$REDO_META_DIR/bin"
mkdir -p "$REDO_BIN_DIR"
export REDO_DB_DIR="$REDO_META_DIR/db"
mkdir -p "$REDO_DB_DIR"
export REDO_PATH="$REDO_BIN_DIR:$PATH"
export REDO_TIMESTAMP=`date +%s`
ensure_symlink "$REDO_BIN_DIR/redo"
register_cmds \
ifchange ifcreate always phony graph log targets sources gc clean \
tmp logwrap
fi
local name=`basename $0`
case $name in
redo)
cli_redo $*
;;
redo-*)
try_dispatch_cmd "${name#redo-}" $*
;;
*)
info_log -$ERROR "Invalid command '$name'"
exit 1
;;
esac
}
cli_redo() {
case $1 in
--*)
local cmd=${1#--}
shift
try_dispatch_cmd "$cmd" $*
;;
*)
export REDO_TIMESTAMP=`date +%s`
REDO_PARENT=
if [ "$#" -eq 0 ]; then
_cli_redo all
else
_cli_redo $*
fi
;;
esac
}
_cli_redo() {
local i
for i in "$@"
do
to_absolute_path "$i"
redo "$path_result"
done
}
redo() {
relative_path_to "$REDO_START_DIR" "$1"
local rel_path="$path_result"
cmd_log redo "$rel_path"
db_get_type "$1"
local file_type="$db_result"
# Find and execute build script
if find_build_script "$1"; then
rm -rf "$1"
db_select "$1"
rm -rf "$db_bucket/prereqs"
rm -rf "$db_bucket/ifcreate"
rm -rf "$db_bucket/checksum"
# only regular target cares about prereq
if [ ! -z "$non_exist_build_scripts" ] && [ "$file_type" = "tr" ]; then
with_parent "$1" redo_ifcreate $non_exist_build_scripts
fi
local script="$build_script"
if run_build_script "$1" "$script"; then
# an always or phony target is always outdated so there's no need
# to update its prereqs' checksums
if [ "$file_type" = "tr" ]; then
db_update_prereqs_checksums "$1"
fi
db_update_own_checksum "$1" || true
db_mark_as_uptodate "$1"
info_log "Done with '$rel_path'"
else
info_log -$ERROR "'${script#${REDO_START_DIR}/}' failed"
exit 1
fi
else
info_log -$ERROR "No build script for '$rel_path'"
exit 1
fi
}
cli_redo_ifchange() {
local i
for i in "$@"
do
to_absolute_path "$i"
redo_ifchange "$path_result"
done
}
redo_ifchange() {
relative_path_to "$REDO_START_DIR" "$1"
local rel_path="$path_result"
db_add_prereq "$REDO_PARENT" "$1"
db_get_type "$1"
local file_type="$db_result"
local file_type_name=
case $file_type in
s)
file_type_name="source file"
;;
tr)
file_type_name="regular target"
;;
ta)
file_type_name="live target"
;;
tp)
file_type_name="phony target"
;;
esac
info_log "Checking $file_type_name '$rel_path'"
if db_is_uptodate "$1"; then
info_log "'$rel_path' was checked previously"
return 0
fi
# A source file only need to have its hash updated
if [ "$file_type" = "s" ]; then
if db_update_own_checksum "$1"; then
info_log "'$rel_path' was updated"
else
info_log "'$rel_path' is unchanged"
fi
db_mark_as_uptodate "$1"
return 0
fi
# An existing regular target file may not need to be rebuilt
db_select "$1"
if [ "$file_type" = "tr" ] && [ -e "$1" ] && [ -f "$db_bucket/checksum" ]; then
local i
local must_build="n"
# Check all "if-create" prereqs
db_get_ifcreate_prereqs "$1"
for i in $db_result
do
if [ -e "$i" ]; then
relative_path_to "$REDO_START_DIR" "$i"
info_log "'$path_result' was created"
must_build="y"
break
fi
done
if [ "$must_build" = "n" ]; then
# Check if pre reqs are up-to-date
db_get_prereqs "$1"
for i in $db_result
do
relative_path_to "$REDO_START_DIR" "$i"
local prereq_rel_path="$path_result"
info_log "Checking prereq '$prereq_rel_path' of '$rel_path'"
# Update prereq if needed
if db_outdated "$i"; then
with_parent "$1" redo_ifchange "$i"
fi
# Even if prereq is up-to-date, we may still depend on an old
# version of it, so check if it was rebuilt since last time
if db_prereq_rebuilt "$1" "$i"; then
info_log "'$prereq_rel_path' was updated"
must_build="y"
fi
done
fi
# If file exists and it is not required to build, bail out
if [ "$must_build" = "n" ]; then
db_mark_as_uptodate "$1"
info_log "'$rel_path' is up-to-date"
return 0
fi
fi
redo "$1"
}
cli_redo_ifcreate() {
local i
for i in "$@"
do
to_absolute_path "$i"
redo_ifcreate "$path_result"
done
}
redo_ifcreate() {
if [ ! -e "$1" ]; then
db_add_ifcreate_prereq "$REDO_PARENT" "$1"
else
relative_path_to "$REDO_START_DIR" "$1"
info_log -$ERROR "'$path_result' exists"
fi
}
cli_redo_always() {
if [ -z "$REDO_PARENT" ]; then
info_log -$ERROR "redo-always can only be called inside a do script"
exit 1
fi
db_select "$REDO_PARENT"
db_write "$db_bucket/type" "ta"
}
cli_redo_phony() {
if [ -z "$REDO_PARENT" ]; then
info_log -$ERROR "redo-phony can only be called inside a do script"
exit 1
fi
db_select "$REDO_PARENT"
db_write "$db_bucket/type" "tp"
}
cli_redo_graph() {
local i
local j
echo "digraph redo {"
local files=`find $REDO_DB_DIR -type f -path '*/.redo/type' -printf '%P\n'`
for i in $files
do
local filename="${i%/.redo/type}"
local file_type=
if db_has_type "$filename"; then
db_get_type "$filename"
file_type="$db_result"
fi
local attrs=
case $file_type in
s)
attrs="[ shape=box ]"
;;
tr)
attrs="[ shape=ellipse ]"
;;
ta)
attrs="[ shape=diamond ]"
;;
tp)
attrs="[ shape=diamond, color=green ]"
;;
*)
attrs="[ ]"
;;
esac
echo " \"$filename\" $attrs"
db_get_prereqs "$filename"
for j in $db_result
do
relative_path_to "$PWD" "$j"
echo " \"$filename\" -> \"$path_result\";"
done
db_get_ifcreate_prereqs "$filename"
for j in $db_result
do
relative_path_to "$PWD" "$j"
echo " \"$filename\" -> \"$path_result\" [ arrowhead=box, color=red ];"
done
done
echo "}"
}
cli_redo_log() {
case $1 in
-*)
info_log $*
;;
*)
info_log -$INFO $*
;;
esac
}
cli_redo_logwrap() {
if set -o pipefail 2> /dev/null; then
$* 2>&1 | \
while read line
do
info_log -$INFO $line
done
else
info_log -$ERROR "Cannot logwrap in this shell"
exit 1
fi
}
cli_redo_run() {
REDO_SCRIPT=$1
shift
REDO_PARENT=$1
export REDO_PARENT
export PATH="$REDO_PATH"
export REDO_LOG_INDENT="$REDO_LOG_INDENT "
local shell_flag=
if [ "$TRACE" -le "$REDO_LOG_LEVEL" ]; then
export PS4="${REDO_LOG_INDENT}: > "
shell_flag=-ex
else
shell_flag=-e
fi
cd `dirname $REDO_SCRIPT`
sh $shell_flag $REDO_SCRIPT $*
}
cli_redo_tmp() {
if [ -z "$REDO_PARENT" ]; then
info_log -$ERROR "redo-tmp can only be called inside a do script"
exit 1
fi
local tmp_dir="${REDO_META_DIR}/tmp/${REDO_PARENT#${REDO_ROOT_DIR}/}/${1}"
mkdir -p $tmp_dir
echo "$tmp_dir"
}
cli_redo_gc() {
enum_files gc
}
cli_redo_clean() {
rm -rf `$REDO --targets`
}
cli_redo_targets() {
enum_files print_target
}
cli_redo_sources() {
enum_files print_source
}
print_target() {
case $1 in
t*)
echo $2
;;
*)
;;
esac
}
print_source() {
if [ "$1" = "s" ]; then
echo $2
fi
}
gc() {
local entry_path="$REDO_DB_DIR/$2"
case $1 in
s)
if [ ! -e $2 ]; then
rm -rf $entry_path
info_log -$INFO "Removed db entry for '$2' (non-existent source)"
fi
;;
t*)
if [ ! -e "$2" ]; then
if find_build_script "$2"; then
return 0
fi
rm -rf $entry_path
info_log -$INFO "Removed db entry for '$2' (target without build script)"
fi
;;
esac
}
enum_files() {
local i
local types=`find $REDO_DB_DIR -path '*/.redo/type' -printf '%p\n'`
for i in $types
do
local full_key_path=`readlink -f $i`
local file_path="${full_key_path#${REDO_DB_DIR}/}"
file_path="${file_path%/.redo/type}"
local file_type=`cat $i`
$1 $file_type $file_path
done
}
cli_redo_activate() {
local old_path="$PATH"
local new_path="$REDO_PATH"
activate_script=`cat << SCRIPT
PATH="$new_path"
deactivate_redo() {
PATH="$old_path"
unset -f deactivate_redo
}
SCRIPT`
echo "$activate_script"
}
with_parent() {
local old_parent="$REDO_PARENT"
REDO_PARENT="$1"
local cmd="$2"
shift 2
"$cmd" $*
REDO_PARENT="$old_parent"
}
db_get_type() {
db_select "$1"
local key="$db_bucket/type"
if [ -f "$key" ]; then
db_result=`cat "$key"`
else
if [ -e "$1" ]; then
db_result="s"
if find_build_script "$1"; then
info_log -$WARN "'$1' is new but it already has a build script: '$build_script'"
info_log -$WARN "It will be treated as a source file."
info_log -$WARN "To make it a target, remove it and run redo-gc before building"
fi
else
db_result="tr"
fi
db_write "$key" "$db_result"
fi
}
db_has_type() {
db_select "$1"
local key="$db_bucket/type"
[ -f "$key" ]
}
db_add_prereq() {
if [ -z "$1" ]; then
return 0
fi
db_select "$1"
local ifchange="$db_bucket/prereqs/${2#${REDO_ROOT_DIR}/}"
local ifcreate="$db_bucket/ifcreate/${2#${REDO_ROOT_DIR}/}"
db_affirm_key "$ifchange"
rm -rf "$ifcreate"
}
db_get_prereqs() {
db_select "$1"
local key="$db_bucket/prereqs"
if [ ! -d "$key" ]; then
db_result=
else
db_result=`find $key -type f -printf "$REDO_ROOT_DIR/%P\n"`
fi
}
db_add_ifcreate_prereq() {
if [ -z "$1" ]; then
return 0
fi
db_select "$1"
local ifchange="$db_bucket/prereqs/${2#${REDO_ROOT_DIR}/}"
local ifcreate="$db_bucket/ifcreate/${2#${REDO_ROOT_DIR}/}"
db_affirm_key "$ifcreate"
rm -rf "$ifchange"
}
db_get_ifcreate_prereqs() {
db_select "$1"
local key="$db_bucket/ifcreate"
if [ ! -d "$key" ]; then
db_result=
else
db_result=`find $key -type f -printf "$REDO_ROOT_DIR/%P\n"`
fi
}
db_update_own_checksum() {
local checksum=`sha1sum "$1" 2> /dev/null | cut -d' ' -f1`
db_select "$1"
local key="$db_bucket/checksum"
db_affirm_key "$key"
local old_checksum=`cat "$key"`
db_write "$key" "$checksum"
[ "$old_checksum" != "$checksum" ]
}
db_update_prereqs_checksums() {
local i
db_get_prereqs "$1"
for i in $db_result
do
db_select "$i"
local checksum=`cat "$db_bucket/checksum"`
db_select "$1"
db_write "$db_bucket/prereqs/${i#${REDO_ROOT_DIR}/}" "$checksum"
done
}
db_mark_as_uptodate() {
db_select "$1"
local key="$db_bucket/uptodate"
local key_dir=`dirname "$key"`
mkdir -p "$key_dir"
touch "$key"
}
db_is_uptodate() {
db_select "$1"
local key="$db_bucket/uptodate"
if [ -f "$key" ]; then
local timestamp=`stat -c %Y "$key"`
[ "$REDO_TIMESTAMP" -le "$timestamp" ]
else
return 1
fi
}
db_outdated() {
db_select "$1"
local key="$db_bucket/uptodate"
if [ -f "$key" ]; then
local timestamp=`stat -c %Y "$key"`
[ "$REDO_TIMESTAMP" -gt "$timestamp" ]
else
return 0
fi
}
db_prereq_rebuilt() {
db_select "$2"
local new_hash=`cat "$db_bucket/checksum"`
local old_hash=
db_select "$1"
local key="$db_bucket/prereqs/${2#${REDO_ROOT_DIR}/}"
if [ -f "$key" ]; then
old_hash=`cat "$key"`
fi
[ "$new_hash" != "$old_hash" ]
}
db_write() {
local key_dir=`dirname "$1"`
mkdir -p "$key_dir"
echo "$2" > "$1"
}
db_affirm_key() {
local key_dir=`dirname "$1"`
mkdir -p "$key_dir"
if [ ! -f "$1" ]; then
touch "$1"
fi
}
db_select() {
db_bucket="$REDO_DB_DIR/${1#${REDO_ROOT_DIR}/}/.redo"
}
find_build_script() {
non_exist_build_scripts=
build_script="${1}.do"
if [ -f "$build_script" ]; then
return 0
fi
non_exist_build_scripts="${non_exist_build_scripts}${IFS}${build_script}"
local target_name=`basename "$1"`
local search_dir=`dirname "$1"`
while :
do
local search_base="${target_name}.do"
search_base="${search_base#*.}"
while :
do
local filename="${search_dir}/default.${search_base}"
if [ -f "$filename" ] && [ "$filename" != "$1" ]; then
build_script="$filename"
return 0
else
non_exist_build_scripts="${non_exist_build_scripts}${IFS}${filename}"
fi
local next_search_base="${search_base#*.}"
if [ "$next_search_base" = "$search_base" ]; then
break
else
search_base="$next_search_base"
fi
done
local next_search_dir=`dirname "$search_dir"`
if [ "$next_search_dir" = "$search_dir" ]; then
break
else
search_dir="$next_search_dir"
fi
done
build_script=
return 1
}
run_build_script() {
relative_path_to "$REDO_START_DIR" "$1"
local target_rel_path="$path_result"
relative_path_to "$REDO_START_DIR" "$2"
local script_rel_path="$path_result"
info_log -$DEBUG "'$target_rel_path' <- '$script_rel_path'"
db_add_prereq "$1" "$2"
db_update_own_checksum "$2"
db_select "$1"
local temp_file="$db_bucket/doing"
rm -rf "$temp_file"
local script_path=`readlink -f "$2"`
local target_basename=`basename "$1"`
$REDO --run \
"$script_path" "$1" "$target_basename" "$temp_file" > "$temp_file"
local status="$?"
if [ $status -eq 0 ]; then
db_get_type "$1"
local file_type="$db_result"
if [ "$file_type" != "tp" ]; then
local target_dir=`dirname "$1"`
mkdir -p "$target_dir"
mv "$temp_file" "$1"
fi
return 0
else
return $status
fi
}
register_cmds() {
local i
for i in "$@"
do
ensure_symlink "$REDO_BIN_DIR/redo-${i}"
done
}
ensure_symlink() {
if [ `readlink -f "$1"` != "$REDO" ]; then
rm -f "$1"
ln -s "$REDO" "$1"
fi
}
try_dispatch_cmd() {
local cmd_name="$1"
local func_name="cli_redo_${cmd_name}"
shift
if type $func_name 1> /dev/null 2> /dev/null ; then
$func_name $*
else
info_log -$ERROR "Invalid command 'redo-${cmd_name}'"
exit 1
fi
}
info_log() {
local msg_level=$DEFAULT_LEVEL
case $1 in
-*)
msg_level=${1#-}
shift
;;
esac
if [ $msg_level -le $REDO_LOG_LEVEL ]; then
echo "${REDO_LOG_INDENT}: ${*}" >&2
fi
}
cmd_log() {
local cmd=$1
shift
if [ $INFO -le $REDO_LOG_LEVEL ]; then
echo "${REDO_LOG_INDENT}${cmd} ${*}" >&2
fi
}
relative_path_to() {
to_absolute_path $2
path_result="${path_result#${1}/}"
}
to_absolute_path() {
case $1 in
/*)
straighten_path $1
;;
*)
straighten_path "$PWD/$1"
;;
esac
}
straighten_path() {
local old_IFS="$IFS"
IFS='/'
local i
local constructed_path=
for i in $1
do
if [ -z "$i" ]; then
continue
fi
case $i in
.)
continue
;;
..)
constructed_path="${constructed_path%/*}"
;;
*)
constructed_path="$constructed_path/$i"
;;
esac
done
IFS="$old_IFS"
path_result="$constructed_path"
}
main $*