#!/bin/sh
# ports/Mk/Scripts/check-stagedir.sh - called from ports/Mk/bsd.stage.mk
# $FreeBSD: head/Mk/Scripts/check-stagedir.sh 505130 2019-06-26 01:07:41Z bdrewery $
#
# MAINTAINER: portmgr@FreeBSD.org
#
# This script serves 2 purposes:
# 1. Generate a plist
# 2. Test a plist for issues:
#  a. Files in STAGEDIR that are missing from plist
#  b. Files in plist missing from STAGEDIR
#  c. Files in plist which are owned by dependencies/MTREEs

set -e

. ${SCRIPTSDIR}/functions.sh

# lists an mtree file's contents, prefixed to dir.
listmtree() { # mtreefile prefix
	{
		echo '#mtree'
		sed 's/nochange$//;' $1
	} | tar -tf- | sed "s,^,$2/,;s,^$2/\.$,$2,;s,^$,/,"
}

### PRODUCE MTREE FILE
parse_mtree() {
	{
		listmtree /etc/mtree/BSD.root.dist ""
		listmtree /etc/mtree/BSD.usr.dist /usr
		listmtree /etc/mtree/BSD.var.dist /var

		# Use MTREE_FILE if specified and it doesn't already
		# match LOCALBASE
		if [ -n "${MTREE_FILE}" ]; then
			if [ "${PREFIX}" != "${LOCALBASE}" -o "${MTREE_FILE}" \
			    != "${PORTSDIR}/Templates/BSD.local.dist" ]; then
				listmtree "${MTREE_FILE}" "${PREFIX}"
			fi
		fi
		listmtree "${PORTSDIR}/Templates/BSD.local.dist" "${LOCALBASE}"

		unset MTREE_FILE

		# Add LOCALBASE
		a=${LOCALBASE}
		while :; do
			echo ${a}
			a=${a%/*}
			[ -z "${a}" ] && break
		done

		# Add in PREFIX if this port wants it
		if [ ${NO_PREFIX_RMDIR} -eq 0 ]; then
			a=${PREFIX}
			while :; do
				echo ${a}
				a=${a%/*}
				[ -z "${a}" ] && break
			done
		fi
	} >${WRKDIR}/.mtree
}

# Sort a directory list by the order of the dfs-sorted file (from find -d -s)
sort_dfs() {
	while read -r dir; do
		grep "^[0-9]* ${dir}$" ${WRKDIR}/.staged-dirs-dfs-sorted
	done | sort -n | cut -d ' ' -f2-
}

# Prepare sed(1) regex for PLIST_SUB_SED/PORTEXAMPLES/OPTIONS/...
setup_plist_seds() {
	### HANDLE PORTDOCS/PORTEXAMPLES
	sed_portdocsexamples="/%%DOCSDIR%%/s!^!%%PORTDOCS%%!g; /%%EXAMPLESDIR%%/s!^!%%PORTEXAMPLES%%!g;"
	if [ ${makeplist} -eq 0 ]; then
	#	echo "=====> Using OPTIONS: ${PORT_OPTIONS}" | /usr/bin/fmt -w 79 | \
	#	    sed -e '2,$s/^/                      /'
		# Handle magical PORT* features
		for option in DOCS EXAMPLES; do
			want_option=0
			case " ${PORT_OPTIONS} " in
			*\ ${option}\ *) want_option=1 ;;
			esac
			[ ${want_option} -eq 0 ] && \
			    sed_portdocsexamples="${sed_portdocsexamples} /^%%PORT${option}%%/d;"
		done
		unset PORT_OPTIONS
	fi

	sed_plist_sub=$(mktemp -t sed_plist_sub)
	# We only exit 0 or exit 1
	trap "rm -f ${sed_plist_sub}" EXIT 1
	echo "${PLIST_SUB_SED}" | /bin/sh ${SCRIPTSDIR}/plist_sub_sed_sort.sh ${sed_plist_sub}
	unset PLIST_SUB_SED
	# Used for generate_plist
	sed_files_gen="${sed_portdocsexamples} /^share\/licenses/d; \
	    \#${LOCALBASE}/lib/debug#d;"
	sed_dirs_gen="s,^,@dir ,; \
	    ${sed_portdocsexamples} \
	    /^@dir share\/licenses/d;"

	# These prevent ignoring DOCS/EXAMPLES dirs with sed_portdocsexamples
	sed_files="/^share\/licenses/d; \
	    \#${LOCALBASE}/lib/debug#d;"
	sed_dirs="s,^,@dir ,; \
	    /^@dir share\/licenses/d;"

}

# Generate plist from staged files
generate_plist() {
	: >${WRKDIR}/.staged-plist

	### HANDLE FILES
	find ${STAGEDIR} -type f -o -type l | sort | \
	    sed -e "s,${STAGEDIR},," >${WRKDIR}/.staged-files
	comm -13 ${WRKDIR}/.plist-files ${WRKDIR}/.staged-files | \
	    sed -e "s!^${PREFIX}/!!g;" -f "${sed_plist_sub}" -e "${sed_files_gen}" \
	     >>${WRKDIR}/.staged-plist || :

	### HANDLE DIRS
	cat ${WRKDIR}/.plist-dirs-unsorted ${WRKDIR}/.mtree \
	    | sort -u >${WRKDIR}/.traced-dirs
	find ${STAGEDIR} -type d | sed -e "s,^${STAGEDIR},,;/^$/d" | sort \
	    >${WRKDIR}/.staged-dirrms-sorted
	find -s -d ${STAGEDIR}${PREFIX} -type d -empty | sed -e "s,^${STAGEDIR},,;\,^${PREFIX}$,d;/^$/d" \
	    >${WRKDIR}/.staged-dirs-dfs
	find -s -d ${STAGEDIR} -type d ! -path "${STAGEDIR}${PREFIX}/*" | sed -e "s,^${STAGEDIR},,;\,^${PREFIX}$,d;/^$/d" \
	    >>${WRKDIR}/.staged-dirs-dfs
	sort ${WRKDIR}/.staged-dirs-dfs >${WRKDIR}/.staged-dirs-sorted
	awk '{print FNR, $0}' ${WRKDIR}/.staged-dirs-dfs \
	    >${WRKDIR}/.staged-dirs-dfs-sorted
	# Find all staged dirs and then sort them by depth-first (find -d -s)
	comm -13 ${WRKDIR}/.traced-dirs ${WRKDIR}/.staged-dirs-sorted \
	    | sort_dfs | sed -e "s!^${PREFIX}/!!g;" -f "${sed_plist_sub}" -e "${sed_dirs_gen}" \
	    >>${WRKDIR}/.staged-plist || :
}

# Check for files in STAGEDIR missing from plist
check_orphans_from_plist() {
	local ret=0

	echo "===> Checking for items in STAGEDIR missing from pkg-plist"
	# Handle whitelisting
	while read -r path; do
		case "${path}" in
		*.bak) ;;
		*.orig) ;;
		*/.DS_Store) ;;
		*/.cvsignore) ;;
		*/.git/*|'@dir '*/.git) ;;
		*/.gitattributes|*/.gitignore|*/.gitmodules) ;;
		*/.svn/*|'@dir '*/.svn) ;;
		*/.svnignore) ;;
		*/CVS/*|'@dir '*/CVS) ;;
		*/info/dir|info/dir|info/*/dir|share/info/*/dir) ;;
		share/fonts/*/fonts.dir) ;;
		share/fonts/*/fonts.scale) ;;
		share/applications/mimeinfo.cache) ;;
		share/mime/XMLnamespaces) ;;
		share/mime/aliases) ;;
		share/mime/generic-icons) ;;
		share/mime/globs) ;;
		share/mime/globs2) ;;
		share/mime/icons) ;;
		share/mime/magic) ;;
		share/mime/mime.cache) ;;
		share/mime/subclasses) ;;
		share/mime/treemagic) ;;
		share/mime/types) ;;
		share/mime/version) ;;
		'@dir etc/gconf/gconf.xml.defaults');;
		*)
			# An orphan was found, return non-zero status
			ret=1
			echo "Error: Orphaned: ${path}" >&2
		;;
		esac
	done < ${WRKDIR}/.staged-plist
	return ${ret}
}

