#!/bin/bash
# Copyright 2007-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2 or later

# Authors:
# Christian Faulhammer <fauli@gentoo.org>
# Ulrich Müller <ulm@gentoo.org>

VERSION=1.19
EMACS=/usr/bin/emacs
GETOPT=/usr/bin/getopt
SITELISP=/usr/share/emacs/site-lisp

# Default actions
ACTIONS="rebuild"

# Default package manager
PM_COMMAND=pm_auto
PM_EXTRAOPTS=( )

# Other default variable settings
BATCH=
EXACT=
MAJOR=
ORPHANS=
PRETEND=

# Check for NO_COLOR environment variable: https://no-color.org/
COLOUR=${NO_COLOR:+no}

usage() {
    sed -e 's/^X//' <<-EOF
	Usage: ${0##*/} [OPTION]...
	Rebuild Emacs packages that were compiled by a different GNU Emacs
	version, or all Emacs packages.
	X
	X  -a, --action=ACTION[,ACTION]...
	X                        specify actions, comma-separated list of:
	X                        rebuild: rebuild packages with elisp files
	X                          byte-compiled by a different Emacs version
	X                        all: rebuild all packages that have
	X                          byte-compiled elisp files
	X                        (default: rebuild)
	X  -b, --batch           batch mode, don't ask any questions
	X      --color[=MODE], --colour[=MODE]
	X                        control colour output. MODE is yes, no,
	X                        or auto. For 'auto', colour is enabled if
	X                        standard output is to a terminal
	X                        (default: auto)
	X  -e, --exact           match exact versions when remerging packages
	X  -m, --major           use only the major version when comparing
	X                        Emacs version numbers
	X  -o, --orphans         list orphan files (implies '--action all')
	X  -p, --pretend         don't actually emerge packages
	X  -P, --package-manager=PM
	X                        select a package manager. PM is one out of
	X                        portage, pkgcore, or paludis
	X                        (default: automatically determined)
	X      --package-manager-command=CMD
	X                        call CMD instead of the default package
	X                        manager command. If CMD includes options,
	X                        the whole command string must be quoted
	X  -h, --help            display this help and exit
	X      --version         output version information and exit
	X
	X  -- OPTION...          pass additional options to package manager
	EOF
    exit $1
}

version() {
    cat <<-EOF
	Emacs updater version ${VERSION}
	Copyright 2007-2024 Gentoo Authors
	Distributed under the terms of the GNU GPL version 2 or later

	Gentoo Emacs project: <https://wiki.gentoo.org/wiki/Project:Emacs>
	EOF
    exit
}

# Wrapper for package manager commands
have_portage() { type -P emerge >/dev/null; }
pm_portage() { emerge --usepkg=n --getbinpkg=n --oneshot "$@"; }

have_pkgcore() { type -P pmerge >/dev/null; }
pm_pkgcore() { pmerge --oneshot "$@"; }

have_paludis() { type -P cave >/dev/null; }
pm_paludis() { cave resolve --execute --preserve-world "$@"; }

pm_auto() {
    local pm
    # Try environment variable first
    case ${PACKAGE_MANAGER} in
	portage|pkgcore|paludis) pm=${PACKAGE_MANAGER} ;;
    esac
    for pm in ${pm} portage pkgcore paludis; do
	if have_${pm}; then
	    PM_COMMAND=pm_${pm}
	    ${PM_COMMAND} "$@"
	    return
	fi
    done
    failure "No package manager found"
    return 1
}

# Read in all command-line options and force English output
OPTIONS=$(LC_ALL=C "${GETOPT}" -o a:behmopP: \
    --long action:,batch,color::,colour::,exact,help,major,orphans \
    --long pretend,package-manager:,package-manager-command:,version \
    -n 'emacs-updater' -- "$@") \
    || usage $?

eval set -- "${OPTIONS}"

