#!/bin/sh
#
# Check what's installed on a PCP/PCPQA VM looking for missing apps
# and packages
#

if [ -f /etc/pcp.conf ]
then
    . /etc/pcp.conf
else
    # punt
    #
    case `uname`
    in
	Darwin|*BSD)
	    PCP_ECHO_PROG=echo
	    PCP_ECHO_N=
	    PCP_ECHO_C='\c'
	    ;;
	*)
	    PCP_ECHO_PROG=echo
	    PCP_ECHO_N=-n
	    PCP_ECHO_C=
	    ;;
    esac
fi

# same function is in allow-pmlc-access ... need to track changes
#
_getnetworkaddr()
{
    __verbose=false
    $very_verbose && __verbose=true
    if `which hostname >/dev/null 2>&1`
    then
	host=`hostname`
	if `which host >/dev/null 2>&1`
	then
	    host_out=`host $host`
	    if echo "$host_out" | grep ' has address ' >/dev/null
	    then
		addr=`echo "$host_out" | sed -e 's/.*address //'`
		$__verbose && echo "_getnetworkaddr: host -> addr=$addr" >&2
		if `which ifconfig >/dev/null 2>&1`
		then
		    # ifconfig line of interest looks like:
		    # inet 192.168.1.100  netmask 255.255.255.0  ...
		    # or
		    # inet addr:192.168.1.224  Bcast:192.168.1.255  Mask:255.255.255.0
		    # or
		    # inet 192.168.1.238/24 broadcast 192.168.1.255 flags 0x0
		    #
		    __config=`ifconfig | grep "[ :]$addr[ /]"`
		    if echo "$__config" | grep '[Mm]ask' >/dev/null
		    then
			mask=`echo "$__config" | sed -e 's/.*ask[ :]\([^ ][^ ]*\).*/\1/'`
		    elif echo "$__config" | grep " $addr/" >/dev/null
		    then
			mask=`echo "$__config" | sed -e '/\/24/s/.*/255.255.255.0/'`
		    else
			echo "_getnetworkaddr: botched config line: $__config" >&2
			mask=''
		    fi
		    $__verbose && echo "_getnetworkaddr: ifconfig -> mask=$mask" >&2
		    case "$mask"
		    in
			255.255.255.0|0xffffff00|ffffff00)	# /24 network
			    echo "$addr" | sed -e 's/\.[0-9]*$/.*/'
			    ;;
			# pmcd's [access] is not smart enough to handle other
			# than /24 networks, so map the other likely options
			# to the broader /24 network
			#
			255.255.255.128|255.255.255.192|255.255.255.224|255.255.255.240|255.255.255.248|255.255.255.252|255.255.255.254)
			    echo "$addr" | sed -e 's/\.[0-9]*$/.*/'
			    ;;
			*)
			    echo >&2 "_getnetworkaddr: Warning: cannot handle network mask: $mask"
			    ;;
		    esac
		elif `which ip >/dev/null 2>&1`
		then
		    # ip line of interest looks like:
		    # 4: br0    inet 192.168.1.100/24 ...
		    #
		    mask=`ip -f inet -o address | grep " $addr/" | sed -e 's/.*inet //' -e 's/[0-9.][0-9.]*\/\([^ ][^ ]*\) .*/\1/'`
		    $__verbose && echo "_getnetworkaddr: ip -> mask=$mask" >&2
		    if [ "$mask" != 24 ]
		    then
			# pmcd's [access] is not smart enough to handle other
			# than /24 networks, so map the other likely options
			echo >&2 "_getnetworkaddr: Warning: cannot handle network mask: $mask"
		    fi
		    # /24 netmask
		    mask=255.255.255.0
		    echo "$addr" | sed -e 's/\.[0-9]*$/.*/'
		else
		    echo >&2 "Neither ifconfig(1) nor ip(1)? Not sure how to get primary ip addr and netmask"
		fi
	    else
		echo >&2 "Unexpected host(1) output: $host_out ... cannot get ip addr and netmask"
	    fi
	else
	    echo >&2 "No host(1)? Not sure how to get primary ip addr and netmask"
	fi
    else
	echo >&2 "No hostname(1)? Not sure how to get primary ip addr and netmask"
    fi
}

_usage()
{
    echo "Usage: $0 [options]"
    echo "  -b           base package set, skip the N/A and optional ones"
    echo "  -f           force, don't try to guess the version of Python, Qt, ..."
    echo "  -m manifest  use alternative manifest file"
    echo "  -o otherdir  use alternative other-packages directory"
    echo "  -p           generate list of likely packages that should be"
    echo "               installed with dnf or apt-get or yum or ..."
    echo "  -P           same as -p, but emit shell commands for installer"
    echo "  -r require   use alternative require file"
    echo "  -s skip      use alternative skip file"
    echo "  -S skipbase  use alternative basename for skipbase.<hostname> files"
    echo "  -u unavail   use alternative unavailable file"
    echo "  -v           verbose (debugging)"
    exit 1
}

# Need directory where check-vm script is located so we can find
# the default other-packages/manifest and other-packages/unavailable
# files and the whatami script
#
home=`echo $0 | sed -e 's/\/*check-vm$//'`
if [ -z "$home" -o ! -d "$home" ]
then
    echo "Botch: \$0=$0 -> bad \$home=$home ?"
    exit 1
fi