# Check for items in plist not in STAGEDIR (pkg lstat(2) errors)
check_missing_plist_items() {
	local ret=0
	echo "===> Checking for items in pkg-plist which are not in STAGEDIR"
	: >${WRKDIR}/.invalid-plist-missing
	comm -23 ${WRKDIR}/.plist-files-no-comments ${WRKDIR}/.staged-files | \
	    sed -e "s!^${PREFIX}/!!g;" -f "${sed_plist_sub}" -e "${sed_files}" \
	    >>${WRKDIR}/.invalid-plist-missing || :

	# Look for directories, then sort them by DFS. Must create the dirs
	# so find -ds can be used to sort them.
	rm -rf ${WRKDIR}/.missing-dirs > /dev/null 2>&1 || :
	mkdir ${WRKDIR}/.missing-dirs
	comm -23 ${WRKDIR}/.plist-dirs-sorted-no-comments \
	    ${WRKDIR}/.staged-dirrms-sorted > ${WRKDIR}/.missing-plist-dirs
	# Creates the dirs in WRKDIR/.missing-dirs and ensure spaces are
	# quoted.
	sed -e "s,^,${WRKDIR}/.missing-dirs," \
	    -e 's,^\(.*\)$,"\1",' \
	    ${WRKDIR}/.missing-plist-dirs | xargs mkdir -p
	find -d -s ${WRKDIR}/.missing-dirs | \
	    sed -e "s,^${WRKDIR}/.missing-dirs,," | \
	    while read -r dir; do \
	    grep -x "${dir}" ${WRKDIR}/.missing-plist-dirs || :; done | \
	    sed -e "s!^${PREFIX}/!!g;" -f "${sed_plist_sub}" -e "${sed_dirs}" \
	    >>${WRKDIR}/.invalid-plist-missing || :
	rm -rf ${WRKDIR}/.missing-dirs
	if [ -s "${WRKDIR}/.invalid-plist-missing" ]; then
		ret=1
		while read -r line; do
			echo "Error: Missing: ${line}" >&2
		done < ${WRKDIR}/.invalid-plist-missing
	fi
	return ${ret}
}

