build.sh/forgebuild.sh

479 lines
15 KiB
Bash
Executable File

#! /bin/bash
VERSION="0.4.0"
# 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
# If LANG is a file, load it directly
if [ -f "$LANG" ]; then
locale_strings="$(cat $LANG)"
else
# Enable colors (escape codes) as echo option
COLORS=1
# Extract two letters from $LANG
locale="${LANG:0:2}"
# Figure out where to load translations from. If the person is hacking on a new version and/or forgebuild
# hasn't been setup, it's very likely that the build spec repo can be found at either ../build or ./spec
# relative to forgebuild.sh. If none of these exist, try to load translations from a normal setup:
# from ~/.local/forgebuild/i18n or /usr/share/forgebuild/i18n
BINDIR="$(dirname "$0")"
# ../build has higher precedence in case you're hacking on several implementations
if [ -d "$BINDIR"/../build/i18n ]; then
I18N_DIR="$BINDIR"/../build/i18n
elif [ -d "$BINDIR"/spec/i18n ]; then
# We are in a cloned repo and submodule has been initialized
I18N_DIR="$BINDIR"/spec/i18n
else
# Either submodule hasn't been initalized, or we are not in a repo at all
# First try normal install paths, then try to clone spec submodule
# In this order because we'd like to rely on Internet access as last resort only
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
elif [ -d "$BINDIR"/spec ]; then
# Submodule isn't initialized
PREVDIR="$(pwd)"
cd "$BINDIR"
git submodule init
if ! git submodule update; then
echo "ERROR: Failed to find local translations and to download them from the Internet"
exit 1
fi
cd "$PREVDIR"
I18N_DIR="$BINDIR"/spec/i18n
else
# We are not in a repo and local translations were not found, abort!
echo "ERROR: could not find translations. Maybe you need to run the setup.sh script?"
exit 1
fi
fi
# TODO: add debug message about where translations are loaded from
# Initialize translations
[ -f $I18N_DIR/$locale.json ] && locale_strings="$(cat $I18N_DIR/$locale.json)" || locale_strings="$(cat $I18N_DIR/en.json)"
fi
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 "$(trans info)$(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
}
version() {
echo "forgebuild $VERSION"
}
help () {
version
echo "Usage: 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: load tasks from BASEDIR (defaults to ~/.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 -u; 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) &
FORGEBUILDCONF="$CONFDIR" FORGEBUILDDIR="$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)"
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" = "-V" ]] || [[ "$arg" = "--version" ]]; then
version
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
# Error when the requested basedir (or the default ~/.forgebuild) does not exist
if [ ! -d $BASEDIR ]; then
export i18n_basedir="$BASEDIR"
error "missing_basedir"
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