440 lines
13 KiB
Bash
Executable File
440 lines
13 KiB
Bash
Executable File
#! /bin/bash
|
|
|
|
# Catch signals so we don't leave zombie tasks behind if we Ctrl^C
|
|
# No idea why but it seems to prevent the script from working? Disable!
|
|
#trap "exit" INT TERM ERR
|
|
#trap "kill 0" EXIT
|
|
|
|
# Default logging levels (error is always enabled)
|
|
INFO=1
|
|
DEBUG=0
|
|
|
|
# TODO: Remove this quickhack which prevents from using real value in tasks
|
|
# (but enables unit testing of host-based config)
|
|
if [ ! -z $HOST ]; then
|
|
HOSTNAME="$HOST"
|
|
fi
|
|
|
|
# Check whether we use translations, or use debug output
|
|
# If LANG is not special-value "NONE", then:
|
|
# - we load the requested language if it exists, fail otherwise
|
|
# - when no language is requested, default to something (TODO)
|
|
if [[ "$LANG" != "NONE" ]]; then
|
|
# Enable colors (escape codes) as echo option
|
|
COLORS=1
|
|
|
|
# Extract two letters from $LANG
|
|
locale="${LANG:0:2}"
|
|
|
|
# Ensure translations have been setup
|
|
if [ -d /usr/share/forgebuild/i18n ]; then
|
|
I18N_DIR=/usr/share/forgebuild/i18n
|
|
elif [ -d $HOME/.local/share/forgebuild/i18n ]; then
|
|
I18N_DIR=$HOME/.local/share/forgebuild/i18n
|
|
else
|
|
echo "ERROR: could not find translations. Maybe you need to run the setup.sh script?"
|
|
exit 1
|
|
fi
|
|
|
|
# Initialize translations
|
|
[ -f $I18N_DIR/$locale.json ] && locale_strings="$(cat $I18N_DIR/$locale.json)" || locale_strings="$(cat $I18N_DIR/en.json)"
|
|
|
|
fi # End translations initialization
|
|
|
|
# Takes one argument, looks up translation
|
|
trans() {
|
|
# If translations are disabled (for output testing),
|
|
# return the name of the requested key witout translating
|
|
[[ "$LANG" == "NONE" ]] && echo -n "$1" && exit 0
|
|
# We want translation
|
|
res="$(echo "$locale_strings" | jq ".$1")"
|
|
if [[ "$res" = "" ]]; then
|
|
echo "ERROR: Failed to translate $1"
|
|
exit 1
|
|
fi
|
|
res=${res#"\""}
|
|
res=${res%"\""}
|
|
echo -n "$res"
|
|
}
|
|
|
|
# We set custom logging functions so that later we can decorate stuff
|
|
warn() {
|
|
[[ $INFO != 1 ]] && return
|
|
[[ $COLORS = 1 ]] && echo -e "\e[33m$(trans warning)\e[0m$(trans $1)" | envsubst > /dev/stderr || echo "$(trans warning)$(trans $1)" | envsubst > /dev/stderr
|
|
}
|
|
|
|
info() {
|
|
[[ $INFO != 1 ]] && return
|
|
echo "[git-build] $(trans $1)" | envsubst > /dev/stderr
|
|
}
|
|
|
|
error () {
|
|
[[ $COLORS = 1 ]] && echo -e "\e[31m$(trans error)\e[0m$(trans $1)" | envsubst > /dev/stderr || echo "$(trans error)$(trans $1)" | envsubst > /dev/stderr
|
|
}
|
|
|
|
debug () {
|
|
[[ $DEBUG != 1 ]] && return
|
|
[[ $COLORS = 1 ]] && echo -e "\e[36m$(trans debug)\e[0m$(trans $1)" | envsubst > /dev/stderr || echo "$(trans debug)$(trans $1)" | envsubst > /dev/stderr
|
|
}
|
|
|
|
help () {
|
|
echo "forgebuild [tasks]"
|
|
echo " Trigger updates on \`tasks\` (all tasks by default)"
|
|
echo "ARGS:"
|
|
echo " -f: force run of tasks regardless of updates on the repository"
|
|
echo " -b BASEDIR: consider tasks from BASEDIR, not ~/.forgebuild"
|
|
}
|
|
|
|
# DVCS stuff (for tasks with source)
|
|
clone () {
|
|
dvcs="$1"
|
|
src="$2"
|
|
dest="$3"
|
|
case "$dvcs" in
|
|
"git")
|
|
git clone --recursive "$2" "$3"
|
|
;;
|
|
"mercurial"|"hg")
|
|
hg clone "$2" "$3"
|
|
;;
|
|
*)
|
|
echo "UNIMPLEMENTED"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Checks for updates, if any download them
|
|
# Returns:
|
|
# - 0 when no updates
|
|
# - 1 when updates were fetched
|
|
# - anything else when there was an error
|
|
updates() {
|
|
dvcs="$1"
|
|
case "$dvcs" in
|
|
"git")
|
|
# Refresh remote before comparing with local
|
|
if ! git fetch --quiet origin; then
|
|
error pull_failed
|
|
return 2
|
|
fi
|
|
p_branch="$(branch git)"
|
|
git diff --quiet remotes/origin/$p_branch
|
|
if [[ $? != 0 ]]; then
|
|
info pull
|
|
if ! git pull origin $p_branch --quiet; then
|
|
error pull_failed
|
|
return 3
|
|
fi
|
|
# Maybe some new submodule was added? We need to clone it
|
|
if ! git submodule update --init --recursive; then
|
|
error pull_failed
|
|
return 3
|
|
fi
|
|
return 1
|
|
fi
|
|
return 0
|
|
;;
|
|
"mercurial"|"hg")
|
|
if hg incoming; then
|
|
info pull
|
|
if ! hg pull; then
|
|
error pull_failed
|
|
return 3
|
|
fi
|
|
return 1
|
|
else
|
|
return 0
|
|
fi
|
|
;;
|
|
*)
|
|
echo "UNIMPLEMENTED"
|
|
return 4
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Checkout a specific branch/commit
|
|
# returns 0 on success, 1 if the checkout failed
|
|
# 2 if the backend is unknown
|
|
checkout () {
|
|
dvcs="$1"
|
|
target="$2"
|
|
case "$dvcs" in
|
|
"git")
|
|
git checkout $target
|
|
;;
|
|
"mercurial"|"hg")
|
|
hg update $target
|
|
;;
|
|
*)
|
|
echo "UNIMPLEMENTED"
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Checks for submodule updates
|
|
# Returns 0 for no updates, 1 for updates performed
|
|
# and anything else when there was an error
|
|
subupdates() {
|
|
dvcs="$1"
|
|
case "$dvcs" in
|
|
"git")
|
|
# List submodule paths
|
|
FOUND_UPDATES=0
|
|
DIR="$(pwd)"
|
|
for submodule in $(git config --file .gitmodules --get-regexp path | awk '{ print $2 }'); do
|
|
cd $submodule
|
|
updates git
|
|
res=$?
|
|
cd $DIR
|
|
[ $res -eq 0 ] && continue # No updates found, skip to next submodule
|
|
[ $res -eq 1 ] && FOUND_UPDATES=1 && continue # Found updates, try next submodule
|
|
# There was an error, abort
|
|
return $res
|
|
done
|
|
return $FOUND_UPDATES
|
|
;;
|
|
*)
|
|
echo "UNIMPLEMENTED"
|
|
return 4
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Print the current branch for specified DVCS
|
|
# Return 0 when ok, 1 on error, 2 on unknown DVCS
|
|
branch () {
|
|
dvcs="$1"
|
|
case "$dvcs" in
|
|
"git")
|
|
git rev-parse --abbrev-ref HEAD || exit 1
|
|
;;
|
|
"mercurial"|"hg")
|
|
hg identify -b || exit 1
|
|
;;
|
|
*)
|
|
echo "UNIMPLEMENTED"
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Logging is done with the LOG="debug|info|error" environment variable
|
|
if [ ! -z $LOG ] && [ "$LOG" != "" ]; then
|
|
case $LOG in
|
|
"debug"|"DEBUG")
|
|
DEBUG=1
|
|
;;
|
|
"error"|"ERROR")
|
|
INFO=0
|
|
;;
|
|
"info"|"INFO")
|
|
# Default settings
|
|
;;
|
|
*)
|
|
warn loglevel_bad
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
run() {
|
|
p_name="$1"
|
|
i18n_task="$p_name" info run
|
|
# Run in background and redirect output to $p_name.log
|
|
#(GITBUILDCONF="$CONFDIR" GITBUILDDIR="$BASEDIR" nohup $BASEDIR/$p_name $p_name > $BASEDIR/$p_name.log 2>&1) &
|
|
GITBUILDCONF="$CONFDIR" GITBUILDDIR="$BASEDIR" $BASEDIR/$p_name $p_name > $BASEDIR/$p_name.log 2>&1
|
|
}
|
|
|
|
# Overriden by -f/--force to force rebuild when no update is available
|
|
FORCE=0
|
|
|
|
# Find targeted projects and extra flags from args
|
|
PROJECTS=()
|
|
BASEDIR="$HOME/.forgebuild"
|
|
FOUND_BASEDIR=0
|
|
for arg in "$@"; do
|
|
if [ $FOUND_BASEDIR -eq 1 ]; then
|
|
BASEDIR="$(readlink -m $arg)"
|
|
if [ ! -d $BASEDIR ]; then
|
|
i18n_basedir="$BASEDIR"
|
|
error "missing_basedir"
|
|
exit 1
|
|
fi
|
|
FOUND_BASEDIR=0
|
|
elif [[ "$arg" = "-f" ]] || [[ "$arg" = "--force" ]]; then
|
|
info force_flag
|
|
FORCE=1
|
|
elif [[ "$arg" = "-h" ]] || [[ "$arg" = "--help" ]]; then
|
|
help
|
|
exit 0
|
|
elif [[ "$arg" = "-b" ]] || [[ "$arg" = "--basedir" ]]; then
|
|
FOUND_BASEDIR=1
|
|
elif [ -x $BASEDIR/$arg ]; then
|
|
export i18n_task="$arg"
|
|
debug found_task
|
|
PROJECTS+=("$arg")
|
|
# Maybe it's a repo URL and we find a corresponding task?
|
|
# Used for interop (for example forgehook-notify)
|
|
# --no-messages does not fail when the glob pattern doesn't match
|
|
# --regexp (compared to raw pattern) avoids leaking a wrong forgebuild argument to grep
|
|
elif matches="$(grep --no-messages --files-with-matches --word-regexp --regexp "$arg" $BASEDIR/*.source)"; then
|
|
# Iterate over the files found to extract the task name
|
|
while IFS= read -r file; do
|
|
task_name="$(basename "$file" .source)"
|
|
# Make sure we don't have a .source file without name lying around, just in case
|
|
if [[ "$task_name" != "" ]]; then
|
|
export i18n_task="$task_name" i18n_source="$arg"
|
|
debug found_url
|
|
echo "FOUND $task_name"
|
|
PROJECTS+=("$task_name")
|
|
fi
|
|
done <<< "$matches"
|
|
else
|
|
export i18n_arg="$arg"
|
|
error unknown_arg
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Still the default BASEDIR, if it doesn't exist we create it as a convenience
|
|
if [ ! -d $BASEDIR ]; then
|
|
echo "ERROR: No such basedir: $BASEDIR. Creating it and aborting"
|
|
exit 1
|
|
fi
|
|
|
|
# So scripts can know we're still running (for autoupdater)
|
|
echo "$BASHPID" > $BASEDIR/.LOCK
|
|
|
|
# If no project argument passed, default to all projects
|
|
if [[ ${#PROJECTS[*]} = 0 ]]; then
|
|
info no_task
|
|
for file in $BASEDIR/*; do
|
|
# Skip non-executable files, and directories (which can be +x)
|
|
[ ! -x $file ] && continue
|
|
[ ! -f $file ] && continue
|
|
# Extract the project name from path
|
|
task="$(basename $file .source)"
|
|
export i18n_task="$task"
|
|
debug found_task
|
|
PROJECTS+=("$task")
|
|
done
|
|
fi
|
|
|
|
# Check if we have host-specific config or default to the config/ folder
|
|
[ -d $BASEDIR/$HOSTNAME ] && CONFDIR="$BASEDIR/$HOSTNAME" || CONFDIR=$BASEDIR/config
|
|
export i18n_config="$CONFDIR"
|
|
info config
|
|
|
|
for p_name in ${PROJECTS[*]}; do
|
|
# For translations
|
|
export i18n_task="$p_name"
|
|
debug start_proc
|
|
# Check if task should run on host
|
|
if [ -f $BASEDIR/$p_name.hosts ]; then
|
|
if ! grep "^\s*$HOSTNAME\s*$" $BASEDIR/$p_name.hosts > /dev/null; then
|
|
# The task is specifically configured for different hosts, ignore
|
|
export i18n_host="$for_host"
|
|
debug skipped
|
|
continue
|
|
fi
|
|
fi
|
|
# The task has a PROJECT.ignore file in host config, ignore
|
|
if [ -f $BASEDIR/$HOSTNAME/$p_name.ignore ]; then
|
|
info host_ignored
|
|
continue
|
|
fi
|
|
info process
|
|
|
|
# Before we clone/checkout the source, maybe it's a sourceless task?
|
|
if [ ! -f $BASEDIR/$p_name.source ]; then
|
|
# If $p_name.done exists, the task was already run
|
|
[ -f $BASEDIR/$p_name.done ] && continue
|
|
# If the task fails, don't register the .done
|
|
cd $BASEDIR
|
|
run $p_name && touch $BASEDIR/$p_name.done
|
|
continue
|
|
fi
|
|
|
|
# So we have a source, check the associated DVCS
|
|
[ -f $BASEDIR/$p_name.dvcs ] && dvcs="$(cat $BASEDIR/$p_name.dvcs)" || dvcs="git"
|
|
|
|
p_dir="$BASEDIR/.$p_name"
|
|
if [ ! -d $p_dir ]; then
|
|
source="$(cat $BASEDIR/$p_name.source)"
|
|
export i18n_source="$source"
|
|
info clone
|
|
# Suppress clone stderr, TODO: maybe save in some log file?
|
|
if ! clone "$dvcs" "$source" "$p_dir" 2> /dev/null; then
|
|
error clone_failed
|
|
exit 1
|
|
fi
|
|
cd $p_dir
|
|
|
|
# Submodule updates, maybe?
|
|
if [ -f $BASEDIR/$p_name.subupdates ]; then
|
|
subupdates $dvcs
|
|
fi
|
|
|
|
# checkout a specific branch/commit
|
|
if [ -f $BASEDIR/$p_name.checkout ]; then
|
|
p_branch="$(cat $BASEDIR/$p_name.checkout)"
|
|
export i18n_branch="$p_branch"
|
|
info to_branch
|
|
debug checkout
|
|
if ! checkout $dvcs "$p_branch"; then
|
|
error checkout_failed
|
|
exit 1
|
|
fi
|
|
else
|
|
p_branch="$(branch $dvcs)"
|
|
export i18n_branch="$p_branch"
|
|
debug default_branch
|
|
fi
|
|
|
|
run $p_name
|
|
# Skip to the next task!
|
|
continue
|
|
fi
|
|
debug already_cloned
|
|
cd "$p_dir"
|
|
|
|
# Omit "" on p_branch so if it's empty it's not registered as argument (and the default is used)
|
|
updates $dvcs $p_branch
|
|
res="$?"
|
|
case "$res" in
|
|
"0")
|
|
if [[ $FORCE = 1 ]]; then
|
|
debug forcing
|
|
run $p_name
|
|
else
|
|
# Submodule updates, maybe?
|
|
if [ ! -f $BASEDIR/$p_name.subupdates ]; then
|
|
debug no_update
|
|
continue
|
|
fi
|
|
subupdates $dvcs
|
|
subres=$?
|
|
[ $subres -eq 0 ] && debug no_update && continue
|
|
[ ! $subres -eq 1 ] && continue # subupdates errored
|
|
run $p_name
|
|
fi
|
|
;;
|
|
"1")
|
|
run $p_name
|
|
;;
|
|
*)
|
|
# Updates errored, skip task
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
done
|
|
|
|
# Check the PID in lockfile is still ours, we don't want
|
|
# to remove the lockfile if another git-build "owns" it
|
|
if [[ "$(cat $BASEDIR/.LOCK)" = "$BASHPID" ]]; then
|
|
rm $BASEDIR/.LOCK
|
|
fi
|