basic=false
force=false
pkglist=false
pkgcmd=false
verbose=false
very_verbose=false
check_manifest=false
check_manifest_args=''
manifest="$home"/other-packages/manifest		# default, see -m
otherdir=other-packages					# default, see -o
require="$home"/other-packages/require			# default, see -r
skip="$home"/other-packages/skip			# default, see -s
skipbase="$home"/other-packages/skip			# default, see -S
unavailable="$home"/other-packages/unavailable		# default, see -u
while getopts 'bcfm:o:pPr:s:S:u:v?' p
do
    case "$p"
    in
	b)
		basic=true
		;;
	c)	# exec check-manifest when manifest has been build
		# ... expert only, so not even mentioned in Usage
		#
		check_manifest=true
		;;

	f)
		force=true
		;;

	m)	# alternate manifest
		manifest="$OPTARG"
		;;

	o)	# alternate other-packages directory
		otherdir="$OPTARG"
		;;

	p)	# just list most likely package for yum, dnf, apt-get, ...
		pkglist=true
		;;

	P)	# same as -P but emit shell commands for installer
		pkgcmd=true
		pkglist=true
		;;

	r)	# alternate require
		require="$OPTARG"
		;;

	s)	# alternate skip
		skip="$OPTARG"
		;;

	S)	# alternate skip basename
		skipbase="$OPTARG"
		;;

	u)	# alternate unavailable
		unavailable="$OPTARG"
		;;

	v)	if $verbose
		then
		    very_verbose=true
		else
		    verbose=true
		fi
		check_manifest_args="$check_manifest_args -v"
		;;

	?)	_usage
		# NOTREACHED
    esac
done
shift `expr $OPTIND - 1`
[ $# -eq 0 ] || _usage

if $very_verbose
then
    tmp=tmp
else
    tmp=/var/tmp/$$
    trap "rm -f $tmp.*; exit 0" 0 1 2 3 15
fi
rm -f $tmp.*

if [ -f /etc/pcp.conf ]
then
    . /etc/pcp.conf
else
    # punt!
    #
    if which gawk >/dev/null 2>&1
    then
	PCP_AWK_PROG=gawk
    else
	PCP_AWK_PROG=awk
    fi
fi

# version1 is on line 1
# version2 is on line 2
# relop is "<", "<=", "=", ">=" or ">"
# return value is 0 (true) if version1 relop version2 is true
# else return value is 1
#
_compare()
{
    relop="$1"
    awk -F. >$tmp.compare '
NR == 1	{ for (i = 1; i <= NF; i++)
	    v[i] = $i
	  nv = NF
	  next
	}
NR == 2	{ nf = NF
	  if (nv > nf) nf = nv
	  for (i = 1; i <= nf; i++) {
	    if (v[i]+0 == $i+0) continue
	    if (v[i]+0 < $i+0) {
		print "<"
		exit
	    }
	    if (v[i]+0 > $i+0) {
		print ">"
		exit
	    }
	  }
	  print "="
	}'
    ret=1
    case `cat $tmp.compare`
    in
	"<")
	    [ "$relop" = "<" -o "$relop" = "<=" ] && ret=0
	    ;;
	"=")
	    [ "$relop" = "=" -o "$relop" = "<=" -o "$relop" = ">=" ] && ret=0
	    ;;
	">")
	    [ "$relop" = ">" -o "$relop" = ">=" ] && ret=0
	    ;;
	*)
	    echo "Arrgh ... installed version $version, want $relop $specversion,"
	    echo "but failed to extract relop (`cat $tmp.compare`)"
	    ;;
    esac
    return $ret
}

# The require, unavailable and skip files have the same format,
# listing packages and package patterns that apply to a particular
# distribution, version and architecture.
#
# This function parses one of these files (specified as $1) and the
# selected packages (if any) are written on std output
#
_parse_file()
{
    $PCP_AWK_PROG '{ for (i = 3; i <= NF; i++) printf $i " "; print "" }' <$tmp.whatami \
    | sed \
	-e 's/ \[.*//' \
	-e 's/ ([^)]*)//' \
	-e 's/RHEL Server/RHEL/' \
	-e 's/Arch Linux/ArchLinux /' \
	-e '/CentOS/{
s/CentOS Linux/CentOS/
s/CentOS\([0-9]\)/CentOS \1/
}' \
	-e '/FreeBSD/s/-RELEASE//' \
	-e '/SLES\([0-9][0-9]*\) SP/s//\1-SP/' \
	-e '/openSUSE/s/ Leap//' \
	-e '/OpenIndiana/s/Hipster //' \
	-e 's/^/_arch=/' \
	-e 's/ /+_distro=/' \
	-e 's/ / _version="/' \
	-e 's/+_/ _/' \
	-e 's/$/"/' \
    | tr ' ' '\012' >$tmp.eval
    $very_verbose && echo >&2 "_parse_file($1): vars: `tr '\012' ' ' <$tmp.eval`"
    eval `cat $tmp.eval`
    rm -f $tmp.matches
    sed <$1 \
	-e 's/#.*//' \
	-e '/^[ 	]*$/d' \
    | while read distro_pat version_pat arch_pat packages
    do
	if echo "$_distro" | grep "^$distro_pat" >/dev/null
	then
	    # Distro matches, check versions
	    #
	    _match=true
	    if [ "X$version_pat" = "X-" -o -z "$_version" ]
	    then
		:
	    else
		if echo "$_version" | grep "^$version_pat" >/dev/null
		then
		    :
		else
		    _match=false
		fi
	    fi
	    if $_match
	    then
		# Distro and version matches, check architecture
		if [ "X$arch_pat" = "X-" -o -z "$_arch" ]
		then
		    :
		else
		    if echo "$_arch" | grep "^$arch_pat" >/dev/null
		    then
			:
		    else
			_match=false
		    fi
		fi
	    fi
	    if $_match
	    then
		echo "$packages" >>$tmp.matches
	    fi
	fi
    done
    if [ -s $tmp.matches ]
    then
	tr '\012' ' ' <$tmp.matches
    else
	$very_verbose && echo >&2 "_parse_file: $1: no matches"
    fi
}