while true
do
    case $1 in
	-h|--help)      usage 0 ;;
	--version)      version ;;
	-b|--batch)     BATCH="true"; shift ;;
	-e|--exact)     EXACT="true"; shift ;;
	-m|--major)     MAJOR="true"; shift ;;
	-o|--orphans)   ORPHANS="true"; ACTIONS="all"; shift ;;
	-p|--pretend)   PRETEND="true"; shift ;;
	--color|--colour)
	    case $2 in
		yes|y|always|force|"") COLOUR="yes" ;;
		no|n|never|none) COLOUR="no" ;;
		auto|tty|if-tty) COLOUR="" ;;
		*)
		    echo "Invalid argument for '$1' option"
		    usage 1
		    ;;
	    esac
	    shift 2
	    ;;
	-a|--action)
	    ACTIONS=
	    for action in ${2/,/ }; do
		case ${action} in
		    rebuild|all)
			ACTIONS="${ACTIONS}${ACTIONS:+ }${action}"
			;;
		    *)
			echo "Invalid action '$action' given!"
			usage 1
			;;
		esac
	    done
	    shift 2
	    ;;
	-P|--package-manager)
	    case $2 in
		auto|automatic) PM_COMMAND=pm_auto ;;
		portage|pkgcore|paludis)
		    if ! have_$2; then
			echo "Package manager '$2' not found!"
			exit 1
		    fi
		    PM_COMMAND=pm_$2
		    ;;
		*)
		    echo "Package manager '$2' not known!"
		    usage 1
		    ;;
	    esac
	    shift 2
	    ;;
	--package-manager-command) PM_COMMAND=$2; shift 2 ;;
	--)
	    shift
	    if [[ $# -gt 0 && ${1#-} = "$1" ]]; then
		# first parameter after -- should be an option
		echo "Non-option parameter '$1' for package manager specified!"
		usage 1
	    fi
	    PM_EXTRAOPTS=( "$@" )
	    break
	    ;;
	*)
	    # this should not happen; getopt should return bad status
	    echo "Invalid option '$1' given!"
	    usage 1
	    ;;
    esac
done

# Set colours based on the --colour option and output redirection status
if [[ -z ${COLOUR} && -t 1 ]] || [[ ${COLOUR} = yes ]]; then
    BOLD=$(tput bold)
    NORMAL=$(tput sgr0)
    RED=$(tput setaf 1)${BOLD}
    GREEN=$(tput setaf 2)${BOLD}
    YELLOW=$(tput setaf 3)${BOLD}
    BLUE=$(tput setaf 4)${BOLD}
    MAGENTA=$(tput setaf 5)${BOLD}
    CYAN=$(tput setaf 6)${BOLD}
else
    BOLD=; NORMAL=; RED=; GREEN=; YELLOW=; BLUE=; MAGENTA=; CYAN=
fi

# Some functions for output
message() {
    local OUTPUT="$@"
    echo "${GREEN}*${NORMAL}${BOLD} ${OUTPUT}${NORMAL}"
}

warning() {
    local OUTPUT="$@"
    echo "${YELLOW}*${NORMAL}${BOLD} ${OUTPUT}${NORMAL}"
}

failure() {
    local OUTPUT="$@"
    echo "${RED}*${NORMAL}${BOLD} ${OUTPUT}${NORMAL}"
}

emacs_version() {
    "${EMACS}" -batch -q --no-site-file --eval "(princ emacs-version)"
}

# Get Emacs version from byte-compiled file
bytecomp_version() {
    sed -n '/^[^;]/q;s/\.$//;s/.*[Ee]macs version \([0-9].*\)/\1/p' "$1"
}

action_rebuild() {
    local active version elc ret

    message "Searching for byte-compiled elisp files ..."
    active=$(emacs_version)
    ret=$?
    [[ ${ret} -eq 0 ]] || { failure "Error running Emacs"; exit ${ret}; }
    [[ ${active} ]] || { failure "Cannot determine Emacs version"; exit 1; }
    # Drop last component (build number) from versions below 26
    [[ ${active%%.*} -lt 26 ]] && active=${active%.*}
    message "Currently selected GNU Emacs version: ${active}"

    for elc in $(find "${ROOT}${SITELISP}" -name "*.elc")
    do
	version=$(bytecomp_version "${elc}")
	ret=$?
	[[ ${ret} -eq 0 ]] || { failure "Error running sed"; exit ${ret}; }
	version=${version:-unknown}
	[[ ${version%%.*} -lt 26 ]] && version=${version%.*}

	if [[ -z ${MAJOR} && ${version} != "${active}" ]] \
	    || [[ ${version%%.*} != "${active%%.*}" ]] \
	    || [[ $1 = all ]]
	then
	    echo "Found ${elc##*/} (compiled by Emacs ${version})"
	    echo "${elc}" >> "${TMPFILE}"
	else
	    echo "Skipping ${elc##*/} (compiled by Emacs ${version})"
	fi
    done
    echo
}

action_all() {
    action_rebuild all
}

cleanup() {
    rm -f "${TMPFILE}" "${PKGFILE}"
}

trap cleanup EXIT
set -o pipefail

TMPFILE="$(mktemp ${TMPDIR:-/tmp}/emacs-updater.files.XXXXXX)"
PKGFILE="$(mktemp ${TMPDIR:-/tmp}/emacs-updater.pkgs.XXXXXX)"

for action in ${ACTIONS}; do
    action_${action}
done

if [[ ! -s ${TMPFILE} ]]; then
    warning "No files to update, quitting."
    exit 0
fi

NO_OF_FILES=$(wc -l <"${TMPFILE}")
[[ ${NO_OF_FILES} -eq 1 ]] && s= || s=s
message "Assigning ${NO_OF_FILES} file${s} to packages ..."

if [[ ${ORPHANS} ]]; then
    xargs qfile -oCR <"${TMPFILE}" | sort -u >"${PKGFILE}"
elif [[ ${EXACT} ]]; then
    xargs qfile -vqCR <"${TMPFILE}" | sort -u | sed 's/^/=/' >"${PKGFILE}"
else
    # Get package and slot number, requires >=portage-utils-0.3
    xargs qfile -SqCR <"${TMPFILE}" | sort -u >"${PKGFILE}"
fi

ret=$?
[[ ${ret} -eq 0 ]] || { failure "Error running qfile pipeline"; exit ${ret}; }

NO_OF_PACKAGES=$(wc -l <"${PKGFILE}")
[[ ${NO_OF_PACKAGES} -eq 1 ]] && s= || s=s
if [[ ${ORPHANS} ]]; then
    message "${NO_OF_PACKAGES} orphan file${s} found:"
else
    message "${NO_OF_PACKAGES} package${s} to remerge:"
fi

cat "${PKGFILE}"

if [[ ${PRETEND} || ${ORPHANS} ]]; then
    exit 0
fi

echo
if [[ ${NO_OF_PACKAGES} -eq 0 ]]; then
    warning "No packages to update, quitting."
    exit 0
fi

if [[ ${BATCH} ]]; then
    message "Remerging packages ..."
else
    echo -n "${BOLD}Remerge packages?${NORMAL} [${GREEN}Yes${NORMAL}/${RED}No${NORMAL}] "
    read choice
    echo
    case ${choice} in
	y*|Y*|"") ;;
	*) warning "Quitting."; exit 0 ;;
    esac
fi

${PM_COMMAND} "${PM_EXTRAOPTS[@]}" $(<"${PKGFILE}")
ret=$?

warning "If a package is being rebuilt over and over again,"
warning "please report it on https://bugs.gentoo.org/"

exit ${ret}
