#! /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 # 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)" # Takes one argument, looks up translation trans() { 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 ]] && echo -e "\e[33m$(trans warning)\e[0m$(trans $1)" | envsubst > /dev/stderr } info() { [[ $INFO = 1 ]] && echo "[git-build] $(trans $1)" | envsubst } error () { echo -e "\e[31m$(trans error)\e[0m$(trans $1)" | envsubst > /dev/stderr } debug () { [[ $DEBUG = 1 ]] && echo -e "\e[36m$(trans debug)\e[0m$(trans $1)" | envsubst } 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 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 echo "ERROR: Could not find task directory: $BASEDIR" exit 1 fi FOUND_BASEDIR=0 elif [[ "$arg" = "-f" ]] || [[ "$arg" = "--force" ]]; then info force_flag FORCE=1 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) elif matches="$(grep --files-with-matches --word-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 [ ! -x $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 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 if ! clone "$dvcs" "$source" "$p_dir"; then error clone_failed exit 1 fi cd $p_dir # 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