#!/bin/ksh
#
# $OpenBSD: portcheck,v 1.86 2014/07/23 22:12:41 zhuk Exp $
# Copyright (c) 2013 Vadim Zhukov
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

set -e
set +X
set -u

usage() {
	echo "usage: ${0##*/} [-dNP] [-p portsdir] [-x glob]" >&2
	echo "       ${0##*/} -A [-dP] [-p portsdir] [-x glob] [subdir ...]" >&2
	exit 1
}


############################################################
# Parsing command line options
#

existing_port=true
ignore_cvs=true
plist_checks=true
portsdir=
rootrun=false
debugging=false

ignore_list=; unset ignore_list[0]

while getopts "AdNPp:x:" OPT; do
	case $OPT in
	A)
		$existing_port || usage
		if ! $rootrun; then
			ignore_list[${#ignore_list[@]}]=.cvsignore
			ignore_list[${#ignore_list[@]}]=.fslckout
			ignore_list[${#ignore_list[@]}]=.git
			ignore_list[${#ignore_list[@]}]=.gitignore
			ignore_list[${#ignore_list[@]}]=.hg
			ignore_list[${#ignore_list[@]}]=.hgignore
			ignore_list[${#ignore_list[@]}]=.svn
			ignore_list[${#ignore_list[@]}]=FINISHED
			ignore_list[${#ignore_list[@]}]=INDEX
			ignore_list[${#ignore_list[@]}]=README
			ignore_list[${#ignore_list[@]}]=README.md
			ignore_list[${#ignore_list[@]}]=bulk
			ignore_list[${#ignore_list[@]}]=distfiles
			ignore_list[${#ignore_list[@]}]=infrastructure
			ignore_list[${#ignore_list[@]}]=lost+found
			ignore_list[${#ignore_list[@]}]=mystuff
			ignore_list[${#ignore_list[@]}]=openbsd-wip
			ignore_list[${#ignore_list[@]}]=packages
			ignore_list[${#ignore_list[@]}]=plist
			ignore_list[${#ignore_list[@]}]=pobj
			ignore_list[${#ignore_list[@]}]=tests
			ignore_list[${#ignore_list[@]}]=update
		fi
		rootrun=true
		;;

	d)
		debugging=true
		;;

	N)
		$rootrun && usage
		existing_port=false
		ignore_cvs=false
		;;

	P)
		plist_checks=false
		;;

	p)
		portsdir=$OPTARG
		;;

	x)
		set -A ignore_list -- "${ignore_list[@]}" "$OPTARG"
		;;

	*)
		usage
		;;
	esac
done

if ! $rootrun && [[ -n $portsdir && ${PWD##"$portsdir"} == "$PWD" ]]; then
	cat >&2 <<EOE
${0##*/}: current directory does not seem to be under the
specified root directory: $portsdir.
EOE
	exit 3
fi

shift $(($OPTIND - 1))
(($# > 0)) && ! $rootrun && usage
(($# == 0)) && set -- .

############################################################
# Detect path to root of directory tree of current port(s) and put it
# in $portsdir, unless it was set by user above. As a last resort, we
# use some heuristics based on the commonly used names.
#
# We also have a $pkgpath variable, that represents subdirectory under
# root ports directory where the port(s) will be imported. In case we
# use heuristics for determining $portsdir, we'll set up $pkgpath, too,
# since we would get this info anyway.
#
# In make_args we write PORTSDIR_PATH override, that allows us to run
# even in ports directory that is not on the PORTSDIR_PATH. This is
# useful, for example, when you check your port on cvs.openbsd.org,
# where you cannot just override mk.conf.
#

pkgpath=

if [[ -z $portsdir ]]; then
	IFS=:
	testp=/usr/ports/devel/quirks
	set -A portsdir_path -- \
		$( (cd $testp && make show=PORTSDIR_PATH 2>/dev/null) || true)
	unset IFS
	if ((${#portsdir_path[@]} > 0)); then
		for p in "${portsdir_path[@]}"; do
			if [[ -z $portsdir && ${PWD#"$p"} != "$PWD" ]]; then
				portsdir=$p
			elif [[ -n $portsdir && ${PWD#"$p"} != "$PWD" &&
			     $p > $portsdir ]]; then
				portsdir=$p
			fi
		done
	fi
fi

if [[ -z $portsdir ]]; then
	# heuristics mode ON
	pkgpath=${PWD##*/ports/*(mystuff/|openbsd-wip/)}
	portsdir=${PWD%"/$pkgpath"}
fi

if [[ -z $portsdir ]]; then
	cat >&2 <<EOE
${0##*/}: could not detect root ports directory. Please provide
one with -p option.
EOE
	exit 2
fi

# This way we can run all checks even on cvs.openbsd.org
set -A make_args -- MASTER_SITE_OPENBSD= \
	PORTSDIR_PATH="$portsdir:$(cd /usr/ports && make -V PORTSDIR_PATH || true)"

if $rootrun; then
	cd -- "$portsdir"
	echo "scanning ports under the $portsdir" >&2
fi

############################################################
# Check and fail routines
#

error=false

err() {
	local prefix=
	while (($# > 0)); do
		printf "$prefix%s" "$1" >&2
		prefix=" "
		shift
	done
	echo >&2
	error=true
}

err_duplicated() {
	err "both $2 and some of its parents has $1"
}

err_coredump_found() {
	err "core dump file found: $1"
}

has_subdirs_only() {
	$debugging && echo "CALLED: has_subdirs_only($*)" >&2

	local dir=$1; shift
	ls -A "$dir" | {
		local has_files=false has_dirs=false
		while read F; do
			$ignore_cvs && [[ $F == CVS ]] && continue
			ignoring "$dir/$F" && continue
			if [[ -d $dir/$F ]]; then
				has_dirs=true
			else
				has_files=true
			fi
		done
		$has_dirs && ! $has_files
	}
}

ignoring() {
	((${#ignore_list[*]} > 0)) || return 1
	local iglob
	for iglob in "${ignore_list[@]}"; do
		[[ ${1#./} == $iglob ]] && return 0
	done
	return 1
}

is_vcs_item() {
	[[ -d "$1" && ${1##*/} == @(CVS|.fslckout|.git|.hg|.svn) ]]
}

handle_extra_file() {
	ignoring "$1" && return 0

	# avoid warning, e.g., about ".*"
	test -e "$1" || return 0

	if is_vcs_item "$1"; then
		if ! $ignore_cvs || [[ ${1##*/} != CVS ]]; then
			err "VCS item detected: $1"
		fi
	elif [[ -f $1 && $1 == *.core ]]; then
		err_coredump_found "$1"
	elif [[ -d $1 ]]; then
		err "extra directory: $1"
	else
		err "extra file: $1"
	fi
}

# Make a path to .py[co] file looks like as if it's in the same dir
# as the corresponding .py file, and has same basename. E.g.:
#   lib/python3.3/__pycache__/Foo/cpython-33.Bar.pyc
# became:
#   lib/python2.7/Foo/Bar.pyc
# which corresponds to:
#   lib/python2.7/Foo/Bar.py
normalize_pyco() {
	local pyco=$1
	[[ $pyco == *.cpython-+([0-9]).py[co] ]] &&
		pyco=${pyco%.cpython-+([0-9]).py[co]}.${pyco##*.}
	[[ $pyco == */__pycache__/* ]] &&
		pyco=${pyco%/__pycache__/*}/${pyco##*/__pycache__/}
	printf "%s" "$pyco"
}

# Print out a ref to the particular subport/subpackage, if needed.
# Port FLAVORs could also be handled, if provided.
# Usage: portref directory [subpackage [flavor all_flavors]]
portref() {
	local dir=$1; shift
	local subpkg= flavor all_flavors=
	if (($# > 0)); then
		subpkg=$1
		shift
	fi
	if (($# > 0)); then
		flavor=$1
		all_flavors=$2
		shift 2
	fi

	local ref=
	if [[ $dir != . ]]; then
		ref="${dir#./}"
		[[ -n $subpkg && $subpkg != "-" ]] && ref="$ref,$subpkg"
	else
		[[ $subpkg != "-" ]] && ref="$subpkg"
	fi

	if [[ -n $all_flavors ]]; then
		[[ -n $ref ]] && ref="$ref, "
		if [[ -z $flavor ]]; then
			ref="${ref}default FLAVOR"
		else
			ref="${ref}FLAVOR \"$flavor\""
		fi
	fi

	[[ -n $ref ]] && echo "in $ref: "
}

# Contains last SUBST_CMD. Filled by check_port_dir(), used
# by check_port_hier() to lazily call the check_pkg_dir().
last_subst_cmd=

# Checks made:
#   * Whitelist filter of what could be in this directory.
check_port_hier() {
	$debugging && echo "CALLED: check_port_hier($*)" >&2

	local distinfo_lives_upper pkg_lives_upper plist_lives_upper
	local dir=$1; shift
	for opt; do
		# looks unsafe but we do not pass anything except
		# "foo=true" and "foo=false" here
		eval "$opt"
	done

	distinfo_lives_upper=${distinfo_lives_upper:-false}
	pkg_lives_upper=${pkg_lives_upper:-false}
	plist_lives_upper=${plist_lives_upper:-false}

	local distinfo_exists=false
	[[ -f $dir/distinfo ]] && distinfo_exists=true
	$distinfo_exists && $distinfo_lives_upper &&
		err_duplicated distinfo "$dir"

	local pkg_exists=false tell_pkg_exists=$pkg_lives_upper
	if [[ -d $dir/pkg ]]; then
		pkg_exists=true
		tell_pkg_exists=true
	fi

	local plist_exists=false
	ls $dir/pkg/PLIST* >/dev/null 2>&1 && plist_exists=true
	$plist_lives_upper && $plist_exists &&
		err_duplicated "packing list(s)" "$dir"

	$distinfo_lives_upper && distinfo_exists=true
	$plist_lives_upper && plist_exists=true

	local recursive_args
	set -A recursive_args -- \
		distinfo_lives_upper=$distinfo_exists \
		pkg_lives_upper=$tell_pkg_exists \
		plist_lives_upper=$plist_exists

	local F
	for F in "$dir"/* "$dir"/.*; do
		F=${F#./}
		ignoring "$F" && continue

		if is_vcs_item "$F"; then
			if ! $ignore_cvs || [[ ${F##*/} != CVS ]]; then
				err "VCS item detected: $F"
			fi
		elif [[ -d $F ]]; then
			case "${F##*/}" in
			files|patches)
				check_${F##*/}_dir "$F"
				;;

			pkg)
				# Do nothing, pkg_exists is already set,
				# and we need to read SUBST_CMD first.
				;;

			patches?(-*))
				check_patches_dir "$F"
				;;

			*)
				if ! ([[ -f $F/Makefile ]] ||
                                      ls $F/*.port.mk >/dev/null 2>&1) &&
				   ! has_subdirs_only "$F"; then
					# Avoid extra spam
					err "not a port directory: $F"
				else
					local pkgpath_set=false
					[[ -n $pkgpath ]] && pkgpath_set=true
					check_port_dir "$F" "${recursive_args[@]}"
					$pkgpath_set || pkgpath=${pkgpath%/*}
				fi
				;;
			esac
		else
			case "${F##*/}" in
			Makefile?(.inc)|*.port.mk)
				check_makefile "$F"
				;;

			distinfo)
				;;

			*)
				handle_extra_file "$F"
				;;
			esac
		fi
	done

	$pkg_exists && check_pkg_dir "$dir"/pkg "$last_subst_cmd"

	$existing_port ||
		egrep -q '^ *SUBDIR[[:space:]]*\+?=' "$dir"/Makefile ||
		err missing subdir Makefile
}

# Checks made:
#   * Whitelist filter of what could be in this directory.
check_port_dir() {
	$debugging && echo "CALLED: check_port_dir($*)" >&2

	local dir=$1; shift
	local distinfo_lives_upper pkg_lives_upper plist_lives_upper
	for opt; do
		# looks unsafe but we do not pass anything except
		# "foo=true" and "foo=false" here
		eval "$opt"
	done

	distinfo_lives_upper=${distinfo_lives_upper:-false}
	pkg_lives_upper=${pkg_lives_upper:-false}
	plist_lives_upper=${plist_lives_upper:-false}
 
	check_perms_in_dir "$dir"

	if [[ -f $dir/Makefile.inc ]] ||
	   egrep -sq '^ *SUBDIR[[:space:]]*\+?=' "$dir"/Makefile ||
	   has_subdirs_only "$dir"; then
		check_port_hier "${dir#./}" "${@:-}"
		return
	fi

	local F
	local distinfo_exists=false
	local mk_exists=false
	local pkg_exists=false
	local plist_exists=false
	local portmk_exists=true
	local non_portmk=0

	for F in "$dir"/* "$dir"/.*; do
		F=${F#./}
		ignoring "$F" && continue
		case ${F##*/} in
		Makefile)
			test -f "$F" || err "$F is not a file"
			check_makefile "$F"
			mk_exists=true
			((++non_portmk))
			;;

		distinfo)
			$distinfo_lives_upper && err_duplicated distinfo "$dir"
			distinfo_exists=true
			test -f "$F" || err "$F is not a file"
			((++non_portmk))
			;;

		*.port.mk)
			test -f "$F" || err "$F is not a file"
			check_makefile "$F"
			portmk_exists=true
			;;

		systrace.filter)
			test -f "$F" || err "$F is not a file"
			((++non_portmk))
			;;

		files|patches)
			if [[ -d $F ]]; then
				check_${F##*/}_dir "$F"
			else
				err "$F" is not a directory
			fi
			((++non_portmk))
			;;

		pkg)
			if [[ -d $F ]]; then
				pkg_exists=true
				# Actual check to be done later, we need to gather
				# additional info through "make show=" call.
				ls "$F"/PLIST* >/dev/null 2>&1 &&
					plist_exists=true
				$plist_lives_upper && $plist_exists &&
					err_duplicated "packing list(s)" "$dir"
			else
				err "$F" is not a directory
			fi
			((++non_portmk))
			;;

		*)
			handle_extra_file "$F"
			;;
		esac
	done

	# examples: lang/clang, www/mozilla
	$portmk_exists && ((non_portmk == 0)) && return

	$mk_exists || err no Makefile in "$dir"
	$pkg_exists || $pkg_lives_upper || err "no pkg/ in $dir"
	$distinfo_lives_upper && distinfo_exists=true
	$distinfo_exists || $existing_port || err "no distinfo in $dir"

	# Now gather and check some info via "make show=...".
	# We request all info at once for speed.

	local dist_subdir distfiles flavor flavors master_sites
	local multi_packages pkgpath_this pseudo_flavor pseudo_flavors
	local shared_libs subst_cmd
	local perm_pkg_cdrom perm_pkg_ftp perm_dist_ftp
	local show_items="DIST_SUBDIR DISTFILES FLAVOR FLAVORS"
	local show_items="$show_items MASTER_SITES MULTI_PACKAGES PKGPATH"
	local show_items="$show_items PSEUDO_FLAVOR PSEUDO_FLAVORS"
	local show_items="$show_items SHARED_LIBS SUBST_CMD"
	local show_items="$show_items PERMIT_PACKAGE_CDROM PERMIT_PACKAGE_FTP"
	local show_items="$show_items PERMIT_DISTFILES_FTP"
	local read_ok=false

	local read_failed=false
	(cd -- "$dir"; make "${make_args[@]}" show="$show_items" || true) </dev/null |&
	read -pr dist_subdir &&
	read -pr distfiles &&
	read -pr flavor &&
	read -pr flavors &&
	read -pr master_sites &&
	read -pr multi_packages &&
	read -pr pkgpath_this &&
	read -pr pseudo_flavor &&
	read -pr pseudo_flavors &&
	read -pr shared_libs &&
	read -pr subst_cmd &&
	read -pr perm_pkg_cdrom &&
	read -pr perm_pkg_ftp &&
	read -pr perm_dist_ftp &&
	read_ok=true
	if $read_ok; then
		exec 3<&p
		exec 3<&-
		wait
	else
		error=true
		return
	fi

	pseudo_flavor=$(echo "$pseudo_flavor" | sed -e 's/,/ /g')
	pseudo_flavor=${pseudo_flavor##" "}

	local f pf found

	local check_flavors=
	[[ $flavor != "$pseudo_flavor" ]] && unset check_flavors[0]

	for f in $flavors; do
		for pf in $pseudo_flavors; do
			[[ $f == "$pf" ]] && continue 2
		done
		[[ $f == debug ]] && continue     # XXX
		check_flavors[${#check_flavors[@]}]=$f
	done

	check_distfiles "$dir" "$dist_subdir" $distfiles
	check_master_sites "$dir" $master_sites
	check_permit_dist "$dir" "$perm_pkg_cdrom" "$perm_pkg_ftp" \
	    "$perm_dist_ftp"
	$pkg_exists && check_pkg_dir "$dir"/pkg "$subst_cmd"
	$existing_port || check_shlibs_versions "$dir" $shared_libs

	for _s in $multi_packages; do
		sub_checks "$dir" "$_s" "${check_flavors[@]}"
	done

	pkgpath=${pkgpath:-"$pkgpath_this"}
	last_subst_cmd="$subst_cmd"
}

# Checks made: obvious
check_trailing_whitespace() {
	egrep -q '[[:space:]]+$' "$1" &&
		err "trailing whitespace in $1"
}

# Checks made: obvious
check_newline_at_eof() {
	(( $(tail -1 -- "$1" | wc -l) == 0)) &&
		err "no newline at EOF in $1"
}

# Checks made:
#   * Every library in SHARED_LIBS has 0.0 version.
check_shlibs_versions() {
	$debugging && echo "CALLED: check_shlibs_versions($*)" >&2

	local dir=$1; shift
	local lib
	local libver
	local portref=$(portref "$dir")

	while (($# > 1)); do
		lib=$1
		libver=$2
		if [[ $libver != 0.0 ]]; then
			err "${portref}the $lib shared library has" \
			    "version $libver instead of 0.0"
		fi
		shift 2
	done
}

# Checks made:
#   * Distfiles with useless names go into DIST_SUBDIR or have {url} suffix.
check_distfiles() {
	$debugging && echo "CALLED: check_distfiles($*)" >&2

	local dir=$1; shift
	local dist_subdir=$1; shift
	local portref=$(portref "$dir")

	# do not care about absent distfiles, this is fine for meta ports
	while (($# > 1)); do
		# try to catch "version-only" names, but not anything more
		if [[ $1 == ?(v)?(.)+([0-9])?(.+([0-9]))*(.+([a-z])) &&
		      -z $dist_subdir && $1 != *\{*\} ]]; then
			err "${portref}badly named distfile $1 without" \
			    "DIST_SUBDIR or {url} postfix"
		fi
		shift
	done
}

# Checks made:
#   * No unreliable (without fixed distfiles) hosting listed in MASTER_SITES.
check_master_sites() {
	$debugging && echo "CALLED: check_master_sites($*)" >&2

	local dir=$1; shift
	local portref=$(portref "$dir")
	local name

	while (($# > 1)); do
		case "$1" in
		http?(s)://bitbucket.com/*)	name=BitBucket;;
		http?(s)://gitorious.com/*)	name=Gitorious;;
		*)				name=;;
		esac
		[[ -n $name ]] && err "$portref$name does not hold real" \
			"releases, please host the distfiles somewhere" \
			"else or ask someone to do this for you"
		shift
	done
}

# Run checks that are FLAVOR/SUBPACKAGE-dependent.
sub_checks() {
	$debugging && echo "CALLED: sub_checks($*)" >&2

	local dir=$1; shift
	local subpkg=$1; shift
	local flavor
	for flavor in "$@"; do
		# avoid extra noise
		[[ ${flavor#no_} != ${flavor} &&
		   ${subpkg#-} == ${flavor#no_} ]] &&
		   continue

		(
			cd -- "$dir"
			portref=$(portref "$dir" "$subpkg" "$flavor" "$*")
			export SUBPACKAGE="$subpkg" FLAVOR="$flavor"

			local wantlib_var=WANTLIB${subpkg%-}
			local vars="COMMENT$subpkg FULLPKGNAME$subpkg"
			vars="$vars MODULES"
			vars="$vars PKG_ARCH$subpkg $wantlib_var"
			vars="$vars PERMIT_PACKAGE_CDROM${subpkg%-}"
			vars="$vars PERMIT_PACKAGE_FTP${subpkg%-}"
			make "${make_args[@]}" show="$vars" | {
				local comment fullpkgname modules pkg_arch
				local wantlib perm_pkg_cdrom perm_pkg_ftp
				read -r comment
				read -r fullpkgname
				read -r modules
				read -r pkg_arch
				read -r wantlib
				read -r perm_pkg_cdrom
				read -r perm_pkg_ftp

				if [[ $comment == @(a|an|the)" "* ]]; then
					err "${portref}no leading articles in" \
					    "COMMENT${subpkg%-}, please"
				fi

				if [[ $pkg_arch == "*" && -n $wantlib ]]; then
					err "${portref}non-empty $wantlib_var for" \
					    "arch-independent package"
				fi

				check_wantlib "$portref" "$modules" $wantlib ||
				    error=true
				check_permit_subpkg "$portref" "$subpkg" \
				    "$perm_pkg_cdrom" "$perm_pkg_ftp" ||
				    error=true

				if $plist_checks; then
					(make "${make_args[@]}" \
					    print-plist-with-depends || true) \
					    </dev/null |&
					check_plist "$portref" "$fullpkgname" \
					    "$flavor" "${subpkg%-}" "$modules"
					wait
				fi

				! $error
			} || error=true

			! $error
		) || error=true
	done
	wait
}

# Checks made:
#   * If package installs system-wide icons, it should have the
#     x11/gtk+2,-guic dependency and @exec/@unexec-delete with
#     %D/bin/gtk-update-icon-cache -q -t %D/share/icons/$theme
#     for each icon theme used in package. If there is an
#     index.theme provided, then, instead of gtk-update-icon-cache,
#     @unexec-delete should contain the following command:
#     rm -f %D/share/icons/$theme/icon-theme.cache
#
#   * If package adds a MIME type handler, it should have the
#     devel/desktop-file-utils dependency and @exec/@unexec-delete with
#     %D/bin/update-desktop-database . Unfortunately, it's hard to tell
#     if there is a MIME type handler in .desktop file, so we just
#     trigger if any .desktop files are added to
#     ${PREFIX}/share/applications/ .
#
#   * If package adds a MIME types package, it should have the
#     misc/shared-mime-info dependency and @exec/@unexec-delete with
#     %D/bin/update-mime-database %D/share/mime
#
#   * If package adds a GLib schema, it should have @exec/@unexec-delete
#     with %D/bin/glib-compile-schemas %D/share/glib-2.0/schemas >/dev/null
#     and "devel/dconf" in MODULES (or at least RDEP on devel/dconf).
#
#   * If package installs .mo files under ${PREFIX}/share/locale/, then
#     run-time dependency on devel/gettext should exists.
#
#   * Each .py should have corresponding .pyc files, to avoid
#     generation of the latter at run-time.
#
#   * Manual (man and info) pages should go under ${PREFIX}/{man,info},
#     not under ${PREFIx}/share/{man,info}.
check_plist() {
	$debugging && echo "CALLED: check_plist($*)" >&2

	local portref=$1; shift
	local fullpkgname=$1; shift
	local flavor_list=$1; shift
	local subpkg=$1; shift
	local modules_list=$1; shift

	local flavor is_static=false
	for flavor in $flavor_list; do
		[[ $flavor == static ]] && is_static=true
	done

	local guic_dep=false
	local guic_dep_needed=false
	local guic_exec_cnt=0
	local guic_unexec_cnt=0

	local mime_dep=false
	local mime_dep_needed=false
	local mime_exec_cnt=0
	local mime_unexec_cnt=0

	local mimepkg_dep=false
	local mimepkg_dep_needed=false
	local mimepkg_exec_cnt=0
	local mimepkg_unexec_cnt=0

	local dconf_module
	if [[ $modules_list == ?(* )devel/dconf?( *) ]]; then
		dconf_module=true
	else
		dconf_module=false
	fi
	local dconf_dep=false
	local dconf_dep_needed=false
	local dconf_exec_cnt=0
	local dconf_unexec_cnt=0

	# Lists of icon themes discovered through reading
	# @file, @exec and @unexec lines, accordingly.
	local icon_themes= exec_icon_themes= unexec_icon_themes=

	# List of icon themes that remove cache file
	local rm_cache_themes=

	local gettext_dep=false
	local translation_found=false

	# Lists of .py, .pyc and .pyo items found, accordingly
	local py_files=   pyc_files=   pyo_files=
	unset py_files[0] pyc_files[0] pyo_files[0]

	local wrong_man=false wrong_info=false

	# Temporary ones
	local app l theme varname py

	while read -pr l; do
		case "$l" in
		"@comment "*)
			# ignore
			;;
		share/icons/*/*/*|share/icons/*/@(index.theme|iconrc?(-png)|default.kde4))
			# Themes have at least two levels in depth.
			#
			# We match directories by purpose, this helps to catch
			# update-plist fuckups, when directories go into one
			# package and actual icons go in another.
			guic_dep_needed=true
			theme=${l#share/icons/}
			theme=${theme%%/*}
			# wrap with the '/' characters to avoid erroneous matching
			echo "$icon_themes" | fgrep -q "/$theme/" ||
				icon_themes="$icon_themes /$theme/"
			if [[ "$l" = "share/icons/$theme/index.theme" ]]; then
				echo "$rm_cache_themes" | fgrep -q "/$theme/" ||
					err "${portref}missing @unexec-delete rm -f" \
					    "%D/share/icons/$theme/icon-theme.cache"
			fi
			;;
		share/icons/*(*/))
			# Do not match intermediate directories to avoid false
			# positives.
			;;
		share/icons/*.xpm)
			app=${l#share/icons/}
			app=${app%%/*}
			app=${app%%.*}
			err "${portref}installs icon ${l##*/} in ${l%/*}, it" \
			    "should likely go in share/pixmaps/ instead"
			;;
		share/icons/*)
			app=${l#share/icons/}
			app=${app%%/*}
			app=${app%%.*}
			err "${portref}installs icon ${l##*/} in ${l%/*}, it" \
			    "should go in share/$app/icons/ or like instead"
			;;
		"@depend x11/gtk+2,-guic"*)
			guic_dep=true
			;;
		"@exec %D/bin/gtk-update-icon-cache -q -t %D/share/icons/"*)
			theme=${l##*/}
			varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')
			((++guic_exec_cnt))
			eval "((++guic_exec_cnt_$varname))"
			exec_icon_themes="$exec_icon_themes /$theme/"
			;;
		"@unexec-delete %D/bin/gtk-update-icon-cache -q -t %D/share/icons/"*)
			theme=${l##*/}
			varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')
			((++guic_unexec_cnt))
			eval "((++guic_unexec_cnt_$varname))"
			unexec_icon_themes="$unexec_icon_themes /$theme/"
			;;
		"@unexec-delete rm -f "%D/share/icons/*/icon-theme.cache)
			# as an alternative, port could zap the theme entirely
			theme=${l#*/icons/}
			theme=${theme%/icon-theme.cache}
			varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')
			((++guic_unexec_cnt))
			eval "((++guic_unexec_cnt_$varname))"
			unexec_icon_themes="$unexec_icon_themes /$theme/"
			rm_cache_themes="$rm_cache_themes /$theme/"
			if echo "$icon_themes" | fgrep -q "/$theme/"; then
				err "${portref}the @unexec-delete line removing" \
				    "%D/share/icons/$theme/icon-theme.cache" \
				    "does not preceed all of the icon theme" \
				    "$theme files"
			fi
			;;
		@?(un)exec?(-delete|-update)" %D/bin/gtk-update-icon-cache"*)
			err "${portref}incorrect gtk-update-icon-cache" \
			    "invocation: ${l#@* }"
			;;

		share/applications/*(*/)*.desktop)
			mime_dep_needed=true
			;;
		"@depend devel/desktop-file-utils"*)
			mime_dep=true
			;;
		"@exec %D/bin/update-desktop-database")
			((++mime_exec_cnt))
			;;
		"@unexec-delete %D/bin/update-desktop-database")
			((++mime_unexec_cnt))
			;;
		@?(un)exec?(-delete|-update)" %D/bin/update-desktop-database"*)
			err "${portref}incorrect update-desktop-database" \
			    "invocation: ${l#@* }"
			;;

		share/mime/packages/*.xml)
			mimepkg_dep_needed=true
			;;
		"@depend misc/shared-mime-info"*)
			mimepkg_dep=true
			;;
		"@exec %D/bin/update-mime-database %D/share/mime")
			((++mimepkg_exec_cnt))
			;;
		"@unexec-delete %D/bin/update-mime-database %D/share/mime")
			((++mimepkg_unexec_cnt))
			;;
		@?(un)exec?(-delete|-update)" %D/bin/update-mime-database"*)
			err "${portref}incorrect update-mime-database" \
			    "invocation: ${l#@* }"
			;;

		share/glib-2.0/schemas/*.xml)
			dconf_dep_needed=true
			;;
		"@depend devel/dconf"*)
			dconf_dep=true
			;;
		"@exec %D/bin/glib-compile-schemas %D/share/glib-2.0/schemas >/dev/null")
			((++dconf_exec_cnt))
			;;
		"@unexec-delete %D/bin/glib-compile-schemas %D/share/glib-2.0/schemas >/dev/null")
			((++dconf_unexec_cnt))
			;;
		@?(un)exec?(-delete|-update)" %D/bin/glib-compile-schemas"*)
			err "${portref}incorrect glib-compile-schemas" \
			    "invocation: ${l#@* }"
			;;

		"@depend devel/gettext"*)
			gettext_dep=true
			;;
		share/locale/*/*/*.mo)
			translation_found=true
			;;

		# XXX KSH arrays are limited to 10239 items
		share/@(doc|*(*/)examples)+(/*).py|?(s)bin/*.py)
			# ignore
			;;
		*.py)
			py_files[${#py_files[@]}]=$l
			;;
		*.pyc)
			pyc_files[${#pyc_files[@]}]=$(normalize_pyco "$l")
			;;
		*.pyo)
			pyo_files[${#pyo_files[@]}]=$(normalize_pyco "$l")
			;;

		share/man/*)
			wrong_man=true
			;;
		share/info/*)
			wrong_info=true
			;;
		esac
	done

	# gtk-update-icon-cache
	$guic_dep_needed && ! $guic_dep &&
	    [[ $fullpkgname != gtk-update-icon-cache-* ]] &&
		err "${portref}missing RDEP on x11/gtk+2,-guic"
	local cnt
	for theme in $icon_themes; do
		theme=${theme#/}
		theme=${theme%/}

		varname=$(echo "$theme" | sed -e 's/[^a-zA-Z_]/_/g')

		((guic_exec_cnt--)) || true
		((guic_unexec_cnt--)) || true
		eval "((guic_exec_cnt_$varname--)) || true"
		eval "((guic_unexec_cnt_$varname--)) || true"

		eval "cnt=\$guic_exec_cnt_$varname"
		if (($cnt > 0)); then
			err "${portref}extra @exec of gtk-update-icon-cache" \
			    "for icon theme $theme"
			((guic_exec_cnt--)) || true
		elif (($cnt < 0)); then 
			err "${portref}missing @exec of gtk-update-icon-cache" \
			    "for icon theme $theme"
		fi

		eval "cnt=\$guic_unexec_cnt_$varname"
		if (($cnt > 0)); then
			err "${portref}extra @unexec-delete of gtk-update-icon-cache" \
			    "for icon theme $theme"
			((guic_unexec_cnt--)) || true
		elif (($cnt < 0)); then 
			err "${portref}missing @unexec-delete of gtk-update-icon-cache" \
			    "for icon theme $theme"
		fi
	done

	for theme in $exec_icon_themes; do
		theme=${theme#/}
		theme=${theme%/}
		echo "$icon_themes" | fgrep -q "/$theme/" ||
			err "${portref}doing @exec of gtk-update-icon-cache" \
			    "for absent icon theme $theme"
	done

	for theme in $unexec_icon_themes; do
		theme=${theme#/}
		theme=${theme%/}
		echo "$icon_themes" | fgrep -q "/$theme/" ||
			err "${portref}doing @unexec-delete of gtk-update-icon-cache" \
			    "for absent icon theme $theme"
	done

	((guic_exec_cnt > 0)) &&
		err "${portref}extra @exec of gtk-update-icon-cache"
	((guic_unexec_cnt > 0)) &&
		err "${portref}extra @unexec-delete of gtk-update-icon-cache"

	# desktop-file-utils (simplier than previous, isn't it?)
	$mime_dep_needed && ! $mime_dep &&
	    [[ $fullpkgname != desktop-file-utils-* ]] &&
		err "${portref}missing RDEP on devel/desktop-file-utils"
	if $mime_dep_needed; then
		((mime_exec_cnt--)) || true
		((mime_unexec_cnt--)) || true
	fi
	if ((mime_exec_cnt > 0)) &&
	    [[ $fullpkgname != desktop-file-utils-* ]]; then
		err "${portref}extra @exec of update-desktop-database"
	elif ((mime_exec_cnt < 0)); then
		err "${portref}missing @exec of update-desktop-database"
	fi
	if ((mime_unexec_cnt > 0)); then
		err "${portref}extra @unexec-delete of update-desktop-database"
	elif ((mime_unexec_cnt < 0)); then
		err "${portref}missing @unexec-delete of update-desktop-database"
	fi

	# update-mime-database (same as previous)
	$mimepkg_dep_needed && ! $mimepkg_dep &&
	    [[ $fullpkgname != shared-mime-info-* ]] &&
		err "${portref}missing RDEP on misc/shared-mime-info"
	if $mimepkg_dep_needed; then
		((mimepkg_exec_cnt--)) || true
		((mimepkg_unexec_cnt--)) || true
	fi
	if ((mimepkg_exec_cnt > 0)) &&
	    [[ $fullpkgname != shared-mime-info-* ]]; then
		err "${portref}extra @exec of update-mime-database"
	elif ((mimepkg_exec_cnt < 0)); then
		err "${portref}missing @exec of update-mime-database"
	fi
	if ((mimepkg_unexec_cnt > 0)); then
		err "${portref}extra @unexec-delete of update-mime-database"
	elif ((mimepkg_unexec_cnt < 0)); then
		err "${portref}missing @unexec-delete of update-mime-database"
	fi

	# glib-compile-schemas (almost same as previous)
	#
	# TODO: detect situation of extra devel/dconf in MODULES
	# (requires investigation of all subpackages).
	if $dconf_dep_needed; then
		if ! $dconf_module; then
			err "${portref}GLib2 XML schemas found without" \
			    "devel/dconf in MODULES"
		elif ! $dconf_dep; then
			err "${portref}missing" \
			    "RUN_DEPENDS${subpkg}+=\${MODDCONF_RUN_DEPENDS}"
		fi
	fi

	if $dconf_dep_needed; then
		((dconf_exec_cnt--)) || true
		((dconf_unexec_cnt--)) || true
	fi
	if ((dconf_exec_cnt > 0)) &&
	    [[ $fullpkgname != glib2-* ]]; then
		err "${portref}extra @exec of glib-compile-schemas"
	elif ((dconf_exec_cnt < 0)); then
		err "${portref}missing @exec of glib-compile-schemas"
	fi
	if ((dconf_unexec_cnt > 0)); then
		err "${portref}extra @unexec-delete of glib-compile-schemas"
	elif ((dconf_unexec_cnt < 0)); then
		err "${portref}missing @unexec-delete of glib-compile-schemas"
	fi

	# gettext
	$translation_found && ! $gettext_dep && ! $is_static &&
		[[ $fullpkgname != gettext-* ]] &&
		err "${portref}translation file(s) found without" \
		    "devel/gettext dependency in MODULES or RUN_DEPENDS"

	# Python modules
	((${#py_files[@]} > 0))  && set -sA py_files  -- "${py_files[@]}"
	((${#pyc_files[@]} > 0)) && set -sA pyc_files -- "${pyc_files[@]}"
	((${#pyo_files[@]} > 0)) && set -sA pyo_files -- "${pyo_files[@]}"
	local ic=0 io=0
	if ((${#py_files[@]} > 0)); then for py in "${py_files[@]}"; do
		while [[ $ic -lt ${#pyc_files[@]} ]]; do
			[[ ${pyc_files[$ic]} < "$py"c ]] || break
			# allowed behaviour
			#err "${portref}compiled Python module without" \
			#    "source, expected: ${pyc_files[$ic]%c}"
			((++ic))
		done
		if [[ $ic -lt ${#pyc_files[@]} &&
		      ${pyc_files[$ic]} == "$py"c ]]; then
			((++ic))
		else
			err "${portref}Python module without" \
			    "compiled version, consider using" \
			    "\${MODPY_BIN} \${MODPY_LIBDIR}/compileall.py: $py"
		fi

		while [[ $io -lt ${#pyo_files[@]} ]]; do
			[[ ${pyo_files[$io]} < "$py"o ]] || break
			# allowed behaviour
			#err "${portref}optimized Python module without" \
			#    "source, expected: ${pyo_files[$io]%o}"
			((++io))
		done
		if [[ $io -lt ${#pyo_files[@]} &&
		      ${pyo_files[$io]} == "$py"o ]]; then
			((++io))
		# too much noise, maybe enable in the future
		#else
		#	err "${portref}Python module without" \
		#	    "optimized version: $py"
		fi
	done; fi

	# allowed behaviour
	#while (($ic < ${#pyc_files[@]})); do
	#	err "${portref}compiled Python module without source," \
	#	    "expected: ${pyc_files[$ic]%c}"
	#	((++ic))
	#done

	# allowed behaviour
	#while (($io < ${#pyo_files[@]})); do
	#	err "${portref}optimized Python module without source," \
	#	    "expected: ${pyo_files[$io]%o}"
	#	((++io))
	#done

	$wrong_man && err "${portref}manual pages should go under" \
	    "\${PREFIX}/man/ rather than under \${PREFIX}/share/man/"
	$wrong_info && err "${portref}info pages should go under" \
	    "\${PREFIX}/info/ rather than under \${PREFIX}/share/info/"
}

# Checks made:
#   * devel/gettext and converters/libiconv MODULES are not forgotten.
#   * lib/qt[34]/, lib/kde/ and lib/kde4/ prefixes not missing where
#     applicable.
#   * stdc++ doesn't get into WANTLIB when gcc4.port.mk is used.
check_wantlib() {
	local portref="$1"; shift
	local modules="$1"; shift

	local iconv_wantlib=false
	local intl_wantlib=false
	local phonon_s_wantlib=false

	local gettext_module=false
	local iconv_module=false
	local qt3_module=false
	local qt4_module=false
	local kde3_module=false
	local kde4_module=false
	local phonon_module=false
	local gcc4_module=false

	local v

	for v in $modules; do case $v in
		converters/libiconv)	iconv_module=true;;
		devel/gettext)		gettext_module=true;;
		gcc4)			gcc4_module=true;;
		multimedia/phonon)	phonon_module=true;;
		x11/qt3)		qt3_module=true;;
		x11/qt4)		qt4_module=true;;
		x11/kde)		kde3_module=true;;
		x11/kde4)		kde4_module=true;;
	esac; done

	for v; do case $v in
		iconv?(?(">")=+([0-9])))
			iconv_wantlib=true
			;;

		intl?(?(">")=+([0-9])))
			intl_wantlib=true
			;;

		phonon_s?(?(">")=+([0-9])))
			phonon_s_wantlib=true
			;;

		@(Qt+([A-Za-z0-9])|phonon)?(?('>')=+([0-9])))
			err "$portref$v instead of lib/qt4/$v" \
			    "in WANTLIB"
			;;

		@(qt-mt|qui|qui-mt)?(?('>')=+([0-9])))
			err "$portref$v instead of lib/qt3/$v" \
			    "in WANTLIB"
			;;

		@(smbclient|wbclient)?(?('>')=+([0-9])))
			err "$portref$v instead of lib/samba/$v" \
			    "in WANTLIB"
			;;

		@(DCOP|soundserver_idl|vcard)?(?('>')=+([0-9])))
			err "$portref$v instead of \${KDE}/$v" \
			    "in WANTLIB (check other libs, too!)"
			;;

		@(kdecore|kdeui|kio)?(?('>')=+([0-9])))
			if $kde4_module; then
				err "$portref$v instead of \${KDE4LIB}/$v" \
				    "in WANTLIB (check other libs, too!)"
			elif $kde3_module; then 
				err "$portref$v instead of \${KDE}/$v" \
				    "in WANTLIB (check other libs, too!)"
			else
				err "$portref$v WANTLIB without x11/kde*" \
				    "in MODULES (check other libs, too!)"
			fi
			;;
		stdc++?(?('>')=+([0-9])))
			if $gcc4_module; then
				err "$portref$v in WANTLIB when gcc4 is" \
				    "in MODULES; run port-lib-depends-check" \
				    "and if stdc++ is still there, check" \
				    "actual build thoroughly, it's broken"
			fi
	esac; done

	if $intl_wantlib && ! $gettext_module; then
		err "${portref}missing devel/gettext in MODULES"
	elif $iconv_wantlib && ! $gettext_module && ! $iconv_module; then
		err "${portref}missing converters/libiconv in MODULES"
	fi

	if $phonon_s_wantlib && ! $phonon_module; then
		err "${portref}missing multimedia/phonon in MODULES"
	fi

	! $error
}

# Checks made:
#   * No extra PERMIT_DISTFILES_FTP variables in Makefile.
#   * PERMIT_DISTFILES_FTP should not contain just "No" but a reason.
#
# Runs in the port directory.
# XXX does not handle Makefile.inc and other .include cases correctly.
check_permit_dist() {
	$debugging && echo "CALLED: check_permit_dist($*)" >&2

	local portref=$(portref $1); shift
	local perm_pkg_cdrom=$(echo "$1" | tr '[:upper:]' '[:lower:]')
	local perm_pkg_ftp=$(echo "$2" | tr '[:upper:]' '[:lower:]')
	local perm_dist_ftp=$(echo "$3" | tr '[:upper:]' '[:lower:]')

	if [[ ($perm_pkg_cdrom == yes || $perm_pkg_ftp == yes) && \
	    $perm_dist_ftp == yes ]]; then
		egrep -q "^ *PERMIT_DISTFILES_FTP[[:space:]]*=" Makefile &&
			err "${portref}extra PERMIT_DISTFILES_FTP line(-s)"
	fi

	if [[ $perm_dist_ftp == no ]]; then
		err "${portref}PERMIT_DISTFILES_FTP should be either" \
		    "\"Yes\" or a reason for being non-redistributable"
	fi

	true
}

# Checks made:
#   * No extra PERMIT_PACKAGE_FTP variables in Makefile.
#   * PERMIT_PACKAGE_* should not contain just "No" but a reason.
#
# Runs in the port directory.
# XXX does not handle Makefile.inc and other .include cases correctly.
check_permit_subpkg() {
	$debugging && echo "CALLED: check_permit_subpkg($*)" >&2

	local portref=$1; shift
	local subpkg=${1%-}; shift
	local perm_pkg_cdrom=$(echo "$1" | tr '[:upper:]' '[:lower:]')
	local perm_pkg_ftp=$(echo "$2" | tr '[:upper:]' '[:lower:]')

	if [[ $perm_pkg_cdrom == yes && $perm_pkg_ftp == yes ]]; then
		egrep -q "^ *PERMIT_PACKAGE_FTP${subpkg}[[:space:]]*=" Makefile &&
			err "${portref}extra PERMIT_PACKAGE_FTP lines"
	fi

	if [[ $perm_pkg_cdrom == no ]]; then
		err "${portref} PERMIT_PACKAGE_CDROM should be either" \
		    "\"Yes\" or a reason for being non-redistributable"
	fi
	if [[ $perm_pkg_ftp == no ]]; then
		err "${portref} PERMIT_PACKAGE_FTP should be either" \
		    "\"Yes\" or a reason for being non-redistributable"
	fi

	true
}

# Checks made:
#   * Directory is not empty
#   * No '*.core' files present
check_files_dir() {
	$debugging && echo "CALLED: check_files_dir($*)" >&2

	find -f "$1" -- -type f | {
		local empty=true
		local mode
		while read F; do
			ignoring "$F" && continue
			mode=$(stat -f %p "$F" || true)
			(( (0$mode & 0111) != 0 )) &&
				err "executable file: $F"
			empty=false
			[[ $F == *.core ]] &&
				err_coredump_found "$F"
		done
		$empty && err "there are no files, please remove the $1 directory"
		! $error
	} || error=true
}

# Checks made:
#   * Each patch contains OpenBSD RCS tag.
#   * Directory is not empty and consists only of plain files starting
#     with 'patch-' and not ending with '.orig'.
check_patches_dir() {
	$debugging && echo "CALLED: check_patches_dir($*)" >&2

	local empty=true
	local F

	check_perms_in_dir "$1"

	for F in "$1"/* "$1"/.*; do case "${F##*/}" in
	patch-*.orig)
		handle_extra_file "$F"
		;;

	patch-*)
		empty=false
		test -f "$F" ||
			err "$F is not a file"
		$rootrun || head -n 1 -- "$F" | egrep -q '^\$OpenBSD.*\$$' ||
			err "$F does not have \$OpenBSD\$ RCS tag at the top"
		;;

	*)
		handle_extra_file "$F"
		;;
	esac; done

	$empty && err "there are no patches, please remove the $1 directory instead"
}

# Checks made:
#   * Directory is not empty and consist only of plain files with fixed names.
#   * PFRAG, PLIST, README and .rc files contain appropriate OpenBSD RCS
#     tags; other files should NOT contain OpenBSD RCS tag.
#   * PFRAG.shared should be merged in PLIST if it contains @lib items only.
#   * No trailing whitespace for DESCR, MESSAGE, README, UNMESSAGE and
#     .rc files (PLIST and PFRAG are better checked with "make package").
#   * See also check_plist_file().
check_pkg_dir() {
	$debugging && echo "CALLED: check_pkg_dir($*)" >&2

	local dir=$1; shift
	local subst_cmd
	if (($# > 0)); then
		# XXX should find the way to always obtain SUBST_CMD
		subst_cmd=$1
		shift
	fi
	local empty=true
	local F

	check_perms_in_dir "$dir"

	dir="${dir#./}"
	for F in "$dir"/* "$dir"/.*; do case "${F##*/}" in
	DESCR?(-*))
		empty=false
		[[ -f $F ]] ||
			err "$F is not a file"
		check_trailing_whitespace "$F"
		check_newline_at_eof "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		egrep -q '\$OpenBSD.*\$' "$F" &&
			err "$F should not contain \$OpenBSD\$ tag"
		;;

	PFRAG.shared?(-*))
		empty=false
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_plist_file "$F"
		awk <"$F" '/^(@comment )?@lib /' | {
			local no_a_for_so=false plist=${F##*/} shlibs_found=false
			plist=PLIST${plist##PFRAG.+([!-])}
			while read l; do
				shlibs_found=true
				l=${l##"@comment "}
				l=${l##"@lib "}
				l=${l%%.so.*}.a
				fgrep -q -- "$l" "${F%/*}/$plist" || no_a_for_so=true
			done
			$shlibs_found && ! $no_a_for_so &&
				err "$F should be merged in $plist"
			! $error
		} || error=true
		;;

	PFRAG.*|PLIST?(-*))
		empty=false
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_plist_file "$F"
		;;

	README?(-*))
		[[ -f $F ]] ||
			err "$F is not a file"
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_trailing_whitespace "$F"
		check_newline_at_eof "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		head -n 1 -- "$F" |
			egrep -q '^(#[[:space:]]*)?\$OpenBSD(:.*)?\$$' ||
			err "$F does not have \$OpenBSD\$ RCS tag at the top"
		;;

	*.rc)
		[[ -f $F ]] ||
			err "$F is not a file"
		[[ ${F##*/} == [A-Za-z_]*([A-Za-z0-9_]).rc ]] ||
			err "$F name will not work in rc.subr(8)"
		check_trailing_whitespace "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		head -n 5 -- "$F" |
			egrep -q '^#[[:space:]]*\$OpenBSD(:.*)?\$$' ||
			err "$F does not have \$OpenBSD\$ RCS tag at the top"
		;;

	MESSAGE?(-*)|UNMESSAGE?(-*))
		[[ -f $F ]] ||
			err "$F is not a file"
		[[ -n $subst_cmd ]] && check_subst_vars "$F" "$subst_cmd"
		check_trailing_whitespace "$F"
		check_newline_at_eof "$F"
		check_long_lines "$F"
		check_hardcoded "$F"
		egrep -q '\$OpenBSD.*\$' "$F" &&
			err "$F should not contain \$OpenBSD\$ tag"
		;;

	*)
		handle_extra_file "$F"
		;;
	esac; done

	$empty && err "$dir directory does not contain either DESCR, PFRAG or PLIST files"
}

# Checks made:
#   * There are no hardcoded /usr/local or /var paths in file.
#     /var/log, /var/run and /var/tmp are perfectly fine, though.
check_hardcoded() {
	$debugging && echo "CALLED: check_hardcoded($*)" >&2

	perl -n -e 'BEGIN { $ec=1; }
		    if (m,/usr/local\b,o) { $ec=0; close ARGV; }
		    if (m,/var((?:/+[^/\s]+)*)(?:\s.*)?$,o) {
			unless ($1 =~ m,^/+(?:log|run|tmp),o) {
			    $ec=0; close ARGV;
			}
		    }
		    END { $? = $ec; }' \
		"$1" && err "hardcoded paths detected in $1, consider using" \
		    "SUBST_VARS and TRUEPREFIX/LOCALBASE/LOCALSTATEDIR/VARBASE"
}

# Checks made:
#   * There are no lines longer than 80 characters that have at least
#     one space (avoids warnings on long URLs etc.).
check_long_lines() {
	$debugging && echo "CALLED: check_long_lines($*)" >&2
	local file=$1; shift

	local n=$(awk <"$file" \
		  '/[[:space:]]/ && length > 80 { n++ } END { print n+0 }')
	(($n > 0 )) &&
		err "$n line(s) longer than 80 chars in $file"
}

# Checks made:
#   * There is an OpenBSD RCS tag at the top.
#   * No items with ${FULLPKGNAME} are allowed, except readme.
#   * No empty lines.
check_plist_file() {
	$debugging && echo "CALLED: check_plist_file($*)" >&2

	[[ -f $1 ]] ||
		err "$1 is not a file"
	head -n 1 -- "$1" |
		egrep -q '^@comment \$OpenBSD.*\$$' ||
		err "$1 does not have \$OpenBSD\$ RCS tag at the top"

	# Do not match just '${FULLPKGNAME}' because many ports use the
	# following trick:
	#    @cwd ${LOCALBASE}/share/doc/pkg-readmes
	#    ${FULLPKGNAME}
	egrep -v '^(share/doc/pkg-readmes/\$\{FULLPKGNAME\}|@comment .*)$' "$1" |
		egrep '.\$\{FULLPKGNAME\}|\$\{FULLPKGNAME\}.' >&2 &&
		err "$1 contains item(s) with \${FULLPKGNAME} in it, see above"

	egrep -q '^[[:space:]]*$' "$1" && err "$1 contains empty lines"
}

# Checks made:
#   * Every variable referenced by ${[A-Z]+} should be in ${SUBST_VARS}.
check_subst_vars() {
	$debugging && echo "CALLED: check_subst_vars($*)" >&2

	local F=$1; shift
	local subst_cmd=$1; shift

	# Add variables sometimes referenced in port docs.
	eval "$subst_cmd" -DPATH=test -DWRKSRC=test <"$F" |
	    egrep '\$\{[A-Z]+\}' >&2 &&
		err "looks like misspelled variables in $F, see above"
}

# Checks made:
#   * Contains OpenBSD RCS tag at the top line.
#   * No REVISION marks present in given file (unless in update mode).
#   * Each REVISION mark presents only once.
#   * BUILD_DEPENDS, MODULES and PERMIT_DISTFILES_FTP are not defined in
#     VAR-subpkg manner.
#   * No trailing whitespace.
#   * SHARED_LIBS are not defined inside ".if" statements.
#   * Variables are not assigned via "=" twice outside of .if statemets.
check_makefile() {
	$debugging && echo "CALLED: check_makefile($*)" >&2

	local F="$1"
	check_trailing_whitespace "$F"
	check_long_lines "$F"
	check_hardcoded "$F"
	head -n 1 -- "$F" |
		egrep -q '^#[[:space:]]*\$OpenBSD.*\$' ||
		err "$F does not have \$OpenBSD\$ RCS tag at the top"

	local iflevel=0 l lnum=0 revs= t r mkvars= var duprevfound
	# do not unset mkvars, having empty element(-s) is fine
	unset revs[0]
	local tab="$(print '\t')"
	while IFS= read -r l; do ((++lnum))
		set -A t -- $l
		duprevfound=false

		case $l in
		*(" ")REVISION*)
			$existing_port ||
				err "REVISION mark found at $F:$lnum"
			var=${t[0]%=}
			if ((${#revs[@]} > 0)); then
				for r in "${revs[@]}"; do
					if [[ $var == "$r" ]]; then
						err "duplicated $r in $F"
						# avoid dup error messages
						duprevfound=true
						break
					fi
				done
			fi
			revs[${#revs[@]}]=${t[0]}
			;;
		*(" ")@(BUILD_DEPENDS|MODULES|PERMIT_DISTFILES_FTP)-*)
			err "${l%%-*} is not a subpackageble variable, see $F:$lnum"
			;;
		*(" ").*(" "|"$tab")if*)
			((++iflevel))
			;;
		*(" ").*(" "|"$tab")endif*)
			((iflevel--))
			;;
		*(" ")SHARED_LIBS*(" "|"$tab")*(+|:|!)=*)
			if ((iflevel > 0)); then
				err "should not be inside .if block ($F:$lnum): $l"
			fi
			;;
		esac

		if [[ $l == *(" ")+([A-Za-z0-9_-])*(" "|"$tab")?(\?)=* ]] &&
		   ((iflevel == 0)) && ! $duprevfound; then
			var=${t[0]%?(\?)=*}
			for v in "${mkvars[@]}"; do
				if [[ $v == "$var" ]]; then
					err "duplicated assignment of $v" \
					    "at $F:$lnum"
					break
				fi
			done
			mkvars[${#mkvars[@]}]=$var
		fi
	done <"$F"
}

# Checks made:
#   * None of executable bits (111) are set on plain files.
check_perms_in_dir() {
	$debugging && echo "CALLED: check_perms_in_dir($*)" >&2

	(find -f "$1" -- -maxdepth 1 -type f \
	    \( -perm -100 -or -perm -010 -or -perm 001 \) \
	    </dev/null || true) |&
	local F
	while read -pr F; do
		F=${F#./}
		ignoring "$F" && continue
		err "executable file: ${F#./}"
	done
}


############################################################
# Run checks. Also calculate and show pkgpath variable,
# unless we're checking the ports tree root dir.
#

for D; do
	if [[ $D == /* ]]; then
		err "absolute path $D ignored"
		continue
	fi
	if [[ $D == *(*/)..*(/*) ]]; then
		err "too many .. in $D, skipping"
		continue
	fi
	check_port_dir "$D"
done

if ! $rootrun; then
	[[ -z $pkgpath ]] && pkgpath=${PWD##"$portsdir/"}

	if [[ $pkgpath == "$PWD" ]]; then
		cat >&2 <<EOE
${0##*/}: could not determine PKGPATH. Please help me with the -p option.
EOE
		exit 2
	fi

	echo "$pkgpath"
fi

! $error
