#!/bin/bash
# extract-debuginfo.sh - automagically generate debug info
#
# Usage: extract-debuginfo.sh [--strict-build-id] [-g] [-r]
#			      [builddir]
#
# The -g flag says to use strip -g instead of full strip on DSOs.
# The --strict-build-id flag says to exit with failure status if
# any ELF binary processed fails to contain a build-id note.
# The -r flag says to use eu-strip --reloc-debug-sections.
#
# All file names in switches are relative to builddir (. if not given).
#

echo "Extracting debuginfo to /usr/lib/debug..."

export LC_ALL=C

# With -g arg, pass it to strip on libraries.
strip_g=false

# with -r arg, pass --reloc-debug-sections to eu-strip.
strip_r=false

# Barf on missing build IDs.
strict=false

BUILDDIR=.
while [ $# -gt 0 ]; do
	case "${1}" in
		--sourcedir=*)
			SOURCEDIR=${1#--sourcedir=}
			;;
		--buildroot=*)
			BUILDROOT=${1#--buildroot=}
			;;
		--strict-build-id)
			strict=true
			;;
		-g)
			strip_g=true
			;;
		-r)
			strip_r=true
			;;
		*)
			BUILDDIR=${1}
			shift
			break
			;;
	esac
	shift
done

debugdir="${BUILDROOT}/usr/lib/debug"

# A list of source files that are included in the -debuginfo packages.
SOURCEFILE="$(mktemp)"

strip_to_debug() {
	local g=
	local r=
	${strip_r} && r=--reloc-debug-sections
	${strip_g} && \
		case "$(file -bi "${2}")" in
			application/x-sharedlib*)
				g=-g
				;;
		esac

	eu-strip --remove-comment ${r} ${g} -f "${1}" "${2}" || exit
	chmod 444 "${1}" || exit
}

# Make a relative symlink to $1 called $3$2
shopt -s extglob
link_relative() {
	local t="$1" f="$2" pfx="$3"
	local fn="${f#/}" tn="${t#/}"
	local fd td d

	while fd="${fn%%/*}"; td="${tn%%/*}"; [ "$fd" = "$td" ]; do
		fn="${fn#*/}"
		tn="${tn#*/}"
	done

	d="${fn%/*}"
	if [ "$d" != "$fn" ]; then
		d="${d//+([!\/])/..}"
		tn="${d}/${tn}"
	fi

	mkdir -p "$(dirname "$pfx$f")" && ln -snf "$tn" "$pfx$f"
}

# Make a symlink in /usr/lib/debug/$2 to $1
debug_link() {
	local l="/usr/lib/debug$2"
	local t="$1"
	link_relative "$t" "$l" "$BUILDROOT"
}