# obtain operating mode from command line
ret=0
makeplist=0
case "$1" in
	checkplist)	;;
	makeplist)	makeplist=1 ;;
	*) echo >&2 "Usage: $0 {checkplist|makeplist}" ; exit 1 ;;
esac

# validate environment
validate_env STAGEDIR PREFIX LOCALBASE WRKDIR WRKSRC MTREE_FILE \
    TMPPLIST PLIST_SUB_SED SCRIPTSDIR PORT_OPTIONS NO_PREFIX_RMDIR

[ -n "${DEBUG_MK_SCRIPTS}" -o -n "${DEBUG_MK_SCRIPTS_CHECK_STAGEDIR}" ] && set -x

set -u

if [ $makeplist = 0 ] ; then
	echo "===> Parsing plist"
	parse_plist "${PREFIX}" 1 < ${TMPPLIST} \
	    3>${WRKDIR}/.plist-dirs-unsorted \
	    >${WRKDIR}/.plist-files-unsorted
	unset TMPPLIST
	# Create the -no-comments files and trim out @comment from the plists.
	# This is used for various tests later.
	sed -e '/^@comment/d' ${WRKDIR}/.plist-dirs-unsorted \
	    >${WRKDIR}/.plist-dirs-unsorted-no-comments
	sed -i '' -e 's/^@comment //' ${WRKDIR}/.plist-dirs-unsorted
	sed -e '/^@comment/d' ${WRKDIR}/.plist-files-unsorted | sort \
	    >${WRKDIR}/.plist-files-no-comments
	sed -e 's/^@comment //' ${WRKDIR}/.plist-files-unsorted | sort \
	    >${WRKDIR}/.plist-files
else
	# generate plist - pretend the plist had been empty
	: >${WRKDIR}/.plist-dirs-unsorted
	: >${WRKDIR}/.plist-files
	echo '/you/have/to/check/what/makeplist/gives/you'
fi

parse_mtree

setup_plist_seds
generate_plist

# If just making plist, show results and exit successfully.
if [ ${makeplist} -eq 1 ]; then
	cat ${WRKDIR}/.staged-plist
	exit 0
fi

check_orphans_from_plist || ret=1

# Prepare plist-dirs for directory checks
sort -u ${WRKDIR}/.plist-dirs-unsorted-no-comments \
    >${WRKDIR}/.plist-dirs-sorted-no-comments

check_missing_plist_items || ret=1

if [ ${ret} -ne 0 ]; then
	echo "===> Error: Plist issues found." >&2
	if [ "${PREFIX}" != "${LOCALBASE}" ]; then
	    echo "===> Warning: Test was done with PREFIX != LOCALBASE"
	    echo "===> Warning: The port may not be properly installing into PREFIX"
	fi
fi

exit ${ret}