_find_installer_cmd()
{
    installcmd=''
    case "`sed <$tmp.whatami -e 's/  */ /g' -e 's/ \[.*//' | cut -d ' ' -f 4-`"
    in
	*Ubuntu*|*Debian*|*LinuxMint*)
	    installcmd="apt install"
	    ;;
	*RHEL*|*Fedora*|*CentOS*|*openSUSE*|*SUSE\ SLES*)
	    if which zypper >/dev/null 2>&1
	    then
		installcmd="zypper install"
	    elif which dnf >/dev/null 2>&1
	    then
		installcmd="dnf install"
	    elif which yum >/dev/null 2>&1
	    then
		installcmd="yum install"
	    fi
	    ;;

	*NetBSD*)
	    installcmd="pkgin -y install"
	    ;;

	*FreeBSD*)
	    installcmd="pkg install"
	    ;;

	*OpenBSD*)
	    installcmd="pkg_add"
	    ;;

	*Gentoo*)
	    installcmd="emerge --ask"
	    ;;

	*Darwin*)
	    installcmd="brew install"
	    ;;

	*OpenIndiana*)
	    installcmd="pkg install"
	    ;;

	*Slackware*)
	    installcmd="slackpkg install"
	    ;;

	*Arch\ Linux*)
	    installcmd="pacman -S"
	    ;;
    esac
    if [ -z "$installcmd" ]
    then
	echo "$0: Botch: don't recognize installcmd for: `cat $tmp.whatami`"
	exit
    fi
}

# add additional and optional directories
for dir in /sbin /usr/sbin
do
    if [ -d "$dir" ]
    then
	if echo ":$PATH:" | grep -q ":$dir:"
	then
	    :
	else
	    export PATH="$PATH:$dir"
	    #debug# echo add $dir to \$PATH
	fi
    fi
done

# Packaging types we know about.
#	dpkg - Debian-based
#	rpm - RPM-based, e.g. RHEL, CentOS, SuSE, Fedora
#	urpmi - OpenMandriva (also RPM based)
#	emerge - Gentoo
#	pkgin - NetBSD
#	pkg_add - OpenBSD
#	F_pkg - pkg(1) on FreeBSD
#	S_pkg - pkg(1) on OpenIndiana
#	slackpkg - Slackware
#	pacman - Arch Linux
#	brew - Mac OS X
#
pkgtypes="dpkg rpm urpmi emerge pkgin pkg_add F_pkg S_pkg slackpkg pacman brew"

# Distros we know about (or more specifically ones with the same 
# packaging tools but different package names).
#
#	debian		Debian, Ubuntu, LinuxMint, ...
#	redhat		RedHat or Fedora
#	centos		CentOS
#	suse		SLES or OpenSuSE
#	arch		Arch Linux
#	mandriva	OpenMandriva
#	freebsd		FreeBSD
#	netbsd		NetBSD
#	openbsd		OpenBSD
#
distros="debian redhat centos suse arch mandriva freebsd netbsd openbsd"

if [ ! -f "$manifest" ]
then
    echo "Botch: cannot find manifest: $manifest"
    exit 1
fi
if [ ! -f "$require" ]
then
    echo "Botch: cannot find require: $require"
    exit 1
fi
if [ ! -f "$unavailable" ]
then
    echo "Botch: cannot find unavailable: $unavailable"
    exit 1
fi
if [ ! -f "$skip" ]
then
    echo "Botch: cannot find skip: $skip"
    exit 1
fi
if [ ! -f "$home"/whatami ]
then
    echo "Botch: cannot find whatami :$home/whatami"
    exit 1
fi
$home/whatami >$tmp.whatami

# Append original line numbers for non-blank lines in the manifest.
# Also rewrite [? ...] (package unknown) to [?:<original line number> ...]
# for easier mapping back to the manifest file.
#
$PCP_AWK_PROG 'NF > 0 { gsub("\\[\\?", "[?:" NR); print $0 " :" NR }' <$manifest >$tmp.manifest

if which $PCP_PYTHON_PROG >/dev/null 2>&1
then
    # For python-ctypes, check for python before 2.5 ... expect something like
    # Python 2.7.3
    eval `$PCP_PYTHON_PROG -V 2>&1 | sed -e 's/Python //' -e 's/^/maj=/' -e 's/\./ min=/' -e 's/\..*//'`
    if [ -n "$maj" -a -n "$min" ]
    then
	rm -f $tmp.need
	if [ "$maj" -lt 2 ]
	then
	    touch $tmp.need
	elif [ "$maj" -eq 2 -a "$min" -lt 5 ]
	then
	    touch $tmp.need
	fi
	[ -f $tmp.need ] && \
	    echo "rpm?	/usr/share/doc/python-ctypes*	[python-ctypes]" >>$tmp.manifest
    fi
fi

# Handle some aliased packaging commands, e.g. pkg(1) is present but
# different on both FreeBSD and OpenIndiana (nee Open Solaris).
# In the package control lines we use F_pkg and S_pkg to differentatiate
# but sort out if we have one or the other here to avoid repeated tests
# in the loop below
#
have_F_pkg=false
have_S_pkg=false
if which pkg >/dev/null 2>&1
then
    if pkg help 2>&1 | grep 'jail(8)' >/dev/null
    then
	# FreeBSD is the only one to have jail(8) in the help text
	#
	have_F_pkg=true
	$verbose && echo >&2 "F_pkg -> pkg for FreeBSD"
    else
	have_S_pkg=true
	$verbose && echo >&2 "S_pkg -> pkg for OpenIndiana"
    fi
fi