# Provide .2, .3, ... symlinks to all filename instances of this build-id.
make_id_dup_link() {
	local id="${1}" file="${2}" idfile

	local n=1
	while true; do
		idfile=".build-id/${id:0:2}/${id:2}.${n}"
		[ $# -eq 3 ] && idfile="${idfile}$3"
		if [ ! -L "${BUILDROOT}/usr/lib/debug/${idfile}" ]; then
			break
		fi
		n=$[${n}+1]
	done
	debug_link "${file}" "/${idfile}"
}

# Make a build-id symlink for id $1 with suffix $3 to file $2.
make_id_link() {
	local id="${1}" file="${2}"
	local idfile=".build-id/${id:0:2}/${id:2}"
	[ $# -eq 3 ] && idfile="${idfile}${3}"
	local root_idfile="${BUILDROOT}/usr/lib/debug/${idfile}"

	if [ ! -L "${root_idfile}" ]; then
		debug_link "${file}" "/${idfile}"
		return
	fi

	make_id_dup_link "$@"

	[ $# -eq 3 ] && return 0

	local other=$(readlink -m "${root_idfile}")
	other=${other#$BUILDROOT}
	if cmp -s "${root_idfile}" "${BUILDROOT}${file}" ||
			eu-elfcmp -q "${root_idfile}" "${BUILDROOT}${file}" 2> /dev/null; then
		# Two copies.  Maybe one has to be setuid or something.
		echo >&2 "*** WARNING: identical binaries are copied, not linked:"
		echo >&2 "        ${file}"
		echo >&2 "   and  ${other}"
	else
		# This is pathological, break the build.
		echo >&2 "*** ERROR: same build ID in nonidentical files!"
		echo >&2 "        ${file}"
		echo >&2 "   and  ${other}"
		exit 2
	fi
}

get_debugfn() {
	dn=$(dirname "${1#$BUILDROOT}")
	bn=$(basename "$1" .debug).debug

	debugdn=${debugdir}${dn}
	debugfn=${debugdn}/${bn}
}

set -o pipefail

strict_error=ERROR
${strict} || strict_error=WARNING


# Strip ELF binaries
find "$BUILDROOT" ! -path "${debugdir}/*.debug" -type f \
     		     \( -perm -0100 -or -perm -0010 -or -perm -0001 \) \
		     -print |
	file -N -f - | sed -n -e 's/^\(.*\):[ 	]*.*ELF.*, not stripped/\1/p' |
	xargs --no-run-if-empty stat -c '%h %D_%i %n' |
	while read nlinks inum f; do
		get_debugfn "${f}"
		[ -f "${debugfn}" ] && continue

		# If this file has multiple links, keep track and make
		# the corresponding .debug files all links to one file too.
		if [ ${nlinks} -gt 1 ]; then
			eval linked=\$linked_${inum}
			if [ -n "${linked}" ]; then
				eval id=\${linkedid_${inum}}
				make_id_dup_link "${id}" "${dn}/$(basename ${f})"
				make_id_dup_link "${id}" "/usr/lib/debug${dn}/${bn}" .debug
				link=${debugfn}
				get_debugfn "${linked}"
				echo "    hard linked ${link} to ${debugfn}"
				mkdir -p "$(dirname "$link")" && ln -nf "$debugfn" "$link"
				continue
			 else
				eval linked_${inum}=\${f}
				echo "    file ${f} has $[${nlinks} - 1] other hard links"
			fi
		fi

		echo "  Extracting debug info from ${f#${BUILDROOT}}"
		id=$(/usr/lib/pakfire/debugedit -i \
			-b "${SOURCEDIR}" \
			-d /usr/src/debug \
			-l "${SOURCEFILE}" \
			"${f}") || exit

		if [ ${nlinks} -gt 1 ]; then
			eval linkedid_${inum}=\${id}
		fi

		if [ -z "$id" ]; then
			echo >&2 "*** ${strict_error}: No build ID note found in ${f#${BUILDROOT}}"
			${strict} && exit 2
		fi

		[ -x /usr/bin/gdb-add-index ] && /usr/bin/gdb-add-index "${f}" > /dev/null 2>&1

		# A binary already copied into /usr/lib/debug doesn't get stripped,
		# just has its file names collected and adjusted.
		case "${dn}" in
			/usr/lib/debug/*)
				[ -z "${id}" ] || make_id_link "${id}" "${dn}/$(basename ${f})"
				continue
				;;
		esac

		mkdir -p "${debugdn}"
		if test -w "${f}"; then
			strip_to_debug "${debugfn}" "${f}"
		else
			chmod u+w "${f}"
			strip_to_debug "${debugfn}" "${f}"
			chmod u-w "${f}"
		fi

		if [ -n "${id}" ]; then
			make_id_link "${id}" "${dn}/$(basename ${f})"
			make_id_link "${id}" "/usr/lib/debug${dn}/${bn}" .debug
		fi
	done || exit

# For each symlink whose target has a .debug file,
# make a .debug symlink to that file.
find $BUILDROOT ! -path "${debugdir}/*" -type l -print |
	while read f; do
		t=$(readlink -m "$f").debug
		f=${f#$BUILDROOT}
		t=${t#$BUILDROOT}
		if [ -f "$debugdir$t" ]; then
			echo "symlinked /usr/lib/debug$t to /usr/lib/debug${f}.debug"
			debug_link "/usr/lib/debug$t" "${f}.debug"
		fi
	done

if [ -s "${SOURCEFILE}" ]; then
	mkdir -p "${BUILDROOT}/usr/src/debug"

	sort -z -u "${SOURCEFILE}" | grep -E -v -z '(<internal>|<built-in>)$' | \
		(cd "${SOURCEDIR}"; cpio -pd0mL "${BUILDROOT}/usr/src/debug" 2>/dev/null)

	# stupid cpio creates new directories in mode 0700, fixup
	find "${BUILDROOT}/usr/src/debug" -type d -print0 | \
		xargs --no-run-if-empty -0 chmod a+rx

	# Fix ownership.
	chown root:root -R ${BUILDROOT}/usr/src/debug
fi

rm -f ${SOURCEFILE}

exit 0
