#! /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