# Strip the packaging-dependent lines that do not apply ...
# this is an optimization remove checks from the main loop below
#
rm -f $tmp.ok
for pkgtype in $pkgtypes
do
    want=true
    tool=$pkgtype
    case $pkgtype
    in
	rpm)
	    # Madriva really uses urpmi, but the rpm binary also is installed
	    # so don't include the rpm? lines
	    #
	    which urpmi >/dev/null 2>&1 && want=false
	    # similarly for Slackware where slackpkg is the real packing
	    # tool
	    #
	    which slackpkg >/dev/null 2>&1 && want=false
	    ;;
	pkg_add)
	    # NetBSD really uses pkgin
	    which pkgin >/dev/null 2>&1 && want=false
	    ;;
	S_pkg)
	    if $have_S_pkg
	    then
		tool=pkg
	    else
		want=false
	    fi
	    ;;
	F_pkg)
	    if $have_F_pkg
	    then
		tool=pkg
	    else
		want=false
	    fi
	    ;;
    esac

    if which $tool >/dev/null 2>&1 && $want
    then
	$very_verbose && echo >&2 "include ${pkgtype}? lines"
	sed -e "s/^${pkgtype}?[ 	]//" <$tmp.manifest >$tmp.tmp
	mv $tmp.tmp $tmp.manifest
	touch $tmp.ok
	# one-trip list all currently installed packages for -p
	#
	if [ ! -f $tmp.installed ]
	then
	    case "$pkgtype"
	    in
		dpkg)
		    dpkg-query -W -f '${package}\n'
		    ;;

		rpm)
		    rpm -qa --qf '%{NAME}\n'
		    ;;

		pkgin)
		    pkgin list | sed -e 's/-[0-9].*//'
		    ;;

		F_pkg)
		    pkg info -a | sed -e 's/-[0-9].*//'
		    ;;

		S_pkg)
		    pkg list -H | sed -e 's/ .*//'
		    ;;

		pkg_add)
		    pkg_info -a | sed -e 's/-[0-9].*//'
		    ;;

		emerge)
		    equery list '*' | sed -e 's/.*].*] //' -e 's/-[0-9].*//'
		    ;;

		slackpkg)
		    ls /var/log/packages | sed -e 's/-[0-9].*//'
		    ;;

		pacman)
		    pacman -Q -i | sed -n -e '/^Name /s/.* : //p'
		    ;;

		brew)
		    echo >&2 "TODO: no list all packages method for brew"
		    ;;

		*)
		    echo >&2 "Botch: no list all packages method for $pkgtype"
		    ;;
	    esac >$tmp.installed
	fi
    else
	$very_verbose && echo >&2 "exclude ${pkgtype}? lines"
	grep -v "^${pkgtype}?[ 	]" <$tmp.manifest >$tmp.tmp
	mv $tmp.tmp $tmp.manifest
    fi
done
if [ ! -f $tmp.ok ]
then
    echo >&2 "Warning: don't understand what packing is being used here ..."
    echo >&2 "         it is none of: $pkgtypes"
fi

# Strip the distro-dependent lines that do not apply ...
# this is an optimization remove checks from the main loop below
#
if [ -n "$PCP_PLATFORM" ]
then
    iam="$PCP_PLATFORM"
else
    # tests here are copied from whatami
    #
    if [ -f /etc/SuSE-release ]
    then
	iam=suse
    elif [ -f /etc/centos-release ]
    then
	iam=centos
    elif [ -f /etc/redhat-release ]
    then
	iam=redhat
    elif [ -f /etc/debian_version ]
    then
	iam=debian
    elif [ -f /etc/mandriva-release ]
    then
	iam=mandriva
    elif [ -f /etc/gentoo-release ]
    then
	iam=gentoo
    elif [ -f /etc/fedora-release ]
    then
	iam=fedora
    elif [ -f /etc/os-release ]
    then
	iam=opensuse
    elif [ -f /etc/release ]
    then
	iam=openindiana
    elif [ -f /etc/slackware-version ]
    then
	iam=slackware
    elif [ -f /etc/arch-release ]
    then
	iam=arch
    elif [ -f /etc/lsb-release ]
    then
	iam=`sed </etc/lsb-release -n -e '/^DISTRIB_ID *= */s///p' | tr '[A-Z]' '[a-z]'`
    fi
fi

for distro in $distros
do
    pick=false
    case "$iam"
    in
	redhat|fedora)
	    [ "$distro" = redhat ] && pick=true
	    ;;
	*)
	    [ "$distro" = "$iam" ] && pick=true
	    ;;
    esac
    if $pick
    then
	$very_verbose && echo >&2 "include ${distro}? lines"
	sed -e "s/^${distro}?[ 	]//" <$tmp.manifest >$tmp.tmp
	mv $tmp.tmp $tmp.manifest
    else
	$very_verbose && echo >&2 "exclude ${distro}? lines"
	grep -v "^${distro}?[ 	]" <$tmp.manifest >$tmp.tmp
	mv $tmp.tmp $tmp.manifest
    fi
done

# the "unavailable" file records packages that are not available for a
# particular distribution and version and archtecture ... 
#
_parse_file $unavailable >$tmp.pkgs
if [ -s $tmp.pkgs ]
then
    unavail_pkg="`cat $tmp.pkgs`"
fi
$very_verbose && echo >&2 "unavailable: $unavail_pkg"

# the "require" file records packages that are must be installed for a
# particular distribution and version and archtecture ... 
#
require_pkg=''
_parse_file $require >$tmp.pkgs
if [ -s $tmp.pkgs ]
then
    $very_verbose && echo >&2 "require: `cat $tmp.pkgs`"
    for _need in `cat $tmp.pkgs`
    do
	if grep "^$_need\$" $tmp.installed >/dev/null
	then
	    :
	else
	    if [ -z "$require_pkg" ]
	    then
		require_pkg="$_need"
	    else
		require_pkg="$require_pkg $_need"
	    fi
	fi
    done
    if $very_verbose
    then
	if [ -n "$require_pkg" ]
	then
	    echo >&2 "require and not installed: $require_pkg"
	else
	    echo >&2 "require and not installed: <none>"
	fi
    fi
fi

if $pkglist
then
    # for -p, don't mark manifest lines with N/A ... this is handled
    # later for each package in [ ... ]
    #
    :
else
    if [ -n "$unavail_pkg" ]
    then
	for _pkg in $unavail_pkg
	do
	    sed <$tmp.manifest >$tmp.tmp -e "/[[ ]$_pkg[] ]/"'{
s/\([[ ]\)'"$_pkg"'\([] ]\)/\1 \2/
s/\(\[.*\) or/\1/g
s/\(\[.*\) ([^)]*)/\1/g
s/  */ /g
}'
	    if [ -z "$_version" ]
	    then
		sed <$tmp.tmp >$tmp.manifest \
		    -e 's;\[  *\([](]\);[N/A on '"$_distro"' \1;' \
		    -e 's/  *]/]/' \
		#end
	    else
		sed <$tmp.tmp >$tmp.manifest \
		    -e 's;\[  *\([](]\);[N/A on '"$_distro"' '"$_version"' \1;' \
		    -e 's/  *]/]/' \
		#end
	    fi
	    $very_verbose && echo >&2 "unavailable: $_pkg"
	done
    fi
fi

# the optional "skip.<hostname>" file records packages that may be available
# but for various reasons should be be installed on a particular host ...
# examples are docker.io that does not get along with qemu or unbound that
# does not co-exist with bind9 or libfooqt.so that is really for the wrong
# version of Qt
#
skip_pkg=''
_hostname=`hostname -s`
if [ -f $skipbase.$_hostname ]
then
    $verbose && echo >&2 "Processing $skipbase.`hostname -s` ..."
    skip_pkg=`sed <$skipbase.$_hostname \
	    -e 's/#.*//' \
	    -e '/^[ 	]*$/d' | tr '\012' ' '`
    if [ -n "$skip_pkg" ]
    then
	$very_verbose && echo >&2 "skip on host: $skip_pkg"
	for _pkg in $skip_pkg
	do
	    sed <$tmp.manifest >$tmp.tmp -e "/[[ ]$_pkg[] ]/"'{
s;\[[^]]*];[SKIP on host '"$_hostname"'];
}'
	    mv $tmp.tmp $tmp.manifest
	done
    fi
fi

# the "skip" file records packages that should not be installed for a
# particular distribution and version and archtecture ... 
#
_parse_file $skip >$tmp.pkgs
if [ -s $tmp.pkgs ]
then
    $very_verbose && echo >&2 "skip: `cat $tmp.pkgs`"
    skip_pkg="$skip_pkg `cat $tmp.pkgs`"
    for _pkg in `cat $tmp.pkgs`
    do
	sed <$tmp.manifest >$tmp.tmp -e "/[[ ]$_pkg[] ]/"'{
s;\[[^]]*];[SKIP];
}'
	mv $tmp.tmp $tmp.manifest
    done
fi

if $force
then
    :
else
    # If we are sure which version of Python we're using, remove lines
    # for the other versions
    #
    if [ -n "$PCP_PYTHON_PROG" ]
    then
	if which "$PCP_PYTHON_PROG" >/dev/null 2>&1
	then
	    pyver=`$PCP_PYTHON_PROG --version 2>&1 | sed -e 's/^[^0-9]*//' -e 's/\..*//'`
	    if [ -z "$pyver" ]
	    then
		echo >&2 "Warning: cannot get Python version from: `$PCP_PYTHON_PROG --version`"
	    else
		#debug# echo "pyver=$pyver"
		sed <$tmp.manifest >$tmp.tmp -e '/\/python[0-9]/{
/\/python'"$pyver"'[./]/!d
}'
		#debug# diff $tmp.manifest $tmp.tmp
		mv $tmp.tmp $tmp.manifest
	    fi
	fi
    fi

    # If we are sure which version of Qt we're using, remove lines for
    # the other versions
    #
    # Logic from Makepkgs
    #
    cull=''
    unset QT_SELECT
    if which qtchooser >/dev/null 2>&1
    then
	if qtchooser -list-versions | grep '^5$' >/dev/null
	then
	    cull=qt4
	else
	    cull=qt5
	fi
    fi
    if [ -n "$cull" ]
    then
	#debug# echo "cull=$cull"
	grep -vi "$cull" <$tmp.manifest >$tmp.tmp
	#debug# diff $tmp.manifest $tmp.tmp
	mv $tmp.tmp $tmp.manifest
    fi
fi

if $check_manifest
then
    # save the unavailable packages list, and hand off
    #
    touch $tmp.unavail
    for _pkg in $unavail_pkg
    do
	echo "$_pkg" >>$tmp.unavail
    done
    echo "Handing off to $home/check-manifest ..."
    exec $home/check-manifest $check_manifest_args $tmp
fi

# skip "optional" and N/A packages for -b
#
if $basic
then
    $very_verbose && echo >&2 "exclude \"optional\" package lines"
    sed -e '/ optional)/d' -e '/N\/A/d' <$tmp.manifest >$tmp.tmp
    mv $tmp.tmp $tmp.manifest
fi

# main loop
#
cat $tmp.manifest \
| sed -e 's/#.*//' -e '/^[ 	]*$/d' \
| while read line
do
    apps=`echo "$line" | sed -e 's/:[0-9][0-9]*$//' -e 's/ $//'`
    lineno=`echo "$line" | sed -e 's/.* :\([0-9][0-9]*\)$/\\1/'`
    rm -f $tmp.ok
    rm -f $tmp.echo
    for app in $apps
    do
	# leading ! negates the guard
	case $app
	in
	    !*)
		app=`echo "$app" | sed -e 's/^!//'`
		negate=true
		;;
	    *)
		negate=false
		;;
	esac
	case $app
	in
	    \[*)
	    	break
		;;
	    *\?)
	    	app=`echo $app | sed -e 's/?$//'`
		optional=true
		;;
	    *)
		optional=false
		;;
	esac
	case $app
	in
	    F_pkg|S_pkg)
		app=pkg
		;;
	esac
	case $app
	in
	    \[*)
	    	break
		;;
	    *::)
		# special case Perl, no module name
		echo "use `echo $app | sed -e 's/::$//'`;" | perl >/dev/null 2>&1
		ok=$?
		;;

	    *::*)
		# normal case Perl, with module name
		echo "use $app;" | perl >/dev/null 2>&1
		ok=$?
		;;
	    *)  # file, directory or executable tests, separated by |
		rm -f $tmp.tmp
		for obj in `echo "$app" | sed -e 's/|/ /g'`
		do
		    case "$obj"
		    in
			/*)
			    if [ -f "$obj" -o -d "$obj" ]
			    then
				touch $tmp.tmp
				break
			    fi
			    ;;
			*)
			    if which $obj >/dev/null 2>&1
			    then
				touch $tmp.tmp
				break
			    fi
			    ;;
		    esac
		done
		[ -f $tmp.tmp ]
		ok=$?
		;;
	esac
	if $negate
	then
	    ok=`expr 1 - $ok`
	fi
	if $verbose
	then
	    $PCP_ECHO_PROG >&2 $PCP_ECHO_N "$app ... "$PCP_ECHO_C
	    $optional && $PCP_ECHO_PROG >&2 $PCP_ECHO_N "[optional] "$PCP_ECHO_C
	    if [ $ok = 0 ]
	    then
		$PCP_ECHO_PROG >&2 $PCP_ECHO_N "yes "$PCP_ECHO_C
	    else
		$PCP_ECHO_PROG >&2 $PCP_ECHO_N "no "$PCP_ECHO_C
	    fi
	    touch $tmp.echo
	fi
	if [ $ok = 0 ]
	then
	    if $optional
	    then
		if [ -f $tmp.echo ]
		then
		    echo >&2
		    rm -f $tmp.echo
		fi
		continue
	    fi
	    touch $tmp.ok
	    break
	else
	    if $optional
	    then
		# guard not true, skip checks for other apps
		touch $tmp.ok
		break
	    fi
	fi
    done
    if [ ! -f $tmp.ok ]
    then
	if $pkglist
	then
	    echo "$apps" \
	    | sed -n \
		-e '/\[SKIP on host/s/on host.*/]/' \
		-e 's/.*\[//' \
		-e 's/]//' \
		-e 's/ ([^)]*)//' \
		-e 's/base [^ ]* install//' \
		-e 's/^or //' \
		-e 's/ or$//' \
		-e 's/^or$//' \
		-e 's/ or / /g' \
		-e 's/[ 	][ 	]*/ /g' \
		-e '/^ *$/d' \
		-e p \
	    | while read _missing
	    do
		# don't list the unavailable packages nor the skipped ones
		# nor the ones already installed
		#
		for _need in $_missing
		do
		    if [ "$_need" = "N/A" ]
		    then
			if $verbose
			then
			    $PCP_ECHO_PROG >&2 $PCP_ECHO_N "[unavailable] "$PCP_ECHO_C
			    touch $tmp.echo
			fi
			continue
		    fi
		    if [ "$_need" = "SKIP" ]
		    then
			if $verbose
			then
			    $PCP_ECHO_PROG >&2 $PCP_ECHO_N "[skip] "$PCP_ECHO_C
			    touch $tmp.echo
			fi
			continue
		    fi
		    if grep "^$_need\$" $tmp.installed >/dev/null
		    then
			[ -f $tmp.echo ] && echo >&2
			echo >&2 "Warning: manifest:$lineno botch? target: \"`echo "$apps" | sed -e 's/[ 	].*//'`\" not found, but package \"$_need\" already installed"
			rm -f $tmp.echo
		    else
			rm -f $tmp.match
			for _pkg in $unavail_pkg
			do
			    if echo " $_need " | grep " $_pkg " >/dev/null
			    then
				if $verbose
				then
				    $PCP_ECHO_PROG >&2 $PCP_ECHO_N "[unavailable] "$PCP_ECHO_C
				    touch $tmp.echo
				fi
				touch $tmp.match
				break
			    fi
			done
			if [ ! -f $tmp.match ]
			then
			    echo "$_need"
			fi
		    fi
		done
	    done
	elif echo "$apps" | grep '\[SKIP' >/dev/null
	then
	    # nothing to report
	    :
	else
	    echo "Missing: `echo "$apps" \
		| sed \
		    -e 's/[ 	][ 	]*/ /g' \
		    -e '/ /{
s/? /?@/
:loop1
s/\(\[.*\) /\1@/
t loop1
:loop2
s/ \([^[]\)/@|@\1/
t loop2
s/@/ /g
}'`"
	fi
    else
	if echo "$apps" | grep 'N/A' >/dev/null
	then
	    [ -f $tmp.echo ] && echo >&2
	    echo >&2 "Warning: manifest:$lineno botch? target: \"`echo "$apps" | sed -e 's/[ 	].*//'`\" found, but marked N/A"
	    rm -f $tmp.echo
	fi
    fi
    [ -f $tmp.echo ] && echo >&2

done >$tmp.out


if $pkglist
then
    # append required packages ... uniq below culls any duplicates
    for _pkg in $require_pkg
    do
	echo "$_pkg" >>$tmp.out
    done
fi

if [ -s $tmp.out ]
then
    if $pkglist
    then
	sort <$tmp.out \
	| uniq >$tmp.tmp
	if $pkgcmd
	then
	    _find_installer_cmd
	    grep -v 'cpan(' <$tmp.tmp | grep -v 'pip3(' >$tmp.pkg
	    if [ -s $tmp.pkg ]
	    then
		echo "sudo $installcmd `tr '\012' ' ' <$tmp.pkg`"
	    fi
	    grep 'cpan(' <$tmp.tmp >$tmp.pkg
	    if [ -s $tmp.pkg ]
	    then
		sed <$tmp.pkg \
		    -e 's/^/sudo cpan /' \
		    -e 's/cpan(/"/' \
		    -e 's/)/"/'
		# end
	    fi
	    grep 'pip3(' <$tmp.tmp >$tmp.pkg
	    if [ -s $tmp.pkg ]
	    then
		echo "sudo pip3 install `tr '\012' ' ' <$tmp.pkg`"
	    fi
	else
	    tr '\012' ' ' <$tmp.tmp \
	    | sed -e 's/  */ /g' -e 's/^ //' -e 's/ $//'
	    # BSD* and Darwin heuristic hack ... may not need extra echo
	    # for these guys, but ... needed on
	    # FreeBSD 12.0
	    #
	    echo
	fi
    else
	cat $tmp.out
    fi
fi

$pkglist && exit

# Any required packages not installed?
#
for _pkg in $require_pkg
do
    echo "Missing: $_pkg [required package not installed]"
done

# Some platform-specific and special case tests
#
if which dpkg >/dev/null 2>&1
then
    # Debian based
    #
    dpkg -l >$tmp.tmp
    for pkg in python-all python-all-dev python3-all python3-all-dev
    do
	if grep "^ii *$pkg" <$tmp.tmp >/dev/null
	then
	    :
	else
	    echo "Need # apt-get install $pkg"
	fi
    done
    # There are some version dependencies in the debian/control file
    # (see Build-Depends and *-Version lines) and some are found in
    # configure.ac ... both need to be mirrored here
    #
    cat <<End-of-File >$tmp.dpkg
# one line per package
# pkg-name	relop	version
# text after # is treated as a comment
# debian/control
debhelper	>=	5
perl		>=	5.6
python		>=	2.6
# configure.ac
libuv-dev	>=	1.0
End-of-File
    cat $tmp.dpkg \
    | sed -e 's/#.*//' -e '/^[ 	]*$/d' \
    | while read pkg relop ctlversion
    do
	# dpkg -l lines like ...
	# ii  debhelper      9.20131227ub all          ...
	#
	if grep "ii[ 	][ 	]*$pkg[ 	]" $tmp.tmp >/dev/null 2>&1
	then
	    version=`awk <$tmp.tmp '$2 == "'"$pkg"'" { print $3 }'`
	    if [ -z "$version" ]
	    then
		echo "Arrgh ... failed to get version for $pkg from ..."
		cat $tmp.tmp
		continue
	    fi
	    ( echo $version; echo $ctlversion ) | _compare $relop
	    if [ $? = 0 ]
	    then
		$verbose && echo >&2 "$pkg: version installed $version, need $relop $ctlversion, OK"
	    else
		echo >&2 "Warning: $pkg version installed $version, need $relop $ctlversion"
	    fi
	fi
    done
fi

if which slackpkg >/dev/null 2>&1
then
    # Slackware ...
    :
elif which rpm >/dev/null 2>&1
then
    # RPM based, there are some version dependencies in the spec
    # file (see BuildRequires: lines build/rpm/pcp.spec.in) and some
    # are found in configure.ac ... both need to be mirrored here
    #
    cat <<End-of-File >$tmp.rpm
# one line per rpm
# rpm-name	relop	version	pcp-pkg
# text after # is treated as a comment
# pcp.spec.in
qt-devel|qt4-devel|libqt4-devel|lib64qt4-devel	>=	4.4
libpfm-devel				>=	4.4	pcp-pmda-perfevent
libpfm|libpfm4				>=	4.4	pcp-pmda-perfevent
libibmad-devel|infiniband-diags-devel	>=	1.1.7	pcp-pmda-infiniband
libibmad|libibmad5|infiniband-diags	>=	1.1.7	pcp-pmda-infiniband
libibumad-devel|rdma-core-devel		>=	1.1.7	pcp-pmda-infiniband
libibumad|libibumad3			>=	1.1.7	pcp-pmda-infiniband
End-of-File
    cat $tmp.rpm \
    | sed -e 's/#.*//' -e '/^[ 	]*$/d' \
    | while read rpmlist relop specversion pcp_pkg
    do
	[ -n "$pcp_pkg" ] && pcp_pkg=" for $pcp_pkg"
	rm -f $tmp.found $tmp.notfound
	for rpm in `echo "$rpmlist" | sed -e 's/|/ /g'`
	do
	    rpm -q $rpm >$tmp.tmp 2>/dev/null
	    if grep 'is not installed' $tmp.tmp >/dev/null 2>&1
	    then
		$verbose && echo >&2 "$rpm: not installed, need $relop $specversion$pcp_pkg, OK"
		echo >&2 "Warning: $rpm not installed, need $relop $specversion$pcp_pkg" >>$tmp.notfound
	    else
		touch $tmp.found
		version=`sed <$tmp.tmp -e "s/^$rpm-//" -e 's/-.*//'`
		( echo $version; echo $specversion ) | _compare $relop
		if [ $? = 0 ]
		then
		    $verbose && echo >&2 "$rpm: version installed $version, need $relop $specversion$pcp_pkg, OK"
		else
		    echo >&2 "Warning: $rpm version installed $version, need $relop $specversion$pcp_pkg"
		fi
	    fi
	done
	if [ -f $tmp.found ]
	then
	    :
	else
	    $verbose || cat >&2 $tmp.notfound
	fi
    done
fi

if which pkg-config >/dev/null 2>&1
then
    # PKG_CHECK_MODULES() in configure.ac
    #
    cat <<End-of-File >$tmp.pkg-config
# one line per rpm
# lib-name	relop	version	pcp-pkg
# text after # is treated as a comment
libuv           >=	1.0
End-of-File
    cat $tmp.pkg-config \
    | sed -e 's/#.*//' -e '/^[ 	]*$/d' \
    | while read lib relop version pcp_pkg
    do
	[ -n "$pcp_pkg" ] && pcp_pkg=" for $pcp_pkg"
	libversion=`pkg-config --modversion "$lib" 2>/dev/null`
	if [ -z "$libversion" ]
	then
	    echo >&2 "Warning: Package $lib not known to pkg-config , need $relop $version$pcp_pkg"
	else
	    ( echo $libversion; echo $version ) | _compare $relop
	    if [ $? = 0 ]
	    then
		$verbose && echo >&2 "$lib: version installed $libversion, need $relop $version$pcp_pkg, OK"
	    else
		echo >&2 "Warning: $lib version installed $libversion, need $relop $version$pcp_pkg"
	    fi
	fi
    done
fi

# Networking goo
#
_check_host()
{
    ipaddr=`sed -n </etc/hosts -e '/^#/d' -e '/::/d' -e 's/$/ /' -e "/[ 	]$1[ 	]/"'{
s/[ 	].*//
p
}'`
    if [ -z "$ipaddr" ]
    then
	echo "No /etc/hosts entry for $1"
	return
    fi

    if [ `echo "$ipaddr" | wc -l | sed -e 's/  *//g'` -gt 1 ]
    then
	echo "Multiple /etc/hosts entries for $1"
	return
    fi

    rm -f $tmp.tmp
    if `which ifconfig >/dev/null 2>&1`
    then
	# ifconfig lines of interest look like
	# br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        #	inet 192.168.1.100  netmask 255.255.255.0  broadcast 192.168.1.255
        # ...
	# lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
	#	inet 127.0.0.1  netmask 255.0.0.0
	#
	ifconfig >$tmp.tmp
    elif `which ip >/dev/null 2>&1`
    then
	ip -f inet address >$tmp.tmp
	# ip lines of interest look like
	# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc ...
	#     inet 127.0.0.1/8 scope host lo
	#     ...
	# 4: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc ...
	#     inet 192.168.1.100/24 brd 192.168.1.255 scope global br0
	#     ...
    else
	echo >&2 "Neither ifconfig(1) nor ip(1)? Not sure how to get primary ip addr"
	return
    fi
    sed <$tmp.tmp \
	-e 's/^[0-9][0-9]*: //' \
	-e 's/: / /g' \
	-e '/ inet /s/\/.*/ /' \
    | awk >&2 '
/^[^ 	]/	{ iface = $1; next }
/inet addr:'$ipaddr' / || /inet '$ipaddr'[ \/]/ {
			  if (iface == "lo")
			    print "Warning: '$1' associated with loopback network interface"
			  found = 1
			  exit
			}
END		{ if (found != 1)
		    print "Warning: '$1' ('$ipaddr') not associated with a network interface"
		}'
}

host=`hostname`
_check_host $host
if which pmhostname >/dev/null 2>&1
then
    pmhost=`pmhostname`
    if [ -z "$pmhost" ]
    then
	echo >&2 "Warning: pmhostname returns nothing!"
    else
	case $pmhost
	in
	    $host|$host.*)
		    ;;
	    *)
		    echo >&2 "Warning: hostname ($host) is not a prefix of pmhostname ($pmhost)"
		    ;;
	esac
	_check_host $pmhost
    fi
fi

if [ -n "$PCP_VAR_DIR" ]
then
    # need QA access to pmlogger via pmlc from local subnet
    #
    network=`_getnetworkaddr`
    if [ -n "$network" ]
    then
	if [ -f $PCP_VAR_DIR/config/pmlogger/config.default ]
	then
	    if grep -q "allow $network" $PCP_VAR_DIR/config/pmlogger/config.default
	    then
		:
	    else
		echo "Missing: \"allow $network : all;\" [access] in $PCP_VAR_DIR/config/pmlogger/config.default"
		echo "Use \"$ sudo -E .../qa/admin/allow-pmlc-access\" to fix this."
	    fi
	else
	    echo >&2 "Warning: \"$PCP_VAR_DIR/config/pcp/pmlogger/config.default\" is missing"
	fi
    else
	echo >&2 "Please ignore Warnings from _getnetworkaddr unless you wish to run the"
	echo >&2 "full PCP QA suite."
    fi
else
    echo >&2 "Warning: \"/etc/pcp.conf\" is missing"
fi

if sudo -u pcp id >/dev/null
then
    # pcp user appears to exist ...
    #
    sudo -u pcp [ -x $HOME ] || echo "Error: $HOME is not searchable by user \"pcp\""
fi

# Now some platform-specific tests
#
case "`sed <$tmp.whatami -e 's/  */ /g' -e 's/ \[.*//' | cut -d ' ' -f 4-`"
in
    *OpenBSD*)
	if false
	then
	    # redundant now the openbsd PMDA no longer reads /dev/mem
	    # directly
	    #
	    allowkmem=`sysctl kern.allowkmem | sed -e 's/.*=//'`
	    if [ "$allowkmem" != 1 ]
	    then
		echo >&2 "Warning: kern.allowkmem is \"$allowkmem\" not 1 and so openbsd PMDA will not be able"
		echo "         to access /dev/kmem"
		echo "         Suggest adding kern.allowkmem=1 to etc/sysctl.conf and rebooting."
	    fi
	fi
	;;

esac

$very_verbose && echo >&2 "temp files:" $tmp.*
